[源码分析] 消息队列 Kombu 之 mailbox

733 阅读12分钟

0x00 摘要

本系列我们介绍消息队列 Kombu。Kombu 的定位是一个兼容 AMQP 协议的消息队列抽象。通过本文,大家可以了解 Kombu 中的 mailbox 概念,顺便可以把之前几篇文章内容再次梳理下。

0x01 示例代码

本文实例代码来自 liqiang.io/post/celery…

示例代码分为两部分˛

Node可以理解为广播Consumer。Client可以认为是广播发起者。

1.1 Node

import sys
import kombu
from kombu import pidbox

hostname = "localhost"
connection = kombu.Connection('redis://localhost:6379')
mailbox = pidbox.Mailbox("testMailbox", type="direct")
node = mailbox.Node(hostname, state={"a": "b"})
node.channel = connection.channel()

def callback(body, message):
    print(body)
    print(message)

def main(arguments):
    consumer = node.listen(callback=callback)
    try:
        while True:
            print('Consumer Waiting')
            connection.drain_events()
    finally:
        consumer.cancel()

if __name__ == '__main__':
    sys.exit(main(sys.argv[1:]))

1.2 client

import sys
import kombu
from kombu import pidbox

def callback():
    print("callback")

def main(arguments):
    connection = kombu.Connection('redis://localhost:6379')
    mailbox = pidbox.Mailbox("testMailbox", type="direct")
    bound = mailbox(connection)
    bound._broadcast("print_msg", {'msg': 'Message for you'})

if __name__ == '__main__':
    sys.exit(main(sys.argv[1:]))

0x02 核心思路

广播功能是利用了Redis的 pubSub 机制完成。

2.1 Redis PubSub

为了支持消息多播,Redis单独使用了一个模块,也就是PubSub。

Redis作为消息发布和订阅之间的服务器,起到桥梁的作用。在Redis里面有一个channel的概念,也就是频道,发布者通过指定发布到某个频道,只要有订阅者订阅了该频道,该消息就会发送给订阅者。

2.2 概述

在 Kombu 的 mailbox 实现中,分为 Consumer 和 Producer两部分。

在 Kombu 的 Channel 类中,当注册 listener 时候,实际就是利用了 redis 驱动的 PubSub功能,把 consumer 注册订阅到了一个 key 上。从而 Consumer 的 queue 和 回调函数 就通过 Channel 与 redis 联系起来。这样后续就可以从 Redis 读取消息。

psubscribe, client.py:3542
_subscribe, redis.py:664
_register_LISTEN, redis.py:322
get, redis.py:375
drain_events, base.py:960
drain_events, connection.py:318
main, node.py:24

0x03 Consumer

下面我们依据示例代码,一步一步剖析如何完成广播功能。

3.1 建立Connection

完成以下代码之后,系统建立了Connection。

connection = kombu.Connection('redis://localhost:6379')

具体如下,我把问题域分为用户域和Kombu域,以便理解:

user scope                 +            kombu scope
                           |
                           |
+------------+             |            +--------------------------------------+
| connection | +----------------------> | Connection: redis://localhost:6379// |
+------------+             |            +--------------------------------------+
                           |
                           |
                           |
                           |
                           |
                           +

3.2 建立mailbox

完成以下代码后,系统建立了Connection和mailbox。

connection = kombu.Connection('redis://localhost:6379')
mailbox = pidbox.Mailbox("testMailbox", type="fanout")

但此时两者没有建立联系,mailbox的某些成员变量也没有实际含义。举例如下:

mailbox = {Mailbox} <kombu.pidbox.Mailbox object at 0x7fea4b81df28>
 accept = {list: 1} ['json']
 clock = {LamportClock} 0
 connection = {NoneType} None
 exchange = {Exchange} Exchange testMailbox.pidbox(fanout)
 exchange_fmt = {str} '%s.pidbox'
 namespace = {str} 'testMailbox'
 node_cls = {type} <class 'kombu.pidbox.Node'>
 oid = {str} '9386a23b-ae96-3c6c-b036-ae7646455ebb'
 producer_pool = {NoneType} None
 queue_expires = {NoneType} None
 queue_ttl = {NoneType} None
 reply_exchange = {Exchange} Exchange reply.testMailbox.pidbox(direct)
 reply_exchange_fmt = {str} 'reply.%s.pidbox'
 reply_queue = {Queue} <unbound Queue 9386a23b-ae96-3c6c-b036-ae7646455ebb.reply.testMailbox.pidbox -> <unbound Exchange reply.testMailbox.pidbox(direct)> -> 9386a23b-ae96-3c6c-b036-ae7646455ebb>
 reply_queue_expires = {float} 10.0
 reply_queue_ttl = {NoneType} None
 serializer = {NoneType} None
 type = {str} 'fanout'
 unclaimed = {defaultdict: 0} defaultdict(<class 'collections.deque'>, {})

此时逻辑如下:

3.3 建立Node

完成以下代码之后,系统建立了Connection,mailbox和node。

Node是mailbox中的概念,可以理解为具体邮箱。

connection = kombu.Connection('redis://localhost:6379')
mailbox = pidbox.Mailbox("testMailbox", type="fanout")
node = mailbox.Node(hostname, state={"a": "b"})

node举例如下:

node = {Node} <kombu.pidbox.Node object at 0x7fea4b8bffd0>
 channel = {NoneType} None
 handlers = {dict: 0} {}
 hostname = {str} 'localhost'
 mailbox = {Mailbox} <kombu.pidbox.Mailbox object at 0x7fea4b81df28>
 state = {dict: 1} {'a': 'b'}

逻辑如下:

3.4 建立channel

经过如下代码后,才建立channel。

connection = kombu.Connection('redis://localhost:6379')
mailbox = pidbox.Mailbox("testMailbox", type="fanout")
node = mailbox.Node(hostname, state={"a": "b"})
node.channel = connection.channel()

3.4.1 联系

