消息服务实践

本文探讨了消息服务在生产者和消费者端的最佳实践,包括维持连接、持久化、确认机制、错误处理和扩展性。生产者端强调了持久化、确认机制的使用和错误日志记录;消费者端关注连接的维护、顺序性、确认模式、prefetch值的设定以及幂等性设计。错误处理部分讨论了nack、reject和不做操作的区别,以及如何处理消息积压和过期失效问题。此外,提出了动态添加消息的挑战及解决方案。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

生产者端

同一服务下,维护生产者在一个vhost下的connection连接和channel,会断线重连

由于TCP的开启和关闭都是昂贵的开销,而且会存在慢启动的问题,不能一上来就传送大量的数据。假设每次发送消息都要重新建立一个connection,那如果同一时间发送消息的频率过于频繁,或者消息量过大,就可能出现异常(可用for循环连发消息1000次验证:1,每次都建立connection和channel,2,建立一次connection,每次发消息都建立channel,3,建立一次connection和channel,直接发送消息)。

因此,每个服务在启动时,即建立好所有vhost下的connection连接和channel。当需要发送消息的时候,直接用对应的channel去声明exchange和Publish即可。connection和channel在服务器运行的过程中不会主动close,当服务发生异常时会尝试主动close。

持久化和ack

从生产者发送消息到rabbitmq把消息发给队列,过程中的每个时刻都可能丢消息。

首先,为了确保rabbitmq不丢消息,需要持久化queue的元信息和队列中的消息。前者需要在assertExchange的时候讲durabel设为true,后者需要把publish方法里options里的persistent设为true(或者把deliveryMode设为2,但persistent的优先级高于deliveryMode)。这样,RabbitMQ就会将持久化消息写入磁盘上的持久化日志文件,如果rabbitmq挂了之后再次重启,也会恢复queue和这个queue里的数据。当然,写入磁盘有开销,持久化会降低rabbitmq的性能,但可以有效防止rabbitmq挂了之后丢消息。可以根据项目需求来灵活运用,只对内容重要、难以找回,不易再次触发的消息开启持久化。

其次,要处理生产者丢消息的情况。一般有两种做法,使用rabbitmq事务,或者引入confirm机制。事务是同步的,confirm机制是异步的。前者会大大降低性能,根据rabbitmq的官网,吞吐量会以250倍的因子减少。故而后者是通常推荐的做法。

引入confirm机制后,对于可路由消息,当所有队列都接受消息时,rabbitmq将发送basic.ack给生产者。对于路由到持久队列的持久性消息,这意味着持久化到磁盘。目前前端所有的消息都是routed的,即是知道要发送到哪个路由的。

开启持久化后会有延迟。如果没有空闲的队列,RabbitMQ消息会存储在一段时间(几百毫秒)之后批量传递消息到磁盘,以最小化同步的消息数。这意味着在恒定负载下,basic.ack的延迟可以达到几百毫秒。因此,需要异步发送消息,并尽量把发消息作为方法中的最后一环,从而不会block接下来的行为。

尽量对所有的生产者都开启confirm机制,因为它本身是异步的,对性能影响较小。

var open = require('amqplib').connect();
open.then(function(c) {
  c.createConfirmChannel().then(function(ch) {
    ch.sendToQueue('foo', new Buffer('foobar'), {},
      function(err, ok) {
        if (err !== null)
          console.warn('Message nacked!');
        else
          console.log('Message acked');
    });
  });
});

错误处理

建立生产者的消息日志数据库,主要用于标识未成功发送的消息,和该发送,但没发送的消息。

对有必要补发的消息,在开启confirm机制后,如果消息没有成功ack,则记一条错误日志。可每日跑定时任务,在凌晨补发消息。

漏发消息还可能有另一种情况,那就是根本没有调用publish方法。因此mq utils中的publish方法还提供一个log的property,如果设为true,则在每次发消息前都会记一条日志。这样就可以确认到底是发了消息,但是消息丢了,还是根本就没有发消息。

消费者端

每个服务下,对每个vhost的消费者维护一个connection连接,一旦建立则不会主动断开,会断线重连

在消费者端可能会遇到消息堆积的情况,更需要避免connection的频繁创建和关闭。

顺序性

为了保证顺序性,将对处理顺序有要求的消息放在一个consumer里完成,一个channel对应一个consumer。

持久化和ack

ack具有两种模式,自动的发送确认模式和手动的ack。前者只要发了消息,就认为consumer这一端是ack的,然后就删掉消息。这样可能会导致1,消息丢失,2,consumer过载。手动确认模式通常与prefetch一起使用,该prefetch限制了信道上未完成(“进行中”)交付的数量。而自动ack则没有。因此,消费者可能会因交付速度而不堪重负,可能会积累内存,耗尽性能或使操作系统终止其进程。一些客户端会应用tcp反压,即未处理的delivery达到某一限制后不再从socket读取数据。因此,只有当可以高效、稳定地处理delivery时,才对消费者推荐使用自动ack模式。

