
Говорят, что нельзя полностью понять систему, пока не поймёшь её сбои. Ещё будучи студентом я ради забавы написал реализацию TCP, а потом несколько лет проработал в IT, но до сих пор продолжаю глубже и глубже изучать работу TCP — и его ошибки. Самое удивительное, что некоторые из этих ошибок проявляются в базовых вещах. И они неочевидны. В этой статье я преподнесу их как головоломки, в стиле Car Talk или старых головоломок Java. Как и любые другие хорошие головоломки, их очень просто воспроизвести, но решения обычно удивляют. И вместо того, чтобы фокусировать наше внимание на загадочных подробностях, эти головоломки помогают изучить некоторые глубинные принципы работы TCP.
Необходимые условия
Эти головоломки подразумевают наличие базовых знаний о работе TCP на Unix-подобных системах. Но вам не нужно быть мастером, чтобы вникнуть в них. Например:
- Информацию о состояниях сеанса TCP, трёх этапах соединения и об этапах его завершения можно найти в Википедии.
- Программы, как правило, взаимодействуют с сокетами, используя
read,write,connect,bind,listenиaccept. Помимо этого, есть такжеsendиrecv, но в наших примерах они будут вести себя какreadиwrite. - Я буду использовать в этой статье
poll. Хотя многие системы используют что-то более эффективное, например,kqueueилиepoll, в рамках нашей задачи все эти инструменты будут эквивалентны. Что касается приложений, использующих операции блокирования, а не какой-либо из этих механизмов: один раз поняв, как ошибки TCP отражаются наpoll, вам будет проще догадаться, какой эффект они окажут на любые операции блокирования.
Вы можете повторить все эти примеры самостоятельно. Я использовал две виртуальные машины, запущенные с помощью VMware Fusion. Результаты получились такие же, как на production-сервере. Для тестирования я использовал
nc(1) на SmartOS, и не поверю, что любая из воспроизводимых неполадок будет специфична для конкретной ОС. Для отслеживания системных вызовов и сбора грубой информации о таймингах я использовал утилиту truss(1) из проекта illumos. Вы можете получить подобную информацию с помощью dtruss(1m) под OS X или strace(1) под GNU/Linux.nc(1) очень простая программа. Мы будем использовать её в двух режимах:- Как сервер. В этом режиме nc создаст сокет, будет прослушивать его, вызовет
acceptи заблокирует, пока не будет установлено соединение. - Как клиент. В этом режиме
ncсоздаст сокет и установит соединение с удалённым сервером.
В обоих режимах после установки соединения каждая из сторон использует
poll для ожидания стандартного ввода или подключения сокета, имеющего готовые для чтения данные. Входящие данные выводятся в терминал. Данные, которые вы вводите в терминал, отправляются через сокет. При нажатии CTRL-C сокет закрывается и процесс останавливается.В примерах мой клиент будет называться
kang, а сервер — kodos.Разминка: нормальный разрыв TCP
Начнём с базовой ситуации. Представим, что мы настроили сервер на
kodos:Сервер
[root@kodos ~]# truss -d -t bind,listen,accept,poll,read,write nc -l -p 8080
Base time stamp: 1464310423.7650 [ Fri May 27 00:53:43 UTC 2016 ]
0.0027 bind(3, 0x08065790, 32, SOV_SOCKBSD) = 0
0.0028 listen(3, 1, SOV_DEFAULT) = 0
accept(3, 0x08047B3C, 0x08047C3C, SOV_DEFAULT, 0) (sleeping...)(Напоминаю, что в этих примерах я использую
truss для вывода системных вызовов, которые делает nc. Информация о времени выводится с помощью флага -d, а -t позволяет выбрать, какие из вызовов мы хотим увидеть.)Теперь я устанавливаю соединение на
kang:Клиент
[root@kang ~]# truss -d -t connect,pollsys,read,write,close nc 10.88.88.140 8080
Base time stamp: 1464310447.6295 [ Fri May 27 00:54:07 UTC 2016 ]
...
0.0062 connect(3, 0x08066DD8, 16, SOV_DEFAULT) = 0
pollsys(0x08045670, 2, 0x00000000, 0x00000000) (sleeping...)На
kodos мы видим:Сервер
23.8934 accept(3, 0x08047B3C, 0x08047C3C, SOV_DEFAULT, 0) = 4
pollsys(0x08045680, 2, 0x00000000, 0x00000000) (sleeping...)Подключение TCP находится в состоянии ESTABLISHED, а оба процесса в
poll. Мы можем увидеть это на каждой системе с помощью netstat:Сервер
[root@kodos ~]# netstat -f inet -P tcp -n
TCP: IPv4
Local Address Remote Address Swind Send-Q Rwind Recv-Q State
–––––––––––––––––––– –––––––––––––––––––– ––––– –––––- ––––– –––––- ––––––––––-
10.88.88.140.8080 10.88.88.139.33226 1049792 0 1049800 0 ESTABLISHED
...Клиент
[root@kang ~]# netstat -f inet -P tcp -n
TCP: IPv4
Local Address Remote Address Swind Send-Q Rwind Recv-Q State
–––––––––––––––––––– –––––––––––––––––––– ––––– –––––- ––––– –––––- ––––––––––-
10.88.88.139.33226 10.88.88.140.8080 32806 0 1049800 0 ESTABLISHED
...Вопрос: когда мы завершим один из процессов, что случится с другим? Поймёт ли он, что произошло? Как он это поймёт? Попробуем предугадать поведение конкретных системных вызовов и объяснить, почему каждый из них делает то, что делает.
Нажмём CTRL-C на
kodos:Сервер
pollsys(0x08045680, 2, 0x00000000, 0x00000000) (sleeping...)
^C127.6307 Received signal #2, SIGINT, in pollsys() [default]А вот что мы видим на
kang:Клиент
pollsys(0x08045670, 2, 0x00000000, 0x00000000) (sleeping...)
126.1771 pollsys(0x08045670, 2, 0x00000000, 0x00000000) = 1
126.1774 read(3, 0x08043670, 1024) = 0
126.1776 close(3) = 0
[root@kang ~]# Что случилось? Давайте разберемся:
- Осуществляя выход из процесса, мы отправили SIGINT на сервер. После выхода закрылись дескрипторы файлов.
- Когда закрывается последний дескриптор для сокета
ESTABLISHED, стек TCP наkodosотправляет через соединение FIN и переходит в состояниеFIN_WAIT_1. - Стек TCP на
kangполучает пакет FIN, переводит собственное соединение в состояниеCLOSE_WAITи отправляет в ответ ACK. Пока клиентncблокирует сокет — он готов к чтению, ядро будит этот тред с помощьюPOLLIN. - Клиент
ncвидитPOLLINдля сокета и вызываетread, который тут же возвращает 0. Это означает конец соединения.ncрешает, что мы закончили работу с сокетом, и закрывает его. - Тем временем, стек TCP на
kodosполучает ACK и переходит в состояниеFIN_WAIT_2. - Пока клиент
ncна kang закрывает свой сокет, стек TCP наkangотправляет FIN наkodos. Соединение наkangпереходит в состояниеLAST_ACK. - Стек TCP на
kodosполучает FIN, соединение переходит в состояниеTIME_WAIT, и стек наkodosподтверждает FIN. - Стек TCP на
kangполучает ACK для FIN и полностью удаляет соединение. - Спустя две минуты соединение TCP на
kodosзакрывается, и стек полностью удаляет соединение.
Очерёдность этапов может незначительно меняться. Также
kang может вместо FIN_WAIT_2 проходить через состояние CLOSING.Вот так, согласно netstat, выглядит финальное состояние:
Сервер
[root@kodos ~]# netstat -f inet -P tcp -n
TCP: IPv4
Local Address Remote Address Swind Send-Q Rwind Recv-Q State
–––––––––––––––––––– –––––––––––––––––––– ––––– –––––- ––––– –––––- ––––––––––-
10.88.88.140.8080 10.88.88.139.33226 1049792 0 1049800 0 TIME_WAITНа
kang для этого соединения нет никаких исходящих данных.Промежуточные состояния проходят очень быстро, но вы можете отследить их с помощью DTrace TCP provider. Поток пакетов можно посмотреть с помощью snoop(1m) или tcpdump(1).
Выводы: Мы увидели нормальный путь прохождения системных вызовов во время установки и закрытия соединения. Обратите внимание, что
kang незамедлительно обнаружил факт закрытия соединения на kodos — он был разбужен из poll, а возвращение нуля read обозначило завершение потока передачи. В этот момент kang решил закрыть сокет, что привело к закрытию соединения с kodos. Мы вернёмся к этому позже и посмотрим, что будет, если kang не станет закрывать сокет в этой ситуации.Головоломка 1: Перезапуск электропитания
Что случится с установленным неактивным TCP подключением при перезапуске питания одной из систем?
Поскольку в процессе запланированной перезагрузки многие процессы завершаются корректным образом (с использованием команды “reboot”), то результат будет такой же, если ввести в консоль
kodos команду “reboot” вместо завершения работы сервера с помощью CTRL-C. Но что случится, если в предыдущем примере мы просто отключим электропитание для kodos? В конечном итоге kang об этом узнает, верно?Давайте проверим. Устанавливаем подключение:
Сервер
[root@kodos ~]# truss -d -t bind,listen,accept,poll,read,write nc -l -p 8080
Base time stamp: 1464312528.4308 [ Fri May 27 01:28:48 UTC 2016 ]
0.0036 bind(3, 0x08065790, 32, SOV_SOCKBSD) = 0
0.0036 listen(3, 1, SOV_DEFAULT) = 0
0.2518 accept(3, 0x08047B3C, 0x08047C3C, SOV_DEFAULT, 0) = 4
pollsys(0x08045680, 2, 0x00000000, 0x00000000) (sleeping...)Клиент
[root@kang ~]# truss -d -t open,connect,pollsys,read,write,close nc 10.88.88.140 8080
Base time stamp: 1464312535.7634 [ Fri May 27 01:28:55 UTC 2016 ]
...
0.0055 connect(3, 0x08066DD8, 16, SOV_DEFAULT) = 0
pollsys(0x08045670, 2, 0x00000000, 0x00000000) (sleeping...)Для эмуляции перезапуска электропитания я воспользуюсь функцией «reboot» из VMware. Обратите внимание, что это будет настоящий перезапуск — всё, что приводит к постепенному выключению, больше похоже на первый пример.
Спустя 20 минут
kang всё в том же состоянии:Клиент
pollsys(0x08045670, 2, 0x00000000, 0x00000000) (sleeping...)Мы склонны верить, что работа TCP заключается в постоянном поддержании абстракции (а именно, TCP-соединения) между несколькими системами, так что подобные случаи сломанной абстракции выглядят удивительно. И если вы считаете, что это какая-то проблема nc(1), то вы ошибаетесь. «netstat» на
kodos не показывает никакого соединения с kang, но при этом kang покажет полностью рабочее подключение к kodos:Клиент
[root@kang ~]# netstat -f inet -P tcp -n
TCP: IPv4
Local Address Remote Address Swind Send-Q Rwind Recv-Q State
–––––––––––––––––––– –––––––––––––––––––– ––––– –––––- ––––– –––––- ––––––––––-
10.88.88.139.50277 10.88.88.140.8080 32806 0 1049800 0 ESTABLISHED
...Если оставить всё как есть, то
kang никогда не узнает, что kodos был перезагружен.Теперь предположим, что
kang пытается отправить данные kodos. Что произойдёт?Клиент
pollsys(0x08045670, 2, 0x00000000, 0x00000000) (sleeping...)
kodos, are you there?
3872.6918 pollsys(0x08045670, 2, 0x00000000, 0x00000000) = 1
3872.6920 read(0, " k o d o s , a r e y".., 1024) = 22
3872.6924 write(3, " k o d o s , a r e y".., 22) = 22
3872.6932 pollsys(0x08045670, 2, 0x00000000, 0x00000000) = 1
3872.6932 read(3, 0x08043670, 1024) Err#131 ECONNRESET
3872.6933 close(3) = 0
[root@kang ~]#Когда я ввожу сообщение и жму Enter,
kodos просыпается, читает сообщение из stdin и отправляет его через сокет. Вызов write успешно завершён! nc возвращается в poll в ожидании следующего события, и в конце концов приходит к выводу, что сокет не может быть прочитан без блокировки, после чего вызывает read. В этот раз read падает со статусом ECONNRESET. Что это значит? Документация к read(2) говорит нам:[ECONNRESET]
Была попытка чтения из сокета, то соединение было принудительно закрыто пиром.Другой источник содержит чуть больше подробностей:
ECONNRESET
Аргумент filedes ссылается на сокет с установленным соединением. Оно было принудительно закрыто пиром и больше недействительно. Операции ввода/вывода больше не могут выполняться с filedes.Эта ошибка не означает какую-то конкретную проблему с вызовом
read. Она лишь говорит о том, что сокет был отключён. По этой причине большинство операций с сокетом приведут к ошибке.Так что же случилось? В тот момент, когда
nc на kang попытался отправить данные, стек TCP всё ещё не знал, что подключение уже мертво. kang отправил пакет данных на kodos, который ответил RST, потому что ничего не знал о подключении. kang увидел RST и прервал подключение. Файловый дескриптор сокета закрыть невозможно, — файловые дескрипторы работают не так, — но последующие операции будут неудачными со статусом ECONNRESET, пока nc не закроет файловый дескриптор.Выводы:
- Жесткое отключение энергии сильно отличается от аккуратного выключения. При тестировании распределённых систем нужно отдельно проверять и этот сценарий. Не ждите, что всё будет так же, как и при обычной остановке процесса (kill).
- Бывают ситуации, когда одна сторона уверена, что TCP-соединение установлено, а другая — не уверена, и эта ситуация никогда не будет решена автоматически. Управлять решением таких проблем можно с использованием keep-alive для соединений на уровне приложения или TCP.
- Единственная причина, по которой
kangвсё-таки узнал об исчезновении удалённой стороны, заключается в том, что он отправил данные и получил ответ, сигнализирующий об отсутствии подключения.
Возникает вопрос: а что если
kodos по какой-то причине не отвечает на отправку данных?Головоломка 2: Отключение электропитания
Что случится, если конечная точка TCP соединения отключится от сети на какое-то время? Узнают ли об этом остальные узлы? Если да, то как? И когда?
Вновь установим соединение с помощью
nc:Сервер
[root@kodos ~]# truss -d -t bind,listen,accept,poll,read,write nc -l -p 8080
Base time stamp: 1464385399.1661 [ Fri May 27 21:43:19 UTC 2016 ]
0.0030 bind(3, 0x08065790, 32, SOV_SOCKBSD) = 0
0.0031 listen(3, 1, SOV_DEFAULT) = 0
accept(3, 0x08047B3C, 0x08047C3C, SOV_DEFAULT, 0) (sleeping...)
6.5491 accept(3, 0x08047B3C, 0x08047C3C, SOV_DEFAULT, 0) = 4
pollsys(0x08045680, 2, 0x00000000, 0x00000000) (sleeping...)Клиент
[root@kang ~]# truss -d -t open,connect,pollsys,read,write,close nc 10.88.88.140 8080
Base time stamp: 1464330881.0984 [ Fri May 27 06:34:41 UTC 2016 ]
...
0.0057 connect(3, 0x08066DD8, 16, SOV_DEFAULT) = 0
pollsys(0x08045670, 2, 0x00000000, 0x00000000) (sleeping...)Теперь внезапно выключим питание
kodos и попытаемся отправить данные с kang:Клиент
pollsys(0x08045670, 2, 0x00000000, 0x00000000) (sleeping...)
114.4971 pollsys(0x08045670, 2, 0x00000000, 0x00000000) = 1
114.4974 read(0, "\n", 1024) = 1
114.4975 write(3, "\n", 1) = 1
pollsys(0x08045670, 2, 0x00000000, 0x00000000) (sleeping...)Вызов
write завершается нормально, и я долго ничего не вижу. Только через пять минут появляется:Клиент
pollsys(0x08045670, 2, 0x00000000, 0x00000000) (sleeping...)
425.5664 pollsys(0x08045670, 2, 0x00000000, 0x00000000) = 1
425.5665 read(3, 0x08043670, 1024) Err#145 ETIMEDOUT
425.5666 close(3) = 0Эта ситуация очень похожа на ту, когда мы перезапускали электропитание вместо полного отключения. Есть два отличия:
- системе потребовалось 5 минут на осознание ситуации,
- статус ошибки был ETIMEDOUT.
Снова обратите внимание — это истёкший тайм-аут
read. Мы бы увидели ту же самую ошибку и при других операциях с сокетом. Это происходит потому, что сокет входит в состояние, когда истёк тайм-аут подключения. Причина этого в том, что удалённая сторона слишком долго не подтверждала пакет данных — 5 минут, в соответствии с настройками этой системы.Выводы:
- Когда удалённая система вместо перезапуска электропитания просто выключается, то первая система может узнать об этом, только отправив данные. В ином случае она никогда не узнает об обрыве соединения.
- Когда система слишком долго пытается отправить данные и не получает ответа, TCP-соединение закрывается и все операции с сокетом будут завершаться с ошибкой ETIMEDOUT.
Головоломка 3: Нарушение соединения без его падения
На этот раз вместо того, чтобы описывать вам специфическую ситуацию и спрашивать, что происходит, я поступлю наоборот: опишу некое наблюдение и посмотрю, сможете ли вы понять, как такое произошло. Мы обсуждали несколько ситуаций, в которых
kang может верить, что он подключён к kodos, но kodos об этом не знает. Возможно ли для kang быть подключённым к kodos так, чтобы kodos не знал об этом в течение неопределённого срока (т.е. проблема не решится сама собой), и при этом не было бы отключения или перезапуска электропитания, никакой другой ошибки операционной системы kodos или сетевого оборудования?Подсказка: рассмотрим вышеописанный случай, когда соединение застряло в статусе ESTABLISHED. Справедливо считать, что ответственность за решение этой проблемы несёт приложение, так как оно держит сокет открытым и может обнаружить посредством отправки данных, когда соединение было прервано. Но что если приложение уже не держит сокет открытым?
В разминке мы рассматривали ситуацию, когда nc на
kodos закрыло сокет. Мы сказали, что nc на kang прочитало 0 (указатель окончания передачи) и закрыло сокет. Допустим, сокет остался открытым. Очевидно, что из него невозможно было бы читать. Но касательно TCP ничего не говорится о том, что вы не можете отправлять дополнительные данные той стороне, которая послала вам FIN. FIN означает лишь закрытие потока данных в том направлении, по которому был послан FIN. Чтобы продемонстрировать это, мы не можем использовать
nc на kang, потому что оно автоматически закрывает сокет после получения 0. Поэтому, я написал демо-версию nc, под названием dnc, которая пропускает этот момент. Также dnc явным образом выводит системные вызовы, которые она совершает. Это даст нам шанс отследить состояния TCP. Сперва настроим подключение:
Сервер
[root@kodos ~]# truss -d -t bind,listen,accept,poll,read,write nc -l -p 8080
Base time stamp: 1464392924.7841 [ Fri May 27 23:48:44 UTC 2016 ]
0.0028 bind(3, 0x08065790, 32, SOV_SOCKBSD) = 0
0.0028 listen(3, 1, SOV_DEFAULT) = 0
accept(3, 0x08047B2C, 0x08047C2C, SOV_DEFAULT, 0) (sleeping...)
1.9356 accept(3, 0x08047B2C, 0x08047C2C, SOV_DEFAULT, 0) = 4
pollsys(0x08045670, 2, 0x00000000, 0x00000000) (sleeping...)Клиент
[root@kang ~]# dnc 10.88.88.140 8080
2016-05-27T08:40:02Z: establishing connection
2016-05-27T08:40:02Z: connected
2016-05-27T08:40:02Z: entering poll()Теперь убедимся, что на обеих сторонах подключение находится в статусе ESTABLISHED:
Сервер
[root@kodos ~]# netstat -f inet -P tcp -n
TCP: IPv4
Local Address Remote Address Swind Send-Q Rwind Recv-Q State
–––––––––––––––––––– –––––––––––––––––––– ––––– –––––- ––––– –––––- ––––––––––-
10.88.88.140.8080 10.88.88.139.37259 1049792 0 1049800 0 ESTABLISHEDКлиент
[root@kang ~]# netstat -f inet -P tcp -n
TCP: IPv4
Local Address Remote Address Swind Send-Q Rwind Recv-Q State
–––––––––––––––––––– –––––––––––––––––––– ––––– –––––- ––––– –––––- ––––––––––-
10.88.88.139.37259 10.88.88.140.8080 32806 0 1049800 0 ESTABLISHEDНа
kodos применим CTRL-C для процесса nc:Сервер
pollsys(0x08045670, 2, 0x00000000, 0x00000000) (sleeping...)
^C[root@kodos ~]# На
kang сразу увидим следующее:Клиент
2016-05-27T08:40:12Z: poll returned events 0x0/0x1
2016-05-27T08:40:12Z: reading from socket
2016-05-27T08:40:12Z: read end-of-stream from socket
2016-05-27T08:40:12Z: read 0 bytes from socket
2016-05-27T08:40:12Z: entering poll()Теперь посмотрим на статус подключений TCP:
Сервер
[root@kodos ~]# netstat -f inet -P tcp -n
TCP: IPv4
Local Address Remote Address Swind Send-Q Rwind Recv-Q State
–––––––––––––––––––– –––––––––––––––––––– ––––– –––––- ––––– –––––- ––––––––––-
10.88.88.140.8080 10.88.88.139.37259 1049792 0 1049800 0 FIN_WAIT_2Клиент
[root@kang ~]# netstat -f inet -P tcp -n
TCP: IPv4
Local Address Remote Address Swind Send-Q Rwind Recv-Q State
–––––––––––––––––––– –––––––––––––––––––– ––––– –––––- ––––– –––––- ––––––––––-
10.88.88.139.37259 10.88.88.140.8080 1049792 0 1049800 0 CLOSE_WAITЭто имеет смысл: kudos отправил FIN на
kang. FIN_WAIT_2 показывает, что kodos получил ACK от kang в ответ на посланный им FIN, а CLOSE_WAIT показывает, что kang получил FIN, но не отправил FIN в ответ. Это вполне нормальное состояние TCP-подключения, которое может длится бесконечно. Представьте, что kodos отправил запрос kang и не планировал отправлять ничего больше; kang может часами счастливо отправлять данные в ответ. Только в нашем случае kodos фактически закрыл сокет.Давайте подождём минуту и вновь проверим статус TCP-подключений. Выяснилось, что на
kodos подключение полностью пропадает, но всё ещё существует на kang:Клиент
[root@kang ~]# netstat -f inet -P tcp -n
TCP: IPv4
Local Address Remote Address Swind Send-Q Rwind Recv-Q State
–––––––––––––––––––– –––––––––––––––––––– ––––– –––––- ––––– –––––- ––––––––––-
10.88.88.139.37259 10.88.88.140.8080 1049792 0 1049800 0 CLOSE_WAITМы столкнулись с менее известной ситуацией, связанной со TCP-стеком: когда приложение закрыло сокет, стек отправил FIN, удалённый стек его распознал FIN, а локальный стек ожидает фиксированный период времени и закрывает соединение. Причина? Удалённая сторона была перезагружена. Этот случай аналогичен тому, когда подключение на одной стороне находится в статусе ESTABLISHED, а другая сторона об этом не знает. Разница заключается лишь в том, что приложение закрыло сокет, и нет никакого другого компонента, который мог бы разобраться с проблемой. В результате TCP-стек ждёт установленный период времени и закрывает соединение (ничего не посылая на другую сторону).
Вопрос вдогонку: что случится, если в этой ситуации
kang отправит данные к kodos? Не забывайте, kang всё ещё считает, что соединение открыто, хотя на стороне kodos оно уже завершено. Клиент
2016-05-27T08:40:12Z: entering poll()
kodos, are you there?
2016-05-27T08:41:34Z: poll returned events 0x1/0x0
2016-05-27T08:41:34Z: reading from stdin
2016-05-27T08:41:34Z: writing 22 bytes read from stdin to socket
2016-05-27T08:41:34Z: entering poll()
2016-05-27T08:41:34Z: poll returned events 0x0/0x10
2016-05-27T08:41:34Z: reading from socket
dnc: read: Connection reset by peerЭто то же самое, что мы видели в Головоломке 1:
write() успешно выполняется, так как TCP-стек ещё не знает, что соединение закрыто. Но затем идёт RST, который пробуждает находящийся в poll() тред, и последующий запрос read() возвращает ECONNRESET.Выводы:
- Возможна ситуация, когда обе стороны не сходятся во мнениях относительно статуса соединения, хотя при этом не было ошибки операционной системы, сети или железа.
- В описанном выше случае
kangне имеет возможности узнать, ожидает лиkodosполучения данных отkang, или жеkodosзакрыл сокет и не прослушивает его (по крайней мере, не без отправки пакета). Поэтому не стоит проектировать систему, которая при нормальных условиях эксплуатации в течение длительного времени будет использовать сокеты в подобных полуоткрытых состояниях.
Заключение
TCP обычно представляется нам как протокол, который поддерживает абстракцию — «TCP-соединение» — между двумя системами. Мы знаем, что из-за некоторых программных или сетевых проблем соединение упадёт. Но не всегда очевидно, что бывают случаи возникновения сбоев самой абстракции, из-за чего системы расходятся во мнении по поводу состояния подключения. Например:
- Одна система может считать, что у неё есть рабочее соединение с удаленной системой, которая, в свою очередь, ничего не знает об этом соединении.
- Это может происходить без каких-либо ошибок операционной системы, сети или другого оборудования.
Такое поведение не говорит о недостатках TCP. Наоборот, в подобных случаях TCP ведёт себя наиболее разумно, с учетом ситуации. Если бы мы пытались реализовать свой собственный механизм передачи данных вместо TCP, то подобные случаи напомнили бы нам о том, насколько сложные проблемы могут возникнуть. Это внутренние проблемы, связанные с распределёнными системами, а TCP-подключение по сути является распределённой системой.
Тем не менее, наиболее важный урок, который можно вынести из всего этого, заключается в том, что понятие «TCP-соединения, охватывающего несколько систем» — это удобная фикция. Когда что-то идёт не так, очень важно, чтобы две разные машины одновременно пытались согласованное представление о соединении. Приложение начинает решать возникающие проблемы в тех случаях, когда машины действую по-разному (для этого часто используется механизм keep-alive).
Кроме того, дескриптор файла «оторван» от соответствующего TCP-соединения. Соединения существуют (в разных, связанных с закрытием состояниях) даже тогда, когда приложение закрыло дескриптор файла. А иногда дескриптор файла может быть открыт, хотя TCP-соединение было закрыто в результате ошибки.
Остальные уроки, о которых стоит помнить:
- Неаккуратная перезагрузка системы (когда падает операционная система) — это не то же самое, что обычный выход или закрытие процесса. Важно тестировать этот случай отдельно. Перезагрузка, когда удалённая система возвращается в онлайн — это не то же самое, что отключение удалённой машины.
- От ядра не поступает упреждающих сигналов, когда закрывается TCP-сокет. Вы можете узнать об этом, только вызывая
read(),write(), или выполняя другие операции с дескриптором файла сокетом. Если ваша программа по какой-то причине этого не делает, то вы никогда не узнаете об ошибке соединения.
Некоторые малоизвестные моменты:
- ECONNRESET — это ошибка сокета, которую мы можем получить от
read(),write()и других операций. Она означает, что удалённый компьютер послал RST. - ETIMEDOUT — это ошибка сокета, которую можно получить от
read(),write()и остальных операций. Она означает, что истёк некий таймаут, имеющий отношение к соединению. В большинстве случаев это происходит, когда удалённая сторона слишком долго не признаёт пакет. Обычно это пакеты данных, пакет FIN или сигнал KeepAlive.
Важно отметить, что эти ошибки не означают, будто что-то пошло не так с вашими операциями чтения или записи. Это лишь означает, что закрыт сам сокет.
Комментарии (13)
DenMMM
25.11.2016 16:32+1Это хорошо изложено в книге «Эффективное программирование TCP/IP», (С) Йон Снейдер.
http://mexalib.com/view/2345

