高并发系统设计:消息队列,解决高并发写问题

什么是消息队列

消息队列可以看成是存储数据的一个容器,可以用来平衡低速系统和高速系统处理任务时间差的工具。

很多组件中都有消息队列的影子:

  • 在java线程池中我们会使用一个队列来暂时存储提交的任务,等待有空闲的线程去处理这些任务
  • 操作系统中,中断的下半部分也会使用工作队列来实现延后执行
  • 我们在实现一个RPC框架时,也会将从网络上接收到的请求写到队列里,再启动若干个工作线程来处理

总之,队列是在系统设计时的一种常见的组件。如果你的系统想要提升写入性能,实现系统的低耦合,想要抵挡高并发的写流量,那么你就可以考虑使用消息队列来完成。

为什么要使用消息队列

三个最主要的应用场景:解耦、异步、削峰

  • 削峰填谷(最主要的作用)可以削去到达系统的峰值流量,让业务逻辑的处理更加缓和;但是会造成请求处理的延迟
  • 异步处理可以简化业务流程中的步骤,提升系统性能;
    • 需要分清同步流程和异步流程的边界
    • 消息存在着丢失的风险
  • 解耦合可以将系统和系统解耦开,这样两个系统的任何变更都不会影响到另一个系统

削峰

  • 传统模式:并发量大的时候,所有的请求直接怼到数据库,造成数据库连接异常
    在这里插入图片描述

  • 中间件模式:系统A慢慢的按照数据库能处理的并发量,从消息队列中慢慢拉取消息。在生产中,这个短暂的高峰期积压是允许的。
    在这里插入图片描述

解耦

  • 传统模式:系统间耦合性太强,如下图所示,系统A在代码中直接调用系统B和系统C的代码,如果将来D系统接入,系统A还需要修改代码,过于麻烦!

在这里插入图片描述

  • 中间件模式:将消息写入消息队列,需要消息的系统自己从消息队列中订阅,从而系统A不需要做任何修改
    在这里插入图片描述

异步

  • 传统模式:一些非必要的业务逻辑以同步的方式运行,太耗费时间。
    在这里插入图片描述

  • 中间件模式: 将消息写入消息队列,非必要的业务逻辑以异步的方式运行,加快相应速度。
    在这里插入图片描述

举例:秒杀与消息队列

削去秒杀场景下的峰值写流量

在秒杀开始的时候,会有大量的写请求进来,我们可以将秒杀请求暂存在消息队列中

  • 在后台启动若干个队列处理程序,消费消息队列中的消息,再执行校验库存,下单等逻辑。
  • 因为只有有限个队列处理线程正在执行,所以落入后端数据库上的并发请求是有限的。
  • 而请求是可以在消息队列中被短暂的堆积,当库存被消耗完之后,消息队列中堆积的请求就可以被丢弃了

这就是消息队列在秒杀系统中最主要的作用:削峰填谷,也就是说它可以削平短暂的流量高峰,

  • 虽然堆积会造成请求被短暂延迟处理,但是只要我们时刻监控消息队列中的堆积长度,在堆积量超过一定量时,增加队列处理机的数量,来提升消息的处理能力就好了
  • 而且秒杀的用户对于短暂延迟知晓秒杀的结果,也是有一定的容忍度的。
  • 但是,注意必须“短暂”延迟,如果长时间没有给用户公示秒杀结果,那么用户可能就会怀疑你的秒杀活动有猫腻了。所以,在使用消息队列应对流量峰值时,需要对队列处理的时间、前端写入流量的大小,数据库处理能力做好评估,然后根据不同的量级来决定部署署多少台队列处理程序
  • 比如你的秒杀商品有 1000 件,处理一次购买请求的时间是 500ms,那么总共就需要 500s的时间。这时,你部署 10 个队列处理程序,那么秒杀请求的处理时间就是 50s,也就是说用户需要等待 50s 才可以看到秒杀的结果,这是可以接受的。这时会并发 10 个请求到达数据库,并不会对数据库造成很大的压力。

在这里插入图片描述

通过异步处理简化秒杀请求中的业务流程

其实,在大量的写请求“攻击”你的系统时,消息队列除了发挥主要的削峰填谷的作用之外,还可以实现异步处理来简化秒杀请求中的业务流程,提升系统的性能。

比如说,在秒杀请求时,需要500ms。但是分析整个购买流程之后,可以发现这里面有主要的业务逻辑,也会有次要的业务逻辑:比如,主要的流程是生成订单、扣减库存;次要的流程是下单成功之后会给用户发放优惠券,会增加用户的积分。

