Как ServicePointManager.ReusePort и SO_REUSE_UNICASTPORT уменьшают исчерпание эфемерного порта?

В Windows 10 и Windows Server 2016 появилась опция сокета SO_REUSE_UNICASTPORT. Он стал доступен для использования в .NET, начиная с версии 4.6, через статическое свойство ServicePointManager.ReusePort. Я страдаю от эфемерного исчерпания портов в приложении .NET во время очень высоких нагрузок (много одновременных исходящих запросов через HttpClient), и я рассматриваю возможность использования этой опции для решения этой проблемы. Я знаю другие способы решения этой проблемы (такие как редактирование реестра Windows для изменения максимального количества эфемерных портов или сокращение TIME_WAIT), но я также хотел бы полностью использовать это решение.

Документация для ServicePointManager.ReusePort очень минимальна:

Установка для этого свойства значения true приводит к тому, что все исходящие TCP-подключения из HttpWebRequest используют собственный параметр сокета SO_REUSE_UNICASTPORT для сокета. Это приводит к совместному использованию базовых исходящих портов. Это полезно для сценариев, когда за короткое время выполняется большое количество исходящих подключений, и у приложения есть риск нехватки портов.

Просмотр документации для SO_REUSE_UNICASTPORT не дает никаких дополнительные сведения:

Если установлено, разрешить повторное использование эфемерного порта для функций подключения Winsock API, которые требуют явного связывания, например ConnectEx. Обратите внимание, что для функций подключения с неявной привязкой (таких как подключение без явной привязки) этот параметр установлен по умолчанию. Используйте этот параметр вместо SO_PORT_SCALABILITY на платформах, где доступны оба варианта.

Я не смог найти в Интернете никаких объяснений того, как именно достигается это «повторное использование эфемерных портов», как именно оно работает на техническом уровне и насколько хорошо оно снижает риск исчерпания эфемерных портов. Какого улучшения я могу ожидать? Как с помощью этой функции рассчитать новый лимит для моего приложения? Есть ли какие-либо недостатки в том, чтобы включить это?

Все это окутано тайной, и я был бы рад, если бы кто-нибудь объяснил этот новый механизм и его последствия.


person Allon Guralnek    schedule 14.06.2017    source источник
comment
Можете ли вы поделиться, какие журналы или счетчики вы смотрели на то, что предполагаемое исчерпание портов происходило в вашем приложении?   -  person Anirudh Goel    schedule 09.08.2018
comment
@AnirudhGoel: мы получили ужасное исключение System.Net.Sockets.SocketException: Only one usage of each socket address (protocol/network address/port) is normally permitted, и запуск nestat -a -o на машине с этим исключением показал, что используются многочисленные эфемерные порты.   -  person Allon Guralnek    schedule 09.08.2018


Ответы (1)


TCP-соединение однозначно идентифицируется (локальный IP-адрес, локальный порт, удаленный IP-адрес, удаленный порт). Это означает, что вполне возможно использовать одну и ту же пару (локальный IP-адрес, локальный порт) для нескольких сокетов, подключающихся к разным удаленным конечным точкам. Предположим, вы хотите сделать http-запрос к «site1.com» и «site2.com». Вы используете сокеты со следующим кодом:

using (var socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp)) {                
    socket.Bind(new IPEndPoint(IPAddress.Parse("some local ip"), 45455));
    socket.Connect(server, port);
    socket.Send(someBytes);
    // ...
}

Таким образом, вы привязываете сокет к определенной локальной конечной точке с портом 45455. Если вы сейчас попытаетесь сделать это одновременно, чтобы сделать запросы к «site1.com» и «site2.com», вы получите исключение «адрес уже используется».

Но если вы добавите опцию ReuseAddress (обратите внимание, что это не вариант вашего вопроса) перед привязкой:

socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);

Вы сможете привязываться к сокетам к одному и тому же локальному (ip, порт) и увидите в netstat два УСТАНОВЛЕННЫХ соединения.

Все вышеизложенное должно показать, что теоретически ничто не мешает повторно использовать даже эфемерный порт для создания нескольких подключений к разным удаленным конечным точкам. Но, когда вы привязываетесь к эфемерному порту (0) — еще неизвестно, к какой удаленной конечной точке вы собираетесь подключаться. Предположим, что все эфемерные порты используются, и вы привязываетесь к 0. ОС предоставляет вам некоторый порт для повторного использования на этапе привязки, есть один сокет, использующий этот порт, уже подключенный к «site1.com». Вы также пытаетесь подключиться к «site1.com», и это не удается (поскольку все 4 значения, идентифицирующие соединение tcp, одинаковы для обоих сокетов).

Что делает SO_REUSE_UNICASTPORT, так это откладывает выбор эфемерного порта, когда вы привязываетесь к 0, до фактической фазы соединения (например, вызов Connect()). На этом этапе (в отличие от привязки) вы уже знаете локальный ip, удаленный ip, удаленный порт, к которому вы собираетесь подключиться, и вам нужно выбрать эфемерный порт. Предположим, что все порты заняты. Теперь вместо того, чтобы выбирать какой-то случайный порт для повторного использования (что может привести к сбою позже при подключении), вы можете выбрать порт, который подключен к другой удаленной конечной точке (отличной от той, к которой пытается подключиться текущий сокет).