关于channel,Connection 与 Transport 联系解释如下:

  • Connection:对 MQ 连接的抽象,一个 Connection 就对应一个 MQ 的连接;Connection 是 AMQP 对 连接的封装;
  • Channel:与AMQP中概念类似,可以理解成共享一个Connection的多个轻量化连接;Channel 是 AMQP 对 MQ 的操作的封装;
  • Transport:kombu 支持将不同的消息中间件以插件的方式进行灵活配置,使用transport这个术语来表示一个具体的消息中间件,可以认为是对broker的抽象:
    • 对 MQ 的操作必然离不开连接,但是,Kombu 并不直接让 Channel 使用 Connection 来发送/接受请求,而是引入了一个新的抽象 Transport,Transport 负责具体的 MQ 的操作,也就是说 Channel 的操作都会落到 Transport 上执行。引入transport这个抽象概念可以使得后续添加对non-AMQP的transport非常简单;
    • Transport是真实的 MQ 连接,也是真正连接到 MQ(redis/rabbitmq) 的实例,区分底层消息队列的实现;
    • 当前Kombu中build-in支持有Redis、Beanstalk、Amazon SQS、CouchDB,、MongoDB,、ZeroMQ,、ZooKeeper、SoftLayer MQ和Pyro;

3.4.2 Channel

在 Transport 有两个channels 列表:

self._avail_channels
self.channels

如果_avail_channels 有内容则直接获取,否则生成一个新的Channel。

在真正连接时候,会调用 establish_connection 放入self._avail_channels。

def establish_connection(self):
    # creates channel to verify connection.
    # this channel is then used as the next requested channel.
    # (returned by ``create_channel``).
    self._avail_channels.append(self.create_channel(self))
    return self     # for drain events

Channel关键初始化代码如下:

class Channel(virtual.Channel):
    """Redis Channel."""

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

        self._queue_cycle = cycle_by_name(self.queue_order_strategy)()
        self.Client = self._get_client()

        self.active_fanout_queues = set()
        self.auto_delete_queues = set()
        self._fanout_to_queue = {}
        self.handlers = {'BRPOP': self._brpop_read, 'LISTEN': self._receive}

        self.connection.cycle.add(self)  # add to channel poller. # 加入消息循环

            if register_after_fork is not None:
                register_after_fork(self, _after_fork_cleanup_channel)

3.4.3 MultiChannelPoller

MultiChannelPoller 定义如下,可以理解为执行engine,主要作用是:

  • 收集channel;
  • 建立fd到channel的映射;
  • 建立channel到socks的映射;
  • 使用poll;
class MultiChannelPoller:

    def __init__(self):
        # active channels
        self._channels = set()
        # file descriptor -> channel map.
        self._fd_to_chan = {}
        # channel -> socket map
        self._chan_to_sock = {}
        # poll implementation (epoll/kqueue/select)
        self.poller = poll()
        # one-shot callbacks called after reading from socket.
        self.after_read = set()

    def add(self, channel):
        self._channels.add(channel)

最后Channel变量举例如下:

self = {Channel} <kombu.transport.redis.Channel object at 0x7ffc6d9c5fd0>
 Client = {type} <class 'redis.client.Redis'>
 Message = {type} <class 'kombu.transport.virtual.base.Message'>
 QoS = {type} <class 'kombu.transport.redis.QoS'>
 active_fanout_queues = {set: 0} set()
 active_queues = {set: 0} set()
 async_pool = {ConnectionPool} ConnectionPool<Connection<host=localhost,port=6379,db=0>>
 auto_delete_queues = {set: 0} set()
 body_encoding = {str} 'base64'
 channel_id = {int} 1
 client = {Redis} Redis<ConnectionPool<Connection<host=localhost,port=6379,db=0>>>
 connection = {Transport} <kombu.transport.redis.Transport object at 0x7ffc6d9c5f60>
 cycle = {FairCycle} <FairCycle: 0/0 []>
 exchange_types = {dict: 3} {'direct': <kombu.transport.virtual.exchange.DirectExchange object at 0x7ffc6d9c5f98>, 'topic': <kombu.transport.virtual.exchange.TopicExchange object at 0x7ffc6d9c5d68>, 'fanout': <kombu.transport.virtual.exchange.FanoutExchange object at 0x7ffc6d9d2b70>}
 handlers = {dict: 2} {'BRPOP': <bound method Channel._brpop_read of <kombu.transport.redis.Channel object at 0x7ffc6d9c5fd0>>, 'LISTEN': <bound method Channel._receive of <kombu.transport.redis.Channel object at 0x7ffc6d9c5fd0>>}
 health_check_interval = {int} 25
 keyprefix_fanout = {str} '/0.'
 keyprefix_queue = {str} '_kombu.binding.%s'
 pool = {ConnectionPool} ConnectionPool<Connection<host=localhost,port=6379,db=0>>
 priority_steps = {list: 4} [0, 3, 6, 9]
 qos = {QoS} <kombu.transport.redis.QoS object at 0x7ffc6d9fbc88>
 queue_order_strategy = {str} 'round_robin'
 state = {BrokerState} <kombu.transport.virtual.base.BrokerState object at 0x7ffc6d969e10>
 subclient = {PubSub} <redis.client.PubSub object at 0x7ffc6d9fbd68>

最后Transport变量举例如下:

