Вопрос или проблема
Мое понимание TCP-соединения заключается в том, что исходный ПОРТ остается эксклюзивным для одного соединения, независимо от назначения, так что количество соединений с локального порта 12345, например, не может превышать 1.
Недавно я прочитал, что TCP-соединение идентифицируется по <исходный IP, исходный ПОРТ, целевой IP, целевой ПОРТ>
TCP позволяет совместное использование исходного порта несколькими процессами, но каждый процесс требует свободного порта для закрепления соединения
Поэтому я решил проверить, что такое “совместное использование порта между процессами”: это должно означать, что один и тот же исходный порт может использоваться для подключения к различным назначениям.
Однако, экспериментируя с этим, я попробовал эти две команды:
nc -v -p 12345 google.com 80
которая работает хорошо (опция -v для подробного вывода, и -p для указания исходного порта как 12345, для целей обучения)
Теперь одновременно запустив эту команду в другом окне терминала
nc -v -p 12345 github.com 80
появляется ошибка:
nc: connectx to github.com port 80 (tcp) failed: Address already in use
Причина, по которой я указал тот же исходный порт с -p, заключается в том, чтобы подтвердить, что исходный порт может быть совместно использован. Практически в этом нет необходимости; в реальной ситуации я бы вообще не беспокоился об исходном порте. Исходя из этого, действительно ли это правда, что исходный порт будет использоваться только один раз?
Нет причин, по которым два процесса не могут использовать один и тот же исходный порт, если только они не оба подключаются к одной и той же цели (хосту, порту). На многих системах UNIX установите SO_REUSEPORT
, чтобы разрешить процессам использовать один и тот же номер порта; на Windows установите SO_REUSEADDR
. Например, с помощью socat
:
- процесс 1:
socat stdio tcp:google.com:80,bind=:12345,reuseport
- процесс 2:
socat stdio tcp:bing.com:80,bind=:12345,reuseport
Эти два процесса могут работать одновременно и оба будут иметь исходный порт 12345 (как вы можете подтвердить с помощью netstat
).
Однако обратите внимание, что вы почти наверняка столкнетесь с проблемами, если сокеты не будут корректно закрыты с обеих сторон, поскольку незакрытые TCP-сокеты войдут в состояние TIME_WAIT
, что приведет к блокировке четверки (srcaddr, srcport, dstaddr, dstport) до истечения периода ожидания. Поэтому, когда вы используете один исходный порт для нескольких соединений, вы не сможете повторно подключиться к точно тому же серверу и порту, если предыдущее соединение полностью не завершено или период TIME_WAIT
не истек.
Любой порт всегда выделяется только одному процессу в любой момент времени. Этот процесс может установить любое количество соединений, с тем ограничением, что целевой IP-адрес или номер порта должны различаться между соединениями.
Например, TCP 10.0.0.10:49152 может подключиться только один раз к TCP 10.0.0.2:80, но в то же время может подключиться к TCP 10.0.0.3:80 или TCP 10.0.0.2:8080.
Хотя сам протокол TCP позволяет произвольные комбинации локальных и удаленных портов и адресов, большинство реализаций Unix упрощают управление портами. Причина в том, что процесс настройки сокетов разделен на отдельные этапы.
-
Сначала вы устанавливаете локальный порт с помощью
bind()
. Этот шаг необходим при создании прослушивающего TCP-сокета (вы должны указать, на каком порту он слушает), он не обязателен перед установлением исходящего соединения сconnect()
(будет назначен произвольный локальный порт). Поскольку мы еще не знаем удаленный адрес или порт, невозможно сказать, уникален ли он. Так что просто проверяется, доступен ли запрашиваемый порт. Если у сокета установлен параметрSO_REUSEADDR
, он игнорирует подключенные сокеты при проверке, используется ли локальный адрес, но все равно не получится, если на порту имеется прослушивающий сокет. -
Затем для исходящего соединения вы вызываете
connect()
, указывая удаленный адрес. Вы можете вызватьconnect()
только один раз на сокете, и поскольку мы проверили локальный порт во времяbind()
, это никогда не приведет к дублированию локального/удаленного адреса/порта. -
Для входящего соединения вы вызываете
accept()
на прослушивающем сокете. Снова, потому что мы проверяем, что локальный порт прослушивающего сокета не используется, когда мы его связываем, не может быть дублирующей комбинации.
Отсрочка проверок портов до получения удаленной информации усложнила бы обработку ошибок. Текущий дизайн просто проверяет на дублирование в одном месте: bind()
.
После некоторых исследований я обнаружил, что совместное использование исходного порта разрешено только для нескольких исходящих соединений из одного и того же процесса. Операционной системе нужно знать, какому процессу перенаправить поток соединения.
Я собираюсь выбрать этот ответ через два дня согласно политике сайта.
Я собираюсь высказать мнение с другой точки зрения: истощение исходных портов, когда у вас высокая скорость соединений/разъединений из-за TIME_WAIT
.
Первое примечание: TIME_WAIT
не зависит от tcp_fin_timeout
. Вместо этого, он жестко закодирован на 60 секунд. tcp_fin_timeout
относится к другой стороне.
Например, если у вас есть веб-сервер, который обрабатывает 1000 запросов в секунду, и ваш веб-сервер имеет несчастную, но часто используемую модель «начать код в рабочем процессе/выполнить действия/завершить» (как PHP FPM), каждый запрос будет открывать и закрывать соединение с базой данных. Это приводит к 60000 портам к базе данных в состоянии TIME_WAIT
, в соответствии с жестко закодированным временем 60 секунд. Это слишком много, и вы получаете сообщение ‘не удается назначить запрашиваемый адрес’.
Все эти разговоры о SO_REUSEPORT бессмысленны, потому что у вас нет контроля над этим в соединителе mysql. Более того, если у вас есть другие процессы, работающие под другим UID, вы не можете делиться.
Драйвер PHP MySQL не поддерживает выбор исходного адреса, как это делает curl.
Таким образом, решением будет добавление большего количества IP-адресов на машину и использование haproxy, например, для ротации исходного адреса:
# Способ избежать проблемы TIME_WAIT (исчерпание портов на нашем исходном
# адресе), ротируя по нашим IP
frontend mysqlfront
mode tcp
bind 127.0.0.1:3306
default_backend mysqlback
# Способ избежать проблемы TIME_WAIT (исчерпание портов на нашем исходном
# адресе), ротируя по нашим IP
backend mysqlback
mode tcp
balance leastconn
server mysql_s1 server.com:3306 source 172.31.7.124
server mysql_s2 server.com:3306 source 172.31.12.99
Ответ или решение
Понимание совместного использования портов источника TCP
Вопрос совместного использования портов источника в TCP — это сложная тема, которая затрагивает различные аспекты работы сетей и сокетов. Давайте рассмотрим основные моменты, чтобы прояснить данную концепцию, и ответить на возникшую у вас ситуацию.
Основы идентификации TCP-соединений
Как вы верно указали, TCP-соединение определяется уникальным четырехкратным значением <src IP, src PORT, dst IP, dst PORT>
. Это означает, что комбинация адресов и портов источника и назначения должна быть уникальной для каждого установления соединения. Следовательно, порт источника не может использоваться одновременно для двух соединений с одним и тем же адресом назначения и портом назначения. Ваша догадка об этом правильна.
Поведение систем при создании соединений
Когда вы используете команду nc
(netcat) с указанием порта (-p
), система пытается привязать сокет к данному порту. Если на этот порт уже есть активное соединение, вторая попытка их использования не удается, и вы получаете сообщение об ошибке "Address already in use". Это происходит потому, что стандартные настройки sock’et`ов не позволяют другому процессу (даже если он работает на другом удаленном адресе) использовать тот же источник порт.
Таким образом, для успешного использования одного и того же порта источника двумя процессами их назначения должны отличаться.
Опции для совместного использования портов
Для решения этой проблемы в Unix-подобных системах существует опция SO_REUSEPORT
. Она позволяет нескольким процессам привязываться к одному и тому же порту, если они соединяются с разными удалёнными адресами. На Windows аналогичная функция достигается с помощью SO_REUSEADDR
. Однако, даже с этими опциями, если оба процесса попытаются использовать один и тот же порт к одному и тому же адресу назначения, возникнет конфликты.
Влияние состояния TIME_WAIT
Состояние TIME_WAIT
также может стать важным фактором в управлении портами. По умолчанию, после закрытия TCP-соединения сокет остается в состоянии TIME_WAIT
на определенный период (обычно 60 секунд). Это сделано для обеспечения надежной передачи данных и предотвращения конфликта старых и новых соединений. Если у вас возникает высокая нагрузка на соединения и открытия/закрытия портов, необходимо обратить внимание на управление состоянием TIME_WAIT
. Вы можете столкнуться с сообщением "cannot assign requested address", если слишком много соединений остается в этом состоянии.
Практическое применение и возможные решения
Для случаев с высокой частотой подключений, как в примере с веб-сервером, вы можете улучшить управление портами, используя такие инструменты, как HAProxy
, которые могут распределять нагрузки и контролировать использование IP-адреса путем ротации адресов источника. Это позволит вам избежать исчерпания доступных портов.
frontend mysqlfront
mode tcp
bind 127.0.0.1:3306
default_backend mysqlback
backend mysqlback
mode tcp
balance leastconn
server mysql_s1 server.com:3306 source 172.31.7.124
server mysql_s2 server.com:3306 source 172.31.12.99
В заключение, важно отметить, что хотя TCP и позволяет выделять один и тот же порт для разных соединений, необходимо учитывать ограничения системы и конфликты на уровне адреса назначения и порта. Эффективное управление состоянием соединений и использование соответствующих опций может помочь оптимизировать работу вашего приложения в многопользовательской среде.