kombu 连接池详解

起因

使用 nameko 的时候,想看看 nameko 的连接复用原理(指的是和 rabbitmq 的 amqp 网络连接的复用)

一般连接复用有两种方案:

  • TLS(Thread Local Storage)
  • 连接池

第一种方案,实现最简单,但是有局限性。比如使用线程池的情况下才有用,比如使用协程、或者无复用的线程就不合适了

第二种方案,连接池是最通用的方案,但也是最麻烦的方案。

那 nameko 使用的是哪种方案呢?答案是:连接池

好了,既然我们已经知道了这个事实,在深入这个事实之前,先来了解一下 kombu 的连接池机制吧!

nameko 获取连接

site-packages/nameko/amqp/publish.py

import warnings
from contextlib import contextmanager

from kombu import Connection
from kombu.exceptions import ChannelError
from kombu.pools import connections, producers

from nameko.constants import (
    DEFAULT_RETRY_POLICY, DEFAULT_TRANSPORT_OPTIONS, PERSISTENT
)


@contextmanager
def get_connection(amqp_uri, ssl=None, login_method=None, transport_options=None):
    if not transport_options:
        transport_options = DEFAULT_TRANSPORT_OPTIONS.copy()
    conn = Connection(
        amqp_uri, transport_options=transport_options, ssl=ssl,
        login_method=login_method
    )
    with connections[conn].acquire(block=True) as connection:
        yield connection

我看到 conn = Connection() 的时候,觉得,这不是每次调用 get_connection 都会创建 amqp 连接吗?这不得完蛋吗?还连接复用啥?还池化个鬼?

真相是什么呢?

conn = Connection() 实例化,直接创建一个连接对象,但是并不会创建网络连接(不会发起 TCP 连接请求,可以理解为这个连接是惰性的,只有真的使用的时候,才会创建网络连接)

好了,这句代码讲清楚了

就该下面这句话了:

with connections[conn].acquire(block=True) as connection:
    yield connection

看起来是不是不好理解?

这里先要解释两个东西:

  • kombu connection 的 poolgroup

每次调用 get_connection 都会创建一个 conn 对象,然后

from kombu.pools import connections
from kombu import Connection

uri = 'amqp://pon:[email protected]:5672//'

connection = Connection(uri)

with connections[connection].acquire(block=True) as conn:
    pass

换成下面这样就好理解了

from kombu.pools import connections
from kombu.connection import ConnectionPool
from kombu import Connection

uri = 'amqp://pon:[email protected]:5672//'

connection = Connection(uri)


def get_connection_pool(connection: Connection) -> ConnectionPool:
    """
    connections 是 Connections 的实例, Connections 是 PoolGroup 的子类
    """
    connection_pool: ConnectionPool = connections[connection]
    return connection_pool


def get_connection_from_pool(pool: ConnectionPool) -> Connection:
    return pool.acquire(block=True)


def get_connection_from_pool_group(connection: Connection) -> Connection:
    return get_connection_from_pool(get_connection_pool(connection))


with get_connection_from_pool_group(connection) as conn:
    pass

每次都会 connections[connection] 会不会有问题呢?

其实不会哦,因为 connections 这个 dict 的子类的 __setitem__ 方法被重写了
site-packages/kombu/utils/collections.py

class EqualityDict(dict):
    """Dict using the eq operator for keying."""

    def __getitem__(self, key):
        h = eqhash(key)
        if h not in self:
            return self.__missing__(key)
        return super().__getitem__(h)

    def __setitem__(self, key, value):
        return super().__setitem__(eqhash(key), value)

    def __delitem__(self, key):
        return super().__delitem__(eqhash(key))
connections 是 Connections 的实例,Connections 是 PoolGroup 的子类,PoolGroup 是 EqualityDict 的子类,EqualityDict 是 dict 的子类

connections[connection] 的时候,会执行 EqualityDict 的 __setitem__ 方法,可以看到,调用 dict 的 __setitem__ 方法的时候,会调用 eqhash 来获取 connection 的 hash 值

eqhash 是什么呢?
site-packages/kombu/utils/collections.py

def eqhash(o):
    """Call ``obj.__eqhash__``."""
    try:
        return o.__eqhash__()
    except AttributeError:
        return hash(o)