connection = {Transport} <kombu.transport.redis.Transport object at 0x7ffc6d9c5f60>
 Channel = {type} <class 'kombu.transport.redis.Channel'>
 Cycle = {type} <class 'kombu.utils.scheduling.FairCycle'>
 Management = {type} <class 'kombu.transport.virtual.base.Management'>
 channels = {list: 1} [<kombu.transport.redis.Channel object at 0x7ffc6da8c748>]
 client = {Connection} <Connection: redis://localhost:6379// at 0x7ffc6d5f0e80>
 connection_errors = {tuple: 8} (<class 'amqp.exceptions.ConnectionError'>, <class 'kombu.exceptions.InconsistencyError'>, <class 'OSError'>, <class 'OSError'>, <class 'OSError'>, <class 'redis.exceptions.ConnectionError'>, <class 'redis.exceptions.AuthenticationError'>, <class 'redis.exceptions.TimeoutError'>)
 cycle = {MultiChannelPoller} <kombu.transport.redis.MultiChannelPoller object at 0x7ffc6d9d2198>
  after_read = {set: 0} set()
  eventflags = {int} 25
  fds = {dict: 0} {}
  poller = {_poll} <kombu.utils.eventio._poll object at 0x7ffc6d9d21d0>
 driver_name = {str} 'redis'
 driver_type = {str} 'redis'
 implements = {Implements: 3} {'asynchronous': True, 'exchange_type': frozenset({'direct', 'fanout', 'topic'}), 'heartbeats': False}
 manager = {Management} <kombu.transport.virtual.base.Management object at 0x7ffc6da8c6d8>
 state = {BrokerState} <kombu.transport.virtual.base.BrokerState object at 0x7ffc6d969e10>

此时逻辑如下:

3.5 建立 Consumer

如下代码建立一个Consumer,也建立了对应的Queue。就是说,广播还是需要依赖Consumer完成,或者说是借助Consumer功能。

def main(arguments):
    consumer = node.listen(callback=callback)

listen代码如下:

def listen(self, channel=None, callback=None):
    consumer = self.Consumer(channel=channel,
                             callbacks=[callback or self.handle_message],
                             on_decode_error=self.on_decode_error)
    consumer.consume()
    return consumer

此时对应Queue变量如下:

queue = {Queue} <unbound Queue localhost.testMailbox.pidbox -> <unbound Exchange testMailbox.pidbox(fanout)> -> >
 ContentDisallowed = {type} <class 'kombu.exceptions.ContentDisallowed'>
 alias = {NoneType} None
 auto_delete = {bool} True
 binding_arguments = {NoneType} None
 bindings = {set: 0} set()
 can_cache_declaration = {bool} False
 channel = {str} 'line 178, in _getPyDictionary\n    attr = getattr(var, n)\n  File "
 consumer_arguments = {NoneType} None
 durable = {bool} False
 exchange = {Exchange} Exchange testMailbox.pidbox(fanout)

逻辑如下:

3.5.1 binding 写入Redis

此时会把binding关系写入Redis,这样后续就可以利用这个binding来进行路由。

具体堆栈如下:

sadd, client.py:2243
_queue_bind, redis.py:817
queue_bind, base.py:568
bind_to, entity.py:674
queue_bind, entity.py:662
_create_queue, entity.py:617
declare, entity.py:606
declare, messaging.py:417
revive, messaging.py:404
__init__, messaging.py:382
Consumer, pidbox.py:78
listen, pidbox.py:91
main, node.py:20
<module>, node.py:29

逻辑如图,此时出现了Redis。

3.5.2 配置

代码来到了kombu/transport/virtual/base.py,这里工作如下:

  • 把 consumer 的 queue 加入到 Channel;
  • 把回调函数加入到 Channel;
  • 把 Consumer 加入循环;

这样,Comuser 的 queue 和 回调函数 就通过 Channel 联系起来。

代码如下:

    def basic_consume(self, queue, no_ack, callback, consumer_tag, **kwargs):
        """Consume from `queue`."""
        self._tag_to_queue[consumer_tag] = queue
        self._active_queues.append(queue)

        def _callback(raw_message):
            message = self.Message(raw_message, channel=self)
            if not no_ack:
                self.qos.append(message, message.delivery_tag)
            return callback(message)

        self.connection._callbacks[queue] = _callback
        self._consumers.add(consumer_tag)

        self._reset_cycle()

调用堆栈如下:

basic_consume, base.py:635
basic_consume, redis.py:598
consume, entity.py:738
_basic_consume, messaging.py:594
consume, messaging.py:473
listen, pidbox.py:92
main, node.py:20
<module>, node.py:29

此时依然在Channel

self = {Channel} <kombu.transport.redis.Channel object at 0x7fc252239908>
 Client = {type} <class 'redis.client.Redis'>
 Message = {type} <class 'kombu.transport.virtual.base.Message'>
 active_fanout_queues = {set: 1} {'localhost.testMailbox.pidbox'}
 active_queues = {set: 0} set()
 async_pool = {ConnectionPool} ConnectionPool<Connection<host=localhost,port=6379,db=0>>
 auto_delete_queues = {set: 1} {'localhost.testMailbox.pidbox'}
 body_encoding = {str} 'base64'
 channel_id = {int} 1
 client = {Redis} Redis<ConnectionPool<Connection<host=localhost,port=6379,db=0>>>
 closed = {bool} False
 codecs = {dict: 1} {'base64': <kombu.transport.virtual.base.Base64 object at 0x7fc25218f5c0>}
 connection = {Transport} <kombu.transport.redis.Transport object at 0x7fc2522295f8>
 cycle = {FairCycle} <FairCycle: 0/1 ['localhost.testMailbox.pidbox']>
 deadletter_queue = {NoneType} None
 default_priority = {int} 0
 do_restore = {bool} True
 exchange_types = {dict: 3} {'direct': <kombu.transport.virtual.exchange.DirectExchange object at 0x7fc252239fd0>, 'topic': <kombu.transport.virtual.exchange.TopicExchange object at 0x7fc252239f60>, 'fanout': <kombu.transport.virtual.exchange.FanoutExchange object at 0x7fc252239f28>}
 handlers = {dict: 2} {'BRPOP': <bound method Channel._brpop_read of <kombu.transport.redis.Channel object at 0x7fc252239908>>, 'LISTEN': <bound method Channel._receive of <kombu.transport.redis.Channel object at 0x7fc252239908>>}
 keyprefix_fanout = {str} '/0.'
 keyprefix_queue = {str} '_kombu.binding.%s'
 pool = {ConnectionPool} ConnectionPool<Connection<host=localhost,port=6379,db=0>>
 priority_steps = {list: 4} [0, 3, 6, 9]
 qos = {QoS} <kombu.transport.redis.QoS object at 0x7fc252264320>
 queue_order_strategy = {str} 'round_robin'
 state = {BrokerState} <kombu.transport.virtual.base.BrokerState object at 0x7fc25218f6a0>
 subclient = {PubSub} <redis.client.PubSub object at 0x7fc252264400>

