oslo.messaging 0x01: kombu

oslo.messaging 中的 kombu

代码结构图如下:

                                                            +----------------------------------------+
                                                            |                                        |
nova.rpc.get_server --> messaging.server.get_rpc_server --> | messaging.server.MessageHandlingServer |
                                                            |                                        |
                                                            |   self.transport                       |
                                                            |         |                              |
                                                            +---------+------------------------------+
                                                                      |                                                                     
                                                                      v                                                                     
+----------------------------------------------+            +----------------------------------------+            +--------------------------+
|                                              |            |                                        |            |                          |
| messaging._drivers.impl_rabbit.RabbitDriver  |            | messaging.transport.Transport          |            |  messaging.rpc.RPCClient |    <---- nova.rpc.get_client
|                                              |            |                                        |            |                          |
|    self._connection_pool                     | <----------+-- self._driver                         |  <---------+---- self.transport       |
|            |                                 |            |                                        |            |                          |
+------------+---------------------------------+            +----------------------------------------+            |     self.call()          |
             |                                                                                                    |     self.cast()          |
             v                                                                                                    |                          | 
+----------------------------------------------+            +-------------------------------------------+         +--------------------------+ 
|                                              |            |                                           |
| messaging._driver.amqp.ConnectionPool        |            | messaging._drivers.impl_rabbit.Connection |
|                                              |            |                                           |
|   self.connection_cls -----------------------+----------> |   self.connection ------------------------+------------> kombu.connection.Connection()
|   self.create()                              |            |                                           |
|   self.get()                                 |            |   self.declare_consumer()        |        |              +------------------------------------------------+
|   self.put()                                 |            |   self.declare_direct_consumer() +--------+------------> |                                                |
+----------------------------------------------+            |   self.declare_fanout_consumer() |        |              | messaging._drivers.impl_rabbit.DirectConsumer  |
                                                            |   self.declare_topic_consumer()  |        |              | messaging._drivers.impl_rabbit.FanoutConsumer  |
                                                            |                                           |              | messaging._drivers.impl_rabbit.TopicConsumer   |
                                                            |   self.consumers                          |              |                                                |           
                                                            |   self.consume()                          |              |    self.exchange  -----------------------------+-------->  kombu.entity.Exchange()
                                                            |                                           |              |                                                |           
                                                            |                                           |              |    self.queue ---------------------------------+-------->  kombu.entity.Queue()
                                                            |   |self.direct_send()                     |              |                                                |           
                                                            |   |self.fanout_send()                     |              |    self.consume()  ----------------------------+-------->  kombu.entity.Queue().consume()
                                                            |   |self.topic_send()                      |              |                                                |           
                                                            |   |self.notify_send()                     |              +------------------------------------------------+           
                                                            |   |self.publisher_send()                  |                                                                         
                                                            |           |                               |
                                                            +-----------+-------------------------------+                                                                         
                                                                        |                                                                                                      
                                                                        |                                                                                                       
                                                                        v                                                                                                       

                                                            +------------------------------------------------+                                                        
                                                            |                                                |
                                                            | messaging._drivers.impl_rabbit.DirectPublisher |                                        
                                                            | messaging._drivers.impl_rabbit.FanoutPublisher |
                                                            | messaging._drivers.impl_rabbit.TopicPublisher  |
                                                            | messaging._drivers.impl_rabbit.NotifyPublisher |
                                                            |                                                |
                                                            |   self.exchange -------------------------------+--------->  kombu.entity.Exchange()
                                                            |                                                |
                                                            |   self.producer -------------------------------+--------->  kombu.messaging.Producer()
                                                            |                                                |
                                                            |   self.send() ---------------------------------+--------->  kombu.messaging.Producer().publish()
                                                            |                                                |
                                                            +------------------------------------------------+

Transport

