RabbitMQ exchange route queue原理

根据前一篇文档介绍的《RabbitMQ exchange binding queue原理》后,这篇文章将介绍exchange如何route消息到正确的queue中,其代码流程如下:

RabbitMQ exchange route queue原理_第1张图片

在RabbitMQ的客户端发送消息时,将指定exchange和routing_key,在exchange收到消息后,将根据routing_key将消息发送到正确的queue中。

1 exchange route消息到queue

当RabbitMQ收到客户端发送过来的basic.publish消息时,首先校验消息以及exchange相关信息,如msg是否超过RabbitMQ所允许的最大值,exchange是否为内部exchange,当exchange为内部exchange时,RabbitMQ是不允许向其发送消息的,内部exchange是其exchange结构体的internal字段进行判断。

Exchange route消息时,首先需要查找到相对应的queue,而查找工作通过rabbit_exchange:route函数进行完成。

%% rabbit_exchange.erl
%% 在X交换机下根据路由规则得到对应队列的名字(如果存在exchange交换机绑定exchange交换机,则会跟被绑定的exchange交换机继续匹配)
route(#exchange{name = #resource{virtual_host = VHost, name = RName} = XName,
                decorators = Decorators} = X,
      #delivery{message = #basic_message{routing_keys = RKs}} = Delivery) ->
    case RName of
        <<>> ->
            %% 如果exchange交换机的名字为空,则根据路由规则判断是否有指定的消费者,如果有则直接将消息发送给指定的消费者
            RKsSorted = lists:usort(RKs),
            [rabbit_channel:deliver_reply(RK, Delivery) ||
               RK <- RKsSorted, virtual_reply_queue(RK)],
            [rabbit_misc:r(VHost, queue, RK) || RK <- RKsSorted,
                                                not virtual_reply_queue(RK)];
        _ ->
            %% 获得exchange交换机的描述模块
            Decs = rabbit_exchange_decorator:select(route, Decorators),
            %% route1进行实际的路由,寻找对应的队列
            lists:usort(route1(Delivery, Decs, {[X], XName, []}))
end.