具体循环如下:

def _reset_cycle(self):
    self._cycle = FairCycle(
        self._get_and_deliver, self._active_queues, Empty)

FairCycle定义如下:

class FairCycle:
    """Cycle between resources.

    Consume from a set of resources, where each resource gets
    an equal chance to be consumed from.

    Arguments:
        fun (Callable): Callback to call.
        resources (Sequence[Any]): List of resources.
        predicate (type): Exception predicate.
    """

    def __init__(self, fun, resources, predicate=Exception):
        self.fun = fun
        self.resources = resources
        self.predicate = predicate
        self.pos = 0

    def _next(self):
        while 1:
            try:
                resource = self.resources[self.pos]
                self.pos += 1
                return resource
            except IndexError:
                self.pos = 0
                if not self.resources:
                    raise self.predicate()

    def get(self, callback, **kwargs):
        """Get from next resource."""
        for tried in count(0):  # for infinity
            resource = self._next()
            try:
                return self.fun(resource, callback, **kwargs)
            except self.predicate:
                # reraise when retries exchausted.
                if tried >= len(self.resources) - 1:
                    raise

回调函数如下:

fun = {method} <bound method AbstractChannel._get_and_deliver of <kombu.transport.redis.Channel object at 0x7fc252239908>>
resources = {list: 1} ['localhost.testMailbox.pidbox']

逻辑如下:

 user scope        +      Kombu                                        +-----------------------+    +-----------------------+                                   +          redis
                   |                                                   | Transport             |    | MultiChannelPoller    |                                   |
                   |                                                   |                       |    |                       |                                   |
                   |       +--------------------------------------+    |            cycle +-------> |           _channels +----+                                |
                   |       |                                      |    |                       |    +-----------------------+  |                                |
 +------------+    |       | Connection: redis://localhost:6379// |    |            channels +--------+                        v                                |
 | Connection|-----------> |                                      |    |                       |      |      +-----------------+---+                            |
 +------------+    |       |                                      |    |     _avail_channels+---------+      | Channel             |  <------------+            |
                   |       |                       connection+-------> |                       |      |      |                     |               |            |
                   |       |                                      |    +-----------------------+      |      |     _active_queues +------------------------+    |
                   |       +--------------------------------------+                                   |      |                     |               |       |    |
                   |                                                                                  +----->+                     |     +---------+-+     |    |
                   |                                               +----------------------------+            |            cycle +------> | FairCycle |     |    |
                   |                                               | Exchange                   |            |                     |     |           |     |    |
                   |       +------------------------------+   +--> |                            |  <-----+   |                     |     +-----------+     |    |
 +---------+       |       | Mailbox                      |   |    | testMailbox.pidbox(fanout) |        |   |           handlers+-----+                   |    |
 | mailbox|--------------> |                              |   |    +----------------------------+        |   +-+--+----------------+   |                   |    |
 +---------+       |       |                              |   |                                          |     ^  ^                    |                   |    |
                   |       |        exchange  +---------------+    +---------------------------------+   |     |  |                    v                   |    |
                   |       |                              |        | Exchange                        |   |     |  |    +---------------+---------------+   |    |
                   |       |        reply_exchange +------------>  |                                 |   |     |  |    |  'BRPOP': Channel._brpop_read |   |    |
                   |       |                              |        | reply.testMailbox.pidbox(direct)|   |     |  |    |                               |   |    |
                   |       |        reply_queue +-------------+    +-------------------+-------------+   |     |  |    |  'LISTEN': Channel._receive   |   |    |
                   |       |                              |   |                        ^                 |     |  |    |                               |   |    |
                   |       |                              |   |    +--------+          |                 |     |  |    +-------------------------------+   |    |
                   |       +-------------------------+----+   +--> | Queue  +----------+                 |     |  |                                        |    |
                   |                                 ^             +--------+                            |     |  |                                        |    |
                   |                                 |                                                   |     |  |                                        |    |  +----------------------------------------------------+
                   |       +---------------------+   |                                                   |     |  |                                        |    |  |      _kombu.binding.testMailbox.pidbox             |
 +-----+           |       |                     |   |                                                   |     |  |                                        |    |  |                                                    |
 |node | +---------------->+ Node      channel+-------------------------------------------------------------------+                                        |    |  |                                                    |
 +-----+           |       |                     |   |                                                   |     |                                           |    |  |   "\x06\x16\x06\x16localhost.testMailbox.pidbox"   |
                   |       |           mailbox +-----+                                                   |     |                                           |    |  |                                                    |
                   |       |                     |        +----------------------------------------------------+                                           |    |  +---------+------------------------------------------+
                   |       +---------------------+        |                                              |                                                 |    |            ^
                   |                                      |                                              |                                                 |    |            |
                   |                                      |                                              |                                                 |    |            |
                   |       +------------------------+     |         +-----------------------------------------------------------------------------+        |    |            |
+----------+       |       |                        |     |         | Queue                              |                                        |        |    |            |
| consumer |       |       | Consumer    channel  +-------+         |                                    +                                        |  <-----+    |            |
+----------+       |       |                        |               |                                 exchange                                    |             |            |
                   |       |             queues  +--------------->  |                                                                             |             |            |
                   |       |                        |               |                                                                             | +------------------------+
+-----------+      |       |             callbacks  |               |     <localhost.testMailbox.pidbox -> Exchange testMailbox.pidbox(fanout)>   |             |
| callback  |      |       |                  +     |               |                                                                             |             |
+------+----+      |       +------------------------+               +-----------------------------------------------------------------------------+             |
       ^           |                          |                                                                                                                 |
       |           |                          |                                                                                                                 |
       +--------------------------------------+                                                                                                                 +
                   |

