「源码解析」 消息队列Kombu基本架构综述

#: of connection failure (initialized by :attr:failover_strategy).

cycle = None

#: Additional transport specific options,

#: passed on to the transport instance.

transport_options = None

#: Strategy used to select new hosts when reconnecting after connection

#: failure. One of “round-robin”, “shuffle” or any custom iterator

#: constantly yielding new URLs to try.

failover_strategy = ‘round-robin’

#: Heartbeat value, currently only supported by the py-amqp transport.

heartbeat = None

failover_strategies = failover_strategies

4.3 Channel

===============

Channel:与AMQP中概念类似,可以理解成共享一个Connection的多个轻量化连接。就是真正的连接。

  • Connection 是 AMQP 对 连接的封装;

  • Channel 是 AMQP 对 MQ 的操作的封装;

Channel 可以认为是 redis 操作和连接的封装。每个 Channel 都可以与 redis 建立一个连接,在此连接之上对 redis 进行操作,每个连接都有一个 socket,每个 socket 都有一个 file,从这个 file 可以进行 poll。

4.3.1 定义

============

简化版定义如下:

class Channel(virtual.Channel):

“”“Redis Channel.”“”

QoS = QoS

_client = None

_subclient = None

keyprefix_queue = ‘{p}_kombu.binding.%s’.format(p=KEY_PREFIX)

keyprefix_fanout = ‘/{db}.’

sep = ‘\x06\x16’

_fanout_queues = {}

unacked_key = ‘{p}unacked’.format(p=KEY_PREFIX)

unacked_index_key = ‘{p}unacked_index’.format(p=KEY_PREFIX)

unacked_mutex_key = ‘{p}unacked_mutex’.format(p=KEY_PREFIX)

unacked_mutex_expire = 300 # 5 minutes

unacked_restore_limit = None

visibility_timeout = 3600 # 1 hour

max_connections = 10

queue_order_strategy = ‘round_robin’

_async_pool = None

_pool = None

from_transport_options = (

virtual.Channel.from_transport_options +

(‘sep’,

‘ack_emulation’,

‘unacked_key’,

‘max_connections’,

‘health_check_interval’,

‘retry_on_timeout’,

‘priority_steps’) # <-- do not add comma here!

)

connection_class = redis.Connection if redis else None

self.handlers = {‘BRPOP’: self._brpop_read, ‘LISTEN’: self._receive}

4.3.2 redis消息回调函数

=====================

关于上面成员变量,这里需要说明的是

handlers = {dict: 2}

{

‘BRPOP’: <bound method Channel._brpop_read of <kombu.transport.redis.Channel object at 0x7fe61aa88cc0>>,

‘LISTEN’: <bound method Channel._receive of <kombu.transport.redis.Channel object at 0x7fe61aa88cc0>>

}

这是redis有消息时的回调函数,即:

  • BPROP 有消息时候,调用 Channel._brpop_read;

  • LISTEN 有消息时候,调用 Channel._receive;

大约如下:

±--------------------------------------------------------------------------------------------------------------------------------------+

| ±-------------+ 6 parse_response |

| ±-> | Linux Kernel | ±–+ |

| | ±-------------+ | |

| | | |

| | | event |

| | 1 | |

| | | 2 |

| | | |

±------±–+ socket + | |

| redis | <------------> port ±-> fd ±–>+ v |

| | | ±-----±-------+ |

| | socket | | Hub | |

| | <------------> port ±-> fd ±–>----------> | | |

| port=6379 | | | | |

| | socket | | readers ±----> Transport.on_readable |

| | <------------> port ±-> fd ±–>+ | | + |

±----------+ ±--------------+ | |

| |

3 | |

±---------------------------------------------------------------------------------------+ |

| v

| _receive_callback

| 5 ±------------+ ±----------+

±-----------±-----+ ±------------------------+ ‘BRPOP’ = Channel._brpop_read ±----> | Channel | ±-----------------> | Consumer |

| Transport | | MultiChannelPoller | ±-----> channel . handlers ‘LISTEN’ = Channel._receive ±------------+ ±–±------+

| | | | | 8 |

| | on_readable(fileno) | | | ^ |

| cycle ±--------------------> | _fd_to_chan ±---------------> channel . handlers ‘BRPOP’ = Channel._brpop_read | |