amarao
25.11.2016 17:29+4Сломанные трубы в ssh хорошо помогают понять что такое rst и когда его присылают.
Куда интереснее и печальнее ситуация, когда ребутящийся клиент обнаруживает, что сервер его больше не хочет принимать, потому что коннекты закончились, а предыдущие соединения висят часами, потому что сервер ничего не хочет слать клиенту просто так.
degs
25.11.2016 19:52+2Вроде все правильно написано, но с другой стороны зачем? Классическая книга Ричарда Стивенса, изданная в 1994 году, покрывает эти и еще многие другие случаи гораздо подробнее и с детальным анализом tcpdump. Да она и написана интереснее на мой вкус.
Я надеялся увидеть обзор новых фишек TCP, которых с тех пор появилось множество и которые нигде систематически не описаны, но увы...
DrZlodberg
26.11.2016 08:51Так вот почему у меня так странно себя qutim вёл. Про обрыве соединения (подозреваю, там физически болтался контакт и система постоянно переустанавливала соединение) джаббер позволял отправлять сообщение, однако они никуда не приходили. И сообщений об ошибке тоже не было.
vadimr
26.11.2016 16:22TCP придуман как раз затем, чтобы абстрагироваться от проблем среды передачи данных, и диагностировать при его использовании подобные аппаратные проблемы очень непросто. Если вам нужно оперативно реагировать на такие вещи, лучше использовать UDP.