Вы можете подтвердить это, например, на этом Статья службы поддержки MS:

SO_REUSE_UNICASTPORT

Чтобы сценарий подключения был реализован, параметр сокета должен быть установлен до привязки сокета. Этот параметр указывает системе отложить выделение портов до времени соединения, когда будет известен 4-кортеж (четверка) для соединения.

Обратите внимание, что SO_REUSE_UNICASTPORT влияет только на явные привязки (как указано в цитате вашего вопроса, но все же стоит повторить). Если вы привязываетесь неявно (например, когда вы просто Connect() без привязки) - этот параметр уже установлен по умолчанию (конечно, там, где он поддерживается).

О том, какое влияние это оказывает на ваше конкретное приложение. Во-первых, из вышеизложенного должно быть ясно, что если ваше приложение делает множество запросов к одной и той же удаленной конечной точке (например, к одному и тому же http-серверу) - эта опция не будет иметь никакого эффекта. Однако, если вы делаете много запросов к разным конечным точкам, это должно помочь предотвратить исчерпание портов. Я думаю, что эффект самого ServicePointManager.ReusePort зависит от того, как HttpClient работает с сокетами внутри. Если это просто Connect() без явной привязки - эта опция должна быть включена (в поддерживаемых системах) по умолчанию, поэтому установка ServicePointManager.ReusePort на true не будет иметь дополнительного эффекта, в противном случае он будет. Поскольку вы не знаете (и не должны полагаться) на его внутреннюю реализацию, стоит включить ServicePointManager.ReusePort в вашем конкретном сценарии.

Вы также можете выполнить тесты с этой опцией вкл\выкл, ограничив диапазон эфемерных портов (командой вроде netsh int ipv4 set dynamicport tcp) небольшими количествами и посмотреть, как пойдет.

person Evk    schedule 29.10.2017
comment
Это отличный ответ, особенно объясняющий различные эффекты между вызовами на один и тот же набор серверов и на множество разных серверов. В нашем случае проблема заключалась в том, что наше приложение .NET действовало как шлюз API, который иногда перенаправлял вызов в другую систему (как есть). Когда у другой системы были периоды замедления, количество одновременных вызовов к тому же набору серверов росло, потому что время отклика увеличивалось, а входящий трафик оставался постоянным, что в конечном итоге приводило к исчерпанию портов. Так что похоже, что в данном случае ServicePointManager.ReusePort нам не поможет. - person Allon Guralnek; 30.10.2017
comment
@AllonGuralnek Да, похоже. В этом случае я бы предложил добавить в вашу систему больше IP-адресов (интерфейсов), потому что тогда у вас будет (количество ips * диапазон динамических портов) эфемерные порты для использования (в этом случае также взгляните на SO_PORT_SCALABILITY, хотя как Я понимаю, что SO_REUSE_UNICASTPORT уже подразумевает SO_PORT_SCALABILITY). - person Evk; 30.10.2017
comment
Хорошее предложение. В дополнение к добавлению IP-адресов к исходным компьютерам добавление дополнительных IP-адресов для целевой системы также может помочь при использовании с SO_REUSE_UNICASTPORT, поскольку будет больше IP-адресов назначения, а значит, больше возможностей повторного использования для каждого подключения. Кстати, я не нашел способа использовать SO_PORT_SCALABILITY в .NET, особенно с высокоуровневыми сетевыми API, такими как HttpClient (или даже HttpWebRequest). Вы знаете способ? - person Allon Guralnek; 30.10.2017
comment
@AllonGuralnek Нет, я не знаю, как (и если вообще возможно) добраться до базового сокета из HttpClient или HttpWebRequest. Но в документации говорится, что по возможности используйте SO_REUSE_UNICASTPORT вместо SO_PORT_SCALABILITY, что предполагает, что первое уже подразумевает второе, поэтому вы можете просто использовать ServicePointManager.ReusePort для достижения этого. - person Evk; 30.10.2017
comment
@AllonGuralnek, но если у вас есть сокет в руке, я думаю, вы можете установить этот параметр следующим образом: socket.SetSocketOption(SocketOptionLevel.Socket, (SocketOptionName) 0x3006, true); SocketOptionName не содержит предопределенного значения для этого, но в конце концов enum - это просто число в .NET, поэтому вы можете использовать любое число, даже то, что не имеет предопределенного значения в самом перечислении. Код, использующий перечисление, может иметь проверку с помощью Enum.IsDefined, чтобы проверить, действительно ли число имеет соответствующее значение перечисления, но это не относится к параметрам сокета — оно передается как есть в собственный API. - person Evk; 30.10.2017
comment
К сожалению, использование сокетов для нас не вариант, так как нам необходимо повторное использование и конвейерная обработка HTTP-соединений в масштабе всего приложения, что обеспечивается ServicePointManager, только при использовании HTTP API высокого уровня, а не сокетов. Я не нашел способа указать базовым сокетам этих API использовать определенные параметры. - person Allon Guralnek; 30.10.2017