上个月我们线上RabbitMQ突然丢了大概两千多条消息,当时查了很久发现不是消费端bug,而是我手贱把queue的max-length设置成了10000,然后又开着镜像队列,结果在某个瞬间积压的消息超过了这个阈值,直接从头淘汰旧消息。你以为queue-length-set是只限制新消息吗?不是的,它连还没被确认的消息一起算,一旦超了,旧消息直接死掉,没有任何警告。后来我换成quorum queue才敢重新弄,但这东西的吞吐比经典队列低了差不多30%,我用的是三节点集群,单条消息1KB,经典队列能到每秒12000,quorum只有8000。如果你在追求极致吞吐的电商秒杀场景,别用RabbitMQ,真的,去用Kafka。但你要是需要复杂路由、优先级、死信重试,RabbitMQ比Kafka好用十倍,这不是性能问题,是思维模型问题。
再来说prefetch count,这是我见过最容易被忽略的参数。默认是0,也就是无限拉取,生产上会死得很难看。我之前用Java客户端,消费端处理一条数据库请求要150ms,然后我并发开20个线程,没设置prefetch,结果每个线程一次拉500条,瞬间内存爆掉,消费端OOM重启,消息又全部重新入队,整个集群处于循环崩溃状态。后来我调成prefetch=1,虽然吞吐从每秒300掉到每秒180,但每个消息都不会被浪费,配合手动ack,终于稳定了。注意prefetch是针对每个消费者设置的,不是每个队列。当你用Spring AMQP时,配置项是spring.rabbitmq.listener.simple.prefetch=1,但如果你的消费逻辑里有耗时网络IO,建议prefetch设置在50到100之间,具体值要压测,我的经验是prefetch=3 * 线程数 / 处理耗时(秒),比如线程20,处理耗时0.15s,那么prefetch≈400,但别超过500,不然又回到内存炸弹。
内存水流控也是个暗坑。默认memory threshold是0.4,也就是内存使用超过4GB时(前提你给RabbitMQ分了8GB堆),它就会开始阻塞connection。我原来以为只是发消息变慢,结果发现它连消费端的ack也会被卡住,因为ack也算一种frame,也被流控了。那次我压测到6万条消息堆积,内存到了3.5GB,RabbitMQ直接停止接收新消息,但消费端却还在处理,因为TCP backlog里还有数据,最后造成数据不一致。解决办法有两个:一是调高watermark到0.6,但前提是你的机器内存够大,而且要有swap;二是改用惰性队列,把消息都置换到磁盘。惰性队列的代价是单条消息的延迟从2ms涨到20ms,但好在吞吐反而稳定了,因为减少了内存回收频率。注意在RabbitMQ 3.12中,惰性队列已经成为默认队列模式,但别天真以为新版本就自动帮你优化了,它的磁盘IO压力会暴增,如果你用的是机械硬盘会直接歇菜。
最后我要说个反直觉的独立观点:RabbitMQ的性能调优不是靠调参,而是靠减少功能叠加。很多人上来就把确认模式、事务、优先级、死信、镜像/仲裁全部打开,然后问怎么这么慢。我的建议是,先把所有高级功能关掉,测出基线,再一个个打开,每开一个重新压测。我做过一次对比:基础配置下RabbitMQ每秒钟能处理8000条消息,开启publisher confirm后掉了25%,开启mandatory标记后掉10%,同时开启transactions(不推荐)直接掉到2500,再叠加上死信和TTL,只有1200了。你需要的应该是消息不丢,而不是每个特性都上。还有,不要依赖rabbitmqctl set_policy去动态改队列参数,那只会让你在集群中制造一堆不同配置的队列,真正的做法是写在你自己的代码里,用队列声明参数固定住,比如arguments.put("x-queue-type", "classic") 要明确写死,不要用默认。调优不是玄学,是你知道每个参数背后会牺牲什么。