| | 4 | | | ‘LISTEN’ = Channel._receive | |

| _callbacks[queue]| | | | | on_m | 9

| + | ±------------------------+ ±-----> channel . handlers ‘BRPOP’ = Channel._brpop_read | |

±------------------+ ‘LISTEN’ = Channel._receive | |

| | v

| 7 _callback |

±----------------------------------------------------------------------------------------------------------------------------------------+ User Function

手机如图:

4.4 Transport

=================

Transport:真实的 MQ 连接,也是真正连接到 MQ(redis/rabbitmq) 的实例。就是存储和发送消息的实体,用来区分底层消息队列是用amqp、Redis还是其它实现的。

我们顺着上文理一下:

  • Connection 是 AMQP 对 连接的封装;

  • Channel 是 AMQP 对 MQ 的操作的封装;

  • 那么两者的关系就是对 MQ 的操作必然离不开连接,但是 Kombu 并不直接让 Channel 使用 Connection 来发送/接受请求,而是引入了一个新的抽象 Transport,Transport 负责具体的 MQ 的操作,也就是说 Channel 的操作都会落到 Transport 上执行;

在Kombu 体系中,用 transport 对所有的 broker 进行了抽象,为不同的 broker 提供了一致的解决方案。通过Kombu,开发者可以根据实际需求灵活的选择或更换broker。

Transport负责具体操作,但是 很多操作移交给 loop 与 MultiChannelPoller 进行。

其主要成员变量为:

  • 本transport的驱动类型,名字;

  • 对应的 Channel;

  • cycle:MultiChannelPoller,具体下文会提到;

其中重点是MultiChannelPoller。一个Connection有一个Transport, 一个Transport有一个MultiChannelPoller,对poll操作都是由MultiChannelPoller完成,redis操作由channel完成。

定义如下:

class Transport(virtual.Transport):

“”“Redis Transport.”“”

Channel = Channel

polling_interval = None # disable sleep between unsuccessful polls.

default_port = DEFAULT_PORT

driver_type = ‘redis’

driver_name = ‘redis’

implements = virtual.Transport.implements.extend(

asynchronous=True,

exchange_type=frozenset([‘direct’, ‘topic’, ‘fanout’])

)

def init(self, *args, **kwargs):

super().init(*args, **kwargs)

All channels share the same poller.

self.cycle = MultiChannelPoller()

4.5 MultiChannelPoller

==========================

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

  • 收集 channel;

  • 建立 socks fd 到 channel 的映射;

  • 建立 channel 到 socks fd 的映射;

  • 使用 poll;

或者从逻辑上这么理解,MultiChannelPoller 就是:

  • 把 Channel 对应的 socket 同 poll 联系起来,一个 socket 在 linux 系统中就是一个file,就可以进行 poll 操作;

  • 把 poll 对应的 fd 添加到 MultiChannelPoller 这里,这样 MultiChannelPoller 就可以 打通 Channel —> socket —> poll —> fd —> 读取 redis 这条通路了,就是如果 redis 有数据来了,MultiChannelPoller 就马上通过 poll 得到通知,就去 redis 读取;

具体定义如下:

class MultiChannelPoller:

“”“Async I/O poller for Redis transport.”“”

eventflags = READ | ERR

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()

4.6 Consumer

================

Consumer 是消息接收者。Consumer & 相关组件 的作用主要如下:

  • Exchange:MQ 路由,消息发送者将消息发至 Exchange,Exchange 负责将消息分发至队列。

  • Queue:对应的队列抽象,存储着即将被应用消费掉的消息,Exchange 负责将消息分发 Queue,消费者从Queue 接收消息;

  • Consumers 是接受消息的抽象类,consumer 需要声明一个 queue,并将 queue 与指定的 exchange 绑定,然后从 queue 里面接收消息。就是说,从用户角度,知道了一个 exchange 就可以从中读取消息,而具体这个消息就是从 queue 中读取的。

在具体 Consumer 的实现中,它把 queue 与 channel 联系起来。queue 里面有一个 channel,用来访问redis,也有 Exchange,知道访问具体 redis 哪个key(就是queue对应的那个key)。

Consumer 消费消息是通过 Queue 来消费,然后 Queue 又转嫁给 Channel。