以 nova 为例,每个 nova-* 进程有一个全局的 Transport 实例,该实例是对消息库如
kombu 的封装。该全局的实例在 nova/rpc.py 中创建。

每个进程的 main() 都会有这一句:

config.parse_args(sys.argv)

parse_args 中调用了rpc.init:

def parse_args(argv, default_config_files=None):

    ... ... 
    rpc.init(CONF)

下面看一下 rpc.init 如何初始化 Transport 实例:

def init(conf):
    global TRANSPORT, NOTIFIER
    exmods = get_allowed_exmods()
    # 调用 oslo.messaging 接口来创建 Transport 实例。
    TRANSPORT = messaging.get_transport(conf,
                                        allowed_remote_exmods=exmods,
                                        aliases=TRANSPORT_ALIASES)

get_transport 根据配置加载相应的消息库 driver,配置与 driver 的对应关系
在 setup.cfg 中定义:

# oslo.messaging/setup.cfg
[entry_points]
oslo.messaging.drivers =
    rabbit = oslo.messaging._drivers.impl_rabbit:RabbitDriver
    kombu = oslo.messaging._drivers.impl_rabbit:RabbitDriver


# oslo/messaging/transport.py
# get_transport 使用 stevedore 来管理消息库模块
from stevedore import driver
def get_transport(conf, url=None, allowed_remote_exmods=None, aliases=None):
    try:
        mgr = driver.DriverManager('oslo.messaging.drivers',
                                url.transport.split('+')[0],
                                invoke_on_load=True,
                                invoke_args=[conf, url],
                                invoke_kwds=kwargs)
    except RuntimeError as ex:
        raise DriverLoadFailure(url.transport, ex)

    return Transport(mgr.driver)

Transport 实例的几个方法,分别去直接调用相应消息库 driver 的方法。

# oslo/messaging/transport.py
class Transport(object):
    def _send(self, target, ctxt, message, wait_for_reply=None, timeout=None,
            retry=None):
        if not target.topic:
            raise exceptions.InvalidTarget('A topic is required to send',
                                        target)
        return self._driver.send(target, ctxt, message,
                                wait_for_reply=wait_for_reply,
                                timeout=timeout, retry=retry)

    def _listen(self, target):
        if not (target.topic and target.server):
            raise exceptions.InvalidTarget('A server\'s target must have '
                                        'topic and server names specified',
                                        target)
        return self._driver.listen(target)

MessageHandlingServer

每个充当 rpcserver 角色的 nova-* 进程,都有自己的 MessageHandlingServer 实例。
创建 MessageHandlingServer 实例时将全局的 Transport 实例传递进去,
MessageHandlingServer 对与消息服务器的交互都通过 Transport 实例的方法来完成,
具体来说主要就是 Transport()._listen()。

#nova/service.py
class Service(service.Service):
    ... ...
    def start(self):
        ... ...
        self.rpcserver = rpc.get_server(target, endpoints, serializer)
        self.rpcserver.start()

# nova/rpc.py
def get_server(target, endpoints, serializer=None):
    assert TRANSPORT is not None
    serializer = RequestContextSerializer(serializer)
    return messaging.get_rpc_server(TRANSPORT,
                                    target,
                                    endpoints,
                                    executor='eventlet',
                                    serializer=serializer)

# oslo/messaging/rpc/server.py
def get_rpc_server(transport, target, endpoints,
                executor='blocking', serializer=None):
    dispatcher = rpc_dispatcher.RPCDispatcher(target, endpoints, serializer)
    return msg_server.MessageHandlingServer(transport, dispatcher, executor)

在服务启动的时候,调用 Transport 实例的 _listen 方法。该方法会通过底层的
消息库在消息服务器上创建消息交换机与相关队列。

# MessageHandlingServer
def start(self):
    try:
        listener = self.dispatcher._listen(self.transport)
    except driver_base.TransportDriverError as ex:
        raise ServerListenError(self.target, ex)