手机如下

3.5.3 配置负载均衡

回到 Channel 类,这里最后会配置负载均衡,就是具体下一次使用哪一个 Queue 的消息。

def basic_consume(self, queue, *args, **kwargs):
    if queue in self._fanout_queues:
        exchange, _ = self._fanout_queues[queue]
        self.active_fanout_queues.add(queue)
        self._fanout_to_queue[exchange] = queue
    ret = super().basic_consume(queue, *args, **kwargs)

    # Update fair cycle between queues.
    #
    # We cycle between queues fairly to make sure that
    # each queue is equally likely to be consumed from,
    # so that a very busy queue will not block others.
    #
    # This works by using Redis's `BRPOP` command and
    # by rotating the most recently used queue to the
    # and of the list.  See Kombu github issue #166 for
    # more discussion of this method.
    self._update_queue_cycle()
    return ret
  
  
def _update_queue_cycle(self):
    self._queue_cycle.update(self.active_queues)

堆栈如下:

update, scheduling.py:75
_update_queue_cycle, redis.py:1018
basic_consume, redis.py:610
consume, entity.py:738
_basic_consume, messaging.py:594
consume, messaging.py:473
listen, pidbox.py:92
main, node.py:20
<module>, node.py:29

策略如下:

class round_robin_cycle:
    """Iterator that cycles between items in round-robin."""

    def __init__(self, it=None):
        self.items = it if it is not None else []

    def update(self, it):
        """Update items from iterable."""
        self.items[:] = it

    def consume(self, n):
        """Consume n items."""
        return self.items[:n]

    def rotate(self, last_used):
        """Move most recently used item to end of list."""
        items = self.items
        try:
            items.append(items.pop(items.index(last_used)))
        except ValueError:
            pass
        return last_used

逻辑如下:

 user scope        +      Kombu                                        +-----------------------+    +-----------------------+                                   +          redis
                   |                                                   | Transport             |    | MultiChannelPoller    |                                   |
                   |                                                   |                       |    |                       |                                   |
                   |       +--------------------------------------+    |            cycle +-------> |           _channels +----+                                |
                   |       |                                      |    |                       |    +-----------------------+  |                                |
 +------------+    |       | Connection: redis://localhost:6379// |    |            channels +--------+                        v                                |
 | Connection|-----------> |                                      |    |                       |      |      +-----------------+---+                            |
 +------------+    |       |                                      |    |     _avail_channels+---------+      | Channel             |  <------------+            |
                   |       |                       connection+-------> |                       |      |      |                     |               |            |
                   |       |                                      |    +-----------------------+      |      |     _active_queues +------------------------+    |
                   |       +--------------------------------------+                                   |      |                     |               |       |    |
                   |                                                                                  +----->+            cycle +------>  +--------+--+    |    |
                   |                                               +----------------------------+            |                     |      | FairCycle |    |    |
                   |                                               | Exchange                   |            |                     |      +-----------+    |    |
                   |       +------------------------------+   +--> |                            |  <-----+   |       _queue_cycle+-----------+             |    |
 +---------+       |       | Mailbox                      |   |    | testMailbox.pidbox(fanout) |        |   |                     |         |             |    |
 | mailbox|--------------> |                              |   |    +----------------------------+        |   |           handlers  |         v             |    |
 +---------+       |       |                              |   |                                          |   |               +     |      round_robin_cycle|    |
                   |       |        exchange  +---------------+    +---------------------------------+   |   +-+--+----------------+                       |    |
                   |       |                              |        | Exchange                        |   |     ^  ^          |                             |    |
                   |       |        reply_exchange +------------>  |                                 |   |     |  |          |                             |    |
                   |       |                              |        | reply.testMailbox.pidbox(direct)|   |     |  |          |                             |    |
                   |       |        reply_queue +-------------+    +-------------------+-------------+   |     |  |          v                             |    |
                   |       |                              |   |                        ^                 |     |  |    +-----+-------------------------+   |    |
                   |       |                              |   |    +--------+          |                 |     |  |    |  'BRPOP': Channel._brpop_read |   |    |
                   |       +-------------------------+----+   +--> | Queue  +----------+                 |     |  |    |                               |   |    |
                   |                                 ^             +--------+                            |     |  |    |  'LISTEN': Channel._receive   |   |    |
                   |                                 |                                                   |     |  |    |                               |   |    |  +----------------------------------------------------+
                   |       +---------------------+   |                                                   |     |  |    +-------------------------------+   |    |  |      _kombu.binding.testMailbox.pidbox             |
 +-----+           |       |                     |   |                                                   |     |  |                                        |    |  |                                                    |
 |node | +---------------->+ Node      channel+-------------------------------------------------------------------+                                        |    |  |                                                    |
 +-----+           |       |                     |   |                                                   |     |                                           |    |  |   "\x06\x16\x06\x16localhost.testMailbox.pidbox"   |
                   |       |           mailbox +-----+                                                   |     |                                           |    |  |                                                    |
                   |       |                     |        +----------------------------------------------------+                                           |    |  +---------+------------------------------------------+
                   |       +---------------------+        |                                              |                                                 |    |            ^
                   |                                      |                                              |                                                 |    |            |
                   |                                      |                                              |                                                 |    |            |
                   |       +------------------------+     |         +-----------------------------------------------------------------------------+        |    |            |
+----------+       |       |                        |     |         | Queue                              |                                        |        |    |            |
| consumer |       |       | Consumer    channel  +-------+         |                                    +                                        |  <-----+    |            |
+----------+       |       |                        |               |                                 exchange                                    |             |            |
                   |       |             queues  +--------------->  |                                                                             |             |            |
                   |       |                        |               |                                                                             | +------------------------+
