RabbitMQ exchange route queue原理

根據前一篇文檔介紹的《RabbitMQ exchange binding queue原理》後,這篇文章將介紹exchange如何route消息到正確的queue中,其代碼流程如下:

在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。即

我們可以查看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參數保證消息不被丟棄。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章