# RPCDispatcher
def _listen(self, transport):
    return transport._listen(self._target)

RPCClient

需要往消息服务器发送消息时,生成 RPCClient 的实例,然后调用实例的 call 与 cast
方法来进行消息的发送。而这两个方法,则直接调用 Transport()._send() 方法。

# oslo/messaging/rpc/client.py
class RPCClient(object):

    def call(self, ctxt, method, **kwargs):
        ... ...
        try:
            result = self.transport._send(self.target, msg_ctxt, msg,
                                        wait_for_reply=True, timeout=timeout,
                                        retry=self.retry)
        except driver_base.TransportDriverError as ex:
            raise ClientSendError(self.target, ex)
        ... ...


    def cast(self, ctxt, method, **kwargs):
        ... ...
        try:
            self.transport._send(self.target, ctxt, msg, retry=self.retry)
        except driver_base.TransportDriverError as ex:
            raise ClientSendError(self.target, ex)
        ... ...

RabbitDriver

RabbitDriver 是对 kombu 消息库封装的一部分,结合连接封装 Connection 和连接池封装
ConnectionPool 对外提供消息的控制接口。 接口交付于 Transport 实例。

# oslo/messaging/_drivers/impl_rabbit.py 和 oslo/messaging/_drivers/amqpdriver.py
class RabbitDriver(amqpdriver.AMQPDriverBase):

    def __init__(self, conf, url,
                default_exchange=None,
                allowed_remote_exmods=None):
        # RabbitDriver 实例在初始化时,创建一个连接池,连接池 ConnectionPool
        # 的实现后续介绍
        connection_pool = rpc_amqp.get_connection_pool(conf, url, Connection)

            ... ...
        self._connection_pool = connection_pool

    # 发送消息的接口, 上文已提到 Transport 实例的 _send 方法调用该方法。
    def send(self, target, ctxt, message, wait_for_reply=None, timeout=None,
            retry=None):
        return self._send(target, ctxt, message, wait_for_reply, timeout,
                        retry=retry)

    def _send(self, target, ctxt, message,
            wait_for_reply=None, timeout=None,
            envelope=True, notify=False, retry=None):

        try:
            # get_connection 从 connection_pool 中获取一个可用连接 Connection 实例。
            with self._get_connection() as conn:
                # 对应不同的消息发送方式,分别调用 Connection 实例的发送方法。
                if notify:
                    conn.notify_send(self._get_exchange(target),
                                    target.topic, msg, retry=retry)
                elif target.fanout:
                    conn.fanout_send(target.topic, msg, retry=retry)
                else:
                    topic = target.topic
                    if target.server:
                        topic = '%s.%s' % (target.topic, target.server)
                    conn.topic_send(exchange_name=self._get_exchange(target),
                                    topic=topic, msg=msg, timeout=timeout,
                                    retry=retry)
            ... ...

    # 建立对消息的监听,上文已提到 Transport 实例的 _listen 方法会调用该方法。
    def listen(self, target):
        # 首先从连接池中取到一个可用连接。
        conn = self._get_connection(pooled=False)

        # 创建一个工厂类来做为后续所有 Consumer 的回调函数,负责处理收到的消息。
        # AMQPListener 定义了 __call__ 方法,可以直接调用。
        listener = AMQPListener(self, conn)

        # 通过 Connection 实例的 declare_* 方法创建消息消费者
        conn.declare_topic_consumer(exchange_name=self._get_exchange(target),
                                    topic=target.topic,
                                    callback=listener)
        conn.declare_topic_consumer(exchange_name=self._get_exchange(target),
                                    topic='%s.%s' % (target.topic,
                                                    target.server),
                                    callback=listener)
        conn.declare_fanout_consumer(target.topic, listener)

        return listener

Connection 和 ConnectionPool

前方已提到 Oslo.messaging 实现了自己的连接池机制,而没有使用 kombu 所提供的。
Oslo.messaging 的连接池是 collections.deque() 的数据结构。