+-----------+      |       |             callbacks  |               |     <localhost.testMailbox.pidbox -> Exchange testMailbox.pidbox(fanout)>   |             |
| callback  |      |       |                  +     |               |                                                                             |             |
+------+----+      |       +------------------------+               +-----------------------------------------------------------------------------+             |
       ^           |                          |                                                                                                                 |
       |           |                          |                                                                                                                 |
       +--------------------------------------+                                                                                                                 +
                   |

手机如下:

3.6 消费

3.2.1 消费主体

如下代码完成消费。

def main(arguments):
    consumer = node.listen(callback=callback)
    try:
        while True:
            print('Consumer Waiting')
            connection.drain_events()
    finally:
        consumer.cancel()

具体就是使用 drain_events 里读取消息,其代码如下:

def drain_events(self, connection, timeout=None):
    time_start = monotonic()
    get = self.cycle.get
    polling_interval = self.polling_interval
    if timeout and polling_interval and polling_interval > timeout:
        polling_interval = timeout
    while 1:
        try:
            get(self._deliver, timeout=timeout)
        except Empty:
            if timeout is not None and monotonic() - time_start >= timeout:
                raise socket.timeout()
            if polling_interval is not None:
                sleep(polling_interval)
        else:
            break

3.2.2 业务逻辑

3.2.2.1 注册

get方法功能如下(需要注意的是,每次消费都要使用一次get函数,即,都要进行注册,消费....):

  • 注册响应方式;
  • 进行poll操作,这是通用操作,或者 BRPOP,或者 LISTEN;
  • 调用 handle_event 进行读取redis,具体消费;
def get(self, callback, timeout=None):
    self._in_protected_read = True
    try:
        for channel in self._channels:
            if channel.active_queues:           # BRPOP mode?
                if channel.qos.can_consume():
                    self._register_BRPOP(channel)
            if channel.active_fanout_queues:    # LISTEN mode?
                self._register_LISTEN(channel)

        events = self.poller.poll(timeout)
        if events:
            for fileno, event in events:
                ret = self.handle_event(fileno, event) # 具体读取redis,进行消费
                if ret:
                    return
        # - no new data, so try to restore messages.
        # - reset active redis commands.
        self.maybe_restore_messages()
        raise Empty()
    finally:
        self._in_protected_read = False
        while self.after_read:
            try:
                fun = self.after_read.pop()
            except KeyError:
                break
            else:
                fun()

因为这里利用了pubsub,所以调用到 channel._subscribe 来注册订阅,具体如下:

def _register_LISTEN(self, channel):
    """Enable LISTEN mode for channel."""
    if not self._client_registered(channel, channel.subclient, 'LISTEN'):
        channel._in_listen = False
        self._register(channel, channel.subclient, 'LISTEN')
    if not channel._in_listen:
        channel._subscribe()  # send SUBSCRIBE

具体类如下:

self = {MultiChannelPoller} <kombu.transport.redis.MultiChannelPoller object at 0x7fc2522297f0>

_register会把channel,socket fd的信息结合起来,作用就是:如果对应的socket fd有poll,就会调用对应的channel。

def _register(self, channel, client, type):
    if (channel, client, type) in self._chan_to_sock:
        self._unregister(channel, client, type)
    if client.connection._sock is None:   # not connected yet.
        client.connection.connect()
    sock = client.connection._sock
    self._fd_to_chan[sock.fileno()] = (channel, type)
    self._chan_to_sock[(channel, client, type)] = sock
    self.poller.register(sock, self.eventflags)

具体_subscribe就是与具体redis联系,进行注册。

这样,对于 consumer 来说,redis 也联系上了,poll 也联系上了,下面就可以消费了。

def _subscribe(self):
    keys = [self._get_subscribe_topic(queue)
            for queue in self.active_fanout_queues]
    if not keys:
        return
    c = self.subclient
    if c.connection._sock is None:
        c.connection.connect()
    self._in_listen = c.connection
    c.psubscribe(keys)

堆栈如下:

_subscribe, redis.py:663
_register_LISTEN, redis.py:322
get, redis.py:375
drain_events, base.py:960
drain_events, connection.py:318
main, node.py:24
<module>, node.py:29

相应变量如下,这里 client 是 redis 驱动的 PubSub 对象:

c = {PubSub} <redis.client.PubSub object at 0x7fc252264400>

keys = {list: 1} ['/0.testMailbox.pidbox']

self = {Channel} <kombu.transport.redis.Channel object at 0x7fc252239908>

此时逻辑如下:

                                                                                                                                                                +
