Массовый gen_tcp:send из одного процесса

В книжках и мануалах по эрлангу есть несколько тонких мест, в которых нельзя слушать мануалы, а надо читать исходники. Одно из этих мест — gen_tcp:send. Если вы хотите раздать много данных, то надо пользоваться недокументированными возможностями.

gen_tcp:send устроен следующим образом: сначала вызывается port_command, это от 1 до 2 микросекунд; потом ожидается сообщение {inet_reply,Socket,Reply}

Сообщение inet_reply приходит, когда весь буфер эрланга опустошается. Важно понимать, что есть два буфера: один внутри эрланговского драйвера, второй внутри TCP стека ядра. И опустошение каждого из них не означает, что данные дошли до клиента.

Таким образом gen_tcp:send гарантирует вам что вы опустошили буфер эрланговского драйвера и запихнули всё в драйвер ядра. Это означает, что если вы шлете большие данные медленному клиенту, то ваш процесс будет работать условно со скоростью перетекания данных к клиенту.

Если вы шлете много данных разным клиентам из одного процесса, то вы тем самым лимитируете производительность программы скоростью самых медленных клиентов. По опыту эрливидео это означало где-то 500 клиентов. Но если всё сложится удачно, то одна паршивая овца всё стадо испортит и процесс будет заниматься только одним клиентом.

Для того, что бы избежать этого, надо пользоваться асинхронной записью. Т.е. вызывать port_command и потом постфактум интерпретировать inet_reply. По секрету, это сообщение можно просто сбрасывать и игнорировать.

Однако есть опасность того, что вы забьете выходной буфер драйвера мегабайтами трафика, который клиент не будет вычитывать. Для противодействия этому в port_command есть механизм suspend. Если внутренний буфер переполнен, то драйвер тормозит весь ваш процесс. Это совсем не то, что хочется схлопотать, обслуживая кучу сокетов из одного процесса.

Поэтому надо вызывать port_command(Socket, Data, [nosuspend]). Если он возвращает false, значит писать в этот сокет больше нельзя. Тут надо решить что делать: либо выкидывать этот сокет на какое-то время из обслуживания, либо вообще отключать.

Так же немного неясно, что делать, если сокет закрылся.

Рецепт простой: (catch port_command(Socket, Data, [nosuspend])) либо возвращает true, либо с клиентом что-то плохо. Можно вдаваться в детали, а можно тупо отключить его, потому что он никак не вычитывает данные.

Comments (9)