Erlang gen_tcp accept vs OS-Thread accept

У меня есть две модели прослушивающих сокетов и приемников в Erlang:

------------ПЕРВЫЙ------------

      -module(listeners).
....

start() ->
{ok, Listen}=gen_tcp:listen(....),
accept(Listen).

%%%%%%%%%%%%%%%%%%%%%

accept(Listen) ->
{ok, Socket}=gen_tcp:accept(Listen),
spawn(fun() ->handle(Socket) end),
accept(Listen).

%%%%%%%%%%%%%%%%%%%%%

handle(Socket) ->
.... 

---------ВТОРОЙ----------

      -module(listener).
....

start() ->
supervisor:start_link({local,?MODULE},?MODULE, []). 

%%%%%%%%%%%%%

init([]) ->
{ok, Listen}=gen_tcp:listen(....),
spawn(fun() ->free_acceptors(5) end), 
{ok, {{simple_one_for_one, 5,1},[{child,{?MODULE,accept,[Listen]},....}]}.

%%%%%%%%%%%%%

free_acceptors(N) ->
[supervisor:start_child(?MODULE, []) || _ <-lists:seq(1,N)],
ok.

%%%%%%%%%%%%%

accept(Listen) ->
{ok, Socket}=gen_tcp:accept(Listen). 
handle(Socket). 

%%%%%%%%%%%%%%

handle(Socket) ->
.... 

Первый код прост: основной процесс создает сокет прослушивания и прослушивает, чтобы принимать новые соединения, когда пришло соединение, он принимает соединение, порождает новый процесс для его обработки и возвращается, чтобы принимать другие новые соединения.

Второй код также прост: основной процесс создает дерево супервизора, супервизор создает прослушивающий сокет и запускает 5 дочерних процессов (порождая новый процесс для запуска). free_acceptors/1потому что эта функция вызывает процесс супервизора, а супервизор находится в своей функции инициализации и не может запускать дочерние элементы до своего собственного запуска, поэтому новый процесс будет ждать супервизора, пока он не завершит его инициацию) и передать сокет прослушивания в качестве аргумента для него childs, и пятеро детей начинают прислушиваться, чтобы принимать новые входящие связи в ТО ЖЕ ВРЕМЯ.

Таким образом, мы запускаем два кода каждый на отдельной машине, у которой есть ЦП с одним ядром, и 5 клиентов пытаются одновременно подключиться к первому серверу, а другие 5 - ко второму серверу: с первого взгляда я подумал, что второй сервер работает быстрее, потому что все соединения будут приниматься параллельно и в одно и то же время, а в первом коде пятый клиент будет ждать, пока сервер примет четыре прецедента, чтобы принять его, и так далее. но если углубиться в ERTS, у нас есть один OS-Thread на каждое ядро ​​для обработки процессов Erlang, а поскольку Socket - это структура ОС, то gen_tcp:listen позвоню OS-Thread:listen (это просто псевдокод для понимания) для создания сокета ОС и gen_tcp:accept звонки OS-Thread:acceptчтобы принять новое соединение, и это позже может принимать только одно соединение за раз, а пятый клиент все еще ждет, чтобы сервер принял четвертый прецедент, так есть ли разница между двумя кодами? я надеюсь, что вы меня понимаете.

ПРИМЕЧАНИЕ: Ejabberd использует первую реализацию, а Cowboy - вторую.

2 ответа

На уровне ОС прослушивающий сокет связан с очередью потоков ОС, ожидающих приема соединений, независимо от того, заблокирован ли в этой очереди какой-либо поток ОС или пуст, потому что он будет обрабатываться по-другому (ожидание занятости неблокирующее принятие , выберите, эполл ...).

BEAM не имеет единого потока ОС, даже если вы запускаете его в системе с одним процессором, он имеет разные типы потоков ОС.

