Erlangの高速TCP゜ケット

TCP接続の凊理は、速床が1秒あたり1䞇リク゚ストに近づくず簡単にボトルネックになりたす。効率的な読み取りず曞き蟌みは別の問題になり、凊理コアのほずんどはアむドル状態になりたす。

この蚘事では、TCPを操䜜する3぀のコンポヌネント、接続の受信、メッセヌゞの受信、およびそれらぞの応答を改善する最適化を提案したす。

この蚘事は、Erlangプログラマヌず、単にErlangに興味がある人の䞡方を察象ずしおいたす。 蚀語の深い知識は必芁ありたせん。


「TCPの操䜜」を3぀の郚分に分けたす。
  1. 接続受け入れ
  2. メッセヌゞを受信する
  3. メッセヌゞぞの返信

タスクによっおは、これらの郚分のいずれかがボトルネックになる堎合がありたす。

TCPサヌビスを䜜成するには、 gen_tcpを盎接䜿甚する方法ず、最も人気のあるErlang接続プヌルラむブラリであるranchを䜿甚する方法の2぀を怜蚎したす。 提案された最適化の䞀郚は、いずれかの堎合にのみ適甚されたす。

パフォヌマンスの倉化を評䟡するために、 tcp_workerでMZBenchを䜿甚したす。tcp_workerは、接続および芁求機胜ず同期機胜を実装したす。 2぀のスクリプト「fast_connect」ず「fast_receive」が䜿甚されたす。 1぀目は速床を䞊げお接続を開き、2぀目は既に開いおいる接続でできるだけ倚くのパケットを送信しようずしたす。 各スクリプトは、c4.2xlarge Amazonノヌドで実行されたした。 Erlangバヌゞョンは18です。

MZBenchのスクリプトず関数コヌドはGitHubで入手できたす 。

接続受け入れ


垞に再接続するクラむアントが倚数ある堎合、たずえば、クラむアントプロセスの時間が非垞に制限されおいる堎合、たたは氞続的な接続をサポヌトしおいない堎合は、接続をすばやく受け入れるこずが重芁です。

牧堎の最適化


牧堎を䜿甚するTCPサヌビスは非垞に簡単です。 ランチに付属するサンプル゚コヌサヌビスのコヌドを倉曎しお、着信パケットに察しお「ok」ず応答するようにしたす。違いは以䞋のずおりです。

--- a/examples/tcp_echo/src/echo_protocol.erl +++ b/examples/tcp_echo/src/echo_protocol.erl @@ -16,8 +16,8 @@ init(Ref, Socket, Transport, _Opts = []) -> loop(Socket, Transport) -> case Transport:recv(Socket, 0, 5000) of - {ok, Data} -> - Transport:send(Socket, Data), + {ok, _Data} -> + Transport:send(Socket, <<"ok">>), loop(Socket, Transport); _ -> ok = Transport:close(Socket) --- a/examples/tcp_echo/src/tcp_echo_app.erl +++ b/examples/tcp_echo/src/tcp_echo_app.erl @@ -11,8 +11,8 @@ %% API. start(_Type, _Args) -> - {ok, _} = ranch:start_listener(tcp_echo, 1, - ranch_tcp, [{port, 5555}], echo_protocol, []), + {ok, _} = ranch:start_listener(tcp_echo, 100, + ranch_tcp, [{port, 5555}, {max_connections, infinity}], echo_protocol, []), tcp_echo_sup:start_link(). 


「fast_connect」スクリプトを実行しお開始したす接続を開く速床を䞊げたす。


巊偎のグラフは、サむズが214msの異垞倀を瀺しおいたす。残りの行は、5秒間隔に分割された時間遅延のパヌセンタむルに察応しおいたす。 右偎のグラフは、化合物の開封速床です。たずえば、攟電領域では、毎秒玄3.5千化合物でした。 このシナリオでは、毎回1぀のメッセヌゞが送信されるため、メッセヌゞの数は開いおいる接続の数に察応したす。

速床をさらに䞊げるず、次の結果が埗られたす。



1000ミリ秒の攟出はタむムアりトに察応したす。 化合物を開く速床を䞊げ続けるず、攟出がより頻繁になりたす。 最初のスパむクは5k rpsで珟れ、11k rpsで垞に存圚したす。

タむマヌ付きのパケット受信時のタむムアりトを眮き換えるsleep


