转载请注明出处: https://blog.youkuaiyun.com/jinixin/article/details/84202958
通过消息队列可以非常方便的实现分布式, 上篇文章使用Python的pika搭建的"生产者-消费者"模型就是很好的例子, 但经过一段时间的运行, 有两个问题令我疑惑, 下面拿出来简单谈谈.
问题
问题一: 运行一段时间后, pika会丢失与RabbitMQ的连接.
问题二: 连接建立过多, 资源消耗较严重.
解决
1. 问题1的原因与解决
AMQP协议规定消息队列有心跳检测机制, 即消息队列的消息代理会设置一个心跳超时时间.
当客户端与消息队列的消息代理建立连接后, 客户端隔一定时间就会发送一个心跳检测包, 如果消息代理在心跳超时时间内没有收到心跳检测包, 该连接就会被断开. 对于消息队列来说这是好事情, 因为不用维持一个不常使用的连接, 但对我们编写的客户端(生产者/消费者)来说可不友好. 怎么办呢? 有两种解决的方法.
1) 方法一
使用pika提供的process_data_events方法, 该方法定时自动向RabbitMQ的消息代理发送心跳包来维持这段感情(啊, 是这个连接^_^).
connection.process_data_events() # 保持连接, 程序会阻塞在此处
该方法可选填time_limit参数表示最长的阻塞时间. 参数默认为0, 即有消息需要发送或接收时会立即结束阻塞并进行处理. 阻塞期间连接不会被消息代理断开, 该方法起到了保活作用.
2) 方法二
关闭RabbitMQ的心跳检测机制
pika.ConnectionParameters(host=self.host, port=self.port, heartbeat=0) # 该方法用于生成连接参数
heartbeat表示心跳超时时间, 如果设置的是大于0的数, 则该数会被作为消息代理与该客户端间连接的心跳超时时间. 如果设置的是None, 则会使用消息代理的默认心跳超时时间. 如果是0, 则关闭对该连接的心跳超时检测.
2. 问题2的解释与解决
关于问题二, 你可能没有get到我的意思, 我先解释下哈~
AMQP协议指导客户端与消息队列服务器建立的连接是基于TCP的, 一个连接下可以有多条信道, 每条信道间互不干扰. 对此我们立即就会想到多个消费者(线程)共享一个连接, 每个消费者(线程)独享一条信道, 而AMQP也正是这样建议的, 但现实却是pika是线程不安全的, 尤其是pika所操作的连接与信道不支持多个消费者(线程)共享. 所以我们只能为每个消费者(线程)单独分配一个连接和一个信道.
这样虽然程序能正常运行, 但还是有弊端的, 因为客户端与RabbitMQ服务器的连接是基于TCP的. 试想集群下有多台计算机, 每台计算机上有多个消费者, 每个消费者都会创建一个消息队列客户端, 也就是每个消费者都会与RabbitMQ服务器保持一个连接. 这对消费者所在的计算机来说十几个连接问题不大, 但对RabbitMQ所在服务器来说就是另番光景了. 其需要应对那么多台部署有消费者的计算机, 其所需建立的连接数量会非常大. 而普遍认为一台计算机理论TCP连接数量取决于内存, 在10K-100K间.
不仅TCP连接的建立需要消耗大量资源, 而且连接过多也加大了宕机的风险, 如何解决呢?
惭愧分享下我的想法做个抛砖引玉: 在消费者所在的计算机上维护一个本地队列, 暂存从消息队列处获得的消息, 之后该计算机上的所有消费者都从本地队列中取得消息. 这样即使有众多消费者, 该计算机也仅和消息队列服务器保持一个长连接. Enmmm, 你可能会有下面疑惑:
疑惑一: 增加本地队列, 代码会不会变得很复杂?
的确有些麻烦, 但借助Queue模块中的Queue队列可以简单不少. Queue模块是线程安全的, 且可以设定本地队列的最大存储值, 以防一下子接收消息队列中全部消息.
疑惑二: 如果没开启消息手动确认, 这还到是一个办法. 但如果开启了消息手动确认, 该怎么办呢? 消费者处理完了, 是不是还要写个回调来通知消息代理呢?
不需要这么麻烦, pika提供了add_callback_threadsafe方法来异步通知RabbitMQ消息处理成功. 该方法是pika库中唯一线程安全的, 可被多个消费者(线程)同时调用.
下面是优化后的代码, 和上篇文章中的主体结构是一致的, 主要关闭了心跳检测机制, 增加了本地队列和异步的消息确认.
# coding=utf-8
import logging
import time
from Queue import Queue
import pika
class MQBase(object):
""" 消息队列基类, 该类线程不安全的 """
def __init__(self, host, port, exchange, exchange_type='direct', ack=True, persist=True, **kwargs): # 默认开启手动消息确认, 交换机\队列\消息持久化
""" 当开启手动消息确认, 要考虑消息重入的情况 """
self._conn = None
self._channel = None
self._properties = None
self.host = host
self.port = port
self.exchange = exchange
self.exchange_type = exchange_type
self.ack = ack
self.persist = persist
def _get_channel(self):
""" 创建连接与信道, 声明交换机 """
if self._check_alive():
return
else:
self._clear()
if not self._conn:
self._conn = pika.BlockingConnection(pika.ConnectionParameters(host=self.host,
port=self.port,
heartbeat=0)) # 建立连接并关闭心跳检测
if not self._channel:
self._channel = self._conn.channel() # 建立信道
self._channel.exchange_declare(exchange=self.exchange,
exchange_type=self.exchange_type,
durable=self.persist) # 声明交换机
if self.ack:
self._channel.confirm_delivery() # 在该信道中开启消息确认
self._properties = pika.BasicProperties(delivery_mode=(2 if self.persist else 0)) # 消息持久化
def _clear(self):
""" 清理连接与信道 """
def clear_conn():
if self._conn and self._conn.is_open:
self._conn.close()
self._conn = None
def clear_channel():
if self._channel and self._channel.is_open:
self._channel.close()
self._channel = None
if not (self._conn and self._conn.is_open):
clear_conn() # 清理连接
clear_channel() # 清理信道
def _check_alive(self):
""" 检查连接与信道是否存活 """
return self._channel and self._channel.is_open and self._conn and self._conn.is_open
class MQSender(MQBase):
""" 生产者, 该类是线程不安全的 """
def send(self, route, msg):
def try_send():
try:
self._get_channel()
success = self._channel.basic_publish(exchange=self.exchange,
routing_key=route,
body=msg,
properties=self._properties)
except Exception, e:
success = False
return success
ret = try_send() or try_send()
if not ret:
self._clear()
return ret
class MQReceiver(MQBase):
"""
消费者, 该类是线程安全的
通过receive_queue支持多线程, 不直接创建多实例是想减少底层TCP连接
"""
def __init__(self, *args, **kwargs):
self._has_start = False
self.prefetch_count = kwargs.get('prefetch_count', 1) # 每次只接受prefetch_count条消息, 处理完再接收新的
self.receive_queue = Queue(self.prefetch_count) # 接收缓冲队列, 线程安全的
super(self.__class__, self).__init__(*args, **kwargs)
def _declare_queue(self, queue_name):
""" 声明队列 """
self._channel.queue_declare(queue=queue_name, durable=self.persist) # 声明队列
self._channel.queue_bind(queue=queue_name, exchange=self.exchange, routing_key=queue_name) # 绑定队列到交换机
def _subscribe_queue(self, queue_route):
""" 订阅队列 """
try:
self._declare_queue(queue_route)
self._channel.basic_qos(prefetch_count=self.prefetch_count) # 负载均衡
self._channel.basic_consume(self._handler, queue_route, no_ack=not self.ack) # 订阅队列, 并分配消息处理器
except Exception, e:
return False
return True
def _handler(self, channel, method_frame, header_frame, body): # 消息处理器
""" 收到消息后的回调 """
print ('wait message num is {num}'.format(num=channel.get_waiting_message_count()))
self.receive_queue.put((body, self._conn, channel, method_frame.delivery_tag)) # 压入队列
def start(self, queue_route):
""" 开始消费 """
if self._has_start:
return
try:
self._get_channel()
if self._subscribe_queue(queue_route):
self._channel.start_consuming() # 开始消费, 一个连接下仅能调用该方法一次, 否则抛出RecursionError
except Exception, e:
pass
def _clear(self):
self.receive_queue = Queue(self.prefetch_count) # 避免消息重入缓冲队列
super(self.__class__, self)._clear()
def main():
from threading import Thread, current_thread
from functools import partial
def producer_func():
for index in range(50):
print ('sender send {content}'.format(content=index))
sender.send('test_queue', index)
def consumer_func():
receiver.start('test_queue')
def deal_message(r):
while True:
msg, conn, channel, ack_tag = r.receive_queue.get()
interval = int(msg) / 5.0
print ('{thread_name}: get {msg}, wait {interval}s, ack_tag is {delivery_tag}'.format(
msg=msg, thread_name=current_thread().getName(), interval=interval, delivery_tag=ack_tag))
if current_thread().getName() == 'handler_1': # 测试负载均衡
time.sleep(60)
else:
time.sleep(interval)
conn.add_callback_threadsafe(partial(channel.basic_ack, ack_tag)) # 确认消息成功处理
handler_num = 5
sender = MQSender(host='localhost', port=5680, exchange='test_exchange')
receiver = MQReceiver(host='localhost', port=5680, exchange='test_exchange', prefetch_count=handler_num)
Thread(target=consumer_func).start() # 消费者
Thread(target=producer_func).start() # 生产者
for i in range(handler_num):
Thread(target=deal_message, args=(receiver,), name='handler_%d' % i).start() # 消息处理器
if __name__ == '__main__':
main()
注意如果开启了消息手动确认机制, 一定要记得考虑消息重入的情况.
通过上述的小优化, 现在的"生产者-消费者"模型支持长连接, 不自动断开. 多消费者复用一个连接, 避免消耗过多资源. 多消费者间负载均衡.
文中如有不当之处, 还望包容和指出, 感谢.