分布式应用中,消息系统被大量使用,主要原因有:
逻辑解耦
发送方和接收方不需要相互知道对方,一个只管发,一个只管收,大大简化了处理逻辑。
适配动态流量
如果发送方发送速度快于接收方的接收速度,消息系统就可以暂时将无法处理的消息缓存起来,让接收方慢慢处理。
没有消息系统时,发送方就不得不配合接收方降低处理速度,从而拖慢了整个系统的性能。
那么消息系统能保证消息100%可靠到达吗?
答案是否定的。
因为消息系统是网络调用,只要涉及到网络,就不可能100%可靠,因为通信双方不可能无限次给对方发ACK确认。
那么消息系统如何尽可能保证消息的可靠达到呢?
一般来说,消息系统可以实现3种消息传输模式:
- At least once;
- At most once;
- Exactly once.
这3种模式分别是:
- 消息保证至少发送成功一次,也就是可能会重复发送;
- 消息只保证最多发送一次,那就是要么成功,要么失败;
- 消息保证发送成功且仅发送成功一次,这种理想情况基本不存在,也没有任何基于网络的消息系统能实现这种模式。
所以大部分消息系统都按照At least once来设计。
但是并不是说消息系统就能保证所有消息能100%可靠达到,只要是网络,就存在丢消息的可能性。
如果涉及到交易系统这类绝对不能丢消息的应用,怎么才能保证100%不丢消息,并保证所有消息处理一次且仅处理一次?
首先我们要排除分布式事务消息,因为这种模式不但对数据库提出了XA两阶段提交的要求(需要昂贵的商用数据库),还对消息服务器提出了XA的要求(只有少数如WebLogic的JMS提供此功能),并且性能十分差劲。
而非事务型消息系统,如ActiveMQ、RabbitMQ、Kafka等,不保证100%可靠性。
涉及到交易系统的订单消息,如果一个都不能丢,通过非100%可靠的消息系统,如何保证100%的可靠性?
仅仅依靠消息服务是无法保证的,我们必须在设计上做出更多的容错和自动恢复的机制,来保证100%的可靠性。
以定序服务为例,如果订单已经持久化到数据库中,并且经过定序,下一步,如何保证定序后的订单通过消息发送给撮合服务100%可靠?
解决方法需要从发送方和接收方同时考虑。
考虑接收方处理消息的逻辑,首先要保证接收方能处理重复消息,因此需要对每个订单的消息进行编号,也就是给每个消息标记一个递增的ID(只需要递增,不一定要求连续),这样,接收方维护一个当前ID,凡是收到比当前ID小的消息,直接丢弃。
但是,如果一个消息序列例如A-B-C-D
在发送过程中丢掉了某个消息,变成了A-B-D
,接收方如何能检测出丢失?
除了给每个消息附上一个唯一递增ID外,只需要发送方同时给每个消息附加前一条消息的ID,就可以形成一个微型“区块链”,利用这个链表,接收方很容易识别出漏掉的消息。
如果接收方识别出消息遗漏,它应该怎么从该错误恢复呢?方法也很简单,只需向接收方暴露数据库接口,让接收方自己从数据库中根据ID读取漏掉的消息,就相当于接收方总是能有序且无遗漏地处理所有消息。
假设正常的消息流如下所示:
┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐
│id=1 │ │id=2 │ │id=3 │ │id=4 │
│pid=0│ │pid=1│ │pid=2│ │pid=3│
│msg-A│ │msg-B│ │msg-C│ │msg-D│
└─────┘ └─────┘ └─────┘ └─────┘
由于接收方可以根据递增ID去重,因此,重复发送消息可以被正常处理:
┌─────┐ ┌─────┐ ┌─────┐ ╔═════╗ ╔═════╗ ┌─────┐
│id=1 │ │id=2 │ │id=3 │ ║id=2 ║ ║id=3 ║ │id=4 │
│pid=0│ │pid=1│ │pid=2│ ║pid=1║ ║pid=2║ │pid=3│
│msg-A│ │msg-B│ │msg-C│ ║msg-B║ ║msg-C║ │msg-D│
└─────┘ └─────┘ └─────┘ ╚═════╝ ╚═════╝ └─────┘
如果消息出现丢失:
┌─────┐ ┌─────┐ ┌ ─ ─ ┐ ┌─────┐
│id=1 │ │id=2 │ │id=4 │
│pid=0│ │pid=1│ │ │ │pid=3│
│msg-A│ │msg-B│ │msg-D│
└─────┘ └─────┘ └ ─ ─ ┘ └─────┘
那么,接收方只需要根据当前ID去数据库查询,直到读取到最新的ID为止:
┌───────────┐ ┌───────────┐ ┌───────────┐
│ Sender │─────>│ MQ │─────>│ Receiver │
└───────────┘ └───────────┘ └───────────┘
│ │
│ │
│ │
│ │
│ ┌───────────┐ │
└───────────>│ Database │<───────────┘
└───────────┘
整个过程中,极少量消息丢失不会对系统的可用性造成影响,这样就极大地减少了系统的运维成本和线上排错成本。
对于那些不太需要100%严格有序的消息队列,例如清算消息,就不需要这么复杂的设计,超时重发+定时扫描未处理的消息就足够了。
最后,无论是发送方还是接收方,为了提高收发消息的效率,应该总是使用批处理的方式。测试显示一次收发一条消息和一次收发10条消息时间上并无明细差异,而发送方采用batch落库+batch发送可以显著地提高TPS,当然,这需要消息服务器支持batch模式。