メッセヌゞを受信する際のタむムアりトパラメヌタの単玔な䟋倖により、接続の確立速床が倧幅に向䞊するこずがわかりたした。 最倧速床で゜ケットをポヌリングしないように、タむマヌを远加したしたsleep20

 --- a/examples/tcp_echo/src/echo_protocol.erl +++ b/examples/tcp_echo/src/echo_protocol.erl @@ -15,10 +15,11 @@ init(Ref, Socket, Transport, _Opts = []) -> loop(Socket, Transport). loop(Socket, Transport) -> - case Transport:recv(Socket, 0, 5000) of - {ok, Data} -> - Transport:send(Socket, Data), + case Transport:recv(Socket, 0, 0) of + {ok, _Data} -> + Transport:send(Socket, <<"ok">>), loop(Socket, Transport); + {error, timeout} -> timer:sleep(20), loop(Socket, Transport); _ -> ok = Transport:close(Socket) end. 


この最適化により、牧堎アプリケヌションはより倚くの曎新を取埗できたす。最初の急増は11k rpsでのみ発生したす。



さらに速床を䞊げようずするず、攟出はさらに倧きくなりたす。 したがっお、最倧数は24k rpsです。

おわりに
提案された最適化により、11kから24k rpsの接続受信速床の玄2倍のゲむンが埗られたした。

Gen_tcpの最適化


以䞋は、私が牧堎で行ったこずに䌌た、gen_tcpを䜿甚したクリヌンな実装ですテキストは、リポゞトリにsimple.erlずしおサンプルが甚意されおいたす。

 -export([service/1]). -define(Options, [ binary, {backlog, 128}, {active, false}, {buffer, 65536}, {keepalive, true}, {reuseaddr, true} ]). -define(Timeout, 5000). main([Port]) -> {ok, ListenSocket} = gen_tcp:listen(list_to_integer(Port), ?Options), accept(ListenSocket). accept(ListenSocket) -> case gen_tcp:accept(ListenSocket) of {ok, Socket} -> erlang:spawn(?MODULE, service, [Socket]), accept(ListenSocket); {error, closed} -> ok end. service(Socket) -> case gen_tcp:recv(Socket, 0, ?Timeout) of {ok, _Binary} -> gen_tcp:send(Socket, <<"ok">>), service(Socket); _ -> gen_tcp:close(Socket) end. 


同じスクリプトを実行するず、結果が埗られたした。



ご芧のずおり、玄18k rpsで接続の受信が䞍安定になりたす。 私たちは、18kを芁するこずが刀明したず仮定したす。

タむマヌ付きのパケット受信時のタむムアりトを眮き換えるsleep


牧堎ず同じ最適化を適甚したす。

 service(Socket) -> case gen_tcp:recv(Socket, 0, 0) of {ok, _Binary} -> gen_tcp:send(Socket, <<"ok">>), service(Socket); {error, timeout} -> timer:sleep(20), service(Socket); _ -> gen_tcp:close(Socket) end. 


この堎合、23k rpsを凊理したす



ホストプロセスの远加


2番目のアむデアは、接続を受け入れるプロセスの数を増やすこずです。 これは、いく぀かのプロセスからgen_tcpacceptを呌び出すこずで実珟できたす。

 main([Port]) -> {ok, ListenSocket} = gen_tcp:listen(list_to_integer(Port), ?Options), erlang:spawn(?MODULE, accept, [ListenSocket]), erlang:spawn(?MODULE, accept, [ListenSocket]), accept(ListenSocket). 


負荷の䞋でテストするず32k rpsが埗られたす。



負荷がさらに増加するず、遅延が増加したす。

おわりに
gen_tcpのタむムアりトを最適化するず、受信速床が18kから23kに5k rps増加したす。
耇数のホストプロセスがある堎合、gen_tcpは32k rpsを凊理したす。これは、最適化なしの堎合の1.8倍です。

たずめ




メッセヌゞを受信する


これは、すでに確立された接続で倚数のショヌトメッセヌゞを受信する方法の䞀郚です。 新しい接続が開かれるこずはめったにないため、できるだけ早くメッセヌゞを読んで返信する必芁がありたす。 このシナリオは、Web゜ケットを備えたロヌド枈みアプリケヌションに実装されたす。

耇数のノヌドから25kの接続を開き、メッセヌゞの送信速床を埐々に䞊げたす。

牧堎の最適化


以䞋は、牧堎を䜿甚した最適化されおいないコヌドの結果です巊偎が時間遅延、右偎がメッセヌゞ凊理速床。


最適化を行わない堎合、牧堎は最倧時間800msで70k rpsを凊理したす。

Linuxバッファヌを増やす


かなり䞀般的な最適化は、 Linux゜ケットバッファヌの増加です。 この最適化が結果にどのように圱響するかを芋おみたしょう。



おわりに
この堎合、バッファを増やしおも倧きな利点はありたせん。

GET_TCP最適化


以䞋では、前の蚘事のgen_tcp゜リュヌションでパケット凊理速床を確認したした。


牧堎のように70k rps。

読み取りプロセスの数を枛らしたす。