Karpion
27.11.2016 00:34-1Даже если нет данных для отправки, каждый участник TCP-соединения обязан регулярно слать подтверждения своей готовности принять данные (т.е. сведения о состоянии окна приёма). Если вторая сторона долго не подтверждает готовность принять данные — надо послать ей «нулевые данные» (т.е. пакет как бы с данными, но размер данных равен нулю).; и тогда та сторона обязана подтвердить приём данных. А если не подтвердила (ну, надо убедиться, послав несколько раз) — то надо убивать сессию на своей стороне.

netch80
30.11.2016 10:20> обязан регулярно слать подтверждения своей готовности принять данные (т.е. сведения о состоянии окна приёма).
Только при изменении этого окна.
> надо послать ей «нулевые данные»
«Нулевые данные» невозможно послать средствами уровня сокетов. И противоположная сторона не обязана это подтверждать (она воспринимает такое просто как дежурный ACK). Единственный гарантированный способ получить ответ другой стороны — послать реальные данные.
В случае TCP — только на уровне сессии можно это лечить надёжно и кроссплатформенно (keepalive?ами протокола уже выше уровнем, нежели TCP, тогда можно вводить собственный таймаут).
Bozaro
Насколько я помню, как минимум под Linux вечно соединение без данных жить не будет.
Оно схлопнется, когда истечет параметр
net.netfilter.nf_conntrack_tcp_timeout_establishe(по умолчанию: через 5 суток).lieff
Так же есть еще SO_KEEPALIVE и SO_RCVTIMEO. Тогда соединение вообще будет вести себя предсказуемо, без гиганских задержек.
Bozaro
Я бы не стал полагаться на SO_KEEPALIVE. Там таймаут порядка 2-х часов.
В моей практике был прецедент: между серверами, которые держали неактивное соединение, находился statefull firewall и через час данные переставали через него проходить, хотя формально с обоих сторон соединение открыто.
lieff
Ну по дефолту действительно 2 часа:
cat /proc/sys/net/ipv4/tcp_keepalive_time
7200
cat /proc/sys/net/ipv4/tcp_keepalive_intvl
75
cat /proc/sys/net/ipv4/tcp_keepalive_probes
9
Если изменять системные параметры приемлемо, или 2 часа достаточно, то проблем нет. Если делаешь универсальный софт, который должен зарабоатть из коробки без настроек тогда да, надо пинговать самостоятельно.
Karroplan
так это проблема не серверов, а настройки фаерволла. У любого нормально fw есть механизм dead connection detection, специально для того, чтоб не разрушать длинные tcp-сессии в которых бывают большие периоды без передачи