假如发放优惠券的耗时是 50ms,增加用户积分的耗时也是 50ms,那么如果我们将发放优惠券、增加积分的操作放在另外一个队列处理机中执行,那么整个流程就缩短到了400ms,性能提升了 20%,处理这 1000 件商品的时间就变成了 400s。如果我们还是希望能在 50s 之内看到秒杀结果的话,只需要部署 8 个队列程序就好了。

经过将一些业务流程异步处理之后,我们的秒杀系统部署结构也会有所改变:
在这里插入图片描述

解耦实现秒杀系统模块之间松耦合

除了异步处理和削峰填谷以外,消息队列在秒杀系统中起到的另一个作用是解耦合。

比如数据团队要求,在秒杀活动之后想要统计活动的数据,借此来分析活动商品的受欢迎程度,购买者人群的特点以及用户对于秒杀互动的满意程度等指标。而我们需要将大量的数据发送给数据团队,那么要怎么做呢?

一个思路是:可以使用HTTP或者RPC的方式来同步的调用,这就是数据团队这边提供一个接口,我们实时的将秒杀的数据推送给它,但是这样调用会有两个问题:

  • 整体系统的耦合性比较强,当数据团队的接口发生故障时,会影响秒杀系统的可用性
  • 当数据系统需要新的字段,就要变更接口的参数,那么秒杀系统也要随着一起变更

这时,我们可以使用消息队列降低业务系统和数据系统的直接耦合度

秒杀系统产生一条购买数据之后,我们可以先把全部数据发送给消息队列,然后数据团队再订阅这个消息队列的话题,这样它们就可以接收到数据,然后再做过滤和处理了

秒杀系统在这样解耦合之后,数据系统的故障就不会影响到秒杀系统了。同时,当数据系统需要新的字段时,只需要解析消息队列中的消息,拿到需要的数据就好了
在这里插入图片描述

使用了消息队列会有什么缺点?

  • 系统可用性降低:你想啊,本来其他系统只要运行好好的,那你的系统就是正常的。现在你非要加个消息队列进去,那消息队列挂了,你的系统不是呵呵了。因此,系统可用性降低
  • 系统复杂性增加:要多考虑很多方面的问题:
    • 致性问题、如何保证保证消息可靠传输。
    • 消息是否会丢失,是否会重复?请求的延迟如何能够减少?消息接收的顺序是否会影响到业务流程的正常执行?
    • 如果消息处理流程失败了之后是否需要补发?

消息队列如何选型?

衡量标准

对消息队列进行技术选型时,需要通过以下指标衡量你所选择的消息队列,是否可以满足你的需求:

  • 消息顺序:发送到队列的消息,消费时是否可以保证消费的顺序,比如A先下单,B后下单,应该是A先去扣库存,B再去扣,顺序不能反。
  • 消息路由:根据路由规则,只订阅匹配路由规则的消息,比如有A/B两者规则的消息,消费者可以只订阅A消息,B消息不会消费。
  • 消息可靠性:是否会存在丢消息的情况,比如有A/B两个消息,最后只有B消息能消费,A消息丢失。
  • 消息时序:主要包括“消息存活时间”和“延迟/预定的消息”,“消息存活时间”表示生产者可以对消息设置TTL,如果超过该TTL,消息会自动消失;“延迟/预定的消息”指的是可以延迟或者预订消费消息,比如延时5分钟,那么消息会5分钟后才能让消费者消费,时间未到的话,是不能消费的。
  • 消息留存:消息消费成功后,是否还会继续保留在消息队列。
  • 容错性:当一条消息消费失败后,是否有一些机制,保证这条消息是一种能成功,比如异步第三方退款消息,需要保证这条消息消费掉,才能确定给用户退款成功,所以必须保证这条消息消费成功的准确性。
  • 伸缩:当消息队列性能有问题,比如消费太慢,是否可以快速支持库容;当消费队列过多,浪费系统资源,是否可以支持缩容。
  • 吞吐量:支持的最高并发数。

常用的消息队列

  • RocketMQ:是阿里开源的消息中间件,它是纯Java开发,具有高吞吐量、高可用性、适合大规模分布式系统应用的特点。RocketMQ思路起源于Kafka,但并不是Kafka的一个Copy,它对消息的可靠传输及事务性做了优化,目前在阿里集团被广泛应用于交易、充值、流计算、消息推送、日志流式处理、binglog分发等场景。

  • ActiveMQ:是Apache出品,最流行的,能力强劲的开源消息总线。官方社区现在对ActiveMQ 5.x维护越来越少,较少在大规模吞吐的场景中使用,所以该消息队列也不是我们文章中重点讨论的内容

