Rabbitmq的网络层要点浅析
最近在锋爷的建议下开始读rabbitmq的源码,锋爷说这个项目已经很成熟,并且代码也很有借鉴和学习的意义,在自己写erlang代码之前看看别人是怎么写的,可以少走弯路,避免养成一些不好的习惯,学习一些最佳实践。读了一个星期,这个项目果然非常棒,代码也写的非常清晰易懂,一些细节的处理上非常巧妙,比如我这里想分享的网络层一节。
Rabbitmq是一个MQ系统,也就是消息中间件,它实现了AMQP 0.8规范,简单来说就是一个TCP的广播服务器。AMQP协议,你可以类比JMS,不过JMS仅仅是java领域内的API规范,而AMQP比JMS更进一步,它有自己的wire-level protocol,有一套可编程的协议,中立于语言。简单介绍了Rabbitmq之后,进入正题。' L) q( z5 \ v
Rabbitmq充分利用了Erlang的分布式、高可靠性、并发等特性,首先看它的一个结构图:" k* q1 Q: z7 Q# ~5 J# C9 V+ }
0 G- f9 p5 |8 N+ X8 n
这张图展现了Rabbitmq的主要组件和组件之间的关系,具体到监控树的结构,我画了一张图: g3 L. g( r8 p% p6 L
$ G! ^) x. w9 R1 D& k* P' Y; C) S
# ?% e3 j! _2 \$ S- h3 S) a/ Z! j8 K
顶层是rabbit_sup supervisor,它至少有两个子进程,一个是rabbit_tcp_client_sup,用来监控每个connection的处理进程 rabbit_reader的supervisor;rabbit_tcp_listener_sup是监控tcp_listener和 tcp_acceptor_sup的supervisor,tcp_listener里启动tcp服务器,监听端口,并且通过 tcp_acceptor_sup启动N个tcp_accetpor,tcp_acceptor发起accept请求,等待客户端连接;tcp_acceptor_sup负责监控这些acceptor。这张图已经能给你一个大体的印象。& X5 a p0 r8 d$ v4 I; }' m
讲完大概,进入细节,说说几个我觉的值的注意的地方:) j9 |$ ^* }3 C
1、tcp_accepto.erl,r对于accept采用的是异步方式,利用prim_inet:async_accept/2方法,此模块没有被文档化,是otp库内部使用,通常来说没必要使用这一模块,gen_tcp:accept/1已经足够,不过rabbitmq是广播程序,因此采用了异步方式。使用async_accept,需要打patch,以使得socket好像我们从gen_tcp:accept/1得到的一样:
. `; ~: z |- y+ @+ y1 [1 e
handle_info({inet_async, LSock, Ref, {ok, Sock}},$ k5 ?; N( ?4 X6 ?
State = #state{callback={M,F,A}, sock=LSock, ref=Ref}) ->" V8 Q$ t X |$ ]( X
%%这里做了patch9 e+ M6 B9 f" N" {7 [
%% patch up the socket so it looks like one we got from6 X6 l& | X5 z' u* P) I$ n2 f+ A
%% gen_tcp:accept/1
{ok, Mod} = inet_db:lookup_socket(LSock),
inet_db:register_socket(Sock, Mod),, [; T& @2 W. J# P3 x3 s
try
%% report$ ^6 G- q+ Z2 P0 d$ y: ]
{Address, Port} = inet_op(fun () -> inet:sockname(LSock) end),& E! ^# f/ X' }3 Q+ I1 F( U* ~
{PeerAddress, PeerPort} = inet_op(fun () -> inet:peername(Sock) end),
error_logger:info_msg("accepted TCP connection on ~s:~p from ~s:~p~n",5 b, m2 J$ d9 W) ?
[inet_parse:ntoa(Address), Port,7 f `) C! V. R' n; o5 p) g
inet_parse:ntoa(PeerAddress), PeerPort]),# ]7 r% a" K- t/ J# N9 o
%% 调用回调模块,将Sock作为附加参数% d9 p1 e: V& g$ q2 u9 ~+ J
apply(M, F, A ++ [Sock])
catch {inet_error, Reason} ->3 N, ]( A+ K! o( d
gen_tcp:close(Sock),
error_logger:error_msg("unable to accept TCP connection: ~p~n", e& ]8 R( r0 _+ Q
[Reason])
end,
%% 继续发起异步调用
case prim_inet:async_accept(LSock, -1) of
{ok, NRef} -> {noreply, State#state{ref=NRef}};
Error -> {stop, {cannot_accept, Error}, none}. L+ O1 j0 v9 D. @* k" t1 e
end;
%%处理错误情况
handle_info({inet_async, LSock, Ref, {error, closed}},
State=#state{sock=LSock, ref=Ref}) ->
%% It would be wrong to attempt to restart the acceptor when we
%% know this will fail.' G' c, n5 h* `* p
{stop, normal, State};
2、rabbitmq内部是使用了多个并发acceptor,这在高并发下、大量连接情况下有效率优势,类似java现在的nio框架采用多个reactor类似,查看tcp_listener.erl:
; ?9 `* G' Y4 N7 w! @, s2 d
init({IPAddress, Port, SocketOpts,
ConcurrentAcceptorCount, AcceptorSup,7 }3 w8 X/ W. O {( b
{M,F,A} = OnStartup, OnShutdown, Label}) -># j' C0 k& \6 c7 ]1 i" f4 w+ H4 @+ Y
process_flag(trap_exit, true),
case gen_tcp:listen(Port, SocketOpts ++ [{ip, IPAddress},
{active, false}]) of3 H0 G* B u$ D( j, {
{ok, LSock} ->2 a ^/ a5 W* _3 ?' d
%%创建ConcurrentAcceptorCount个并发acceptor
lists:foreach(fun (_) ->$ @7 U+ y3 Y; a( r+ q. }
{ok, _APid} = supervisor:start_child(; P, @' T3 t& K; X7 a
AcceptorSup, [LSock])) T" L0 l1 c! @1 |, S# t z
end,8 X u$ [( l! m: B
lists:duplicate(ConcurrentAcceptorCount, dummy)),3 @: z7 W5 M4 G+ y
{ok, {LIPAddress, LPort}} = inet:sockname(LSock),: r0 X8 K( L+ }& R7 X0 K. A( J
error_logger:info_msg("started ~s on ~s:~p~n",
[Label, inet_parse:ntoa(LIPAddress), LPort]),
%%调用初始化回调函数
apply(M, F, A ++ [IPAddress, Port]),: N/ N' Q4 V" o" g) F
{ok, #state{sock = LSock,
on_startup = OnStartup, on_shutdown = OnShutdown,
label = Label}};
{error, Reason} ->
error_logger:error_msg(
"failed to start ~s on ~s:~p - ~p~n",: w9 I' t. J: E
[Label, inet_parse:ntoa(IPAddress), Port, Reason]),4 W' s1 ]* C, D7 z# N" y2 y) Y
{stop, {cannot_listen, IPAddress, Port, Reason}}; k5 K( g& }' P& l1 G6 E, L
end.
/ P1 B7 \8 ?+ w r+ w/ X
这里有一个技巧,如果要循环N次执行某个函数F,可以通过lists:foreach结合lists:duplicate(N,dummy)来处理。+ A: i% l8 q) t' r: f/ S( Y
lists:foreach(fun(_)-> F() end,lists:duplicate(N,dummy)).4 X* e9 O( U" B
0 j; \% M0 C! }7 ^: E
3、simple_one_for_one策略的使用,可以看到对于tcp_client_sup和tcp_acceptor_sup都采用了simple_one_for_one策略,而非普通的one_fo_one,这是为什么呢?
这牵扯到simple_one_for_one的几个特点:
1)simple_one_for_one内部保存child是使用dict,而其他策略是使用list,因此simple_one_for_one更适合child频繁创建销毁、需要大量child进程的情况,具体来说例如网络连接的频繁接入断开。
2)使用了simple_one_for_one后,无法调用terminate_child/2 delete_child/2 restart_child/2 6 ~% b, j$ q( g
3)start_child/2 对于simple_one_for_one来说,不必传入完整的child spect,传入参数list,会自动进行参数合并。在一个地方定义好child spec之后,其他地方只要start_child传入参数即可启动child进程,简化child都是同一类型进程情况下的编程。. j4 [2 M2 N- p- Q. k% k% d
, i) k0 ^8 c% U, N4 F
在 rabbitmq中,tcp_acceptor_sup的子进程都是tcp_acceptor进程,在tcp_listener中是启动了 ConcurrentAcceptorCount个tcp_acceptor子进程,通过supervisor:start_child/2方法:7 v$ B; H3 v3 t/ Z! C( a( @
%%创建ConcurrentAcceptorCount个并发acceptor, l( U5 l5 k- q5 Q: {0 |
lists:foreach(fun (_) ->* P* G1 s0 u5 }9 j
{ok, _APid} = supervisor:start_child() R% { P5 i+ R% Y4 k, ]1 F( L
AcceptorSup, [LSock]) s, E: k( P: X6 C4 e6 l! k
end,
lists:duplicate(ConcurrentAcceptorCount, dummy)),7 a+ w( `1 z9 ?& |
# j: p' o9 h' Z; p: `, F
注意到,这里调用的start_child只传入了LSock一个参数,另一个参数CallBack是在定义child spec的时候传入的,参见tcp_acceptor_sup.erl:
init(Callback) ->
{ok, {{simple_one_for_one, 10, 10},
[{tcp_acceptor, {tcp_acceptor, start_link, [Callback]},3 c `8 J$ S5 N9 {/ N8 T/ |6 ?( i Z
transient, brutal_kill, worker, [tcp_acceptor]}]}}.9 X9 F6 W6 N. B" A, r3 b. C
Erlang内部自动为simple_one_for_one做了参数合并,最后调用的是tcp_acceptor的init/2:
" t" g( H' [5 n5 ^/ s: C
init({Callback, LSock}) ->, m3 R, h! u$ v- n! C/ o
case prim_inet:async_accept(LSock, -1) of
{ok, Ref} -> {ok, #state{callback=Callback, sock=LSock, ref=Ref}};
Error -> {stop, {cannot_accept, Error}}1 e1 q6 Z# p6 @; c/ S- ~% a3 i
end.% O$ B; M% e8 c4 B3 d& E* `7 @
对于tcp_client_sup的情况类似,tcp_client_sup监控的子进程都是rabbit_reader类型,在 rabbit_networking.erl中启动tcp_listenner传入的处理connect事件的回调方法是是 rabbit_networking:start_client/1:& L4 [% S; y1 c& m( e
start_tcp_listener(Host, Port) ->( O" m q! [$ b7 k9 I- E: G# H1 R
start_listener(Host, Port, "TCP Listener",. {& T9 B0 j" n4 g+ ^
%回调的MFA
{?MODULE, start_client, []}).
start_client(Sock) ->6 _: C; a* M0 f1 t4 i! z; [
{ok, Child} = supervisor:start_child(rabbit_tcp_client_sup, []),
ok = rabbit_net:controlling_process(Sock, Child),% X! ^9 P. F6 X* B2 |
Child ! {go, Sock},
Child.
start_client调用了supervisor:start_child/2来动态启动rabbit_reader进程。9 D, N2 ~; D* V( E) B) M
- [; k& B4 o- _% y; X
4、协议的解析,消息的读取这部分也非常巧妙,这一部分主要在rabbit_reader.erl中,对于协议的解析没有采用gen_fsm,而是实现了一个巧妙的状态机机制,核心代码在mainloop/4中:3 D6 W/ _; g9 a, s! I# T* n/ i; H. s
%启动一个连接
start_connection(Parent, Deb, ClientSock) ->+ |9 U+ d0 ~ N
process_flag(trap_exit, true),
{PeerAddressS, PeerPort} = peername(ClientSock), \' N+ F2 Q. |3 ]( ] q
ProfilingValue = setup_profiling(),8 s( y) x# g5 d- G9 B: @
try 2 A" @2 T% X% n* ~5 X
rabbit_log:info("starting TCP connection ~p from ~s:~p~n",
[self(), PeerAddressS, PeerPort]),
%延时发送握手协议
Erlang:send_after(?HANDSHAKE_TIMEOUT * 1000, self(),
handshake_timeout),
%进入主循环,更换callback模块,魔法就在这个switch_callback- z$ M8 `6 Z& E$ k3 v6 V* c* l. G
mainloop(Parent, Deb, switch_callback(
#v1{sock = ClientSock,
connection = #connection{
user = none,/ L" I1 N2 M! J9 V( }: h2 a+ i
timeout_sec = ?HANDSHAKE_TIMEOUT,
frame_max = ?FRAME_MIN_SIZE,
vhost = none},
callback = uninitialized_callback,
recv_ref = none,
connection_state = pre_init},
%%注意到这里,handshake就是我们的回调模块,8就是希望接收的数据长度,AMQP协议头的八个字节。
handshake, 8))" _0 a8 p5 h/ O% c
魔法就在switch_callback这个方法上:3 _- v- R# s% P% [# K; c
switch_callback(OldState, NewCallback, Length) ->; z' H8 b+ \; Y) k
%发起一个异步recv请求,请求Length字节的数据
Ref = inet_op(fun () -> rabbit_net:async_recv(
OldState#v1.sock, Length, infinity) end),7 Z. U+ U& s' N* u7 _4 x; t
%更新状态,替换ref和处理模块
OldState#v1{callback = NewCallback,
recv_ref = Ref}.+ k D1 V/ Z& p8 y" q( v c
异步接收Length个数据,如果有,erlang会通知你处理。处理模块是什么概念呢?其实就是一个状态的概念,表示当前协议解析进行到哪一步,起一个label的作用,看看mainloop/4中的应用:9 h5 F0 V6 S9 Z7 @8 h
mainloop(Parent, Deb, State = #v1{sock= Sock, recv_ref = Ref}) ->/ A4 \+ g& t8 b
%%?LOGDEBUG("Reader mainloop: ~p bytes available, need ~p~n", [HaveBytes, WaitUntilNBytes]),
receive
%%接收到数据,交给handle_input处理,注意handle_input的第一个参数就是callback- L# }" I) N2 {7 v, P0 d4 w
{inet_async, Sock, Ref, {ok, Data}} ->
%handle_input处理) k+ _- Z( O! F' T1 `( O& @
{State1, Callback1, Length1} =
handle_input(State#v1.callback, Data,
State#v1{recv_ref = none}),
%更新回调模块,再次发起异步请求,并进入主循环% c5 D: [- } r- ]- a
mainloop(Parent, Deb,
switch_callback(State1, Callback1, Length1));' M5 F0 P$ J5 F6 p/ J
1 k" l$ W3 _7 |1 B% ~
handle_input有多个分支,每个分支都对应一个处理模块,例如我们刚才提到的握手协议:
%handshake模块,注意到第一个参数,第二个参数就是我们得到的数据% ~9 @7 Q. ?9 @
handle_input(handshake, <<"AMQP",1,1,ProtocolMajor,ProtocolMinor>>,
State = #v1{sock = Sock, connection = Connection}) ->" A% O, {( i9 Q' w& x
%检测协议是否兼容
case check_version({ProtocolMajor, ProtocolMinor},
{?PROTOCOL_VERSION_MAJOR, ?PROTOCOL_VERSION_MINOR}) of7 e, B; H; l4 X
true ->
{ok, Product} = application:get_key(id),
{ok, Version} = application:get_key(vsn),
%兼容的话,进入connections start,协商参数
ok = send_on_channel0(
Sock,
#'connection.start'{
version_major = ?PROTOCOL_VERSION_MAJOR,
version_minor = ?PROTOCOL_VERSION_MINOR,
server_properties =% H+ h1 H) J& N
[{list_to_binary(K), longstr, list_to_binary(V)} ||
{K, V} <-
[{"product", Product},
{"version", Version},+ I* T6 _# W! t7 D% c( e9 {1 L
{"platform", "Erlang/OTP"},. K! p, n6 W d9 p. B
{"copyright", ?COPYRIGHT_MESSAGE},5 o# p1 d9 M; n c+ C
{"information", ?INFORMATION_MESSAGE}]],
mechanisms = <<"PLAIN AMQPLAIN">>,
locales = <<"en_US">> }),0 W3 w7 \. z i3 B, `8 u! l
{State#v1{connection = Connection#connection{( W7 ~; L/ F$ m6 y0 D# s
timeout_sec = ?NORMAL_TIMEOUT},) R( f3 Q. L; s6 E7 `) F
connection_state = starting},
frame_header, 7};5 I* ]+ d; n2 k* v0 |$ |
%否则,断开连接,返回可以接受的协议
false ->
throw({bad_version, ProtocolMajor, ProtocolMinor})+ j, [2 D) F7 N9 _# u( ]
end;
其他协议的处理也是类似,通过动态替换callback的方式来模拟状态机做协议的解析和数据的接收,真的很巧妙!让我们体会到Erlang的魅力,FP的魅力。
5、序列图:. @# T6 L, K% C8 N; g+ [% m
1)tcp server的启动过程:
. @8 @0 L; Z3 k" n* F7 y
" ]1 M0 C3 H$ \9 [) A
2)一个client连接上来的处理过程:, L" o) m( {' l6 c6 Y) C' `
$ H* x, K1 g% x( H% D
0 `$ F5 D( [! S
a& R3 m" t1 o! W2 I: Y4 c- P
小结:从上面的分析可以看出,rabbitmq的网络层是非常健壮和高效的,通过层层监控,对每个可能出现的风险点都做了考虑,并且利用了 prime_net模块做异步IO处理。分层也是很清晰,将业务处理模块隔离到client_sup监控下的子进程,将网络处理细节和业务逻辑分离。在协议的解析和业务处理上虽然没有采用gen_fsm,但是也实现了一套类似的状态机机制,通过动态替换Callback来模拟状态的变迁,非常巧妙。如果你要实现一个tcp server,强烈推荐从rabbitmq中扣出这个网络层,你只需要实现自己的业务处理模块即可拥有一个高效、健壮、分层清晰的TCP服务器。
浙公网安备 33010602011771号