route1(Delivery, Decorators,
       {[X = #exchange{type = Type} | WorkList], SeenXs, QNames}) ->
    %% 根据exchange的类型得到路由的处理模块,让该模块进行相关的路由,找到对应的队列
    ExchangeDests  = (type_to_module(Type)):route(X, Delivery),
    %% 获取交换机exchange描述模块提供的额外交换机
    DecorateDests  = process_decorators(X, Decorators, Delivery),
    %% 如果参数中配置有备用的交换机,则将该交换机拿出来
    AlternateDests = process_alternate(X, ExchangeDests),
    route1(Delivery, Decorators,
           lists:foldl(fun process_route/2, {WorkList, SeenXs, QNames},
                       AlternateDests ++ DecorateDests  ++ ExchangeDests)
          ).

这里我们讨论exchange为topic类型的消息route原理,即rabbit_exchange:route1函数中type_to_module(Type)为rabbit_exchange_type_topic,这些Type类型被注册且保存到rabbit_registry表中,该表使用ets进行保存,因此rabbit_registry表内容属于本节点所有。通过ets:tab2list函数可查看表中内容。

[root@master scripts]# ./rabbitmqctl eval 'ets:tab2list(rabbit_registry).'
[{
    {policy_validator,'dead-letter-routing-key'},rabbit_policies},
 {
    {exchange,'x-jms-topic'},rabbit_jms_topic_exchange},
……
 {
    {queue_decorator,federation},rabbit_federation_queue},
 {
    {exchange,topic},rabbit_exchange_type_topic},
 {
    {policy_validator,'alternate-exchange'},rabbit_policies},
 {
    {policy_validator,'ha-promote-on-failure'},rabbit_mirror_queue_misc},
……
 {
    {exchange,headers},rabbit_exchange_type_headers},
 {
    {runtime_parameter,policy},rabbit_policy},
 {
    {ha_mode,nodes},rabbit_mirror_queue_mode_nodes},
……

所以对于topic类型的exchange而言,根据routing_key查找相对应的queue的代码是在rabbit_exchange_type_topic模块进行的。

%% rabbit_exchange_type_topic.erl
%% NB: This may return duplicate results in some situations (that's ok)
%% 路由函数,根据exchange结构和delivery结构得到需要将消息发送到的队列
route(#exchange{name = X},
      #delivery{message = #basic_message{routing_keys = Routes}}) ->
    lists:append([begin
                      %%  将路由键按照.拆分成多个单词(routing key(由生产者在发布消息时指定)有如下规定:由0个或多个以”.”分隔的单词;每个单词只能以一字母或者数字组成)
                      Words = split_topic_key(RKey),
                      mnesia:async_dirty(fun trie_match/2, [X, Words])
                  end || RKey <- Routes]).

%% rabbit_exchange_type_topic.erl
%% 对单个单词进行匹配的入口
trie_match(X, Words) ->
trie_match(X, root, Words, []).

%% rabbit_exchange_type_topic.erl
trie_match(X, Node, [W | RestW] = Words, ResAcc) ->
    lists:foldl(fun ({WArg, MatchFun, RestWArg}, Acc) ->
                         trie_match_part(X, Node, WArg, MatchFun, RestWArg, Acc)
                end, ResAcc, [%% 从路由信息单词判断是否有从Node节点到下一个节点的边
                              {W, fun trie_match/4, RestW},
                              %% 用*判断是否有从Node节点到下一个节点的边
                              {"*", fun trie_match/4, RestW},
                              %% 用#判断是否有从Node节点到下一个节点的边
                              {"#", fun trie_match_skip_any/4, Words}]).

具体的路由原理可查看以下链接:https://www.erlang-solutions.com/blog/rabbit-s-anatomy-understanding-topic-exchanges.html

假设创建了一个topic类型的exchange和一个queue,且使用#.test的binding_key进行binding。即

RabbitMQ exchange route queue原理_第2张图片

我们可以查看topic类型的exchange相关的表数据。

[root@master scripts]# ./rabbitmqctl eval 'ets:tab2list(rabbit_topic_trie_edge).'
[{topic_trie_edge,
     {trie_edge,
         {resource,<<"/">>,exchange,<<"test_topic_exchange">>},
         root,"#"},
     <<225,144,39,46,149,142,166,198,58,230,86,105,29,226,227,223>>},
 {topic_trie_edge,
     {trie_edge,
         {resource,<<"/">>,exchange,<<"test_topic_exchange">>},
         <<225,144,39,46,149,142,166,198,58,230,86,105,29,226,227,223>>,
         "test"},
     <<248,229,107,219,87,141,155,116,112,137,46,194,109,107,205,29>>}]

[root@master scripts]# ./rabbitmqctl eval 'ets:tab2list(rabbit_topic_trie_node).'
[{topic_trie_node,
     {trie_node,{resource,<<"/">>,exchange,<<"test_topic_exchange">>},root},
     1,0},
 {topic_trie_node,
     {trie_node,
         {resource,<<"/">>,exchange,<<"test_topic_exchange">>},
         <<225,144,39,46,149,142,166,198,58,230,86,105,29,226,227,223>>},
     1,0},
 {topic_trie_node,
     {trie_node,
         {resource,<<"/">>,exchange,<<"test_topic_exchange">>},
         <<248,229,107,219,87,141,155,116,112,137,46,194,109,107,205,29>>},
     0,1}]

[root@master scripts]# ./rabbitmqctl eval 'ets:tab2list(rabbit_topic_trie_binding).'
[{topic_trie_binding,
     {trie_binding,
         {resource,<<"/">>,exchange,<<"test_topic_exchange">>},
         <<248,229,107,219,87,141,155,116,112,137,46,194,109,107,205,29>>,
         {resource,<<"/">>,queue,<<"test_queue">>},
         []},
     const}]

对于topic类型的exchange的queue查看,首先查看到root下的rabbit_topic_trie_edge数据,即上面的<<225,144,39,46,149,142,166,198,58,230,86,105,29,226,227,223>>,该id用于唯一标识该root下的# edge,即root在rabbit_topic_trie_edge表中也作为一个根id,对于上述例子,#作为root下的一个edge,#的id为<<225,144,39,46,149,142,166,198,58,230,86,105,29,226,227,223>>,#下还有一个test edge,该edge的id为<<248,229,107,219,87,141,155,116,112,137,46,194,109,107,205,29>>。

同时通过查看rabbit_topic_trie_binding表可知,test_topic_exchang和test_queue便是使用的id为<<248,229,107,219,87,141,155,116,112,137,46,194,109,107,205,29>>的edge。如果查看到相应的queue,则会返回{resource,<<"/">>,queue,<<"test_queue">>}信息。

执行完rabbit_exchange_type_topic:route函数后,回到rabbit_channel模块。

%% rabbit_channel.erl
%% 交付给路由到的队列进程,将message消息交付到各个队列中
deliver_to_queues({Delivery = #delivery{message    = Message = #basic_message{
                                                                              exchange_name = XName},
                                        mandatory  = Mandatory,
                                        confirm    = Confirm,
                                        msg_seq_no = MsgSeqNo},
                   DelQNames}, State = #ch{queue_names    = QNames,
                                           queue_monitors = QMons}) ->
    Qs = rabbit_amqqueue:lookup(DelQNames),
    %% 向已经路由出来的消息队列发送消息
    DeliveredQPids = rabbit_amqqueue:deliver(Qs, Delivery),
    %% The pmon:monitor_all/2 monitors all queues to which we
    %% delivered. But we want to monitor even queues we didn't deliver
    %% to, since we need their 'DOWN' messages to clean
    %% queue_names. So we also need to monitor each QPid from
    %% queues. But that only gets the masters (which is fine for
    %% cleaning queue_names), so we need the union of both.
    %%
    %% ...and we need to add even non-delivered queues to queue_names
    %% since alternative(替代) algorithms(算法) to update queue_names less
    %% frequently would in fact be more expensive in the common case.
    %% 当前channel进程监视路由到的消息队列进程
    {QNames1, QMons1} =
        lists:foldl(fun (#amqqueue{pid = QPid, name = QName},
                         {QNames0, QMons0}) ->
                             {case dict:is_key(QPid, QNames0) of
                                  true  -> QNames0;
                                  false -> dict:store(QPid, QName, QNames0)
                              end, pmon:monitor(QPid, QMons0)}
                    end, {QNames, pmon:monitor_all(DeliveredQPids, QMons)}, Qs),
    %% 更新queue_names字段,queue_monitors字段
    State1 = State#ch{queue_names    = QNames1,
                      queue_monitors = QMons1},
 ……
    ?INCR_STATS([{exchange_stats, XName, 1} |
                     [{queue_exchange_stats, {QName, XName}, 1} ||
                      QPid        <- DeliveredQPids,
                      {ok, QName} <- [dict:find(QPid, QNames1)]]],
                publish, State3),
    State3.

这里rabbit_amqqueue:lookup函数会查找类似{resource,<<"/">>,queue,<<"test_queue">>}的queue数据。

[root@master scripts]# ./rabbitmqctl eval 'ets:lookup(rabbit_queue, {resource,<<"/">>,queue,<<"test_queue">>}).'
[{amqqueue,{resource,<<"/">>,queue,<<"test_queue">>},
           false,false,none,[],<10503.3801.0>,[],[],[],undefined,undefined,[],
           [],live,0,[],<<"/">>,
           #{user => <<"openstack">>}}]

最后执行rabbit_amqqueue:deliver函数将数据传递给相对应的queue。

%% rabbit_amqqueue.erl
%% 向消息队列进程传递消息
deliver([], _Delivery) ->
    %% /dev/null optimisation
[];

%% rabbit_amqqueue.erl
%% 向Qs队列发送消息
deliver(Qs, Delivery = #delivery{flow = Flow}) ->
    {MPids, SPids} = qpids(Qs),
    QPids = MPids ++ SPids,
    %% We use up two credits to send to a slave since the message
    %% arrives at the slave from two directions. We will ack one when
    %% the slave receives the message direct from the channel, and the
    %% other when it receives it via GM.
    %% 进程间消息流的控制
    case Flow of
        flow   -> [credit_flow:send(QPid) || QPid <- QPids],
                  [credit_flow:send(QPid) || QPid <- SPids];
        noflow -> ok
    end,
    
    %% We let slaves know that they were being addressed as slaves at
    %% the time - if they receive such a message from the channel
    %% after they have become master they should mark the message as
    %% 'delivered' since they do not know what the master may have
    %% done with it.
    MMsg = {deliver, Delivery, false},
    SMsg = {deliver, Delivery, true},
    %% 如果有队列进程Pid在远程节点,则通过远程节点的代理进程统一在远程节点上向自己节点上的队列进程发送对应的消息
    delegate:cast(MPids, MMsg),
    delegate:cast(SPids, SMsg),
    QPids.

这里,如果根据exchange和routing_key查找到的queue的为空,则发送的消息将直接被丢弃,我们可以通过可以通过mandatory参数或者使用alternate-exchange参数保证消息不被丢弃。

其中设置mandatory参数保证消息不丢弃的源码如下:

%% rabbit_channel.erl
process_routing_mandatory(false,     _, _MsgSeqNo, _Msg, State) ->
    State;

process_routing_mandatory(true,     [], _MsgSeqNo,  Msg, State) ->
    %% mandatory为true,同时路由不到消息队列,则将消息原样发送给生成者
    ok = basic_return(Msg, State, no_route),
    State;

%% 将mandatory为true,且发送到的队列Pid不为空,则将数据存入到mandatory(dtree数据结构)字段中
process_routing_mandatory(true,  QPids,  MsgSeqNo,  Msg, State) ->
    State#ch{mandatory = dtree:insert(MsgSeqNo, QPids, Msg,
                                      State#ch.mandatory)}.

这里根据生产者设置的是否设置了mandatory参数以及行rabbit_amqqueue:deliver函数返回的QPids来做相应的操作。

  • 生产者没设置mandatory参数,则直接返回状态,此时不能保证消息不丢失。
  • 生产者设置了mandatory参数,但rabbit_amqqueue:deliver函数返回空,即RabbitMQ没有查找的queue,此时RabbitMQ发送basic.return返回消息给生产者。
  • 生产者设置了mandatory参数且RabbitMQ查找到对应的queue,则更新dtree数据结构。

2 总结

         对于topic类型的exchange,其queue的查看是在rabbit_exchange_type_topic模块中进行的,当查找到相对应的queue,会将消息发送到该queue中。如果未查找到queue,则消息将被丢弃。可以设置mandatory参数或者使用alternate-exchange参数保证消息不被丢弃。

你可能感兴趣的:(erlang,RabbitMQ)