Что касается вашего вопроса, я подозреваю, что было бы лучше, если бы несколько акцепторных потоков erlang непрерывно блокировались на gen_tcp:accept вызов, потому что таким образом ERTS знает о коде Erlang, желающем принимать больше соединений ( handle(Socket) во втором примере должен порождаться рабочий или отправлять принятый сокет рабочему и возвращаться, чтобы принимать соединения), в то время как с одним циклом accept-spawn это знание скрыто.

Я недостаточно знаком с кодом, чтобы знать нюансы, но кажется, что код прекрасно обрабатывает несколько приемов, выстраивая их внутреннюю очередь, поэтому было бы немного лучше иметь несколько приемников.
Т.е. в первом примере с единичным запросом есть момент, когда никого нет acceptподключений, в то время как во втором примере вам нужно большее количество одновременных запросов, чтобы это произошло.

Я кончаю из-за множества поисков, так что я думаю, что нашел ответ на этот вопрос, но я хочу просто поправить меня, мистер Хосе, если я в чем-то ошибаюсь:

1-когда мы бежим gen_tcp:listen ERTS открывает порт ERLANG (прослушивающий сокет) для связи со связанным драйвером C, этот драйвер, который работает под ОСНОВНЫМ потоком ОС, открывает НАСТОЯЩИЙ СОКЕТ.

2-когда мы бежим gen_tcp:accept ERTS использует этот порт для вызова драйвера с использованием указанного макроса в качестве аргумента функции erlang:port_control, драйвер MAIN OS-Thread создаст OS-Thread, который будет запускать REAL accept в открытом Socket(Blocking Accept), но это просто мое мнение, я тоже не знаком с функцией C, в любом случае это Ericsson Командная работа.

3- когда клиент отправляет запрос на подключение к этому сокету, OS-Thread принимает соединение и создает новый Socket для связи с этим клиентом, а процесс Erlang создает новый порт и связывает его с этим OS-Thread, чтобы быть драйвер для указанной связи с этим клиентом.

4- когда процесс Erlang отправляет данные через этот новый порт, новый драйвер отправляет эти данные через новый сокет, и то же самое с данными приема.

5. Драйвер MAIN OS-Thread не будет порождать новый OS-Thread на каждом Erlang. Accept и будет поддерживать баланс между OS-Threads и соединениями (опять же, это дизайн Ericsson), и эти Threads будут управлять соединениями с помощью одной из известных функций (select, poll, epoll,.....) и, как правило, это epoll для Linux и Kqueue для систем Bsd и каждый поток ОС будет запускать эту функцию на двух сторонах: на одной стороне для взаимодействия с клиентскими сокетами, а на другой - для взаимодействия с портами Erlang.

это точная работа любого драйвера, он скрывает вещи, и пусть эмулятор ведет себя так, как будто он выполняет работу напрямую.

ответ на первый вопрос заключается в том, что второй код более эффективен, и, как вы мне сказали, когда есть много Erlang-Acceptors, Драйвер знает об этом и порождает много OS-Acceptors, здесь возникает другая проблема: сколько акцепторов я может спавниться за сокет?

конструкция бесплатных приемников предназначена для приема соединений в ПАРАЛЛЕЛЬНОМ режиме, и ясно, что один поток ОС не может принимать два соединения одновременно, поэтому, если количество приемников больше, чем количество ядер, например, если у нас есть 8 ядер и 10 акцепторы и 20 клиентов пришли одновременно, у нас есть 8 принятых подключений параллельно, затем еще 8 параллельно и следующие 4, так что это имеет ту же эффективность, что и мы создаем 8 бесплатных приемников.(я говорю о правильной версии кода, когда у нас есть всегда 8 бесплатных приемников, и когда акцептор принимает соединение, он порождает процесс для обработки этого соединения и возврата, чтобы принять другие соединения)

Сеть - самая важная часть в разработке отказоустойчивых и масштабируемых серверов на Erlang/OTP, и я хочу хорошо понять это, прежде чем что-либо делать, поэтому, пожалуйста, мистер Хосе, если я в чем-то ошибаюсь, просто скажите мне «Спасибо».

Другие вопросы по тегам