Erlang中頻繁發送遠程消息要注意的問題

注:這篇文章可能會有爭議,歡迎提出意見

在Erlang中,如果要實現兩個遠程節點之間的通信,就需要通過網絡來實現,對於消息發送,是使用TCP。如果要在兩個節點間頻繁發送消息,比如每秒幾百上千條,那樣就要注意了。

無論是網遊服務器開發的書籍,或是經驗老道的工程師,都會告訴你,在發送數據包時,儘可能把小的消息組合爲一個比較大的包來發送,畢竟一個TCP包的頭也很大,首先是浪費帶寬,其次調用底層發送的指令也是有開銷的。有工程師告訴我,一般每秒大概是2W次左右。

簡單測試一下,先是代碼

一個接收消息並馬上拋棄的Server:
[code]start() ->
register(nullserver, self()),
loop().

loop() ->
receive
Any ->
loop() %drop message and loop
end.[/code]

一個在循環中向它發送消息的Client:
[code]start() ->
start_send(100).

start_send(0) ->
ok;
start_send(N) ->
{nullserver, '[email protected]'} ! hi,
start_send(N-1).[/code]

然後打開截包工具,運行server和client,截取到接近200個包的發送和接收記錄,其中,大部分是這樣的數據:

[quote]00 14 78 B9 14 BC 00 11-11 9F 91 1A 08 00 45 00
00 45 EE 77 40 00 80 06-80 E4 C0 A8 00 CC DB E8
ED F9 13 58 C1 C6 AA 4E-59 F2 38 CF 22 2D 50 18
FF 19 B9 EE 00 00 00 00-00 19 70 83 68 04 61 06
67 43 CC 00 00 00 01 00-00 00 00 02 43 05 43 BD
83 43 BF [/quote]

[quote]00 14 78 B9 14 BC 00 11-11 9F 91 1A 08 00 45 00
00 45 EE 78 40 00 80 06-80 E3 C0 A8 00 CC DB E8
ED F9 13 58 C1 C6 AA 4E-5A 0F 38 CF 22 2D 50 18
FF 19 B9 D1 00 00 00 00-00 19 70 83 68 04 61 06
67 43 CC 00 00 00 01 00-00 00 00 02 43 05 43 BD
83 43 BF [/quote]


實際上,只有從 00 00-00 19 這裏開始,纔是TCP包的內容,前面都是底層協議的數據,就是這樣的數據包發送了100次,浪費是巨大的。而且,在消息發送後,還收到同樣數目類似

[quote]00 11 11 9F 91 1A 00 14-78 B9 14 BC 08 00 45 00
00 28 8C FC 40 00 32 06-30 7D DB E8 ED F9 C0 A8
00 CC C1 C6 13 58 38 CF-22 2D AA 4E 59 F2 50 10
19 20 D7 01 00 00 00 00-00 00 00 00 [/quote]

這樣的響應包,也浪費着帶寬。


從目前我所閱讀過的文檔來看,暫時沒有有關如何緩存這些消息定期一併發送的參數設置。那麼有什麼解決辦法,我自己有兩種。

一種是將要發送的一批Message打包到一個list發送,接收方從list中取出所有message並處理。

另一種是通過一個Proxy,發送方不通過 {Name, Node} ! Message 這種方式來發送,而是通過一個本地的Proxy Process,代理會將所有發送到某個節點的消息累積起來,定時批量發送過去;接收方也有一個Listening Process,它接收批量的Message,遍歷後發送給本地的相應進程。

這裏是我初步寫出來的實現,不太漂亮,僅供參考~

message_agent.erl: 實現消息的批量發送,接收和轉發

[code]-module(message_agent).
-export([listen/0, proxy/2, block_exit/1]).
-export([loop_receive/0]).
-define(MAX_BATCH_MESSAGE_SIZE, 50).

listen() ->
io:format("Message agent server start listen~n"),
spawn(fun() -> register('MsgServerAgent', self()), loop_receive() end),
ok.

loop_receive() ->
receive
{forward_message, PName, Messages} ->
forward_messages(PName, Messages),
loop_receive();
Any ->
message_agent:loop_receive()
end.

forward_messages(PName, []) ->
ok;
forward_messages(PName, [H|T]) ->
%io:format("Forward message ~w to process ~w~n", [H, PName]),
catch PName ! H,
forward_messages(PName, T).


proxy(Node, PName) ->
spawn_link(fun() -> handle_message_forward(Node, PName, []) end).

block_exit(Agent) ->
Agent ! {block_wait, self()},
receive
{unblock} ->
ok
end.

handle_message_forward(Node, PName, Messages) ->
receive
{block_wait, Pid} ->
catch send_batch(Node, PName, lists:reverse(Messages)),
Pid ! {unblock};
Any ->
NewMessages = [Any|Messages],
case length(NewMessages)>=?MAX_BATCH_MESSAGE_SIZE of
true ->
send_batch(Node, PName, lists:reverse(NewMessages)),
handle_message_forward(Node, PName, []);
false ->
handle_message_forward(Node, PName, NewMessages)
end
after
0 ->
case length(Messages)>0 of
true ->
catch send_batch(Node, PName, lists:reverse(Messages));
false ->
ok
end,
handle_message_forward(Node, PName, [])
end.

send_batch(Node, PName, Messages) ->
%io:format("Send batch message, size ~p~n", [length(Messages)]),
{'MsgServerAgent', Node} ! {forward_message, PName, Messages}.

[/code]


使用方式很簡單,在接收Message的一端調用 message_agent:listen() 啓動監聽代理,客戶端使用 register(agent, message_agent:proxy(?NODE, 'MsgServer')) 的方式啓動代理進程,消息發送給這個代理進程就可以了。下面是我寫的簡單例子:

[code]-module(message_server).
-export([start/0]).
-define(TIMEOUT_MS, 1000).

start() ->
io:format("Message server start~n"),
register('MsgServer', self()),
message_agent:listen(),
loop_receive(0).

loop_receive(Count) ->
receive
Any ->
%io:format("Receive msg ~w~n", [Any]),
loop_receive(Count+1)
after
?TIMEOUT_MS ->
if
Count>0 ->
io:format("Previous receive msg count: ~p~n", [Count]),
loop_receive(0);
true ->
loop_receive(0)
end
end.
[/code]

[code]-module(message_client).
-define(NODE, '[email protected]').
-define(COUNT, 20000).
-export([start/0]).

start() ->
statistics(wall_clock),
register(agent, message_agent:proxy(?NODE, 'MsgServer')),
send_loop(?COUNT).

send_loop(0) ->
message_agent:block_exit(agent),
{_, Interval} = statistics(wall_clock),
io:format("Finished ~p sends in ~p ms, exiting...~n", [?COUNT, Interval]);
send_loop(Count) ->
agent ! {self(), lalala},
send_loop(Count-1).[/code]

這裏要注意的是,消息發送端和接收端都是由一個單獨的進程來處理消息。在Erlang的默認堆實現,是私有堆,本地進程間的消息發送是需要拷貝的,在數據量大的時候,該進程堆的垃圾回收會相當頻繁。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章