011、消息队列应用:RabbitMQ、Kafka与Celery
从一次线上故障说起
上周半夜被报警叫醒,系统里堆积了十几万条未处理的消息。登录服务器一看,RabbitMQ内存占用98%,生产者还在拼命往里塞数据,消费者却全躺平了。紧急扩容后开始排查,发现某个消费者处理单条消息要30秒,而生产速率是每秒50条------典型的"消费能力跟不上生产速度"导致消息堆积。这种场景在我们后端系统里太常见了,今天就来聊聊消息队列这个"系统解耦神器"和"性能杀手"的双面角色。
RabbitMQ:老牌选手的生存之道
RabbitMQ用Erlang实现,稳定得像个老管家。它的AMQP协议设计得很严谨,但刚上手容易懵。看看这个典型的生产者示例:
python
import pika
# 这里踩过坑:线上环境一定要用ConnectionParameters配置心跳
connection = pika.BlockingConnection(
pika.ConnectionParameters(host='localhost', heartbeat=600)
)
channel = connection.channel()
# 声明队列时记得设置持久化,不然重启就丢数据
channel.queue_declare(queue='order_queue', durable=True)
# 发消息时delivery_mode=2是持久化关键
channel.basic_publish(
exchange='',
routing_key='order_queue',
body='订单数据JSON字符串',
properties=pika.BasicProperties(
delivery_mode=2, # 持久化消息
)
)
消费者端更要注意,很多人在这里栽跟头:
python
def callback(ch, method, properties, body):
try:
# 业务处理逻辑
process_order(body)
# 手动确认!别用auto_ack,消息丢了都不知道
ch.basic_ack(delivery_tag=method.delivery_tag)
except Exception as e:
# 处理失败时记录日志并拒绝消息
logger.error(f"处理失败: {e}")
# 第三个参数requeue=True会让消息重新入队
ch.basic_nack(delivery_tag=method.delivery_tag, requeue=True)
# 设置QoS,控制消费者同时处理的消息数
channel.basic_qos(prefetch_count=1) # 一次只处理一条,避免某个消费者负载过高
channel.basic_consume(queue='order_queue', on_message_callback=callback)
RabbitMQ的exchange类型(direct、topic、fanout)选型要看场景。订单系统用direct,日志广播用fanout,灵活路由用topic。但记住:功能越复杂,性能代价越大。
Kafka:吞吐量怪兽的脾气
Kafka是另一种思路。它不在乎消息的"生死",只保证消息按顺序持久化。第一次用Kafka的人常被它的术语搞晕------broker、partition、offset、consumer group。看段生产者代码:
python
from kafka import KafkaProducer
import json
# 序列化器选对很重要,json是最稳妥的选择
producer = KafkaProducer(
bootstrap_servers=['localhost:9092'],
value_serializer=lambda v: json.dumps(v).encode('utf-8'),
# 这两个参数影响可靠性和吞吐的平衡
acks='all', # 所有副本确认才算成功
retries=3 # 失败重试次数
)
# 发送时指定key,相同key的消息会进入同一个partition
producer.send('user_actions',
key=user_id.encode(),
value={'action': 'click', 'page': 'home'})
# 一定要flush,不然可能丢最后一批消息
producer.flush()
消费者这边学问更大:
python
from kafka import KafkaConsumer
consumer = KafkaConsumer(
'user_actions',
bootstrap_servers=['localhost:9092'],
group_id='analytics_group', # 同一个group的消费者共享offset
enable_auto_commit=False, # 手动提交offset,控制更精准
value_deserializer=lambda x: json.loads(x.decode('utf-8'))
)
for message in consumer:
try:
process_action(message.value)
# 处理成功才提交offset
consumer.commit()
except Exception:
# 处理失败不提交,下次还能读到这条
logger.error("处理失败,暂停消费")
time.sleep(5) # 暂停一会儿避免死循环
Kafka的partition数量要在创建topic时就规划好,后期修改很麻烦。经验公式:partition数 = 消费者数量 × 消费能力系数。别设太少成为瓶颈,也别设太多增加管理开销。
Celery:Python开发者的快速解决方案
Celery是另一种思路------它本质是分布式任务队列,但很多人当消息队列用。它的优势是跟Python生态无缝集成:
python
from celery import Celery
# broker用RabbitMQ或Redis都行,RabbitMQ更稳定
app = Celery('tasks',
broker='pyamqp://guest@localhost//',
backend='redis://localhost:6379/0')
@app.task(bind=True, max_retries=3)
def process_image(self, image_path):
try:
# 业务逻辑
result = resize_image(image_path)
return result
except Exception as exc:
# 失败重试,指数退避
raise self.retry(exc=exc, countdown=2 ** self.request.retries)
Celery的worker启动有讲究:
bash
# 别用默认并发数
celery -A tasks worker --loglevel=info --concurrency=4
# 生产环境用gevent或eventlet提升IO密集型任务性能
celery -A tasks worker --loglevel=info --pool=gevent --concurrency=100
监控Celery任务状态很重要,我习惯用flower:
python
# 启动监控
celery -A tasks flower --port=5555
选型实战:什么时候用什么
去年设计电商系统时,我们这样分配:
RabbitMQ处理订单流程------需要严格的消息确认、死信队列、优先级队列。用户下单后,订单消息进RabbitMQ,库存服务、优惠券服务、日志服务各自订阅,一个消息驱动多个动作。
Kafka处理用户行为日志------每天几十GB的点击流数据,允许少量丢失,但要超高吞吐。数据进Kafka后,实时分析服务、离线数仓、推荐系统各取所需。
Celery处理后台任务------用户上传图片生成缩略图、发送营销邮件、数据报表生成。这些任务需要重试机制、进度查询、结果存储。
调试血泪史
RabbitMQ内存爆过三次。第一次是消息没设置过期时间,积压了百万条;第二次是消费者忘记ack,消息重复消费;第三次是connection没设心跳,网络抖动后连接假死。
Kafka的坑在partition。有次设了10个partition但只有2个消费者,8个partition闲置。另一次是consumer group没规划好,多个服务互相抢消息。
Celery最坑的是版本兼容。从3.x升级到4.x时API大变,半夜回滚代码。还有一次backend用Redis,结果Redis内存满了,任务结果全丢。
个人经验包
-
监控必须到位:RabbitMQ的管理界面、Kafka的JMX指标、Celery的flower,一个都不能少。设好告警阈值,别等爆了才发现。
-
消费者要设计成幂等的:消息可能重复投递,你的业务逻辑要能处理重复消息。加个唯一ID,处理前查一下是否已处理过。
-
别迷信吞吐量数字:Kafka宣称百万TPS,那是理想场景。实际业务中序列化、网络、业务逻辑都是瓶颈。先压测,再上线。
-
消息格式向前兼容:JSON比Protobuf灵活,但体积大。我们折中方案:JSON主体加schema版本号,解析时根据版本号处理字段变化。
-
死信队列一定要设:处理失败的消息移到死信队列,方便排查和修复后重试。见过太多因为没死信队列,错误消息在队列里循环的惨剧。
-
本地开发用Docker Compose:一套yml文件把RabbitMQ、Kafka、Redis全启起来,省去安装配置的麻烦。
消息队列用好了是系统骨架,用不好就是事故源泉。每次设计新队列时,多问几句:消息丢了怎么办?重复了怎么办?积压了怎么办?这三个问题想清楚,能避开80%的坑。
夜深了,监控告警又亮了------这次是Kafka的disk usage超过85%。你看,消息队列从来不会让你无聊。去扩容磁盘了,下次聊。