Rabbitmq的网络层浅析

来源:互联网 发布:域名交易排行榜 编辑:程序博客网 时间:2024/05/24 00:35
 发表于 2009-12-15 14:13 | 只看该作者 |只看大图 回帖奖励
最近在锋爷的建议下开始读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

 


) g* \$ x/ ]* P  @

6 O" ]4 z  k( p! w
    顶层是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({CallbackLSock}) ->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服务器。
0 0