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, и я хочу хорошо понять это, прежде чем что-либо делать, поэтому, пожалуйста, мистер Хосе, если я в чем-то ошибаюсь, просто скажите мне «Спасибо».