user scope         +      Kombu                                                                                                                                 |          redis
                   |                                                                               psubscribe                                                   |
                   |                                                                                                                                            |           +----------------------------+
        +--------------------->   drain_events   +--------------------------------------------------------------------------------------------------------------------->    |  '/0.testMailbox.pidbox'   |
        |          |                                                                                                                                            |           +----------------------------+
        |          |                                                                                                                                            |
        |          |                                                   +-----------------------+    +-----------------------+                                   |
        |          |                                                   | Transport             |    | MultiChannelPoller    |                                   |
        |          |                                                   |                       |    |                       |                                   |
        |          |       +--------------------------------------+    |            cycle +-------> |           _channels +----+                                |
        |          |       |                                      |    |                       |    +-----------------------+  |                                |
 +------+-----+    |       | Connection: redis://localhost:6379// |    |            channels +--------+                        v                                |
 | Connection|-----------> |                                      |    |                       |      |      +-----------------+---+                            |
 +------------+    |       |                                      |    |     _avail_channels+---------+      | Channel             |  <------------+            |
                   |       |                       connection+-------> |                       |      |      |                     |               |            |
                   |       |                                      |    +-----------------------+      |      |     _active_queues +------------------------+    |
                   |       +--------------------------------------+                                   |      |                     |               |       |    |
                   |                                                                                  +----->+            cycle +------>  +--------+--+    |    |
                   |                                               +----------------------------+            |                     |      | FairCycle |    |    |
                   |                                               | Exchange                   |            |                     |      +-----------+    |    |
                   |       +------------------------------+   +--> |                            |  <-----+   |       _queue_cycle+-----------+             |    |
 +---------+       |       | Mailbox                      |   |    | testMailbox.pidbox(fanout) |        |   |                     |         |             |    |
 | mailbox|--------------> |                              |   |    +----------------------------+        |   |           handlers  |         v             |    |
 +---------+       |       |                              |   |                                          |   |               +     |      round_robin_cycle|    |
                   |       |        exchange  +---------------+    +---------------------------------+   |   +-+--+----------------+                       |    |
                   |       |                              |        | Exchange                        |   |     ^  ^          |                             |    |
                   |       |        reply_exchange +------------>  |                                 |   |     |  |          |                             |    |
                   |       |                              |        | reply.testMailbox.pidbox(direct)|   |     |  |          |                             |    |
                   |       |        reply_queue +-------------+    +-------------------+-------------+   |     |  |          v                             |    |
                   |       |                              |   |                        ^                 |     |  |    +-----+-------------------------+   |    |
                   |       |                              |   |    +--------+          |                 |     |  |    |  'BRPOP': Channel._brpop_read |   |    |
                   |       +-------------------------+----+   +--> | Queue  +----------+                 |     |  |    |                               |   |    |
                   |                                 ^             +--------+                            |     |  |    |  'LISTEN': Channel._receive   |   |    |
                   |                                 |                                                   |     |  |    |                               |   |    |  +----------------------------------------------------+
                   |       +---------------------+   |                                                   |     |  |    +-------------------------------+   |    |  |      _kombu.binding.testMailbox.pidbox             |
 +-----+           |       |                     |   |                                                   |     |  |                                        |    |  |                                                    |
 |node | +---------------->+ Node      channel+-------------------------------------------------------------------+                                        |    |  |                                                    |
 +-----+           |       |                     |   |                                                   |     |                                           |    |  |   "\x06\x16\x06\x16localhost.testMailbox.pidbox"   |
                   |       |           mailbox +-----+                                                   |     |                                           |    |  |                                                    |
                   |       |                     |        +----------------------------------------------------+                                           |    |  +---------+------------------------------------------+
                   |       +---------------------+        |                                              |                                                 |    |            ^
                   |                                      |                                              |                                                 |    |            |
                   |                                      |                                              |                                                 |    |            |
                   |       +------------------------+     |         +-----------------------------------------------------------------------------+        |    |            |
+----------+       |       |                        |     |         | Queue                              |                                        |        |    |            |
| consumer |       |       | Consumer    channel  +-------+         |                                    +                                        |  <-----+    |            |
+----------+       |       |                        |               |                                 exchange                                    |             |            |
                   |       |             queues  +--------------->  |                                                                             |             |            |
                   |       |                        |               |                                                                             | +------------------------+
+-----------+      |       |             callbacks  |               |     <localhost.testMailbox.pidbox -> Exchange testMailbox.pidbox(fanout)>   |             |
| callback  |      |       |                  +     |               |                                                                             |             |
+------+----+      |       +------------------------+               +-----------------------------------------------------------------------------+             |
       ^           |                          |                                                                                                                 |
       |           |                          |                                                                                                                 |
       +--------------------------------------+                                                                                                                 +
                   |

手机如下

3.2.2.2 消费

前小节提到了,handle_event 之中会具体读取redis,进行消费。

当接受到信息之后,会调用如下:

def _deliver(self, message, queue):
    try:
        callback = self._callbacks[queue]
    except KeyError:
        self._reject_inbound_message(message)
    else:
        callback(message)

堆栈如下:

_deliver, base.py:975
_receive_one, redis.py:721
_receive, redis.py:692
on_readable, redis.py:358
handle_event, redis.py:362
get, redis.py:380
drain_events, base.py:960
drain_events, connection.py:318
main, node.py:24
<module>, node.py:29

此时变量如下,就是 basic_consume 之中的 _callback :

self._callbacks = {dict: 1} 
 'localhost.testMailbox.pidbox' = {function} <function Channel.basic_consume.<locals>._callback at 0x7fc2522c1840>

继续调用,处理信息

def receive(self, body, message):
    """Method called when a message is received.

    This dispatches to the registered :attr:`callbacks`.

    Arguments:
        body (Any): The decoded message body.
        message (~kombu.Message): The message instance.

    Raises:
        NotImplementedError: If no consumer callbacks have been
            registered.
    """
    callbacks = self.callbacks
    [callback(body, message) for callback in callbacks]

堆栈如下:

receive, messaging.py:583
_receive_callback, messaging.py:620
_callback, base.py:630
_deliver, base.py:980
_receive_one, redis.py:721
_receive, redis.py:692
on_readable, redis.py:358
handle_event, redis.py:362
get, redis.py:380
drain_events, base.py:960
drain_events, connection.py:318
main, node.py:24
<module>, node.py:29

变量如下:

body = {dict: 5} {'method': 'print_msg', 'arguments': {'msg': 'Message for you'}, 'destination': None, 'pattern': None, 'matcher': None}