ConnectionPool 的实现如下:

# oslo/messaging/_drivers/pool.py 和 oslo/messaging/_drivers/amqp.py
class ConnectionPool(pool.Pool):
    def __init__(self, max_size=4):
        ...  ...
        # 定义连接池连接数最大值,默认为 4
        self._max_size = max_size
        # 定前连接数
        self._current_size = 0
        self._cond = threading.Condition()
        # 连接池数据结构,放置所有的 Connection 实例。
        self._items = collections.deque()

    # 从连接池中获取一个连接的接口,注意该接口同时也是往连接池中新建连接的接口
    # 接口中没有明显的将连接添加进 self._items 数据结构,实际上一个连接在此方法
    # 中新建,当连接使用完毕进行释放时,会由 put 接口加到 self._items 中。
    def get(self):
        with self._cond:
            while True:
                try:
                    # 首先尝试从连接池数据结构中获取新的可用连接。
                    return self._items.popleft()
                except IndexError:
                    pass

                # 如果没有可用连接,且连接数未达上限,则允许跳出循环下文调用
                # create 方法新建连接。
                if self._current_size < self._max_size:
                    self._current_size += 1
                    break

                # 如果没有可用连接,且连接数已达上限,则只能等待连接放回连接池
                # 的信号,注意有超时限值。
                self._cond.wait(timeout=1)

        try:
            # 新建一个连接 Connection 实例
            return self.create()
        except Exception:
            with self._cond:
                # 建立失败,回滚连接池当前数量
                self._current_size -= 1
            raise

    # 新使用完的连接放回 self._items 数据结构。
    def put(self, item):
        """Return an item to the pool."""
        with self._cond:
            self._items.appendleft(item)
            self._cond.notify()

    # 新建连接实例
    def create(self):
        # connection_cls 对于 Rabbitmq 来说就是 messaging._drivers.impl_rabbit.Connection
        return self.connection_cls(self.conf, self.url)

ConnectionPool 与 Connection 实例之间有一个类型用于衔接,
上文略过未提的 get_connection 方法,可以看到它拿到的是 ConnectionContext 实例。

# oslo/messaging/_drivers/amqpdriver.py
def _get_connection(self, pooled=True):
        return rpc_amqp.ConnectionContext(self.conf,
                                        self._url,
                                        self._connection_pool,
                                        pooled=pooled)

ConnectionContext 实例所起的最主要作用即是协同维护 ConntionPool 的数据结构。

class ConnectionContext(rpc_common.Connection):

    def __init__(self, conf, url, connection_pool, pooled=True):
        ... ...
        self.connection_pool = connection_pool
        if pooled:
        # 从连接池获取连接
            self.connection = connection_pool.get()
        else:
        # ConnectionContext 的另一部分功能,处理不使用连接池的情况。
        # 直接创建真正的连接实例。
            self.connection = connection_pool.connection_cls(conf, url)
        # 标识是否使用了连接池,默认为 True。
        self.pooled = pooled

    def _done(self):
    # 还记得前文所说,连接池中的连接都是由 put 方法放回连接池结构中的吗。
    # 在各种场景下,销毁一个 ConnectionContext 都会走到这里,实际将 Connection
    # 实例通过 connection_pool.put() 方法将实例放回连接池。
        if self.connection:
            if self.pooled:
                # Reset the connection so it's ready for the next caller
                # to grab from the pool
                self.connection.reset()
                self.connection_pool.put(self.connection)
            else:
            # 如果不使用连接池,则可以真正去关闭连接。
                try:
                    self.connection.close()
                except Exception:
                    pass
            self.connection = None

    # 以下三个方法,保证各种销毁 ConnectionContext 时,都不真得销毁,而是
    # 调用 _done 方法。
    def __exit__(self, exc_type, exc_value, tb):
    # 对于与 with 语句搭配使用的情况时调用,释放 Connection
        self._done()

    def __del__(self):
    # 直接清理 ConnectionContext 实例时调用,释放 Connection
        self._done()

    def close(self):
    # 显式关闭连接时调用
        self._done()