Kafka

  • Apache Kafka它最初由LinkedIn公司基于独特的设计实现为一个分布式的提交日志系统( a distributed commit log),之后成为Apache项目的一部分。
  • 号称大数据的杀手锏,谈到大数据领域内的消息传输,则绕不开Kafka,这款为大数据而生的消息中间件,以其百万级TPS的吞吐量名声大噪,迅速成为大数据领域的宠儿,在数据采集、传输、存储的过程中发挥着举足轻重的作用。
    在这里插入图片描述
    在这里插入图片描述

RabbitMQ

  • RabbitMQ 2007年发布,是使用Erlang语言开发的开源消息队列系统,基于AMQP协议来实现。AMQP的主要特征是面向消息、队列、路由(包括点对点和发布/订阅)、可靠性、安全。AMQP协议更多用在企业系统内,对数据一致性、稳定性和可靠性要求很高的场景,对性能和吞吐量的要求还在其次。

在这里插入图片描述

RocketMQ

在这里插入图片描述

ActiveMQ

在这里插入图片描述

怎么选

(1)中小型软件公司,建议选RabbitMQ.

  • 一方面,erlang语言天生具备高并发的特性,而且他的管理界面用起来十分方便。
  • 不考虑rocketmq和kafka的原因是:
    • 一方面中小型软件公司不如互联网公司,数据量没那么大,选消息中间件,应首选功能比较完备的,所以kafka排除。
    • 不考虑rocketmq的原因是,rocketmq是阿里出品,如果阿里放弃维护rocketmq,中小型公司一般抽不出人来进行rocketmq的定制化开发,因此不推荐。
  • 大型软件公司,根据具体使用在rocketMq和kafka之间二选一。
    • 一方面,大型软件公司,具备足够的资金搭建分布式环境,也具备足够大的数据量。针对rocketMQ,大型软件公司也可以抽出人手对rocketMQ进行定制化开发,毕竟国内有能力改JAVA源码的人,还是相当多的。
    • 至于kafka,根据业务场景选择,如果有日志采集功能,肯定是首选kafka了。具体该选哪个,看使用场景。

消息队列模式

消息队列目前主要有2种模型,分别为“点对点模式”和“发布/订阅模式”。

点对点模式

  • 一个具体的消息只能由一个消费者消费。
  • 多个生产者可以向同一个消息队列发送消息
  • 一个消息在被一个消费者处理的时候,这个消息会在队列上被锁住或者被移除并且其他消费者无法处理该消息
  • 注意:如果一个消费者处理一个消息失败了,消息系统一般会把这个消息放回队列,这个其他消费者也可以继续处理

在这里插入图片描述

发布订阅模式

单个消息可以被多个订阅者并发的获取和处理。一般来说,订阅由两种类型:

  • 临时订阅:这种订阅只有在消费者启动并且运行的时候才存在。一旦消费者推出,相应的订阅以及尚未处理的消息就会丢失
  • 持久订阅:这种订阅会一直存在,除非主动去删除。消费者推出后,消息系统会继续维护该订阅,并且后继消息可以被继续处理

在这里插入图片描述

如何降低消息队列系统中消息的延迟

  • 假设当前消息队列中已经堆积了大量的消息,如果此时想要监控消息的堆积情况,首先需要从原理上了解,在消息队列中消费者的消息进度是多少,因为这样才能方便的计算当前的消息延迟是多少。比如说,生产者向队列中一共生产了1000条消息,某一个消费者消费进度是900条,那么这个消费者的消费延迟就是100条消息。
  • 只要涉及到队列,任务的堆积就是一个不可忽视的问题,很多故障都是源于此。因此,只
    要有队列就要监控它的堆积情况

如何监控消息延时

有两种方式:

  • 使用消息队列提供的工具,通过监控消息的堆积来完成
  • 通过生成监控消息的方式来监控消息的延迟情况

使用消息队列提供的工具

在 Kafka 中,消费者的消费进度在不同的版本上是不同的。

  • 在 Kafka0.9 之前的版本中,消费进度是存储在 ZooKeeper 中的,消费者在消费消息的时候,先要从 ZooKeeper 中获取最新的消费进度,再从这个进度的基础上消费后面的消息。
  • 在 Kafka0.9 版本之后,消费进度被迁入到 Kakfa 的一个专门的 topic叫“__consumer_offsets”里面。所以,如果你了解 kafka 的原理,你可以依据不同的版本,从不同的位置,获取到这个消费进度的信息。

当然,作为一个成熟的组件,Kafka 也提供了一些工具来获取这个消费进度的信息,帮助你实现自己的监控,这个工具主要有两个:“kafka-consumer-groups.sh”和JMX(推荐)

通过生成监控消息的方式

具体怎么做呢?

  • 先定义一种特殊的消息,然后启动一个监控程序,将这个消息定时的循环写入到消息队列中,消息的内容可以是生成消息的时间戳,并且也会作为队列的消费者消费数据。
  • 业务处理程序消费到这个消息时直接丢弃掉,而监控程序在消费到这个消息时,就可以和这个消息的生成时间做比较,如果时间差达到某一个阈值就可以向我们报警

