最近在锋爷的建议下开始读rabbitmq的源码,锋爷说这个项目已经很成熟,并且代码也很有借鉴和学习的意义,在自己写erlang代码之前看看别人是怎么写的,可以少走弯路,避免养成一些不好的习惯,学习一些最佳实践。读了一个星期,这个项目果然非常棒,代码也写的非常清晰易懂,一些细节的处理上非常巧妙,比如我这里想分享的网络层一节。 Rabbitmq是一个MQ系统,也就是消息中间件,它实现了AMQP 0.8规范,简单来说就是一个TCP的广播服务器。AMQP协议,你可以类比JMS,不过JMS仅仅是java领域内的API规范,而AMQP比JMS更进一步,它有自己的wire-level protocol,有一套可编程的协议,中立于语言。简单介绍了Rabbitmq之后,进入正题。' h: j' h: ^: g1 |. _! U8 x Rabbitmq充分利用了Erlang的分布式、高可靠性、并发等特性,首先看它的一个结构图:0 Y. `3 e/ v: Z" G- s 这张图展现了Rabbitmq的主要组件和组件之间的关系,具体到监控树的结构,我画了一张图: , g0 l0 }6 q5 c5 x( h* T, y; w* _6 y 顶层是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。这张图已经能给你一个大体的印象。 讲完大概,进入细节,说说几个我觉的值的注意的地方: 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得到的一样:' f9 m; Z9 X8 P% ^! T$ y 1 V- U# }/ t& {" e" h handle_info({inet_async, LSock, Ref, {ok, Sock}}, State = #state{callback={M,F,A}, sock=LSock, ref=Ref}) ->7 j% a( f Q6 Q %%这里做了patch %% patch up the socket so it looks like one we got from %% gen_tcp:accept/1 3 U! o# F O' Z4 C' e- p" { {ok, Mod} = inet_db:lookup_socket(LSock), inet_db:register_socket(Sock, Mod), try, e* F/ z" S# n. F3 s %% report {Address, Port} = inet_op(fun () -> inet:sockname(LSock) end),$ b) y& t1 y6 O1 I& \ {PeerAddress, PeerPort} = inet_op(fun () -> inet:peername(Sock) end),4 a! \+ j9 D1 D1 u3 W error_logger:info_msg("accepted TCP connection on ~s:~p from ~s:~p~n", [inet_parse:ntoa(Address), Port, inet_parse:ntoa(PeerAddress), PeerPort]),, T0 ?' z4 E4 E+ b; k2 w %% 调用回调模块,将Sock作为附加参数 apply(M, F, A ++ [Sock]) catch {inet_error, Reason} ->* ?% a& F$ `& E9 N) b gen_tcp:close(Sock), error_logger:error_msg("unable to accept TCP connection: ~p~n",# |2 O+ ?6 t+ b& [# s0 S m [Reason])2 r2 ^8 z7 ^' [ _5 g end, C0 W: S5 P6 `5 a7 X : \5 w, l2 S" b/ F } %% 继续发起异步调用, |& d3 J b8 N' M1 S case prim_inet:async_accept(LSock, -1) of3 @* r$ m4 `% O6 R+ p$ L" O {ok, NRef} -> {noreply, State#state{ref=NRef}};0 _7 K6 }% {* ^ e3 u Error -> {stop, {cannot_accept, Error}, none} end;3 u) v/ \# T( V+ |, S- p3 H3 f %%处理错误情况 handle_info({inet_async, LSock, Ref, {error, closed}},2 h, K; F1 A H F0 l) j4 }$ J State=#state{sock=LSock, ref=Ref}) ->. g7 h' t) s3 }% O" `6 \! q# R %% It would be wrong to attempt to restart the acceptor when we %% know this will fail. {stop, normal, State};' g9 W+ r2 p/ ^8 W2 M5 o 2、rabbitmq内部是使用了多个并发acceptor,这在高并发下、大量连接情况下有效率优势,类似java现在的nio框架采用多个reactor类似,查看tcp_listener.erl: # p% i8 y" Z# i' t# ^ N init({IPAddress, Port, SocketOpts, ConcurrentAcceptorCount, AcceptorSup, {M,F,A} = OnStartup, OnShutdown, Label}) ->% `7 U$ n" b: H3 i process_flag(trap_exit, true), case gen_tcp:listen(Port, SocketOpts ++ [{ip, IPAddress}, {active, false}]) of1 w3 }0 \' g# O {ok, LSock} -> %%创建ConcurrentAcceptorCount个并发acceptor. f; G, z8 J2 l% J$ \) M2 m lists:foreach(fun (_) -># G4 E ^5 m7 L4 j {ok, _APid} = supervisor:start_child( AcceptorSup, [LSock]) end,& O& m& d/ |9 d( j$ S* P) { lists:duplicate(ConcurrentAcceptorCount, dummy)), {ok, {LIPAddress, LPort}} = inet:sockname(LSock),1 t+ {1 E6 ?7 a! d* s+ \ error_logger:info_msg("started ~s on ~s:~p~n",4 N9 `. Y1 x- o9 J [Label, inet_parse:ntoa(LIPAddress), LPort]), %%调用初始化回调函数# S3 _/ q, E+ u/ s2 H apply(M, F, A ++ [IPAddress, Port]), {ok, #state{sock = LSock, on_startup = OnStartup, on_shutdown = OnShutdown, & c. c3 e$ T- D5 j1 A9 L/ o1 P label = Label}};8 U6 R) Y1 z( T+ H3 b {error, Reason} ->% U# q6 M( [* o$ w6 c2 k error_logger:error_msg( "failed to start ~s on ~s:~p - ~p~n", [Label, inet_parse:ntoa(IPAddress), Port, Reason]), {stop, {cannot_listen, IPAddress, Port, Reason}}$ g9 u7 O( |9 m) g end. 这里有一个技巧,如果要循环N次执行某个函数F,可以通过lists:foreach结合lists:duplicate(N,dummy)来处理。 lists:foreach(fun(_)-> F() end,lists:duplicate(N,dummy)).& {7 M3 v. }$ l& e/ {& v: B 0 s+ n2 {) C) T 3、simple_one_for_one策略的使用,可以看到对于tcp_client_sup和tcp_acceptor_sup都采用了simple_one_for_one策略,而非普通的one_fo_one,这是为什么呢?7 ~8 Q# Z, ?, O/ q, G2 R3 f 这牵扯到simple_one_for_one的几个特点:) g* i, t9 w" U- w. p 1)simple_one_for_one内部保存child是使用dict,而其他策略是使用list,因此simple_one_for_one更适合child频繁创建销毁、需要大量child进程的情况,具体来说例如网络连接的频繁接入断开。% ^9 E [+ `! S+ I 2)使用了simple_one_for_one后,无法调用terminate_child/2 delete_child/2 restart_child/2 ( w7 O8 G6 _/ r9 q8 K8 ?1 R 3)start_child/2 对于simple_one_for_one来说,不必传入完整的child spect,传入参数list,会自动进行参数合并。在一个地方定义好child spec之后,其他地方只要start_child传入参数即可启动child进程,简化child都是同一类型进程情况下的编程。+ `! i* k/ c& z 在 rabbitmq中,tcp_acceptor_sup的子进程都是tcp_acceptor进程,在tcp_listener中是启动了 ConcurrentAcceptorCount个tcp_acceptor子进程,通过supervisor:start_child/2方法: & v0 ?* d0 F6 g1 E2 L %%创建ConcurrentAcceptorCount个并发acceptor lists:foreach(fun (_) -> {ok, _APid} = supervisor:start_child( AcceptorSup, [LSock])6 J$ V; H Y7 w R2 D end, lists:duplicate(ConcurrentAcceptorCount, dummy)), 注意到,这里调用的start_child只传入了LSock一个参数,另一个参数CallBack是在定义child spec的时候传入的,参见tcp_acceptor_sup.erl:) B! V- O2 n1 ~( y; b0 m- T init(Callback) -> {ok, {{simple_one_for_one, 10, 10},* h% M" g' K/ x) D& k [{tcp_acceptor, {tcp_acceptor, start_link, [Callback]}, transient, brutal_kill, worker, [tcp_acceptor]}]}}. Erlang内部自动为simple_one_for_one做了参数合并,最后调用的是tcp_acceptor的init/2:% b* ]- u' x/ D h; N. G# q 0 y( N$ C8 g8 ^5 v+ @& w init({Callback, LSock}) ->2 [: i& m8 Z& N1 p- P0 n1 z0 I case prim_inet:async_accept(LSock, -1) of {ok, Ref} -> {ok, #state{callback=Callback, sock=LSock, ref=Ref}};. L5 g. A. J6 a. e& {, ?+ E" A Error -> {stop, {cannot_accept, Error}}+ }( q& O9 N; ^5 E end.3 G- {/ K' o% k; H; G 对于tcp_client_sup的情况类似,tcp_client_sup监控的子进程都是rabbit_reader类型,在 rabbit_networking.erl中启动tcp_listenner传入的处理connect事件的回调方法是是 rabbit_networking:start_client/1: 0 X9 k$ [" o! |$ N start_tcp_listener(Host, Port) ->" N0 A5 C Z! w# p9 a+ l' ^ start_listener(Host, Port, "TCP Listener", %回调的MFA3 w5 u x! o" U" @ {?MODULE, start_client, []}). start_client(Sock) ->: X6 G; B0 z; u) {) [6 G {ok, Child} = supervisor:start_child(rabbit_tcp_client_sup, []),8 Q$ {: K% t1 F% o, H- [, ` ok = rabbit_net:controlling_process(Sock, Child),/ {& _0 ~5 j* X; K! l Child ! {go, Sock},& M* [; |+ J! w5 j1 v9 v" S Child. : |, G" W3 _5 ?6 \+ I7 W start_client调用了supervisor:start_child/2来动态启动rabbit_reader进程。 4、协议的解析,消息的读取这部分也非常巧妙,这一部分主要在rabbit_reader.erl中,对于协议的解析没有采用gen_fsm,而是实现了一个巧妙的状态机机制,核心代码在mainloop/4中:: p. _" Y/ h5 G5 A# j- ~4 C; r %启动一个连接 start_connection(Parent, Deb, ClientSock) ->$ D; F$ `0 M) J0 ` process_flag(trap_exit, true),, R) b2 k# O. @( g. x6 A3 \# { {PeerAddressS, PeerPort} = peername(ClientSock),% x; a' ~( H2 h ProfilingValue = setup_profiling(),$ F" d5 K5 o, ~ try 2 P5 p: a6 w- d/ c) \7 Z rabbit_log:info("starting TCP connection ~p from ~s:~p~n", [self(), PeerAddressS, PeerPort]), %延时发送握手协议; T. u2 u% T0 | E3 A3 h. K Erlang:send_after(?HANDSHAKE_TIMEOUT * 1000, self(), handshake_timeout),6 b, Z' ^7 @; x, T %进入主循环,更换callback模块,魔法就在这个switch_callback0 Y& q8 c* H- n+ v. i6 X/ q1 i: M mainloop(Parent, Deb, switch_callback(7 e7 ?9 ~" d4 j4 { #v1{sock = ClientSock,( ~+ J1 C2 D } connection = #connection{ user = none, timeout_sec = ?HANDSHAKE_TIMEOUT,& G2 F0 t9 N) J. n5 \ frame_max = ?FRAME_MIN_SIZE, vhost = none}, callback = uninitialized_callback, recv_ref = none, connection_state = pre_init}, %%注意到这里,handshake就是我们的回调模块,8就是希望接收的数据长度,AMQP协议头的八个字节。 handshake, 8)) 魔法就在switch_callback这个方法上:. U; A" V z6 N- Z: x( R switch_callback(OldState, NewCallback, Length) ->) \& ]9 W( M( u e* l %发起一个异步recv请求,请求Length字节的数据( D; g9 J6 S7 \, _" j y! H; O K Ref = inet_op(fun () -> rabbit_net:async_recv(- k& c& T) d, o9 ?- s0 c OldState#v1.sock, Length, infinity) end),5 i [1 I) E2 `9 F- t1 M %更新状态,替换ref和处理模块 OldState#v1{callback = NewCallback,# A3 J9 o9 j5 R+ w$ @ recv_ref = Ref}. 2 y% O* y0 H4 ~: T4 N 异步接收Length个数据,如果有,erlang会通知你处理。处理模块是什么概念呢?其实就是一个状态的概念,表示当前协议解析进行到哪一步,起一个label的作用,看看mainloop/4中的应用: / n# z& g a# G( c( T' c mainloop(Parent, Deb, State = #v1{sock= Sock, recv_ref = Ref}) ->* W0 w+ V, o$ L %%?LOGDEBUG("Reader mainloop: ~p bytes available, need ~p~n", [HaveBytes, WaitUntilNBytes]),% e$ m( Y( D1 \ receive3 |( U" Y! Q5 k9 d0 p! M s* q %%接收到数据,交给handle_input处理,注意handle_input的第一个参数就是callback" ?8 w( c6 t8 m4 q& v {inet_async, Sock, Ref, {ok, Data}} -> %handle_input处理 {State1, Callback1, Length1} = handle_input(State#v1.callback, Data, State#v1{recv_ref = none}), %更新回调模块,再次发起异步请求,并进入主循环. Q) w' Y9 y; @) x j/ j mainloop(Parent, Deb,# ?4 U& t* u2 v/ j: R switch_callback(State1, Callback1, Length1));: S4 j: Y' L! e% @$ T7 Z) ^ 0 {; Y0 H8 N) X! q3 u handle_input有多个分支,每个分支都对应一个处理模块,例如我们刚才提到的握手协议: %handshake模块,注意到第一个参数,第二个参数就是我们得到的数据 handle_input(handshake, <<"AMQP",1,1,ProtocolMajor,ProtocolMinor>>,: J4 |0 Z+ m2 _* Z; `0 ~8 B6 q State = #v1{sock = Sock, connection = Connection}) -> %检测协议是否兼容2 w% C6 S% s2 O8 }# }8 U5 @' j case check_version({ProtocolMajor, ProtocolMinor}, {?PROTOCOL_VERSION_MAJOR, ?PROTOCOL_VERSION_MINOR}) of0 _9 e: T' I8 p true ->: } R6 u/ f& M6 y# g2 M# G- N0 g0 J5 l {ok, Product} = application:get_key(id),' G: p" }# T( [; i( D {ok, Version} = application:get_key(vsn),: M1 K9 H) ]6 F %兼容的话,进入connections start,协商参数 g) f, ~6 u2 g) V% G9 P, k, u8 e) z ok = send_on_channel0(: Q7 H# o4 ]9 e: @' Y Sock, #'connection.start'{# Y4 X, e/ q- ?+ T0 }, L4 H. h) ^ version_major = ?PROTOCOL_VERSION_MAJOR,* B% y' H/ v/ P& ^! j* f8 W! S version_minor = ?PROTOCOL_VERSION_MINOR, server_properties =3 a6 p B/ [- a [{list_to_binary(K), longstr, list_to_binary(V)} ||7 \1 H3 G8 J. w& V7 [4 f9 o {K, V} <-- _! c3 F* Q- b( b8 B0 d [{"product", Product}, {"version", Version}," o' ~4 K5 m g {"platform", "Erlang/OTP"},( F- G3 I9 M, C% Y# g! u {"copyright", ?COPYRIGHT_MESSAGE},8 h$ [2 k7 z7 I4 x" g, P {"information", ?INFORMATION_MESSAGE}]], D6 B7 \4 x6 n mechanisms = <<"PLAIN AMQPLAIN">>, locales = <<"en_US">> }), {State#v1{connection = Connection#connection{ timeout_sec = ?NORMAL_TIMEOUT},1 y" Z) K+ \* o D e* L/ v) u6 S' [ connection_state = starting},1 C- C2 j8 I' D4 T frame_header, 7}; %否则,断开连接,返回可以接受的协议 false -> throw({bad_version, ProtocolMajor, ProtocolMinor})5 U/ I% Y, e5 h3 _+ V end;4 ?' A: m: |8 Q5 J ) ?% b6 }3 s( W3 D, I; d0 i 其他协议的处理也是类似,通过动态替换callback的方式来模拟状态机做协议的解析和数据的接收,真的很巧妙!让我们体会到Erlang的魅力,FP的魅力。* L0 f, q5 ^8 K3 H) c. q2 u v $ Y8 Q! ^! t% ^% S) I& y& q, V 5、序列图:; J5 m2 |1 }6 J" V4 W 1)tcp server的启动过程:
/ i% M3 }( g) o; k; j
8 b' ]1 l: e0 N$ v" f& X" v2 J7 J 2)一个client连接上来的处理过程:
; ^, R$ v: y( f* P& W! I. _
# N( o4 G \4 n. ]: s- v9 J- `& j 小结:从上面的分析可以看出,rabbitmq的网络层是非常健壮和高效的,通过层层监控,对每个可能出现的风险点都做了考虑,并且利用了 prime_net模块做异步IO处理。分层也是很清晰,将业务处理模块隔离到client_sup监控下的子进程,将网络处理细节和业务逻辑分离。在协议的解析和业务处理上虽然没有采用gen_fsm,但是也实现了一套类似的状态机机制,通过动态替换Callback来模拟状态的变迁,非常巧妙。如果你要实现一个tcp server,强烈推荐从rabbitmq中扣出这个网络层,你只需要实现自己的业务处理模块即可拥有一个高效、健壮、分层清晰的TCP服务器。 |
Rabbitmq的网络层浅析
最新推荐文章于 2023-05-19 15:54:03 发布