message = {Message} <Message object at 0x7fc2522e20d8 with details {'state': 'RECEIVED', 'content_type': 'application/json', 'delivery_tag': '7dd6ad01-4162-42c3-b8db-bb40dc7dfda0', 'body_length': 119, 'properties': {}, 'delivery_info': {'exchange': 'testMailbox.pidbox', 'rout

self = {Consumer} <Consumer: [<Queue localhost.testMailbox.pidbox -> <Exchange testMailbox.pidbox(fanout) bound to chan:1> ->  bound to chan:1>]>

最后调用到用户方法:

callback, node.py:15
<listcomp>, messaging.py:586
receive, messaging.py:586
_receive_callback, messaging.py:620
_callback, base.py:630
_deliver, base.py:980
_receive_one, redis.py:721
_receive, redis.py:692
on_readable, redis.py:358
handle_event, redis.py:362
get, redis.py:380
drain_events, base.py:960
drain_events, connection.py:318
main, node.py:24
<module>, node.py:29

这样,mailbox consumer 端分析完毕。

0x04 Producer

Producer 就是发送邮件,此处逻辑要简单许多。

代码如下:

def main(arguments):
    connection = kombu.Connection('redis://localhost:6379')
    mailbox = pidbox.Mailbox("testMailbox", type="fanout")
    bound = mailbox(connection)
    bound._broadcast("print_msg", {'msg': 'Message for you'})

4.1 Mailbox

现在位于Mailbox,可以看到就是调用 _publish。

def _broadcast(self, command, arguments=None, destination=None,
               reply=False, timeout=1, limit=None,
               callback=None, channel=None, serializer=None,
               pattern=None, matcher=None):

    arguments = arguments or {}
    reply_ticket = reply and uuid() or None
    chan = channel or self.connection.default_channel

    # Set reply limit to number of destinations (if specified)
    if limit is None and destination:
        limit = destination and len(destination) or None

    serializer = serializer or self.serializer
    self._publish(command, arguments, destination=destination,
                  reply_ticket=reply_ticket,
                  channel=chan,
                  timeout=timeout,
                  serializer=serializer,
                  pattern=pattern,
                  matcher=matcher)

    if reply_ticket:
        return self._collect(reply_ticket, limit=limit,
                             timeout=timeout,
                             callback=callback,
                             channel=chan)

变量如下:

arguments = {dict: 1} {'msg': 'Message for you'}
self = {Mailbox} <kombu.pidbox.Mailbox object at 0x7fccf19514e0>

继续调用 _publish,其中如果需要回复,则做相应设置,否则直接调用 producer 进行发送。

def _publish(self, type, arguments, destination=None,
             reply_ticket=None, channel=None, timeout=None,
             serializer=None, producer=None, pattern=None, matcher=None):
    message = {'method': type,
               'arguments': arguments,
               'destination': destination,
               'pattern': pattern,
               'matcher': matcher}
    chan = channel or self.connection.default_channel
    exchange = self.exchange
    if reply_ticket:
        maybe_declare(self.reply_queue(channel))
        message.update(ticket=reply_ticket,
                       reply_to={'exchange': self.reply_exchange.name,
                                 'routing_key': self.oid})
    serializer = serializer or self.serializer
    with self.producer_or_acquire(producer, chan) as producer:
        producer.publish(
            message, exchange=exchange.name, declare=[exchange],
            headers={'clock': self.clock.forward(),
                     'expires': time() + timeout if timeout else 0},
            serializer=serializer, retry=True,
        )

此时变量如下:

exchange = {Exchange} Exchange testMailbox.pidbox(fanout)
message = {dict: 5} {'method': 'print_msg', 'arguments': {'msg': 'Message for you'}, 'destination': None, 'pattern': None, 'matcher': None}

4.2 producer

下面产生了producer。于是由producer进行操作。

def _publish(self, body, priority, content_type, content_encoding,
             headers, properties, routing_key, mandatory,
             immediate, exchange, declare):
    channel = self.channel
    message = channel.prepare_message(
        body, priority, content_type,
        content_encoding, headers, properties,
    )

    # handle autogenerated queue names for reply_to
    reply_to = properties.get('reply_to')
    if isinstance(reply_to, Queue):
        properties['reply_to'] = reply_to.name
    return channel.basic_publish(
        message,
        exchange=exchange, routing_key=routing_key,
        mandatory=mandatory, immediate=immediate,
    )

4.3 Channel

继续执行到 Channel,就是要对 redis 进行处理了。

def basic_publish(self, message, exchange, routing_key, **kwargs):
    """Publish message."""
    self._inplace_augment_message(message, exchange, routing_key)
    if exchange:
        return self.typeof(exchange).deliver(  # 这里
            message, exchange, routing_key, **kwargs
        )
    # anon exchange: routing_key is the destination queue
    return self._put(routing_key, message, **kwargs)

4.4 FanoutExchange

直接用 Exchange 进行发送。

class FanoutExchange(ExchangeType):
    type = 'fanout'

    def lookup(self, table, exchange, routing_key, default):
        return {queue for _, _, queue in table}

    def deliver(self, message, exchange, routing_key, **kwargs):
        if self.channel.supports_fanout:
            self.channel._put_fanout(
                exchange, message, routing_key, **kwargs)

4.5 Channel

流程进入到 Channel,这时候调用 redis 驱动进行发送。

def _put_fanout(self, exchange, message, routing_key, **kwargs):
    """Deliver fanout message."""
    with self.conn_or_acquire() as client:
        client.publish(
            self._get_publish_topic(exchange, routing_key),
            dumps(message),
        )

4.6 redis 驱动

最后,redis 驱动进行发送。

def publish(self, channel, message):
    """
    Publish ``message`` on ``channel``.
    Returns the number of subscribers the message was delivered to.
    """
    return self.execute_command('PUBLISH', channel, message)

关键变量如下:

channel = {str} '/0.testMailbox.pidbox'
message = {str} '{"body": "eyJtZXRob2QiOiAicHJpbnRfbXNnIiwgImFyZ3VtZW50cyI6IHsibXNnIjogIk1lc3NhZ2UgZm9yIHlvdSJ9LCAiZGVzdGluYXRpb24iOiBudWxsLCAicGF0dGVybiI6IG51bGwsICJtYXRjaGVyIjogbnVsbH0=", "content-encoding": "utf-8", "content-type": "application/json", "headers": {"cloc
self = {Redis} Redis<ConnectionPool<Connection<host=localhost,port=6379,db=0>>>

0xEE 个人信息

★★★★★★关于生活和技术的思考★★★★★★

微信公众账号:罗西的思考

如果您想及时得到个人撰写文章的消息推送,或者想看看个人推荐的技术资料,敬请关注。

0xFF 参考

Kombu 源码解析一

Kombu 源码解析二

Kombu 源码解析三

Kombu 源码解析四

Kombu 源码解析五