最近在锋爷的建议下开始读rabbitmq的源码,锋爷说这个项目已经很成熟,并且代码也很有借鉴和学习的意义,在自己写erlang代码之前看看别人是怎么写的,可以少走弯路,避免养成一些不好的习惯,学习一些最佳实践。读了一个星期,这个项目果然非常棒,代码也写的非常清晰易懂,一些细节的处理上非常巧妙,比如我这里想分享的网络层一节。2 Z+ N' A5 y: O* `4 /( o3 `1 c; k
Rabbitmq是一个MQ系统,也就是消息中间件,它实现了AMQP 0.8规范,简单来说就是一个TCP的广播服务器。AMQP协议,你可以类比JMS,不过JMS仅仅是java领域内的API规范,而AMQP比JMS更进一步,它有自己的wire-level protocol,有一套可编程的协议,中立于语言。简单介绍了Rabbitmq之后,进入正题。/ m+ u) q3 G& S" x$ V( c6 ?
Rabbitmq充分利用了Erlang的分布式、高可靠性、并发等特性,首先看它的一个结构图:8 ? d) S# i. x! z
这张图展现了Rabbitmq的主要组件和组件之间的关系,具体到监控树的结构,我画了一张图:' S$ F1 s. H2 [$ s p
+ e1 Q2 n$ E& E/ O3 p
* t. X! n7 `4 b/ N
?9 l @" u5 W4 t0 s, l6 d1 c
顶层是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。这张图已经能给你一个大体的印象。
讲完大概,进入细节,说说几个我觉的值的注意的地方:: z, b+ k- a" |% B0 m# S
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得到的一样:
handle_info({inet_async, LSock, Ref, {ok, Sock}},
State = #state{callback={M,F,A}, sock=LSock, ref=Ref}) ->' /3 [/ o. B8 [" ?2 ~
%%这里做了patch
%% patch up the socket so it looks like one we got from, B' z2 X' E/ o2 Z
%% gen_tcp:accept/1 " D+ s' F9 l$ B4 ]+ h
{ok, Mod} = inet_db:lookup_socket(LSock),) g% b, A0 B8 ^1 @, Q( r
inet_db:register_socket(Sock, Mod),
try
%% report+ g+ _: `$ P h
{Address, Port} = inet_op(fun () -> inet:sockname(LSock) end),
{PeerAddress, PeerPort} = inet_op(fun () -> inet:peername(Sock) end),$ w% ?; H& M% d
error_logger:info_msg("accepted TCP connection on ~s:~p from ~s:~p~n",
[inet_parse:ntoa(Address), Port,
inet_parse:ntoa(PeerAddress), PeerPort]),1 O% r6 N: d, M* B
%% 调用回调模块,将Sock作为附加参数
apply(M, F, A ++ [Sock])7 N8 N2 M x, V% |- j+ J* S
catch {inet_error, Reason} ->
gen_tcp:close(Sock),
error_logger:error_msg("unable to accept TCP connection: ~p~n",
[Reason]) n+ ^ l/ b) [/ z2 G
end,6 K" H. /7 c* b# ~& }$ }/ f+ e; [
%% 继续发起异步调用
case prim_inet:async_accept(LSock, -1) of
{ok, NRef} -> {noreply, State#state{ref=NRef}};0 a7 q6 g$ r5 A2 Z( B0 c
Error -> {stop, {cannot_accept, Error}, none}
end;, I4 ~" {; A" C- g( x
%%处理错误情况0 Z# ?: n& u) E% j
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.. b. a8 Q( ^/ F) o8 T
{stop, normal, State};
2 K) f" H. _' c- k+ p3 M% G0 `
2、rabbitmq内部是使用了多个并发acceptor,这在高并发下、大量连接情况下有效率优势,类似java现在的nio框架采用多个reactor类似,查看tcp_listener.erl:
init({IPAddress, Port, SocketOpts,
ConcurrentAcceptorCount, AcceptorSup,
{M,F,A} = OnStartup, OnShutdown, Label}) ->) N- }# X, F# B7 X: V
process_flag(trap_exit, true), u1 ^: ^$ U& ^5 D5 a/ C, C
case gen_tcp:listen(Port, SocketOpts ++ [{ip, IPAddress},
{active, false}]) of) p, Z& v! P9 f- {- N! N) {1 B3 Y
{ok, LSock} ->
%%创建ConcurrentAcceptorCount个并发acceptor! E3 b+ |- e0 j. N8 v
lists:foreach(fun (_) -># y( H; l$ [% y" Y. k7 N% J. f' s
{ok, _APid} = supervisor:start_child(; I+ t7 U9 d- K& A: j
AcceptorSup, [LSock])/ G. O+ s4 l; O7 D
end,5 {6 l" /* F8 x* j/ j
lists:duplicate(ConcurrentAcceptorCount, dummy)),, p" i: n0 [! w! F( B
{ok, {LIPAddress, LPort}} = inet:sockname(LSock),' D- Z5 S8 S* l
error_logger:info_msg("started ~s on ~s:~p~n",1 Q4 i, R4 n- E3 Y3 H
[Label, inet_parse:ntoa(LIPAddress), LPort]),
%%调用初始化回调函数
apply(M, F, A ++ [IPAddress, Port]),
{ok, #state{sock = LSock,
on_startup = OnStartup, on_shutdown = OnShutdown, ; X1 C! O* _, U. _; Q9 M1 e3 S
label = Label}};
{error, Reason} ->
error_logger:error_msg(+ G. R% `! ]* m) T! w
"failed to start ~s on ~s:~p - ~p~n",0 N) h# Z- s; O ?4 [
[Label, inet_parse:ntoa(IPAddress), Port, Reason]),% n6 g1 W7 F9 Y9 J; f
{stop, {cannot_listen, IPAddress, Port, Reason}} ~) h8 E+ g2 e4 V
end.: E7 ~/ v, ?/ w' Z$ h
) A Y! |* Z4 b! j
这里有一个技巧,如果要循环N次执行某个函数F,可以通过lists:foreach结合lists:duplicate(N,dummy)来处理。
M; Z9 f2 u" `/ z! o
lists:foreach(fun(_)-> F() end,lists:duplicate(N,dummy)).; E x$ W8 O# C) h9 d: _0 t
3、simple_one_for_one策略的使用,可以看到对于tcp_client_sup和tcp_acceptor_sup都采用了simple_one_for_one策略,而非普通的one_fo_one,这是为什么呢?
这牵扯到simple_one_for_one的几个特点:9 R# `6 J s0 K
1)simple_one_for_one内部保存child是使用dict,而其他策略是使用list,因此simple_one_for_one更适合child频繁创建销毁、需要大量child进程的情况,具体来说例如网络连接的频繁接入断开。! s2 z4 y2 o8 _, r0 Q
2)使用了simple_one_for_one后,无法调用terminate_child/2 delete_child/2 restart_child/2
3)start_child/2 对于simple_one_for_one来说,不必传入完整的child spect,传入参数list,会自动进行参数合并。在一个地方定义好child spec之后,其他地方只要start_child传入参数即可启动child进程,简化child都是同一类型进程情况下的编程。
4 ~ C- k8 ]+ a, H5 z0 /8 @* D0 }5 h
在 rabbitmq中,tcp_acceptor_sup的子进程都是tcp_acceptor进程,在tcp_listener中是启动了 ConcurrentAcceptorCount个tcp_acceptor子进程,通过supervisor:start_child/2方法:
%%创建ConcurrentAcceptorCount个并发acceptor' z; ?9 `9 ~$ A6 P$ K
lists:foreach(fun (_) -> T& z- L! B0 y8 M, v) y) F
{ok, _APid} = supervisor:start_child(
AcceptorSup, [LSock])! L9 H7 W$ ?4 D" K }
end,+ ~3 Y3 H: I L- i; w1 f
lists:duplicate(ConcurrentAcceptorCount, dummy)),
注意到,这里调用的start_child只传入了LSock一个参数,另一个参数CallBack是在定义child spec的时候传入的,参见tcp_acceptor_sup.erl:
init(Callback) ->
{ok, {{simple_one_for_one, 10, 10},' i( ?1 s w9 V# j; t o
[{tcp_acceptor, {tcp_acceptor, start_link, [Callback]},+ t5 F1 _7 Y e% Y( k: Z' [+ Y
transient, brutal_kill, worker, [tcp_acceptor]}]}}.
Erlang内部自动为simple_one_for_one做了参数合并,最后调用的是tcp_acceptor的init/2:) /" n2 p: |; G ~
init({Callback, LSock}) ->
case prim_inet:async_accept(LSock, -1) of/ @7 w# l2 Z6 Z6 H9 p
{ok, Ref} -> {ok, #state{callback=Callback, sock=LSock, ref=Ref}};; `4 m# R; x0 W7 Y! N* U/ ]% t
Error -> {stop, {cannot_accept, Error}}
end.
! |5 /" k3 `& W- O6 [& {
对于tcp_client_sup的情况类似,tcp_client_sup监控的子进程都是rabbit_reader类型,在 rabbit_networking.erl中启动tcp_listenner传入的处理connect事件的回调方法是是 rabbit_networking:start_client/1:
start_tcp_listener(Host, Port) ->
start_listener(Host, Port, "TCP Listener",* ~8 X2 d. u' G+ D1 ~& H
%回调的MFA$ r" q* E- H' i* ?4 Z) ], Y/ M
{?MODULE, start_client, []}).
, D: V, A/ H4 f
start_client(Sock) ->
{ok, Child} = supervisor:start_child(rabbit_tcp_client_sup, []),
ok = rabbit_net:controlling_process(Sock, Child),
Child ! {go, Sock},
Child.
' h6 K0 [. z- @' o
start_client调用了supervisor:start_child/2来动态启动rabbit_reader进程。0 L0 ]. t$ l& R' k/ g9 S
4、协议的解析,消息的读取这部分也非常巧妙,这一部分主要在rabbit_reader.erl中,对于协议的解析没有采用gen_fsm,而是实现了一个巧妙的状态机机制,核心代码在mainloop/4中:7 W+ X1 ^, X6 P8 Z
%启动一个连接- P7 t! a0 [8 m) ]
start_connection(Parent, Deb, ClientSock) ->% f9 z0 E# Q% m; T: h. ]: Q' i
process_flag(trap_exit, true),( ?& K: _% s t- I
{PeerAddressS, PeerPort} = peername(ClientSock),
ProfilingValue = setup_profiling(),
try
rabbit_log:info("starting TCP connection ~p from ~s:~p~n",0 J! e1 q; r5 d. j1 V3 m/ r- T2 N+ s! I
[self(), PeerAddressS, PeerPort]),
%延时发送握手协议' O/ O2 ^: Y9 k$ A: b
Erlang:send_after(?HANDSHAKE_TIMEOUT * 1000, self(),
handshake_timeout)," b/ d5 /2 ?5 P: W' Y
%进入主循环,更换callback模块,魔法就在这个switch_callback
mainloop(Parent, Deb, switch_callback(
#v1{sock = ClientSock,
connection = #connection{& J# y' O& |6 {4 i
user = none,
timeout_sec = ?HANDSHAKE_TIMEOUT,) R4 W4 I6 d2 a/ X
frame_max = ?FRAME_MIN_SIZE,7 x0 J6 j' [8 k: B' ^8 r6 A
vhost = none},3 x2 B( d; ?3 z1 v! Q
callback = uninitialized_callback,3 P* A8 S6 q3 ~* Z* k: T; x! y
recv_ref = none,
connection_state = pre_init},7 j; C# b2 `9 k y) i+ P
%%注意到这里,handshake就是我们的回调模块,8就是希望接收的数据长度,AMQP协议头的八个字节。
handshake, 8)) A9 ~) ~8 ^" H# m( e6 e4 p1 A
魔法就在switch_callback这个方法上:! K! `. O2 f. A1 r$ }
switch_callback(OldState, NewCallback, Length) ->
%发起一个异步recv请求,请求Length字节的数据
Ref = inet_op(fun () -> rabbit_net:async_recv(
OldState#v1.sock, Length, infinity) end),
%更新状态,替换ref和处理模块
OldState#v1{callback = NewCallback,3 e1 G& c1 S3 q# N
recv_ref = Ref}.- A8 F/ C$ o3 m$ d. B8 Z( X, R$ T1 V
异步接收Length个数据,如果有,erlang会通知你处理。处理模块是什么概念呢?其实就是一个状态的概念,表示当前协议解析进行到哪一步,起一个label的作用,看看mainloop/4中的应用:2 L& @. b" d! V/ R
+ B( V9 @, ?2 J% b
mainloop(Parent, Deb, State = #v1{sock= Sock, recv_ref = Ref}) -> H" |# F2 P2 ^/ H/ D
%%?LOGDEBUG("Reader mainloop: ~p bytes available, need ~p~n", [HaveBytes, WaitUntilNBytes]),
receive) D: }8 ^% j* k/ f; {6 B' R$ V
%%接收到数据,交给handle_input处理,注意handle_input的第一个参数就是callback
{inet_async, Sock, Ref, {ok, Data}} ->: t- f% l& R6 @0 I3 X* @9 h& h0 S
%handle_input处理; S/ { z" P0 x5 B( P N6 T
{State1, Callback1, Length1} =& o4 ]1 d% j Y& h$ c+ C6 r3 [$ n
handle_input(State#v1.callback, Data,1 t. _4 f+ T# u3 t/ j5 z+ l
State#v1{recv_ref = none}),
%更新回调模块,再次发起异步请求,并进入主循环8 n. d. p4 @$ D$ ~' j
mainloop(Parent, Deb,8 T( E$ ~' U! w& x" H
switch_callback(State1, Callback1, Length1));
: X% B5 F3 S- m( U4 T
handle_input有多个分支,每个分支都对应一个处理模块,例如我们刚才提到的握手协议:
%handshake模块,注意到第一个参数,第二个参数就是我们得到的数据
handle_input(handshake, <<"AMQP",1,1,ProtocolMajor,ProtocolMinor>>,
State = #v1{sock = Sock, connection = Connection}) ->1 w- i* A7 f, F8 e2 Z; j& N$ F# P
%检测协议是否兼容- ^7 t( Y! n! n/ U, t, l5 R
case check_version({ProtocolMajor, ProtocolMinor},0 ?1 {3 z. Q1 v! _7 z: d
{?PROTOCOL_VERSION_MAJOR, ?PROTOCOL_VERSION_MINOR}) of
true ->1 Y b$ V; G3 Y8 W5 ]! s
{ok, Product} = application:get_key(id),
{ok, Version} = application:get_key(vsn),
%兼容的话,进入connections start,协商参数( A1 H" C5 q: D$ H2 }2 J
ok = send_on_channel0(
Sock,
#'connection.start'{* V; j. v3 ^. M% X, b
version_major = ?PROTOCOL_VERSION_MAJOR,! N5 X- P5 b: g* E" L/ h
version_minor = ?PROTOCOL_VERSION_MINOR,
server_properties =: N5 k5 /. t$ a* a `
[{list_to_binary(K), longstr, list_to_binary(V)} ||
{K, V} <-
[{"product", Product},5 g. v+ E& t! B" `5 Y l
{"version", Version},
{"platform", "Erlang/OTP"},: x7 N8 o% d2 W/ /4 ^6 @
{"copyright", ?COPYRIGHT_MESSAGE},
{"information", ?INFORMATION_MESSAGE}]],
mechanisms = <<"PLAIN AMQPLAIN">>,
locales = <<"en_US">> }),! |) u1 F5 P1 s
{State#v1{connection = Connection#connection{' B4 z: v C' u$ I8 A
timeout_sec = ?NORMAL_TIMEOUT},
connection_state = starting},+ Y" z& n3 Q3 t) z- F4 `5 }. d3 k; r
frame_header, 7};9 L. r( D+ i4 Q' ?+ P4 Z3 b1 b
%否则,断开连接,返回可以接受的协议; k' U; p0 l: K
false ->+ U" T4 `$ h m( T" D5 h
throw({bad_version, ProtocolMajor, ProtocolMinor}) j* M3 h4 g, q& r
end;& ]: T6 K# [# `3 D0 e
其他协议的处理也是类似,通过动态替换callback的方式来模拟状态机做协议的解析和数据的接收,真的很巧妙!让我们体会到Erlang的魅力,FP的魅力。
5 L( k- u4 {: l; f4 x, s$ e; Q
5、序列图:5 v( ]" y) `4 l! b& T6 t
1)tcp server的启动过程:7 H/ h" T1 `8 f! r8 t
# w# N( p; {9 r* E
% q( z6 K' j5 v1 _
2)一个client连接上来的处理过程:' X: ^4 p$ n& ` s' _" G; h$ g
' v& X' R. ^- M {7 ~ z1 x
小结:从上面的分析可以看出,rabbitmq的网络层是非常健壮和高效的,通过层层监控,对每个可能出现的风险点都做了考虑,并且利用了 prime_net模块做异步IO处理。分层也是很清晰,将业务处理模块隔离到client_sup监控下的子进程,将网络处理细节和业务逻辑分离。在协议的解析和业务处理上虽然没有采用gen_fsm,但是也实现了一套类似的状态机机制,通过动态替换Callback来模拟状态的变迁,非常巧妙。如果你要实现一个tcp server,强烈推荐从rabbitmq中扣出这个网络层,你只需要实现自己的业务处理模块即可拥有一个高效、健壮、分层清晰的TCP服务器。