通过以上代码的分析,可以理清连接池 ConnectionPool 的维护逻辑。下面可以看一下
Connection 实例的结构如何。

下面以 publisher_send 发送消息,与 declare_consumer 创建消息消费者为例
分别分析一下 Connection 实例。 direct_send 与 declare_direct_consumer
等都是调用这两个方法来工作的。

class Connection(object):

    def __init__(self, conf, url):
        ... ...
        # 真正的 kombu Connection 是本 Connection 实例封装的对象。
        self.connection = kombu.connection.Connection(
            self._url, ssl=self._ssl_params, login_method=self._login_method,
            failover_strategy="shuffle")
        ... ...

    def publisher_send(self, cls, topic, msg, timeout=None, retry=None,
                    **kwargs):
        ... ...
        def _publish():
        # 每次发送消息时,都会生成一个 Publisher 实例,并执行消息的发送动作
        # Publisher 实例实现见下文。
            publisher = cls(self.conf, self.channel, topic=topic, **kwargs)
            publisher.send(msg, timeout)

        # self.ensure 处理发送消息的重试与错误处理。
        self.ensure(_error_callback, _publish, retry=retry)

    def declare_consumer(self, consumer_cls, topic, callback):

        def _declare_consumer():
            # 创建 Consumer 实例,具体实例为 DirectConsumer 或 TopicConsumer 等。
            # DirectConsumer 见下文。
            consumer = consumer_cls(self.conf, self.channel, topic, callback,
                                    six.next(self.consumer_num))
            # 维护了 consumers 列表,包含本连接所有 Consumer
            self.consumers.append(consumer)
            return consumer

        # self.ensure 处理发送消息的重试与错误处理。
        return self.ensure(_connect_error, _declare_consumer)


class DirectPublisher(Publisher):

    def __init__(self, channel, exchange_name, routing_key, **kwargs):
        ... ...
        # 初始化时调用重连逻辑。
        self.reconnect(channel)

    def reconnect(self, channel):
        ... ...
        # 将创建 Exchange 与 Producer 的逻辑直接写在重连方法中,故初次连接与故障
        # 重连可以使用相同的一条逻辑。
        self.exchange = kombu.entity.Exchange(name=self.exchange_name,
                                            **self.kwargs)
        self.producer = kombu.messaging.Producer(exchange=self.exchange,
                                                channel=channel,
                                                routing_key=self.routing_key)
        ... ...

    def send(self, msg, timeout=None):
        ... ...
        # 调用底层的 Producer 来发送消息。
        self.producer.publish(msg, headers={'ttl': (timeout * 1000)})

class DirectConsumer(ConsumerBase):

    def __init__(self, conf, channel, msg_id, callback, tag, **kwargs):

            ... ...
        # 创建消息交换机。
        self.exchange = kombu.entity.Exchange(name=msg_id,
                                            type='direct',
                                            durable=options['durable'],
                                            auto_delete=options['auto_delete'])
            ... ...

    def reconnect(self, channel):
        # 将创建 Queue 的逻辑直接写在重连方法中,故初次连接与故障
        # 重连可以使用相同的一条逻辑。
        self.channel = channel
        self.kwargs['channel'] = channel
        self.queue = kombu.entity.Queue(**self.kwargs)
        try:
            # 主动建立将要订阅的队列。
            self.queue.declare()
        except Exception as e:
            ... ...

    def consume(self, *args, **kwargs):
        ... ...
        # 获取一个队列的消息,注意这里省略了 kombu.Consumer,而是直接使用 Queue 的底层接口。
        self.queue.consume(*args, callback=_callback, **options)
        ... ...