起因
使用 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:pon@124.222.178.120: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:pon@192.168.31.245: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)
失效连接怎么办?
这个连接保熟吗?
在原有连接上重新连接
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。