那 connection 的 __eqhash__ 是什么呢?
site-packages/kombu/connection.py

def __eqhash__(self):
    return HashedSeq(self.transport_cls, self.hostname, self.userid,
                     self.password, self.virtual_host, self.port,
                     repr(self.transport_options))

可以看到,eqhash 是使用 connection 的一些连接参数来作为 hash 函数的入参,如果只要我们配置的 connection 的连接参数一样,就不用担心,重复创建 connection pool 的问题。

连接池的基本功能

连接池的基本功能:获取连接和放回连接

让我们一起来看看 kombu 为这两个基本主题给出的解决方案吧:

获取连接

site-packages/kombu/connection.py

def __enter__(self):
    return self

放回连接

with get_connection_from_pool_group(connection) as conn:
    pass

在这样的上下文管理器中,当退出 with body 的时候,会执行 conn 的 __exit__ 方法

site-packages/kombu/connection.py

def __exit__(self, *args):
    self.release()

可以看到,执行的是 release 方法,来看看 Connection 的 release 方法

site-packages/kombu/connection.py

def release(self):
    """Close the connection (if open)."""
    self._close()
close = release

Connection 的 release 方法调用了 _close 方法
site-packages/kombu/connection.py

def _close(self):
    """Really close connection, even if part of a connection pool."""
    self._do_close_self()
    self._do_close_transport()
    self._debug('closed')
    self._closed = True

_close 关闭了连接。

这可不对哦,连接不应该被关闭,而是因为放回连接池哦

那 connection 是被关闭了,而不是被放回 pool?

当然不是,

class ConnectionPool(Resource):
    """Pool of connections."""

当我们执行 ConnectionPool 的 acquire 方法的时候,其实执行的是从 Resource 继承的 acquire 方法,来看看 acquire 方法的内容吧:

def acquire(self, block=False, timeout=None):
    """Acquire resource.

    Arguments:
        block (bool): If the limit is exceeded,
            then block until there is an available item.
        timeout (float): Timeout to wait
            if ``block`` is true.  Default is :const:`None` (forever).

    Raises:
        LimitExceeded: if block is false and the limit has been exceeded.
    """
    if self._closed:
        raise RuntimeError('Acquire on closed pool')
    if self.limit:
        while 1:
            try:
                R = self._resource.get(block=block, timeout=timeout)
            except Empty:
                self._add_when_empty()
            else:
                try:
                    R = self.prepare(R)
                except BaseException:
                    if isinstance(R, lazy):
                        # not evaluated yet, just put it back
                        self._resource.put_nowait(R)
                    else:
                        # evaluted so must try to release/close first.
                        self.release(R)
                    raise
                self._dirty.add(R)
                break
    else:
        R = self.prepare(self.new())

    def release():
        """Release resource so it can be used by another thread.

        Warnings:
            The caller is responsible for discarding the object,
            and to never use the resource again.  A new resource must
            be acquired if so needed.
        """
        self.release(R)
    R.release = release

    return R

看到了吗?当我们从 pool 中获取 connection 的时候,connection 原来的 release 方法被替换掉了,当退出上下文的时候,执行的是偷天换日后的 release。而这个 release 是不关闭网络连接的。

管理正在使用中的连接

这显然也是一个重要的话题,毕竟我们要控制 pool 的大小

这个逻辑里可以从 Resource 类的 acquire 和 release 方法中了解其中的脉络

while 1:
    try:
        R = self._resource.get(block=block, timeout=timeout)
    except Empty:
        self._add_when_empty()
    else:
        try:
            R = self.prepare(R)
        except BaseException:
            if isinstance(R, lazy):
                # not evaluated yet, just put it back
                self._resource.put_nowait(R)
            else:
                # evaluted so must try to release/close first.
                self.release(R)
            raise
        self._dirty.add(R)
        break

当一个连接被弹出的时候,会执行 self._dirty.add(R) 将其添加到 self._dirty 中(self._dirty 的 type 是 set);当一个连接需要被放回 pool 中的时候,会执行 self._dirty.discard(resource)

失效连接怎么办?

这个连接保熟吗?

在原有连接上重新连接

你可能感兴趣的:(python)