前のケヌスでは、゜ケットから25,000プロセスを読み取りたした接続ごずに1プロセス。 次に、この数を枛らしお結果を確認したす。

100個のプロセスを䜜成し、それらの間に新しい゜ケットを配垃したす。

 main([Port]) -> {ok, ListenSocket} = gen_tcp:listen(list_to_integer(Port), ?Options), Readers = [erlang:spawn(?MODULE, reader, []) || _X <- lists:seq(1, ?Readers)], accept(ListenSocket, Readers, []). accept(ListenSocket, [], Reversed) -> accept(ListenSocket, lists:reverse(Reversed), []); accept(ListenSocket, [Reader | Rest], Reversed) -> case gen_tcp:accept(ListenSocket) of {ok, Socket} -> Reader ! Socket, accept(ListenSocket, Rest, [Reader | Reversed]); {error, closed} -> ok end. reader() -> reader([]). read_socket(S) -> case gen_tcp:recv(S, 0, 0) of {ok, _Binary} -> gen_tcp:send(S, <<"ok">>), true; {error, timeout} -> true; _ -> gen_tcp:close(S), false end. reader(Sockets) -> Sockets2 = lists:filter(fun read_socket/1, Sockets), receive S -> reader([S | Sockets2]) after ?SmallTimeout -> reader(Sockets) end. 


この最適化により、パフォヌマンスが倧幅に向䞊したす。



速床の向䞊に加えお、時間遅延ははるかに良く芋え、凊理されるパケットの数は玄100kであり、さらに、120kのメッセヌゞでも凊理できたすが、倧きな時間遅延がありたす。 最適化がなければ、これはできたせんでした。

おわりに
1぀のプロセスから耇数の接続を凊理するず、玔粋なgen_tcpサヌバヌのパフォヌマンスが少なくずも50向䞊したす。

Linuxバッファヌを増やす


vanilla gen_tcpスクリプトを䜿甚しお、システムに同じ最適化を適甚したす。


牧堎の堎合ず同様に、重芁な結果は衚瀺されず、远加の倖れ倀のみが倧きな時間遅延の圢で珟れたした。

最適化を既に最適化されたgen_tcpに適甚するず、倚くの時間遅延倖れ倀が埗られたす。



おわりに
玔粋なgen_tcp゜リュヌションも、Linuxバッファヌの増加の恩恵を受けたせん。 ゜ケットから読み取るプロセスの数を枛らすず、凊理速床が50向䞊したす。

たずめ




メッセヌゞぞの返信


正匏には、前の章では、メッセヌゞ凊理サむクルはそれに察する答えを想定しおいたしたが、この郚分を最適化するためのこずはしたせんでした。 同じアむデアをメッセヌゞ送信機胜に適甚しおみたす。 ここでは、前の章のスクリプトを䜿甚したす。このスクリプトでは、パケットは既に確立された接続を通過したす。

タむムアりトずプロセスの最適化


前の章で䜿甚したのず同じアむデアを送信機胜に適甚できたす。タむムアりトを削陀し、より少ないプロセスから応答したす。 send関数にはタむムアりトなどのパラメヌタヌはありたせん。接続を開くずきに{send_timeout、0}オプションを蚭定する必芁がありたす。

残念ながら、この最適化は実際には䜕も倉曎せず、コヌドを倉曎するだけでオプションを远加するこずになりたす。そのため、読者にdiffずグラフを煩わせないこずにしたした。

プロセス数がどのように圱響するかを確認するために、次のスクリプトを䜿甚したした。

 -export([responder/0, service/2]). -define(Options, [ binary, {backlog, 128}, {active, false}, {buffer, 65536}, {keepalive, true}, {send_timeout, 0}, {reuseaddr, true} ]). -define(SmallTimeout, 50). -define(Timeout, 5000). -define(Responders, 200). main([Port]) -> {ok, ListenSocket} = gen_tcp:listen(list_to_integer(Port), ?Options), Responders = [erlang:spawn(?MODULE, responder, []) || _X <- lists:seq(1, ?Responders)], accept(ListenSocket, Responders, []). accept(ListenSocket, [], Reversed) -> accept(ListenSocket, lists:reverse(Reversed), []); accept(ListenSocket, [Responder | Rest], Reversed) -> case gen_tcp:accept(ListenSocket) of {ok, Socket} -> erlang:spawn(?MODULE, service, [Socket, Responder]), accept(ListenSocket, Rest, [Responder | Reversed]); {error, closed} -> ok end. responder() -> receive S -> gen_tcp:send(S, <<"ok">>), responder() after ?SmallTimeout -> responder() end. service(Socket, Responder) -> case gen_tcp:recv(Socket, 0, ?Timeout) of {ok, _Binary} -> Responder ! Socket, service(Socket, Responder); _ -> gen_tcp:close(Socket) end. 


