此篇文章为个人笔记,记录下能想到的消息队列相关问题
三点:解耦、异步、削峰。
具体说说:
(1)解耦:解除耦合,比如在一个需求中一个系统A,需要发数据给B、C、D系统。后来由于需求变动,C不需要这个数据了,我们就需要你去改A系统的代码。然后过了几天A又需要给E系统发数据,又需要改A代码。耦合度很高。这个时候就需要引入消息队列的概念,只需要A系统配置好消息生产者,BCDE系统可以根据需求去消费,不要再去修改A系统的代码。
(2)异步:异步调用,比如在一个需求中,A系统需要在本地写库,运行时间50ms,然后还需要在B系统写库,运行时间100ms。然后又需要调用C系统方法,运行时间200ms,,那么总共的运行时间是350ms。如果引入消息队列,就可以将B、C的方法修改为异步方法,不用等待BC系统去执行完写库操作,就继续执行A的写库方法,总运行时间50ms。
(3)削峰。削弱峰值,比如正常时间段系统A每秒并发数100个,不会发生任何问题。但是到了高峰时间段,开始抢购商品,每秒并发数暴增到1万个,系统开始撑不住了,系统最大的处理能力就是每秒1000个请求,这个时候就可以引入消息队列。
(1)系统可用性降低。系统引入的外部依赖越多,越容易挂掉。一开始只有ABCD四个系统,现在引入个MQ,万一MQ全挂掉了怎么办,所有系统都运行不下去了。
(2)系统复杂度变高。引入了MQ,就需要考虑和解决MQ带来的问题。比如MQ消息丢失、消费者重复消费、怎么让消费者按顺序消费等问题。
(3)数据一致性。A给BCD三个系统发消息,如果B系统挂了,CD系统没事。就会造成B系统和其他系统数据不一致。
个人总结:
(1)单机吞吐量方面:ActiveMQ和RabbitMQ都是万级的。而RocketMq和kafka都是十万级别
(2)时效性:RabbitMQ最快,为微秒级。其他都为ms级,但是一般情况下也都够用了。
(3)可用性:ActiveMQ和RabbitMQ都是可用性高,基于主从架构。而RocketMq和kafka是非常高,分布式架构
(4)消息可靠性:ActiveMQ有较低的概率丢失数据,RabbitMQ有自己的死信队列机制,一般情况也不会丢。RocketMq和kafka经过参数优化配置,消息可以做到0丢失。
(5)功能特性:
ActiveMQ:
1.基于Java开发,已经非常成熟,功能强大,业内大量的公司在使用。
2.偶然会发生消息丢失的事件、
3.但是社区以及国内部署的越来越少,官方社区已经几个月才更新一次版本。
所以一般只需要解决解耦、和异步问题上才会去考虑。
RabbitMQ:
1,基于erlang开发,性能极其好,延时很低。但是源码看不懂,很难定制开发。
2.吞吐量万级,功能完备。
3.而且开源提供的管理页面非常好用,
4.社区活跃,基本每个月都发布几个版本。国内挺多新型互联网公司在使用RabbitMQ
RocketMQ:
1.基于java开发,接口简单易用。
2.阿里开发,必属精品。 品牌保障
3…日处理消息上百亿之多,可以做到大规模吞吐,性能非常好。
4.分布式扩展方便,可以支撑大规模的topic数量,支持复杂MQ业务场景。
5.社区活跃
6.阿里没准哪天抛弃这个技术,社区也就黄了,没人维护了。是个风险。
Kafka:
1.基于scala开发。
2.仅仅提供较少的核心功能,但是提供超高的吞吐量,ms级的延迟,极高的可用性以及可靠性,而且分布式可以任意扩展。
3.最好是支撑较少的topic数量即可,保证其超高吞吐量
4.唯一的一点劣势是有可能消息重复消费,那么对数据准确性会造成极其轻微的影响,在大数据领域中以及日志采集中,这点轻微影响可以忽略
保证高可用,最容易想到的就是多加机器,如果一台机器挂了,还有其他机器顶着,也就是集群部署。
RabbitMQ的部署方式,分为:单机部署、普通集群模式、镜像集群模式。单机部署不说了。
(1)普通集群:
①:一个消息发给MQ集群,MQ会先随机选取其中一个机器A中存取queue的元数据信息和实际数据,而机器B、C只会同步元数据。当实际消费的时候,消费者如果选取的是另外一个实例,MQ会先从存取queue的实例上拉取数据。
这种方式确实很麻烦,也不怎么好,没做到所谓的分布式,就是个普通集群。因为这导致你要么消费者每次随机连接一个实例然后拉取数据,要么固定连接那个queue所在实例消费数据,前者有数据拉取的开销,后者导致单实例性能瓶颈。
②:如果存取queue的实例A挂了,那么他保存的queue实际数据也就丢失了。其他实例也就无法拉去数据了,如果你开启了消息持久化,让rabbitmq落地存储消息的话,消息不一定会丢,得等这个实例恢复了,然后才可以继续从这个queue拉取数据。
(2)镜像集群
真正的高可用模式,跟普通集群模式不一样的是,你创建的queue,无论元数据还是queue里的消息都会存在于多个实例上,然后每次你写消息到queue的时候,都会自动把消息到多个实例的queue里进行消息同步。
这样的话,好处在于,你任何一个机器宕机了,没事儿,别的机器都可以用。坏处在于,第一,这个性能开销也太大了吧,消息同步所有机器,导致网络带宽压力和消耗很重!第二,这么玩儿,就没有扩展性可言了,如果某个queue负载很重,你加机器,新增的机器也包含了这个queue的所有数据,并没有办法线性扩展你的queue.
kafka一个最基本的架构认识:
天然的分布式消息队列,由多个broker组成,每个broker是一个节点;你创建一个topic,这个topic可以划分为多个partition,每个partition可以存在于不同的broker上,每个partition就放一部分数据。
kafka 0.8以前,任何一个broker宕机了,那个broker上的partition就废了,没法写也没法读,数据不全了,没有什么高可用性可言。
kafka0.8之后,提供了HA机制,就是replica副本机制。每个partition的数据都会同步到其他机器上,形成自己的多个replica副本。然后所有replica会选举一个leader出来,那么生产和消费都跟这个leader打交道,然后其他replica就是follower。写的时候,leader会负责把数据同步到所有follower上去,读的时候就直接读leader上数据即可。只能读写leader?很简单,要是你可以随意读写每个follower,那么就要care数据一致性的问题,系统复杂度太高,很容易出问题。kafka会均匀的将一个partition的所有replica分布在不同的机器上,这样才可以提高容错性。
这么搞,就有所谓的高可用性了,因为如果某个broker宕机了,没事儿,那个broker上面的partition在其他机器上都有副本的,如果这上面有某个partition的leader,那么此时会重新选举一个新的leader出来,大家继续读写那个新的leader即可。
写数据的时候,生产者就写leader,然后leader将数据落地写本地磁盘,接着其他follower自己主动从leader来pull数据。一旦所有follower同步好数据了,就会发送ack给leader,leader收到所有follower的ack之后,就会返回写成功的消息给生产者。
一般的MQ,如ActiveMQ或RabbitMQ.
保持幂等性
(1)写库操作,先根据主键查一下库,如果存在,那就不插库了。
(2)用redis,每次消费到数据的时候,先根据唯一键,比如订单号、联系人等信息,先拼装好,然后放到redis中,然后再次消费的时候,先查一下redis中有没有这个数据,如果有那就证明已经消费过了,不再执行其他操作。
kafka,自带的offset
kafka实际上有个offset的概念,就是每个消息写进去,都有一个offset,代表他的序号,然后consumer消费了数据之后,每隔一段时间,会把自己消费过的消息的offset提交一下,代表我已经消费过了,下次我要是重启啥的,你就让我继续从上次消费到的offset来继续消费吧。
有一个问题,offset不是每次消费的时候都执行,它是分批执行的。如果宕机了,但是还有几个数据没有offset了,那么等机器重启之后,还会重新消费上次offet之后的数据,也就是可能会重复消费到数据。
如果再没防住,就需要上边的保持幂等性操作。
比如,你在mysql里增删改一条数据,对应出来了增删改3条binlog,接着这三条binlog发送到MQ里面,到消费出来依次执行,起码得保证人家是按照顺序来的吧?不然本来是:增加、修改、删除;楞是换了顺序给执行成删除、修改、增加。
顺序会错乱的俩场景
(1)rabbitmq:一个queue,多个consumer,这不明显乱了
(2)kafka:一个topic,一个partition,一个consumer,内部多线程,这不也明显乱了
如何保证消息的顺序性呢?
(1)rabbitmq:拆分多个queue,每个queue一个consumer,就是多一些queue而已,确实是麻烦点;或者就一个queue但是对应一个consumer,然后这个consumer内部用内存队列做排队,然后分发给底层不同的worker来处理
(2)kafka:一个topic,一个partition,一个consumer,内部单线程消费,写N个内存queue,然后N个线程分别消费一个内存queue即可
(1)赶紧修复机器,确保其恢复消费速度,然后将现有cnosumer都停掉
(2)新建一个topic,partition是原来的10倍,临时建立好原先10倍或者20倍的queue数量.
(3)然后写一个临时的分发数据的consumer程序,这个程序部署上去消费积压的数据,消费之后不做耗时的处理,直接均匀轮询写入临时建立好的10倍数量的queue
(4)接着临时征用10倍的机器来部署consumer,每一批consumer消费一个临时queue的数据
(5)这种做法相当于是临时将queue资源和consumer资源扩大10倍,以正常的10倍速度来消费数据
(6)等快速消费完积压数据之后,得恢复原先部署架构,重新用原先的consumer机器来消费消息
(1) RabbitMQ
1.生产者数据丢失,没有发到MQ
(1)开始Rabbit的事务机制,就是生产者发送数据之前开启rabbitmq事务(channel.txSelect),然后发送消息,如果消息没有成功被rabbitmq接收到,那么生产者会收到异常报错,此时就可以回滚事务(channel.txRollback),然后重试发送消息;如果收到了消息,那么可以提交事务(channel.txCommit)。但是问题是,rabbitmq事务机制一搞,基本上吞吐量会下来,因为太耗性能。一般不选用。
最常用的模式:
(2)开启confirm模式,在生产者那里设置开启confirm模式之后,你每次写的消息都会分配一个唯一的id,然后如果写入了rabbitmq中,rabbitmq会给你回传一个ack消息,告诉你说这个消息ok了。如果rabbitmq没能处理这个消息,会回调你一个nack接口,告诉你这个消息接收失败,你可以重试。而且你可以结合这个机制自己在内存里维护每个消息id的状态,如果超过一定时间还没接收到这个消息的回调,那么你可以重发。
2.MQ本身丢失了数据
开启rabbitmq的持久化,就是消息写入之后会持久化到磁盘,哪怕是rabbitmq自己挂了,恢复之后会自动读取之前存储的数据,一般数据不会丢。除非极其罕见的是,rabbitmq还没持久化,自己就挂了,可能导致少量数据会丢失的,但是这个概率较小。
3.消费者没有消费到数据
rabbitmq如果丢失了数据,主要是因为你消费的时候,刚消费到,还没处理,结果进程挂了,比如重启了,那么就尴尬了,rabbitmq认为你都消费了,这数据就丢了。
这个时候得用rabbitmq提供的ack机制,简单来说,就是你关闭rabbitmq自动ack,可以通过一个api来调用就行,然后每次你自己代码里确保处理完的时候,再程序里ack一把。这样的话,如果你还没处理完,不就没有ack?那rabbitmq就认为你还没处理完,这个时候rabbitmq会把这个消费分配给别的consumer去处理,消息是不会丢的。
(2)kafka
从消费者本身
此处的问题是当处理完业务还未来的及offset时宕机了,会导致重复消费,所以要确保数据幂等性。
kafka本身弄丢数据
kafka某个broker宕机,然后重新选举partiton的leader时。要是此时其他的follower刚好还有些数据没有同步,结果此时leader挂了,然后选举某个follower成leader之后,就少了一些数据。
1.给这个topic设置replication.factor参数:这个值必须大于1,要求每个partition必须有至少2个副本
2.在kafka服务端设置min.insync.replicas参数:这个值必须大于1,这个是要求一个leader至少感知到有至少一个follower还跟自己保持联系,没掉队,这样才能确保leader挂了还有一个follower吧
3.在producer端设置acks=all:这个是要求每条数据,必须是写入所有replica之后,才能认为是写成功了
4.在producer端设置retries=MAX(很大很大很大的一个值,无限次重试的意思):这个是要求一旦写入失败,就无限重试,卡在这里了
生产者
如果按照上述操作设置了ack=all就不会丢失数据,要求是leader接收到消息,所有的follower都同步到了消息之后,才认为本次写成功了。如果没满足这个条件,生产者会自动不断的重试,重试无限次。