说到Java中的队列应该都不会陌生。其具有通过先进先出,或者双端进出的方式进行数据管理;通过阻塞以达到自动平衡负载的功能。
消息队列之所以以队列命名,起初也是因为其功能和操作,和java的本地队列有相似之处。所以,我们可以简单的认为消息队列就是为了满足分布式下各服务之间的数据传输、管理和消费的一种中间服务。
问:你们的系统中为什么要引入消息队列?
我们总归需要知晓消息队列的使用价值,以及自己的业务场景下的实际痛点才能回答为什么要用消息队列这个问题,才能回答系统引入消息队列的价值所在。
以前几天在后台和关注公号的一个大佬讨论的广告流水更新的操作为例:
广告检索系统,需要感知广告贴的信息变动来更新自己的索引,但实际上检索系统和投放、物料、资产等系统间没有必要依靠接口对感知行为进行强关联,且接口的方式在维护和系统的压力方面不友好,那么,消息队列的作用就显的很重要了,各系统发布各自的消息,谁需要谁订阅,达到目的同时不会增加额外的系统调用压力。(注:builder的接口调用是为了获取最新的信息,此处可以通过压缩等方式进行优化)。
因此,当系统间无实时数据交互要求,但还需要其业务信息时,可以用消息队列来达到系统间解耦的作用,只要发布方定义好消息队列格式,消费方的任何操作均可和发布方无关,减少了不必要的联调和发布冲突等影响。
最典型的一个例子,就是支付场景下的结果通知功能。
我们知道,一般情况下不管是app push 还是短信通知,都是比较耗时的操作。所以,没有必要因为这些非核心功能的耗时操作而影响了支付的核心操作,只要我们在支付操作完成之后,将支付结果发到短信中心指定的消息topic下,短信中心自然会接收到此消息并保证通知给用户。
因此使用消息队列,让非核心的操作异步化,提高整个业务链路的高效和稳定,是很有效的。
这个功能使我们本篇关注的重点,面对特殊场景如秒杀、春晚红包等万亿级流量的脉冲式压力下,一种保护我们系统的服务免于崩溃的有效手段就是消息队列。
通过消息中心高性能的存储和处理能力,将超过系统处理能力的多余流量暂时存储起来,并在系统处理能力内平缓释放出来,达到削峰的效果。
比如我们的广告计费系统,面对上万并发的商业贴检索量,数千并发的点击操作,实时接口的方式一定是不合适的,毕竟广告行为和支付行为不一样,支付失败用户还可以重试,但用户的商业贴点击行为是不可回放的,本次流量过去就过去了,因此,需要利用消息队列将扣费请求缓存下来,来保证计费系统的稳定。
还如广播、事务型、最终一致性等特性,也是消息队列经常用到的功能。
前面提到,消息队列使得业务非核心流程异步化,可以提高整个业务操作的时效性和流畅度,提升用户操作体验。但,也是因为数据进入队列的原因,不可避免的会耽搁消费速度。导致业务生效不及时。
比如,之前遇到的商品推荐,产品要求推荐列表中不能出现满减秒杀的商品,以消除特殊商品对推荐效果产生影响。除了秒杀,我们还需要感知商品的上下架、黑名单、库存等等,因次,用redis中的bit多个偏移量来维护一个商品的多个状态。然后接收促销组的消息来变更推荐缓存集群中的商品状态,但由于消息的延迟,就有可能导致商品状态变更不及时的情况发生。不过只要权衡之下业务和技术上是可接受的就OK了。
消息队列的引入,相当于在原有的分布式服务链路中新增了一个系统,系统复杂度也随之变大了。同时,消息队列的作用要求其具有高性能和高可用。
所以,面对怎样部署高可用稳定集群、消息发送不成功怎么重试、broker数据同步策略怎么设置、broker异常导致消息重发怎么幂等、消费不成功怎么重试等等问题,需要中间件团队和业务系统一起努力应对。
支撑七年双11零故障的RocketMQ
2020 年双十一交易峰值达到 58.3W 笔/秒。RocketMQ为了阿里的交易生态有很多深度定制,这里我们只介绍其中针对高可用的优化。
个人理解,push消费模式只适合于消费速度远大于生产速度的场景,如果是大流量并发场景,基本还是以Pull消费为主。
而pull前broker和client间会进行负载均衡建立连接,那么,一旦Client被Hang住,(没有宕就不会rebalance,即时宕机也是默认20s才会rebalance),就会让broker与该client关联的队列消息无法及时被消费,导致积压。怎么办:POP,新的消费模式
POP 消费中并不需要rebalance去分配消费队列,取而代之的是请求所有的 broker 获取消息进行消费。broker 内部会把自身的三个队列的消息根据一定的算法分配给等待的 POPClient。即使 PopClient 2 出现 hang,但内部队列的消息也会让 Pop Client1 和 Pop Client2 进行消费。这样避免了消费堆积。
快手万亿级kafka集群的平滑扩容
要实现平滑,则需要让producer无感的实现partition迁移。
大致原理是将待迁移partition的数据和新的partition数据进行同步并持续一段时间,直到消费者全部赶上同步的开始节点,然后再变更路由,删除原partition,完成迁移。
相同的数据同步思路,在facebook的分布式队列灾备方案上也有应用。
快手/美团对kafka缓存污染的优化
kafka的高性能,来源于顺序文件读写和操作系统缓存pagecache的支持,在单partition,单consumer的场景下,kafka表现的非常优秀。但是,如果同一机器上,存在不同的partition,甚至,消费模式有实时和延迟消费的混合场景,将会出现PageCache资源竞争,导致缓存污染,影响broker的服务的处理效率。
美团应对实时/延迟消费缓存污染
将数据按照时间维度分布在不同的设备中,近实时部分的数据缓存在 SSD 中,这样当出现 PageCache 竞争时,实时消费作业从 SSD 中读取数据,保证实时作业不会受到延迟消费作业影响
当消费请求到达 Broker 时,Broker 直接根据其维护的消息偏移量和设备的关系从对应的设备中获取数据并返回,并且在读请求中并不会将 HDD 中读取的数据回刷到 SSD,防止出现缓存污染。同时访问路径明确,不会由于 Cache Miss 而产生的额外访问开销。
快手应对follower数据同步引起的缓存污染
broker 中引入了两个对象:一个是 block cache;另一个是 flush queue。
Producer 的写入请求在 broker 端首先会被以原 message 的形式写入 flush queue 中,之后再将数据写入到 block cache 的一个 block 中,之后整个请求就结束了。在 flush queue 中的数据会由其他线程异步地写入到磁盘中(会经历 page cache 过程)。保证queue不受follower的影响。
consumer 首先会从 block cache 中检索数据,如果命中,则直接返回。否则,则从磁盘读取数据。这样的读取模式保障了 consumer 的 cache miss 读并不会填充 block cache,从而避免了产生污染。
我们可以看出,解决缓存污染的基本出发点,还是要拆解不同消费速度的任务、或不同的数据生产来源,分而治之的思路避免相互间缓存的影响。
7CMQ在红包支付场景下的应用
红包操作的背后流程简化为:从 A 帐号中把余额读出来,然后做减法操作,再把结果写回 A 帐号中;然后拆红包对 B 帐号做加法操作,把结果写到 B 帐号中。
而由于账务系统能承载的压力有限(和账务相关的系统一般都会由于锁、事务等原因影响处理效率),可能导致入账失败,如果按实时业务逻辑,则需要对拆红包进行实时回滚(回滚需要对A的账户再进行一次加法),而引入CMQ后,业务链路变成将失败的请求写入CMQ,由CMQ的高可用来保证数据一致,直到账务系统最终入账成功。简化了账务系统由于系统压力而导致的入账失败而导致红包账务回滚带来的额外系统操作。
本篇从消息队列的作用出发,从阿里双11、快手、美团、微信红包等案例,就消息队列本身的优化方案和业务对消息队列的高效利用,阐述了消息队列在高并发的优化场景下的作用。如有问题,欢迎留言讨论,相互学习。