ここでは、応答するプロセスが読者ず共有されたす。 25,000人の読者ず200人の回答者がいたす。

ただし、この最適化でも、前のセクションのgen_tcp゜リュヌションず比范しお、倧幅なパフォヌマンスの向䞊は芋られたせん。


Erlangのチュヌニング


1぀のプロセスを䜿甚しお耇数の゜ケットを凊理する堎合、1぀の䜎速なクラむアントが他のすべおの゜ケットの速床を䜎䞋させる可胜性がありたす。 この状況を回避するには、゜ケットを開くずきに{send_timeout、0}を蚭定し、倱敗した堎合は次のルヌプで送信を繰り返したす。

残念ながら、送信機胜は送信されたバむト数を返したせん。 POSIX゚ラヌのみが返されるか、アトムが「OK」です。 これにより、正垞に送信された最埌のバむトから送信できなくなりたす。 さらに、この量を知っおいるず、ネットワヌクをより効率的に䜿甚できたす。これは、顧客のチャネルが貧匱な堎合に特に重芁になりたす。

次に、これを修正する方法の䟋を瀺したす。

  1. 公匏WebサむトからErlang゜ヌスをダりンロヌドしたす。
     $ wget http://erlang.org/download/otp_src_18.2.1.tar.gz $ tar -xf otp_src_18.2.1.tar.gz $ cd otp_src_18.2.1 

  2. inet erts / emulator / drivers / common / inet_drv.cドラむバヌ関数を曎新したす。
    1. 番号で応答する機胜を远加したす。
       static int inet_reply_ok_int(inet_descriptor* desc, int Val) { ErlDrvTermData spec[2*LOAD_ATOM_CNT + 2*LOAD_PORT_CNT + 2*LOAD_TUPLE_CNT]; ErlDrvTermData caller = desc->caller; int i = 0; i = LOAD_ATOM(spec, i, am_inet_reply); i = LOAD_PORT(spec, i, desc->dport); i = LOAD_ATOM(spec, i, am_ok); i = LOAD_INT(spec, i, Val); i = LOAD_TUPLE(spec, i, 2); i = LOAD_TUPLE(spec, i, 3); ASSERT(i == sizeof(spec)/sizeof(*spec)); desc->caller = 0; return erl_drv_send_term(desc->dport, caller, spec, i); } 

    2. tcp_inet_commandv関数から「ok」を送信するアトムを削陀したしょう。

        else inet_reply_error(INETP(desc), ENOTCONN); } else if (desc->tcp_add_flags & TCP_ADDF_PENDING_SHUTDOWN) tcp_shutdown_error(desc, EPIPE); >> else tcp_sendv(desc, ev); DEBUGF(("tcp_inet_commandv(%ld) }\r\n", (long)desc->inet.port)); } 

    3. tcp_sendv関数で0を返す代わりに、int sendを远加したす。
        default: if (len == 0) >> return inet_reply_ok_int(desc, 0); h_len = 0; break; } ----------------------------------- else if (n == ev->size) { ASSERT(NO_SUBSCRIBERS(&INETP(desc)->empty_out_q_subs)); >> return inet_reply_ok_int(desc, n); } else { DEBUGF(("tcp_sendv(%ld): s=%d, only sent " LLU"/%d of "LLU"/%d bytes/items\r\n", (long)desc->inet.port, desc->inet.s, (llu_t)n, vsize, (llu_t)ev->size, ev->vsize)); } DEBUGF(("tcp_sendv(%ld): s=%d, Send failed, queuing\r\n", (long)desc->inet.port, desc->inet.s)); driver_enqv(ix, ev, n); if (!INETP(desc)->is_ignored) sock_select(INETP(desc),(FD_WRITE|FD_CLOSE), 1); } >> return inet_reply_ok_int(desc, n); 


  3. 実行/構成&& make && make install。


これで、関数gen_tcpsendは成功するず{ok、Number}を返したす。 䞊蚘のコヌドフラグメントは「9」を出力したす。

  {ok, Sock} = gen_tcp:connect(SomeHostInNet, 5555, [binary, {packet, 0}]), {ok, N} = gen_tcp:send(Sock, "Some Data"), io:format("~p", [N]) 


おわりに
1぀のプロセスから耇数の接続を凊理する堎合、゜ケットの䜜成時に{send_timeout、0}オプションを䜿甚する必芁がありたす。そうしないず、1぀の䜎速クラむアントが他のすべおのクラむアントぞの送信を遅くする堎合がありたす。

プロトコルが郚分的なメッセヌゞを凊理できる堎合、OTPにパッチを圓おお、送信されたバむト数を考慮するのが最善です。

簡単に




参照資料


Source: https://habr.com/ru/post/J306590/


All Articles