在这里插入图片描述

怎么选

推荐两种方式结合来使用。比如优先在监控程序中获取 JMX 中的队列堆积数据,做到 dashboard 报表中,同时也会启动探测进程,确认消息的延迟情况是怎样的。

消息的堆积是对于消息队列的基础监控,这是你无论如何都要做的。但是,了解了消息的堆积情况,并不能很直观地了解消息消费的延迟,你也只能利用经验来确定堆积的消息量到了多少才会影响到用户的体验;而第二种方式对于消费延迟的监控则更加直观,而且从时间的维度来做监控也比较容易确定报警阈值。

了解到消息延迟的监控方式之后,我们再来看看如何提升消息的写入和消费性能,这样才能让异步的消息得到尽快的处理。

减少消息延迟的方法

想要减少消息的处理延迟,我们需要在消费端和消息队列两个层面来完成。

消费端

在消费端,我们的目的是提升消费者的消息处理能力,方法:

  • 优化消费代码提升性能
  • 增加消费者的数量(这个方式比较简单)

不过,第二种方式会受限于消息队列的实现。比如说,如果消息队列使用的是 Kafka 就无法通过增加消费者数量的方式,来提升消息处理能力。

  • 因为在 Kafka 中,一个 Topic(话题)可以配置多个 Partition(分区),数据会被平均或者按照生产者指定的方式,写入到多个分区中,那么在消费的时候,Kafka 约定一个分区只能被一个消费者消费,为什么要这么设计呢?如果有多个 consumer(消费者)可以消费一个分区的数据,那么在操作这个消费进度的时候就需要加锁,可能会对性能有一定的影响。
  • 所以说,话题的分区数量决定了消费的并行度,增加多余的消费者也是没有用处的,那么你可以通过增加分区来提高消费者的处理能力。

在这里插入图片描述
那么,如何在不增加分区的前提下提升消费能力呢?

  • 既然不能增加consumer,那么可以在一个consumer中提升消费的并行度。
  • 所以可以考虑使用多线程的方式来增加处理器:可以预先创建一个或者多个线程池,在接收到消息之后,把消息丢到线程池中来异步的处理
  • 这样,原本串行的消费消息的流程就变成了并行的消费,可以提升消费消息的吞吐量,在并行处理的前提下,我们就可以在一次和消息队列的交互中多拉取几条数据,然后分配给多个线程来处理。
    在这里插入图片描述

另外,在消费队列数据的时候注意消费线程空转的问题。

  • 如果消息队列中,有一段时间没有新的消息,于是消费客户端拉取不到新的消息就会不间断地轮询拉取消息,这个线程就可能把 CPU 跑满了。
  • 所以,你在写消费客户端的时候要考虑这种场景,拉取不到消息可以等待一段时间再来拉取,等待的时间不宜过长,否则会增加消息的延迟。我一般建议固定的 10ms~100ms,也可以按照一定步长递增,比如第一次拉取不到消息等待 10ms,第二次 20ms,最长可以到100ms,直到拉取到消息再回到 10ms。

消息队列

在设计消息中间件时,需要从如下两方面来考虑读取性能问题,比如:

  • 消息的存储
  • 零拷贝技术

针对第一点:

  • 刚开始的时候使用普通数据库来存储消息,但是受限于数据库的性能瓶颈,读取QPS只能到达2000
  • 改进方法是重构存储模块,使用本地磁盘作为存储介质。Page Cache的存在就可以提升消息的读取速度,即使要读取磁盘中的数据,由于消息的读取是顺序的,并且不需要跨网络读取数据,所以读取消息的QPS将会大大增加

针对第二点:

  • 说是零拷贝,其实,我们不可能消费数据的拷贝,只是尽量减少拷贝的次数。在读取消息队列的数据的时候,其实就是把磁盘中的数据通过网络发送给消费客户端,在实现上会有四次拷贝的步骤:
    • 数据从磁盘拷贝到内核缓冲区
    • 系统调用将内核缓冲区的数据拷贝到用户缓冲区
    • 用户缓冲区的数据被写入到socket缓冲区中
    • 操作系统再将socket缓冲区的数据拷贝到网卡的缓冲区
      在这里插入图片描述
  • 操作系统提供了sendfile函数,可以减少数据被拷贝的次数
    • 使用了sendfile之后,在内核缓冲区的数据不会被拷贝到用户缓冲区,而是直接拷贝到了socket缓冲区,节省了一次拷贝的过程,提升了消息发送的性能

在这里插入图片描述

  • 0
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值