因此,尽量采用手动发送确认模式,在consume成功后再发送ack给rabbitmq。从而避免消费者丢失消息的情况。

prefetch

一般而言,手动的ack动会给定prefetch,以防止consuemr过载。

如果什么都不配置,Rabbit会尽可能快速地发送队列中的所有消息到client端。因为consumer在本地缓存所有的message,从而极有可能导致内存耗尽或不足,影响其它进程的正常运行。

prefetch允许为每个consumer指定最大的unacked messages数目。简单来说就是用来指定一个consumer一次可以从Rabbit中获取多少条message并缓存在client中(RabbitMQ提供的各种语言的client library)。一旦缓冲区满了,Rabbit将会停止投递新的message到该consumer中直到它发出ack。

amqp0-9-1中的prefetch是针对每个队列(channel的),而rabbitmq在v3.3.0之后,设置细化到了每个消费者的粒度。amqplib应用的是amqp0-9-1。js组所用到的项目中,代码里一个channel只会对应一个consumer,双机共用的是同一套代码,所以在代码中给定一个prefetch值,就等于同时应用在了该channel下的每个consumer上,这两个consumer分别运行在两台机器上。

找到合适的prefetch值需要不断试验(一般可以设为比 消息来回的总时间/业务处理的时间略大一点),并且会因工作负载而异。100到300范围内的值通常可提供最佳吞吐量,并且不会面临压倒性耗用消费者内存的风险。目前js组的大部分消费者是没有设置prefetch的,这里建议设为100。

幂等性

使用手动确认时,任何未执行的消息将在关闭channel时自动重新入队。 这包括客户端的TCP连接丢失,消费者应用程序(进程)故障和通道级协议异常。但检测不可用的客户端需要一段时间,因此可能出现重复发消息的情况,消费者必须考虑到幂等性。对于重新发送的消息,会有一个特殊的布尔属性,redeliver,由RabbitMQ设置为true。 对于第一次交付,它将被设置为false。可以根据它来分别处理第一次发送的消息和重复发送的消息。

但更好的做法是,保证多条相同的数据过来的时候只处理一条,或者说多条处理和处理一条造成的结果相同,但是具体怎么做要根据业务需求来定,例如insert某一条数据时先去查一下该数据是否存在。

错误处理

nack, reject,和啥都不做的区别

nack默认会把消息重新入队列,但是可以强制不要重新入队列。

reject指明了没有ack,默认也不会重新入队列。会直接丢弃消息。

reject是对于单独的消息做处理,告诉broker要么去丢弃消息,要么去重新把消息入队列。nack不但提供了所有的reject做的事情,还提供了批处理的功能。客户端在方法中把multiple Flag 置为true后,broker将拒绝所有未确认的,已传递的消息。

如果有deadletter exchange,则把nack的第三个参数置为false,即不重新入队列,可以进入到deadletter exchange中。

重新排队的消息可能立即会被重新发送(具体取决于它们在队列中的位置和prefetch值),这意味着如果所有消费者都在某一瞬态条件下而无法处理交付而重新排队,则会创建一个重新排队/重新发送的循环。这会大量耗费网络带宽和CPU资源。因此,建议消费者可以跟踪重新发送的数量,并及时丢弃它们,或在延迟后安排重新排队。

对现有项目的消费者,采用跟生产者相同的处理方式。建立一个消费者的消息日志库。如果在消费消息的时候出现了错误,采用nack的方式,但不把消息重新入队,而是记一条错误日志。建一个每天执行的定时任务,夜深人静的时候再重新发送。

消息大量积压

临时扩容,将queue资源和consumer资源扩大N倍,并以正常的N倍速度来消费数据。

过期失效

用TTL设置过期时间后,超过ttl会直接丢弃数据。可从错误日志中手工查出来补进去。

扩展性

动态添加消息

目前采用的方式是,生产者无脑发消息,消费者筛选出所需要的消息并消费。筛选是通过routingKey。但所有消费者都是在服务启动时建立好的,不能动态添加。

如果要动态添加的话,只能把筛选的过程放在具体的consumeHandler里,从数据库读取筛选条件等。这样的话,也可以进行一些复杂的、不能简单通过字符串匹配完成的筛选。

相比起生产者发所有消息,消费者筛选,还有一种办法是生产者筛选,消费者无脑处理。这种办法相对于前者更好,因为从源头上降低了消息的数量,减轻了mq的压力。因此尽量采用后者。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值