所以服务端的逻辑大致为:

  1. 建立连接;

  2. 创建Exchange ;

  3. 创建Queue,并将Exchange与Queue绑定,Queue的名称为routing_key ;

  4. 创建Consumer对Queue监听;

Consumer 定义如下:

class Consumer:

“”"Message consumer.

Arguments:

channel (kombu.Connection, ChannelT): see :attr:channel.

queues (Sequence[kombu.Queue]): see :attr:queues.

no_ack (bool): see :attr:no_ack.

auto_declare (bool): see :attr:auto_declare

callbacks (Sequence[Callable]): see :attr:callbacks.

on_message (Callable): See :attr:on_message

on_decode_error (Callable): see :attr:on_decode_error.

prefetch_count (int): see :attr:prefetch_count.

“”"

ContentDisallowed = ContentDisallowed

#: The connection/channel to use for this consumer.

channel = None

#: A single :class:~kombu.Queue, or a list of queues to

#: consume from.

queues = None

#: Flag for automatic message acknowledgment.

no_ack = None

#: By default all entities will be declared at instantiation, if you

#: want to handle this manually you can set this to :const:False.

auto_declare = True

#: List of callbacks called in order when a message is received.

callbacks = None

#: Optional function called whenever a message is received.

on_message = None

#: Callback called when a message can’t be decoded.

on_decode_error = None

#: List of accepted content-types.

accept = None

#: Initial prefetch count

prefetch_count = None

#: Mapping of queues we consume from.

_queues = None

_tags = count(1) # global

此时总体逻辑如下图:

±---------------------+ ±------------------+

| Consumer | | Channel |

| | | | ±----------------------------------------------------------+

| | | client ±------------> | Redis<ConnectionPool<Connection<host=localhost,port=6379> |

| channel ±-------------------> | | ±----------------------------------------------------------+

| | | pool |

| | ±--------> | | <------------------------------------------------------------+

| queues | | | | |

| | | ±—> | connection ±--------------+ |

| | | | | | | | |

±---------------------+ | | ±------------------+ | |

| | | v |

| | | ±------------------+ ±–±----------------+ ±-------------------+ |

| | | | Connection | | redis.Transport | | MultiChannelPoller | |

| | | | | | | | | |

| | | | | | | | _channels ±-------+

| | | | | | cycle ±-----------> | _fd_to_chan |

| | | | transport ±--------> | | | _chan_to_sock |

| ±------->+ | | | | | ±-----+ poller |

| | | ±------------------+ ±--------------------+ | | after_read |

| | | | | |

| | | | ±-------------------+

| | | ±-----------------+ ±--------------+

| | | | Hub | |

| | | | | v

| | | | | ±-----±-----+

| | | | poller ±--------------> | _poll |

| | | | | | | ±------+

| | | | | | _poller±--------> | poll |

v | | ±-----------------+ | | ±------+

| | ±------------+

±------------------+ | ±---------------+

| Queue | | | | Exchange |

| _chann+l | ±—+ | |

| | | |

| exchange ±---------------> | channel |

| | | |

| | | |

±------------------+ ±---------------+

手机如下:

现在我们知道:

4.7 Producer

================

Producer 是消息发送者。Producer中,主要变量是:

  • _channel :就是channel;

  • exchange :exchange;

class Producer:

“”"Message Producer.

Arguments:

channel (kombu.Connection, ChannelT): Connection or channel.

exchange (kombu.entity.Exchange, str): Optional default exchange.

routing_key (str): Optional default routing key.

“”"

#: Default exchange

exchange = None

#: Default routing key.

routing_key = ‘’

#: Default serializer to use. Default is JSON.

serializer = None

#: Default compression method. Disabled by default.

compression = None

#: By default, if a defualt exchange is set,

#: that exchange will be declare when publishing a message.

auto_declare = True

#: Basic return callback.

on_return = None

#: Set if channel argument was a Connection instance (using

#: default_channel).

connection = None

逻辑如图:

±---------------------+ ±------------------+

| Producer | | Channel |

| | | | ±----------------------------------------------------------+

| | | client ±------------> | Redis<ConnectionPool<Connection<host=localhost,port=6379> |

| channel ±-----------------> | | ±----------------------------------------------------------+

| | | pool |

| exchange | ±--------> | | <------------------------------------------------------------+

| | | | | |

