UDP і TCP
Навіщо це
Section titled “Навіщо це”Сервер працює вже місяць. Одного ранку нові клієнти отримують Connection refused
або зависають на підключенні, хоча процес живий, процесор простоює, а канал
порожній. У ss -tan двадцять тисяч рядків зі станом TIME-WAIT, і хтось
на форумі радить «вимкнути». Або інакше: у тому ж виводі кілька тисяч
CLOSE-WAIT, і вони не зникають, скільки не чекай.
У цих двох станів протилежні причини, і за різницею між ними про застосунок можна дізнатися більше, ніж із логів. Щоб її прочитати, треба знати, як TCP встановлює й закриває з’єднання, що таке вікно, чому сегмент може прийти двічі і як ядро вирішує, якому з тисячі сокетів віддати щойно прийнятий байт.
Модуль пояснює транспортний рівень так, як його бачить програміст і адміністратор: порти, два протоколи, рукостискання, вікно, повторні передачі, стани, черги на вході сервера. Перевантаження мережі винесено в модуль 12.
Передумови. Сокет і дескриптор файлу (модуль 3),
заголовок IP і MTU (модуль 6). Як один потік обслуговує
тисячі з’єднань через epoll, розібрано в модулі 13 курсу «Операційні системи»,
а тут цього не повторюємо: практична робота з ним — у лабораторній
A7 того курсу.
Порти й демультиплексування
Section titled “Порти й демультиплексування”IP-адреса доставляє пакет на потрібну машину. На машині працює багато процесів, і хтось має вирішити, кому з них призначено цей пакет. Цим займається транспортний рівень, а його інструмент — порт (port): 16-бітне число в заголовку.
Сервіс, що чекає на з’єднання, займає відомий порт (80, 443, 22), а клієнт
отримує тимчасовий порт (ephemeral port) з діапазону, який задає
net.ipv4.ip_local_port_range. У типовому Linux це 32768–60999 (у вашій системі
перевірте sysctl net.ipv4.ip_local_port_range).
TCP-з’єднання однозначно визначає 4-кортеж (4-tuple): адреса й порт відправника плюс адреса й порт отримувача. Одного порту сервера для цього мало.
| Клієнт | Сервер | З’єднання |
|---|---|---|
| 10.0.0.2:43156 | 10.0.0.1:9000 | перше |
| 10.0.0.2:43160 | 10.0.0.1:9000 | друге, той самий клієнт |
| 10.0.0.3:43156 | 10.0.0.1:9000 | третє, інший клієнт з тим самим номером порту |
Усі вони йдуть на порт 9000 одного сервера й різняться лише кортежем.
Так сервер і приймає десятки тисяч клієнтів на одному порту: ядро
шукає пакет у хеш-таблиці за кортежем і знаходить потрібний сокет. Слухаючий
сокет (LISTEN) зустрічає лише перший сегмент нового з’єднання, а далі
accept() повертає вже інший, з’єднаний сокет із власним дескриптором.
Звідси обмеження, яке ще знадобиться: якщо клієнт відкриває багато
з’єднань до одного сервера й порту, вільними для вибору лишаються тільки
тимчасові порти. Їх приблизно 28 тисяч у типовому діапазоні, і це стеля
одночасних (і нещодавно закритих, див. TIME_WAIT) з’єднань від однієї адреси до
однієї пари «адреса сервера + порт».
Для UDP демультиплексування простіше: непідключений сокет визначається
лише локальною адресою й портом, і всі датаграми на цей порт, хоч би від кого,
потрапляють до нього. Тому один UDP-сокет обслуговує всіх клієнтів,
а хто саме надіслав, програма дізнається з адреси, яку повертає recvfrom().
UDP: транспорт без обіцянок
Section titled “UDP: транспорт без обіцянок”Заголовок UDP — 8 байтів: порт відправника, порт отримувача, довжина і контрольна сума. Більше в ньому нічого немає. Немає з’єднання, порядку, підтверджень, повторних передач і керування швидкістю. Датаграма пішла, а що з нею сталося далі, ніхто не повідомить.
- Межі повідомлень зберігаються. Один
sendto()— одна датаграма, одинrecvfrom()— одна датаграма цілком. Якщо буфер занадто малий, хвіст просто відкидається. - Ніяких гарантій. Датаграма може загубитися, дублюватися, прийти поза порядком. Розмір обмежений: понад MTU датаграма фрагментується на рівні IP (модуль 6), а втрата будь-якого фрагмента губить усю датаграму.
UDP тримається поруч із надійнішим TCP, бо надійність не безкоштовна, і не завжди потрібна саме така.
- Запит-відповідь у один пакет. DNS (модуль 13): встановлювати з’єднання заради одного питання вдвічі дорожче за саме питання. Повторить запит сам застосунок.
- Дані, що швидко старіють. Голос, відео в реальному часі, ігри: повторно переслана позиція гравця десятисекундної давності нікому не потрібна, а очікування її блокує все, що йде після.
- Власний транспорт. QUIC (основа HTTP/3, модуль 14) працює поверх UDP: усю надійність і керування перевантаженням він реалізує сам, у просторі користувача, і не залежить від ядер проміжних вузлів.
- Широкомовлення і групова розсилка. TCP створений для пари вузлів, тому DHCP та подібні протоколи використовують UDP.
Якщо UDP-сокет підключити (connect()), він запам’ятовує співрозмовника
і починає отримувати ICMP-помилки: датаграма на закритий порт повертає
ICMP «port unreachable», і наступний виклик повертає ECONNREFUSED. Непідключений
сокет цієї помилки не побачить, і програма подумає, що просто ніхто не відповів.
TCP: потік байтів з гарантіями
Section titled “TCP: потік байтів з гарантіями”TCP дає застосунку впорядкований надійний двонапрямний потік байтів.
Слово «потік» тут головне: TCP не знає про ваші повідомлення. Чотири write()
по 100 байтів можуть прийти одним read() на 400, а один write() на мегабайт
розпадеться на сотні сегментів. Межі повідомлень будує застосунок (довжина
на початку, роздільник, як у HTTP), і саме цим займається лабораторна A1.
Щоб дати гарантії, TCP тримає в кожного кінця стан: які байти надіслано, які підтверджено, скільки ще готовий прийняти співрозмовник. Стан створюється під час рукостискання.
Рукостискання
Section titled “Рукостискання”- Клієнт надсилає сегмент із прапорцем
SYNі своїм початковим номером послідовності. - Сервер відповідає
SYN+ACK: свій початковий номер і підтвердження клієнтського. - Клієнт підтверджує
ACK. Відтепер обидва боки готові до обміну даними.
Кожен бік має надіслати свій початковий номер і отримати підтвердження, що його прийняли. Сервер поєднує підтвердження клієнтського номера зі своїм у другому сегменті, тож замість чотирьох повідомлень виходить три. Початкові номери вибираються непередбачувано: так старий сегмент попереднього з’єднання з тим самим кортежем важче прийняти за новий, а підробити з’єднання стороннім важче.
У самому рукостисканні сторони домовляються про параметри, які вже не
змінюються: максимальний розмір сегмента (MSS), чи вмикати вибіркові
підтвердження (SACK), мітки часу і множник вікна (wscale). Ось справжній
захоплений обмін між двома просторами імен на одній машині. -S вимикає
відносні номери:
ip netns exec cli tcpdump -nn -S -i v2 tcpIP 10.0.0.2.43156 > 10.0.0.1.9000: Flags [S], seq 3444928969, win 64240, options [mss 1460,sackOK,TS val 1669925612 ecr 0,nop,wscale 10], length 0IP 10.0.0.1.9000 > 10.0.0.2.43156: Flags [S.], seq 1590647663, ack 3444928970, win 65160, options [mss 1460,sackOK,TS val 1424475657 ecr 1669925612,nop,wscale 10], length 0IP 10.0.0.2.43156 > 10.0.0.1.9000: Flags [.], ack 1590647664, win 63, options [nop,nop,TS ...], length 0S — це SYN, S. — SYN+ACK, крапка сама — просто ACK. Зверніть увагу
на ack 3444928970: це початковий номер клієнта плюс один. Сам SYN
«займає» один номер послідовності, як і FIN наприкінці. Тому підтвердження
на нього дорівнює seq + 1, хоча даних не було. Друге число, win, — вікно
отримувача (про нього нижче).
Рукостискання коштує одну затримку в обидва боки (RTT), перш ніж клієнт зможе відправити перший байт даних. Між Києвом і серверною за океаном це кілька десятків мілісекунд, вже втрачених до першого запиту, і ще стільки ж додасть TLS (модуль 15). Тому протоколи й переробляють так, щоб обмінів було менше, як-от повторне використання з’єднань у HTTP (модуль 14).
Номери послідовності і підтвердження
Section titled “Номери послідовності і підтвердження”Номер послідовності (sequence number) у заголовку позначає перший байт даних у сегменті, а не сам сегмент: лічильник рахує байти. Підтвердження (acknowledgement number) означає: «усі байти з меншими номерами я отримав, наступним чекаю саме цей». Це кумулятивне підтвердження. Якщо прийшли байти 0–999 і 2000–2999, а 1000–1999 загубилися, отримувач і далі підтверджуватиме 1000 (відправник побачить дублікати ACK).
Одне кумулятивне число мало що каже про те, що прийшло за дірою, і для цього
з’явилися вибіркові підтвердження (selective acknowledgements, SACK): у сегменті ACK
отримувач додатково перелічує діапазони, які в нього вже є. Відправник тоді
повторює лише справжню дірку. Ви бачили sackOK у рукостисканні: обидва боки
погодилися.
Керування потоком: вікно отримувача
Section titled “Керування потоком: вікно отримувача”Швидкий відправник може заповнити буфер повільного отримувача. У кожному сегменті отримувач пише, скільки вільного місця в його буфері прийому: це вікно оголошене отримувачем (receive window). Відправник не має права тримати «в польоті» (надіслано, але ще не підтверджено) більше цього.
Поле вікна в заголовку 16-бітне, тож без доповнення воно обмежене 65 535 байтами,
а це на швидких каналах з великою затримкою нікчемно мало (добуток пропускної
здатності на затримку, модуль 2). Тож під час рукостискання сторони
домовляються за wscale про множник: wscale 10 означає, що значення поля треба помножити
на 2¹⁰. У захопленні вище після рукостискання так і стоїть win 63: справжнє вікно
63 × 1024 = 64 512 байтів. Множник фіксується у SYN і далі не змінюється; якщо
його прибрав файрвол чи проміжний пристрій, вікно лишається 64 КіБ, і швидкість
падає невпізнанно.
Вікно змінюється протягом з’єднання: застосунок повільно читає з сокета, буфер заповнюється, вікно в ACK зменшується. Коли воно дійшло до нуля, відправник зупиняється й час від часу надсилає крихітний пробний сегмент, щоб дізнатися, чи не відкрилося вікно знову.
Швидкість відправника обмежує і вікно отримувача (скільки готовий прийняти співрозмовник), і вікно перевантаження (скільки витримає мережа між вами, модуль 12). Діє менше з них.
Повторні передачі
Section titled “Повторні передачі”Якщо ACK на сегмент не приходить, відправник мусить його повторити. Складність у тому, коли саме: поспішиш, і в мережі з’являться зайві копії, забаришся, і з’єднання простоює.
Відправник вимірює RTT кожного підтвердженого сегмента, згладжує його й
рахує таймер повторної передачі (retransmission timeout, RTO), дещо більший за
типовий RTT з запасом на розкид. Початкові значення й формула — RFC 6298. На Linux
RTO ніколи не падає нижче 200 мс: у виводі ss -ti нижче цілком локальне
з’єднання з RTT в соті частки мілісекунди має rto:204. Після кожної невдалої
спроби таймер подвоюється (експоненціальний відступ), тому сервер, що зник із мережі, виявляється не одразу.
Є й швидший шлях. Якщо приходять три дублікати того самого ACK, відправник робить висновок, що один сегмент загубився, а наступні дійшли, і повторює його негайно, не чекаючи RTO. Це швидка повторна передача (fast retransmit). З SACK відправник ще й точно знає, що повторювати.
Скільки ядро терпить мовчання, для встановлених з’єднань задає net.ipv4.tcp_retries2
(типово 15 спроб, за man 7 tcp від 13 до 30 хвилин залежно від RTO), а для нового
підключення net.ipv4.tcp_syn_retries (типово 6, тобто близько двох хвилин, коли connect()
нарешті здається). Через це «сервер не відповідає» на клієнті так довго тримає програму.
Що лежить у заголовку TCP
Section titled “Що лежить у заголовку TCP”Мінімальний заголовок TCP — 20 байтів, удвічі й більше за UDP, і кожне поле з’явилося заради якогось з механізмів цього розділу.
| Поле | Навіщо |
|---|---|
| порт відправника, порт отримувача | демультиплексування |
| номер послідовності | порядок і місце сегмента в потоці |
| номер підтвердження | кумулятивний ACK |
прапорці SYN, ACK, FIN, RST та інші |
керують життєвим циклом з’єднання |
| вікно | керування потоком (разом із wscale) |
| контрольна сума | виявляє пошкодження заголовка й даних |
| опції | MSS, wscale, SACK, мітки часу |
Контрольна сума ловить випадкові спотворення, але не захищає від зловмисника: його сегмент матиме правильну суму. Захист від підробки дають лише випадкові початкові номери (проти стороннього) і шифрування вище (TLS, модуль 15).
Половина з’єднання: shutdown
Section titled “Половина з’єднання: shutdown”close() закриває дескриптор цілком: бік більше нічого не надішле й нічого
не прочитає. Але два напрямки TCP незалежні, і виклик shutdown(fd, SHUT_WR)
закриває лише запис: надсилається FIN, а читати можна далі. Це потрібно,
коли клієнт має сказати «запит закінчився, більше даних не буде», чекаючи
відповіді на нього. Протоколи без довжини повідомлення (найпростіші, на зразок
«читай до кінця потоку») на цьому й тримаються. Бік, що отримав FIN, бачить
read(), який повертає 0: так виглядає кінець потоку.
Тиша на лінії: хто помітить, що співрозмовник зник
Section titled “Тиша на лінії: хто помітить, що співрозмовник зник”TCP шле пакети лише тоді, коли є що слати. Якщо обидва боки мовчать, а на
шляху тим часом перезавантажили NAT або витягли кабель, жоден не дізнається
про це, бо нічого не відправляв. З’єднання у виводі ss виглядає цілком
ESTABLISHED, хоч насправді мертве. Якщо ж мовчання порушить бік, що пише, він дістане
помилку лише після всіх повторів із подвоєнням RTO (див. вище).
Для таких випадків у сокета є опція SO_KEEPALIVE: після довгого простою ядро
надсилає порожній пробний сегмент, і відсутність відповіді розриває з’єднання.
Вона вимкнена за замовчуванням, а її таймери вимірюються годинами (net.ipv4.tcp_keepalive_time),
тому застосунки, що дбають про швидке виявлення, додають власний heartbeat на рівні протоколу
або змінюють таймери для свого сокета. Окрім того, стан NAT і файрволів, через які йде
з’єднання, живе обмежений час: довге мовчання без keepalive вони просто забувають
(модуль 9), і наступний пакет отримує RST або тишу.
Стани TCP
Section titled “Стани TCP”З’єднання проходить через цілий набір станів, і кожен видно в ss.
Головне на діаграмі — закриття. Кожен напрямок потоку закривається окремо:
той, хто закінчив передавати, шле FIN, і співрозмовник підтверджує його. Тому
повне закриття — це чотири сегменти (у захопленні вище їх три, бо сервер об’єднав
підтвердження й свій FIN в один; так буває, коли застосунок закриває відразу).
Бік, що надіслав FIN першим, називають активним, інший пасивним. Від цього
залежить, у який кінцевий стан потрапить кожен.
TIME_WAIT: навіщо він і на чиєму боці
Section titled “TIME_WAIT: навіщо він і на чиєму боці”Після останнього ACK активний бік не переходить одразу в CLOSED, а тримає
запис про з’єднання ще довго. Стандарт вимагає чекати дві максимальні
тривалості життя сегмента (2×MSL), а Linux замість обчислення бере фіксовані
60 секунд. Чекати доводиться ось чому.
- Останній ACK міг загубитися. Тоді пасивний бік, не діставши підтвердження
свого
FIN, повторить його. Якщо активний бік уже забув про з’єднання, він відповів биRST(«я тебе не знаю»), і пасивний бік побачив би помилку замість чистого закриття. УTIME_WAITактивний бік може ще раз підтвердити. - Запізнілі сегменти старого з’єднання. Мережа може затримати пакет на
кілька секунд. Якщо за ці секунди нове з’єднання з тим самим кортежем
уже відкрито, чужий пакет із правильними номерами міг би
потрапити в новий потік.
TIME_WAITне дає створити з’єднання з тим самим кортежем, доки старі сегменти не вичерпали свого життя.
Ось те саме на живому прикладі. Сервер (у просторі імен srv) закрив
з’єднання першим, і TIME-WAIT лишився саме в нього, а у клієнта запису вже
нема:
ip netns exec srv ss -tanState Recv-Q Send-Q Local Address:Port Peer Address:PortLISTEN 0 5 10.0.0.1:9000 0.0.0.0:*TIME-WAIT 0 0 10.0.0.1:9000 10.0.0.2:52938Тобто TIME_WAIT лишається на боці того, хто закрив першим.
Це не завжди клієнт. HTTP-сервер, який після відповіді закриває з’єднання
сам (HTTP/1.0 або завершення keep-alive за таймаутом), збирає TIME_WAIT у себе.
Клієнт, що відкриває й закриває тисячі короткоживучих з’єднань до одного
сервера, збирає їх у себе й впирається в тимчасові порти.
Самі по собі TIME_WAIT нормальні: вони не тримають дескрипторів і майже
не тримають пам’яті. Проблемою вони стають лише тоді, коли кінчаються тимчасові
порти або кортежі. У такому разі можна:
- повторно використовувати з’єднання (keep-alive, пул), щоб їх було менше;
- розширити діапазон портів або додати ще адрес сервера чи клієнта, щоб розширити простір кортежів;
net.ipv4.tcp_tw_reuseдозволяє вихідним з’єднанням повторно використати сокет уTIME_WAIT(це безпечно завдяки міткам часу). Значення за замовчуванням у нових ядрах 2 (дозволено лише для loopback).
CLOSE_WAIT: симптом помилки в застосунку
Section titled “CLOSE_WAIT: симптом помилки в застосунку”CLOSE_WAIT виникає на пасивному боці. Співрозмовник закрив свій напрямок і
прислав FIN, ядро вже підтвердило його, але застосунок сам ще не викликав
close() на своєму кінці. Ядро чекатиме вічно: таймера, який закрив би
такий сокет за вас, немає, бо вирішувати, коли закінчилась робота, має програма.
Якщо CLOSE_WAIT накопичуються, у коді завжди є помилка: забули закрити сокет на
шляху помилки, загубили дескриптор у пулі, потік, що мав закрити з’єднання,
завис. Демонстрація: сервер приймає з’єднання й «забуває» їх закрити,
поки клієнти вже пішли:
ip netns exec srv ss -tanpState Recv-Q Send-Q Local Address:Port Peer Address:Port ProcessLISTEN 0 5 10.0.0.1:9000 0.0.0.0:* users:(("python3",pid=2089,fd=3))CLOSE-WAIT 0 0 10.0.0.1:9000 10.0.0.2:56130 users:(("python3",pid=2089,fd=5))CLOSE-WAIT 0 0 10.0.0.1:9000 10.0.0.2:56126 users:(("python3",pid=2089,fd=4))Опція -p показує процес і номер дескриптора: тепер відомо, чий це баг. На
клієнті ті самі з’єднання застрягли у FIN-WAIT-2: він відправив FIN, отримав
на нього ACK і чекає на FIN від сервера, який ніколи не прийде. Тут клієнт уже завершився, тож сокети осиротілі (orphan): жоден процес їх не тримає, і ядро зрештою
прибере їх через tcp_fin_timeout; доки клієнтський процес живий і тримає сокет, стан лишається.
Швидка діагностика однією командою: скільки з’єднань у кожному стані.
ss -tan | awk 'NR>1 {print $1}' | sort | uniq -c | sort -rnБагато TIME-WAIT — зазвичай наслідок короткоживучих з’єднань, і шукати треба на
боці, що закриває першим. Багато CLOSE-WAIT — витік у застосунку на цьому
самому хості.
RST: різке завершення
Section titled “RST: різке завершення”Сегмент із прапорцем RST (reset) означає «цього з’єднання нема, забудь про нього».
Його надсилають у кількох ситуаціях:
- підключення до порту, на якому ніхто не слухає: ядро відповідає
RST, а клієнт бачитьConnection refused(ECONNREFUSED); - сегмент прийшов у з’єднання, про яке приймач нічого не знає (наприклад,
після перезавантаження сервера або після
TIME_WAIT, якого не було); - застосунок навмисно перервав з’єднання (
SO_LINGERз нульовим таймаутом), щоб не залишатиTIME_WAIT; - проміжний пристрій (файрвол, балансувальник) вирішив розірвати потік.
ip netns exec cli python3 -c "import sockettry: socket.create_connection(('10.0.0.1', 9999))except Exception as e: print(e)"[Errno 111] Connection refusedВідрізняйте це від іншого симптому. Якщо файрвол мовчки відкидає (DROP)
SYN, відповіді нема взагалі, і клієнт чекатиме на connect() аж до вичерпання
повторів SYN. Тож швидка відмова означає «хост живий, порт закритий», а зависання —
«десь по дорозі мовчки відкидають» (модуль 16).
Nagle і відкладене підтвердження
Section titled “Nagle і відкладене підтвердження”Кожна з цих оптимізацій розумна сама по собі, а разом вони дають найпопулярніше «таємниче затримання на 40 мс».
Алгоритм Нейґла (Nagle). Якщо є непідтверджені дані, TCP не відправляє маленький сегмент, а накопичує нові байти, доки не прийде ACK або доки не набереться повний сегмент. Мета — не засмічувати мережу заголовками по 40 байтів на кожен натиснутий символ.
Відкладене підтвердження (delayed ACK). Приймач не підтверджує кожен сегмент
одразу, а трохи чекає: може, з’явиться відповідь, на якій підтвердження поїде
безкоштовно, або прийде ще один сегмент. У Linux мінімальна затримка близько 40 мс
(у виводі ss -ti це поле ato:40).
Разом вони дають клінч. Застосунок пише запит двома викликами write():
заголовок і тіло. Перший сегмент іде одразу, другий Нейґл тримає, бо перший ще
не підтверджений. Приймач не підтверджує перший, бо чекає, що щось надійде.
Обидва чекають на іншого, аж доки не спливе 40 мс.
Лікування: збирати запит в один write()/writev() або вимкнути Нейґла на
сокеті опцією TCP_NODELAY. Кожен серйозний клієнт HTTP і RPC її вмикає. Але це не
універсальні ліки: якщо ви пишете дрібними шматками, TCP_NODELAY змусить слати кожен
окремим сегментом.
Сервер: SO_REUSEADDR, backlog і черга accept
Section titled “Сервер: SO_REUSEADDR, backlog і черга accept”SO_REUSEADDR
Section titled “SO_REUSEADDR”Перезапуск сервера, який закривав з’єднання першим, наштовхується на TIME_WAIT:
локальний порт 9000 усе ще фігурує в кортежах старих з’єднань, і bind() без
спеціальної опції завершується помилкою:
$ python3 ru.py 0 # без SO_REUSEADDR[Errno 98] Address already in use$ python3 ru.py 1 # з SO_REUSEADDRbind okSO_REUSEADDR дозволяє прив’язатись до порту, на якому лишаються лише
TIME_WAIT. Двох слухачів на одну адресу й порт він не дозволяє (це вміє
інша опція, SO_REUSEPORT, вона потрібна, щоб кілька процесів слухали один
порт і ядро розподіляло між ними нові з’єднання). Практичне правило: майже кожен
TCP-сервер має вмикати SO_REUSEADDR перед bind().
Черги на вході
Section titled “Черги на вході”Коли приходить SYN, ядро не чекає на accept(). Воно саме проводить
рукостискання, а готові з’єднання складає в чергу, з якої accept() їх забирає.
Насправді черг дві:
- Черга неповних з’єднань:
SYNприйшов,SYN+ACKнадіслано, останньогоACKще нема. Її розмір обмежуєnet.ipv4.tcp_max_syn_backlog, а коли вона переповнюється (наприклад, під часSYN-флуду), спрацьовуютьSYN-cookies (net.ipv4.tcp_syncookies), що дають змогу не пам’ятати неповні з’єднання. - Черга готових з’єднань (accept queue): рукостискання завершено, але
застосунок ще не викликав
accept(). Її розмір — менше з аргументаlisten(backlog)іnet.core.somaxconn.
Коли друга черга повна, ядро скидає нові з’єднання, і клієнти бачать не відмову, а
зависання й повторні SYN. Цю картину видно в ss -ltn: у слухаючого сокета
Recv-Q — скільки з’єднань зараз чекає на accept(), Send-Q — максимум черги.
Експеримент: слухач із listen(1), який ніколи не викликає accept(), і шість клієнтів.
ip netns exec srv ss -ltn sport = :9001ip netns exec cli ss -tanip netns exec srv nstat -az TcpExtListenOverflows TcpExtListenDropsState Recv-Q Send-Q Local Address:Port Peer Address:PortLISTEN 2 1 10.0.0.1:9001 0.0.0.0:*
ESTAB 0 0 10.0.0.2:35058 10.0.0.1:9001ESTAB 0 0 10.0.0.2:35068 10.0.0.1:9001SYN-SENT 0 1 10.0.0.2:35078 10.0.0.1:9001SYN-SENT 0 1 10.0.0.2:35090 10.0.0.1:9001SYN-SENT 0 1 10.0.0.2:35104 10.0.0.1:9001SYN-SENT 0 1 10.0.0.2:35110 10.0.0.1:9001
TcpExtListenOverflows 8TcpExtListenDrops 8Send-Q слухача — 1, тобто обіцяний backlog, а в черзі опинилося 2: Linux
допускає на одне з’єднання більше за backlog. Двоє клієнтів вважають, що вони
підключені, четверо застрягли в SYN-SENT. Лічильники ListenOverflows і
ListenDrops у nstat підтверджують: ядро відкидало саме через переповнення.
Тож сервер має швидко викликати accept() (тобто не
робити в тому ж потоці довгу роботу, що й у модулі 13 курсу ОС),
а backlog навантаженого сервера має вимірюватися сотнями.
Як це насправді в Linux
Section titled “Як це насправді в Linux”Інструмент №1 — ss (socket statistics), сучасна заміна netstat.
ss -tan # усі TCP-сокети, числові адреси й портиss -tanp # плюс процес і дескриптор (потрібен root для чужих)ss -tan state time-wait | wc -lss -ltn # лише слухаючі; Recv-Q/Send-Q мають особливий змістss -s # зведення за станамиОпції запам’ятати легко: t — TCP, u — UDP, a — усі стани, n — не
розв’язувати імена, p — процеси, l — лише слухаючі.
Внутрішній стан з’єднання, який видно лише зсередини ядра, дає -i:
ip netns exec cli ss -tin state established # вивід скорочено й переформатованоRecv-Q Send-Q Local Address:Port Peer Address:Port0 0 10.0.0.2:56134 10.0.0.1:9000 ts sack bbr wscale:10,10 rto:204 rtt:0.022/0.012 mss:1448 pmtu:1500 cwnd:11 bytes_sent:1000 bytes_acked:1001 segs_out:3 segs_in:2 send 5792000000bps lastsnd:988 lastack:988 pacing_rate 13242140208bps delivered:2 app_limited rcv_space:14480 snd_wnd:68608Що тут читати:
wscale:10,10— множники вікна обох боків, узгоджені вSYN(див. вище);rtoіrtt— поточний таймер повторної передачі (мс) і виміряний час обороту з розкидом (мс);mss,pmtu— розмір сегмента й MTU шляху;cwnd— вікно перевантаження, у сегментах; воно відповідає за швидкість мережі (модуль 12);bytes_sentіbytes_acked— надіслано й підтверджено (одиничка різниці — самSYN);snd_wnd— вікно, яке оголосив отримувач;retrans:x/yз’являється, коли були повторні передачі (у нашому чистому експерименті його нема), аapp_limitedкаже, що швидкість обмежує не мережа, а сам застосунок, який нічого не пише.
За таким виводом уже можна сказати, чому повільно: якщо snd_wnd мале,
винен отримувач, якщо cwnd маленький і є retrans, то мережа, якщо
app_limited — застосунок.
Діагностика: від симптому до причини
Section titled “Діагностика: від симптому до причини”Більшість розмов про «мережа глючить» закінчується однією з цих картин, і кожну можна розпізнати командою.
| Що бачите | Найімовірніше | Куди дивитись |
|---|---|---|
connect() одразу Connection refused |
на тому кінці ніхто не слухає порт | ss -ltn на сервері: чи є LISTEN, на якій адресі (127.0.0.1 чи 0.0.0.0) |
connect() висить до двох хвилин |
SYN мовчки відкидається: файрвол, маршрут, хост вимкнено |
tcpdump на обох кінцях: чи доходить SYN, чи повертається SYN+ACK |
частина клієнтів підключається, частина зависає, у nstat росте ListenOverflows |
переповнена accept-черга | ss -ltn: Recv-Q слухача впирається в Send-Q; швидше викликайте accept(), збільште backlog |
тисячі CLOSE-WAIT |
застосунок не закриває сокети | ss -tanp: який процес і які дескриптори |
тисячі TIME-WAIT, а нові з’єднання не створюються |
кінчаються тимчасові порти | повторне використання з’єднань, ширший діапазон портів |
| запити повільні, канал вільний | вікно, затримка, Nagle |
ss -ti: snd_wnd, cwnd, retrans; tcpdump із часовими мітками |
Порядок дій завжди той самий. Спершу ss на обох кінцях: у якому стані сокет
і що в його чергах. Потім tcpdump там, де ss залишив сумнів: що реально
пройшло по дроту. Лише тоді гіпотези про «мережу» чи «застосунок».
Сервер, що слухає 127.0.0.1, недосяжний ззовні, хоча ss -ltn показує LISTEN.
Тож дивіться й на адресу, до якої прив’язаний слухач: 127.0.0.1:8080
означає «лише з цієї машини», 0.0.0.0:8080 або *:8080 — «з будь-якого інтерфейсу».
Це найчастіша причина «порт відкритий, а не працює».
Типові помилки розуміння
Section titled “Типові помилки розуміння”«TCP гарантує доставку». TCP гарантує, що байти прийдуть у порядку й без
пошкоджень або що ви дізнаєтесь про розрив. Успішний write() означає лише, що
ядро прийняло байти у свій буфер, а не що співрозмовник їх прочитав. Гарантію
«обробив» дає тільки підтвердження на рівні протоколу застосунку.
«Один send() — один recv()». Це UDP. TCP — потік байтів: recv() може
повернути частину повідомлення або кілька повідомлень зліплених. Цикл читання
до потрібної довжини обов’язковий.
«Багато TIME_WAIT означає, що сервер зламаний». Це нормальний слід закритих
з’єднань, і хворобою він стає тільки за нестачі портів. Причину шукайте в тому,
хто закриває першим, і в частоті нових з’єднань, а не в «вимкненні» стану.
«CLOSE_WAIT зникне сам». Ні. Ядро не закриває такий сокет: це справа
застосунку. Скільки їх, стільки й не закритих ним дескрипторів, які рано чи пізно
дійдуть до EMFILE.
«Порт закритий, отже пакетів нема». Порт закритий → приходить RST
(швидка відмова). Порт «відфільтрований» → приходить тиша. Це різні картини,
і за ними видно різну несправність.
«UDP швидший за TCP». Пакет у мережі йде однаково. UDP не має рукостискання й чекання, але і захисту від перевантаження, тому безрозсудна відправка за UDP лише збільшує втрати. Виграш дає відсутність зайвого очікування, а не швидкість.
Перевір себе
Лабораторна
Section titled “Лабораторна”- A1. Echo-сервер і клієнт: TCP і UDP на одній задачі, часткові читання і межі
повідомлень на
send()/recv(), стани сокетів уss. - A3. Розбір пакетів із pcap: заголовки TCP і UDP, прапорці, номери послідовності.
- A4. Надійна доставка поверх UDP: власне ковзне вікно, таймер повторної передачі й дублікати ACK.
- A6. Зворотний проксі й балансувальник: черги
accept, таймаути й повторне використання з’єднань між проксі й бекендом.
Джерела
Section titled “Джерела”- Kurose, Ross, Computer Networking: A Top-Down Approach, розділ про транспортний рівень
- W. Richard Stevens, TCP/IP Illustrated, Vol. 1, розділи про TCP
- RFC 9293 (Transmission Control Protocol) і RFC 768 (User Datagram Protocol)
- RFC 6298 (Computing TCP’s Retransmission Timer)
man 7 tcp,man 7 udp,man 7 socket,man 8 ss,man 8 nstat- Документація ядра:
Documentation/networking/ip-sysctl.rst - Beej’s Guide to Network Programming