| connection | | ±—> | connection ±--------------+ |

| + | | | | | | |

±-±------------------+ | | ±------------------+ | |

| | | | v |

| | | | ±------------------+ ±–±----------------+ ±-------------------+ |

| | | | | Connection | | redis.Transport | | MultiChannelPoller | |

| ±---------------------> | | | | | | |

| | | | | | | | _channels ±-------+

| | | | | | cycle ±-----------> | _fd_to_chan |

| | | | transport ±--------> | | | _chan_to_sock |

| ±------->+ | | | | | ±-----+ poller |

| | | ±------------------+ ±--------------------+ | | after_read |

| | | | | |

| | | | ±-------------------+

| | | ±-----------------+ ±--------------+

| | | | Hub | |

| | | | | v

| | | | | ±-----±-----+

| | | | poller ±--------------> | _poll |

| publish | | | | | | ±------+

±-------------------------------+ | | | _poller±--------> | poll |

| | | ±-----------------+ | | ±------+

| | | ±------------+

±------------------+ | ±----> ±---------------+

| Queue | | | | Exchange |

| _channel | ±--------+ | |

| | | |

| exchange ±-------------------> | channel |

| | | |

| | | |

±------------------+ ±---------------+

手机如图:

4.8 Hub

===========

用户可以通过同步方式自行读取消息,如果不想自行读取,也可以通过Hub(本身构建了一个异步消息引擎)读取。

4.8.1 自己的poller

===================

Hub 是一个eventloop,拥有自己的 poller。

前面在 MultiChannelPoller 中间提到了,MultiChannelPoller 会建立了自己内部的 poller。但是实际上在注册时候,Transport 会使用 hub 的 poller,而非 MultiChannelPoller 内部的 poller。

4.8.2 Connection

====================

Connection注册到Hub,一个Connection对应一个Hub。

hub = Hub()

conn = Connection(‘redis://localhost:6379’)

conn.register_with_event_loop(hub)

4.8.3 联系

============

在注册过程中,Hub 把自己内部的 poller 配置在 Transport 之中。这样就通过 transport 内部的 MultiChannelPoller 可以把 Hub . poller 和 Channel 对应的 socket 同poll联系起来,一个 socket 在 linux 系统中就是一个file,就可以进行 poll 操作;

因而,如前面所述,这样 MultiChannelPoller 就可以 打通 Channel —> socket —> poll —> fd —> 读取 redis 这条通路了,就是如果 redis 有数据来了,MultiChannelPoller 就马上通过 poll 得到通知,就去 redis 读取。

def register_with_event_loop(self, loop):

self.transport.register_with_event_loop(self.connection, loop)

4.8.4 定义

============

Hub定义如下:

class Hub:

“”"Event loop object.

“”"

def init(self, timer=None):

self.timer = timer if timer is not None else Timer()

self.readers = {}

self.writers = {}

self.on_tick = set()

self.on_close = set()

self._ready = set()

self._create_poller()

@property

def poller(self):

if not self._poller:

self._create_poller()

return self._poller

def _create_poller(self):

self._poller = poll()

self._register_fd = self._poller.register

self._unregister_fd = self._poller.unregister

def add(self, fd, callback, flags, args=(), consolidate=False):

fd = fileno(fd)

try:

self.poller.register(fd, flags)

except ValueError:

self._remove_from_loop(fd)

raise

else:

dest = self.readers if flags & READ else self.writers

if consolidate:

self.consolidate.add(fd)

dest[fd] = None

else:

dest[fd] = callback, args

def run_forever(self):

self._running = True

try:

while 1:

try:

self.run_once()

except Stop:

break

finally:

self._running = False

def run_once(self):

try:

next(self.loop)

except StopIteration:

self._loop = None

def create_loop(self, …):

readers, writers = self.readers, self.writers

poll = self.poller.poll

while 1:

for fd, event in events or ():

cb, cbargs = readers[fd]

if isinstance(cb, generator):

next(cb)

cb(*cbargs)

else:

no sockets yet, startup is probably not done.

sleep(min(poll_timeout, 0.1))

yield

0x05 总结

===========

我们通过文字和图例来总结下本文。

5.1 逻辑

==========

  • Message:消息,发送和消费的主体,其实就是我们所谓的一条条消息;

  • Connection是AMQP对消息队列连接的封装抽象,那么两者的关系就是:对MQ的操作必然离不开连接。

  • Channel是AMQP对MQ的操作的封装,可以理解成共享一个Connection的多个轻量化连接。Channel将Consumer标签,Consumer要消费的队列,以及标签与队列的映射关系都记录下来,等待循环调用。还通过Transport将队列与回调函数列表的映射关系记录下来。Kombu对所有需要监听的队列_active_queues都查询一遍,直到查询完毕或者遇到一个可以使用的Queue,然后就获取消息,回调此队列对应的callback。Channel初始化的过程就是连接的过程。

  • Kombu并不直接让Channel使用Connection来发送/接受请求,而是引入了一个新的抽象Transport,Transport负责具体的MQ的操作,也就是说Channel的操作都会落到Transport上执行。是以Transport为中心,把Channel代表的真实redis与Hub其中的poll联系起来。

  • Queue:消息队列,消息内容的载体,存储着即将被应用消费掉的消息。Exchange 负责将消息分发 Queue,消费者从 Queue 接收消息;

  • Exchange:交换机,消息发送者将消息发至 Exchange,Exchange 负责将消息分发至 Queue;消息发送是交给 Exchange 来做的,但Exchange只是将发送的 routing_key 转化为 queue 的名字,这样发送就知道应该发给哪个queue;实际发送还是得 channel 来干活,即从 exchange 得到 routing_key —> queue 的规则,然后再依据 routing_key 得到 queue。就知道 Consumer 和 Producer 需要依据哪个 queue 交换消息。每个不同的 Transport 都有对应的 Channel;生产者将消息发送到Exchange,Exchange通过匹配BindingKey和消息中的RouteKey来将消息路由到队列,最后队列将消息投递给消费者。

  • Producers: 发送消息的抽象类,Producer 包含了很多东西,有 Exchange、routing_key 和 channel 等等;

  • Consumers:接受消息的抽象类,consumer需要声明一个queue,并将queue与指定的exchange绑定,然后从queue里面接收消息;Consumer绑定了消息的处理函数,每一个Consumer初始化的时候都是和Channel绑定的,也就是说我们Consumer包含了Queue也就和Connection关联起来了。Consumer消费消息是通过Queue来消费,然后Queue又转嫁给Channel,再转给connection。

  • 用户可以通过同步方式自行读取消息,如果不想自行读取,也可以通过Hub(本身构建了一个异步消息引擎)读取。

  • Hub是一个eventloop,Connection注册到Hub,一个Connection对应一个Hub。Hub 把自己内部的 poller 配置在 Transport 之中。这样就通过 transport 内部的 MultiChannelPoller 可以把 Hub . poller 和 Channel 对应的 socket 同poll联系起来,一个 socket 在 linux 系统中就是一个file,就可以进行 poll 操作;

  • MultiChannelPoller是Connection 和 Hub的枢纽,它负责找出哪个 Channel 是可用的,但是这些 Channel 都是来自同一个 Connection。

5.2 示例图

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数Java工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注Java获取)

img

最后

光给面试题不给答案不是我的风格。这里面的面试题也只是凤毛麟角,还有答案的话会极大的增加文章的篇幅,减少文章的可读性

Java面试宝典2021版

最常见Java面试题解析(2021最新版)

2021企业Java面试题精选

《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数Java工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。[外链图片转存中…(img-OlUSxtOM-1713518768360)]

[外链图片转存中…(img-02ytljwe-1713518768361)]

[外链图片转存中…(img-ZMjT7SE9-1713518768362)]

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注Java获取)

img

最后

光给面试题不给答案不是我的风格。这里面的面试题也只是凤毛麟角,还有答案的话会极大的增加文章的篇幅,减少文章的可读性

Java面试宝典2021版

[外链图片转存中…(img-owZzGC5Y-1713518768363)]

[外链图片转存中…(img-0ZanWFax-1713518768365)]

最常见Java面试题解析(2021最新版)

[外链图片转存中…(img-fj965tMS-1713518768366)]

[外链图片转存中…(img-ZdKuu9LF-1713518768367)]

2021企业Java面试题精选

[外链图片转存中…(img-L2DGaUL2-1713518768368)]

[外链图片转存中…(img-zO2CQBjy-1713518768369)]

《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值