消息队列是一种系统间相互协作的通信机制,使用消息队列的场景一般有异步处理、解耦、流量削峰、日志收集、事务最终一致性这几种,主要解决诸如消息堆积、消息持久化、可靠投递、消息重复、严格有序、集群等各种问题,目前的消息中间件种类有很多,比如ActiveMQ/RabbitMQ/RocketMQ/Kafka,我司使用的中间件为包裹RockerMQ的ZMQ,以及用于日志收集的kafka,本文分析了这几种中间件的特点,并结合公司使用情况,重点阐述RocketMQ的使用 参考资料 《分布式消息中间件实战(倪炜)沈剑》 《架构师之路》 《Spring实战》
1、数据驱动的任务依赖
任务之间有一定的依赖关系 1)task3需要使用task2的输出作为输入 2)task2需要使用task1的输出作为输入
2、上游不关心执行结果
上下游逻辑+物理解耦,除了与MQ有物理连接,模块之间都不相互依赖
3、上游关注执行结果,但执行时间很长
典型的是调用离线处理,或者跨公网调用,也经常使用回调网关+MQ来解耦。
例如:微信支付:跨公网调用微信的接口,执行时间会比较长,但调用方又非常关注执行结果
一般采用“回调网关+MQ”方案来解耦:
1)调用方直接跨公网调用微信接口
2)微信返回调用成功,此时并不代表返回成功
3)微信执行完成后,回调统一网关
4)网关将返回结果通知MQ
5)请求方收到结果通知
基于我们项目的限制以及功能和性能做出的选择
两种通信模式?
jms连接工厂—jms连接—jms会话—jms生产者—目的地----jms消费者
通讯协议TCP/UDP等
由两部分组成:报头和消息主体。
jms定义了5种不同的消息正文格式,以及调用的消息类型,允许你发送并接受一些不同形式的数据,提供现有消息格式的一些级别的兼容性。
两种消费模式
使用的jar包,5.11.2版本,不要使用5.12.0版本
1、创建连接工厂
2、配置生产者(声明消息的目的地)
3、配置消费者
推模式还是拉模式?
两个或多个客户端在相互发送或接受消息是,通常使用两种方法来传递消息。
消息是被推送(消息生产者)和拉取的(消费端),
流程:
1、管理员在商品管理模块添加/更新商品时,需要把数据提交到数据库中,同时,还得把数据发送到solr索引系统,来同步索引库的商品,另一方面,缓存系统来删除此商品的缓存
因此,用到了消息队列MQ,来保证消息处理的有序性及是否能重复消费及如何保证重复消费的幂等性。
幂等性
Activemq的作用就是系统之间进行通信。当然可以使用其他方式进行系统间通信,如果使用Activemq的话可以对系统之间的调用进行解耦,实现系统间的异步通信。
Activemq在项目中主要是完成系统之间通信,并且将系统之间的调用进行解耦。例如在添加、修改、删除商品信息后,需要将商品信息同步到solr索引库、同步缓存中的数据redis以及生成静态页面freemarker一系列操作。
使用队列保证最终一致性即可,不需要强一致性。我们也要考虑是否需要保证消息处理的有序性及如何保证,是否能重复消费及如何保证重复消费的幂等性。在实际开发中,我们经常使用队列进行异步处理、系统解耦、数据同步、流量消峰、扩展性、缓冲等。
应用场景:(优点)
异步处理:用户注册后,需要发送注册成功邮件/新用户积分/优惠券等,缓存过期后,先返回过期数据,然后异步更新缓存,异步写日志等。可以提升主流程响应速度。
系统解耦:比如:用户成功支付完成订单后,需要通知生产配货系统、发票系统、库存系统、推荐系统、搜索系统等进行业务处理,而未来需要支持哪些业务是不知道的,并且这些业务不需要实时处理、不需要强一致,只需要保证最终一致性即可。
数据同步:比如:想把Mysql变更的数据同步到redis,或者将mysql的数据同步到mongodb,或者让机房之间的数据同步,或者主从数据同步等,此时可以考虑使用databus、canal、otter等。使用数据总线队列进行数据同步的好处是可以保证数据修改的有序性。
流量削峰:系统瓶颈一般在数据库上,比如扣减库存、下单等。此时可以考虑使用队列将变更请求暂时放入队列,通过缓存+队列暂存的方式将数据库流量削峰。同样,对于秒杀系统,下单服务会是该系统的瓶颈,此时,可以使用队列进行排队和限流,从而保护下单服务,通过队列暂存或者队列限流进行流量削峰。
1、缓冲队列
典型的如log4j日志缓冲区,当我们使用log4j记录日志时,可以配置直字节缓冲区,字节缓冲区满后,会立即同步到磁盘,log4j是使用bufferedWriter实现的。在缓冲区满时,还是会阻塞主线程。
2、任务队列
线程池任务队列 默认使用linkedblockingQueue(重点)和Disruptor任务队列。如用户注册完成后,将发送邮件/送积分/送优惠券任务扔到任务队列中进行异步处理。刷数据时,将任务扔到异步处理,处理成功后在异步通知用户;删除SKU操作,在用户请求时直接将任务分解并扔到队列进行异步处理,处理成功后异步通知用户。
3、消息队列
消息队列有ActiveMQ,Kafka,Redis。使用消息队列存储各业务数据,其他系统根据需要订阅即可。常见的订阅模式是:点对点(一个消息只有一个消费者),发布订阅(一个消息可以有多个消费者)最常使用发布订阅模式。比如:修改商品数据,变更订单状态时,都应该将变更的信息发送到消息队列。其他模块有需要,就直接订阅该消息队列即可。一般我们在应用能够系统中采用双写模式,同时写DB和MQ,然后异构系统订阅MQ进行业务处理。没有事务保证,可能会出现数据不一致的情况。如果对一致性要求没那么严格,这种模式没什么问题。重点来了:如果在事务中发MQ,会存在事务回滚,但是MQ已经发送成功了,则需要消费者进行幂等处理(外部对接口的多次调用得到的结果是相同的)。如果事务提交慢,但是MQ已经发出去了,则此时根据MQ获取数据库的数据可能不是最新的。如果MQ发送慢,则会导致事务无法快速提交,造成数据库堵塞。同样不要在事务中掺杂RPC调用,rpc服务不稳定,同样会引起数据库阻塞。
4、请求队列
5、数据总线队列
6、混合队列
7、其他队列
MQ要想尽量消息必达,架构上有两个核心设计点:
(1)消息落地 (2)消息超时、重传、确认
MQ既然将消息投递拆成了上下半场,为了保证消息的可靠投递,上下半场都必须尽量保证消息必达。
MQ-client–1--> MQ-server–4-->MQ-client
<–3-- l l <–5—
2 6
db
MQ消息投递上半场
(1)MQ-client将消息发送给MQ-server(此时业务方调用的是API:SendMsg)
(2)MQ-server将消息落地,落地后即为发送成功
(3)MQ-server将应答ACK发送给MQ-client(此时回调业务方是API:SendCallback)
MQ消息投递下半场
(1)MQ-server将消息发送给MQ-client(此时回调业务方是API:RecvCallback)
(2)MQ-client回复ACK给MQ-server(此时业务方主动调用API:SendAck)
(3)MQ-server收到ack,将之前已经落地的消息删除,完成消息的可靠投递
MQ消息投递的上下半场,都可以出现消息丢失,为了降低消息丢失的概率,MQ需要进行超时和重传。
上半场的超时与重传:MQ上半场的1或者2或者3如果丢失或者超时,MQ-client-sender内的timer会重发消息,直到期望收到3,如果重传N次后还未收到,则SendCallback回调发送失败,需要注意的是,这个过程中MQ-server可能会收到同一条消息的多次重发。
下半场的超时与重传:MQ下半场的4或者5或者6如果丢失或者超时,MQ-server内的timer会重发消息,直到收到5并且成功执行6,这个过程可能会重发很多次消息,一般采用指数退避的策略,先隔x秒重发,2x秒重发,4x秒重发,以此类推,需要注意的是,这个过程中MQ-client-receiver也可能会收到同一条消息的多次重发
例子:上架图书,后台上传模块负责图书上架,下游索引模块负责更新索引库,通过MQ异步通知,不管是上半场的ACK丢失,导致MQ收到重复的消息,还是下面的ACK丢失,导致索引系统收到重复的商品id。
上半场的幂等:步骤2落地重复的消息,对每条消息,MQ系统内部必须生成一个inner-msg-id,作为去重和幂等的依据,这个内部消息ID的特性是:全局唯一,MQ生成,具备业务无关性。
下半场的幂等:业务消息体中,必须有一个biz-id,作为去重和幂等的依据,这个业务ID的特性是:对于同一个业务场景(支付ID,订单ID,帖子ID),全局唯一,由业务消息消费方负责判重,以保证幂等。
mq为了保证消息必达,消息上下半场均可能发送重复消息,如何保证消息的幂等性?
总结:MQ-client生成inner-msg-id,保证上半场幂等 这个id全局唯一,业务无关,由MQ保证
业务发送发带入biz-id,业务接收方去重保证幂等,这个ID对单业务唯一,业务相关,对MQ透明
RocketMQ中的处理方法:
已业务唯一标识作为幂等处理的关键依据,而业务的唯一标识可以通过消息key进行设置:
Message msg = new Message();
Msg.setKey(“ORDERID_100”);
sendResult sendResult = producer.send(message);
订阅方收到消息时可以根据消息的key进行幂等处理:
public Action consumer(Message message, ConsumeContext context){
String key = message.getKey();
//根据业务唯一标识的key做幂等处理
}
例如:图书商城订单完成后,如果用户一直不评价,一周后会将自动评价。
高效延时消息,包含两个重要的数据结构
(1)环形队列,例如可以创建一个包含3600个slot的环形队列(本质是个数组)
(2)任务集合,环上每一个slot是一个Set
同时,启动一个timer,这个timer每隔1s,在上述环形队列中移动一格,有一个Current Index指针来标识正在检测的slot
Task结构中有两个很重要的属性:
(1)Cycle-Num:当Current Index第几圈扫描到这个Slot时,执行任务
(2)Task-Function:需要执行的任务指针
假设当前Current Index指向第一格,当有延时消息到达之后,例如希望3610秒之后,触发一个延时消息任务,只需:
(1)计算这个Task应该放在哪一个slot =11 (2)计算这个Task的Cycle-Num =1 减法运算 如果变为0,说明马上要执行这个Task了,取出Task-Funciton,使用了“延时消息”方案之后,只需要在订单完成后,触发一个48小时之后的延时消息即可。
总结:环形队列是一个实现“延时消息”的好方法,开源的MQ好像都不支持延迟消息
对比:
系统瓶颈一般在数据库上,比如扣减库存、下单等。此时可以考虑使用队列将变更请求暂时放入队列,通过缓存+队列暂存的方式将数据库流量削峰。
流量冲击:订单模块发起下单操作,下游模块完成业务逻辑(库存检查,库存冻结,余额检查,余额冻结,订单生成,余额扣减,库存扣减,生成流水,余额解冻,库存解冻)
上游业务简单,下游业务复杂,很有可能上游不限速的下单,导致下游系统被压垮。
如何缓冲流量?
可以利用哪个MQ来缓冲,MQ-server推模式改为MQ-client拉模式。MQ-client根据自己的处理能力,每隔一定时间,或者每次拉取若干条消息,实施流控,达到保护自身的效果。
总结:MQ-client提供拉模式,定时或者批量拉取,可以削平流量,下游自我保护的作用(MQ来做)
要想提升整体吞吐量,需要下游优化,例如批量处理等方式。
消息中间件 | 开发语言 | 单机吞吐量 | 时效性 | 消息的存储 | 可用性 | 功能特性 |
---|---|---|---|---|---|---|
activeMQ | java | 万级 | ms级 | 关系型数据库KahaDB | 高(主从架构) | 成熟的产品,在很多公司得到应用,有较多的文档;各种协议支持较好 |
RabbitMQ | erlang | 万级 | us级 | 文件系统 | 高(主从架构) | 基于erlang开发,所以并发能力很强,性能极其好,延时2很低,管理界面较丰富 |
RocketMQ | java | 10万级 | ms级 | 文件系统 | 非常高(分布式架构) | Mq功能比较完善,扩展性佳 |
Kafka | scala | 10万级 | ms级以内 | 文件系统 | 非常高(分布式架构) | 只支持主要的mq功能,像一些消息查询,消息回溯等功能没有提供,毕竟是为大数据准备的,在大数据领域应用广。 |
特点补充
它能够以广播和点对点的技术实现队列,它少量代码就可以高效地实现高级应用场景
它非常重量级,更适合于企业级的开发。同时实现了Broker构架,这意味着消息在发送给客户端时先在中心队列排队。对路由,负载均衡或者数据持久化都有很好的支持
1、是一个分布式的、支持分区的、多副本的、支持zookeeper协调的分布式消息系统
2、可以实时的处理大量数据以满足各种需求场景:比如:基于Hadoop的批处理系统、低延时的实时系统、storm/spark流式处理引擎、web/ nginx日志、访问日志、消息服务等
3、高性能跨语言分布式发布/订阅消息队列系统,可以处理大规模网站中所有动作流数据:网页浏览、搜索和其他用户的行为
3、kafka(scala语言开发): java优先
特点:(消息的顺序保证性强)
kafka设计思想,底层原理 个推
Controller在ZooKeeper注册Watch:
Activemq 有两种通信方式,点到点形式和发布订阅模式。
不能。所以我们应该用zookeeper来选主,让主去消费队列,并且队列要设置成exclusive。这样我们就保证队列中的消息是被顺序消费的
ActiveMQ集群部署
客户端使用failover来容错(失败自动切换,当出现失败,重试其它服务器)
RocketMQ集群搭建
集群特点:
ActiveMQ接收到Message后需要借助持久化方案来完成消息存储。可以通过多种介质完成存储:磁盘文件系统、ActiveMQ内置数据库或第三方关系型数据库
1、 使用自带的db KahaDB (默认的持久化存储方案)
KahaDB 文件所在位置是您的 ActiveMQ 安装路径下的/data/KahaDB 子目录下(没看到)
官方认为 KahaDB 使用了更少的文件描述符,设计目标是支持事务日志、可靠、可扩展、速度快等
2、 AMQ
不依赖于第三方数据库,用户能够快速启动和运行
在此方案下消息以日志的形式实现持久化,存放在 Data Log 里。
3、JDBC
支持使用关系型数据库进行持久化存储–通过JDBC实现的数据库连接。可以支持的关系型数据库包括:db2,mysql,oracle,sqlserver,sybase等
许多企业使用关系数据库作为存储,是因为他们更愿意充分利用这些数据库资源,比如已有的热备和负载方案
4、内存存储
内存消息存储器将所有持久消息保存在内存中。在仅存储有限数量message的情况下,内存消息存储会很有用,因为message通常会被快速消耗。
一般用于实时消息的缓存,只针对非持久订阅的消费者提供了5种订阅恢复策略,可以极大程度增强非持久订阅的可用性。
1、rabbitMQ基本概念
2、消息的分发流程
3、Exchange 类型
它是Exchange在路由消息时的分发策略,目前RabbitQueue常见的类型有三种:Direct Exchange,Fanout Exchange,Topic Exchange,
实际项目中会根据业务特点进行选型
4、rabbitMQ知识点
5、JMS和AMQP的区别?
JMS AMQP
1、定义 java api 网络线级协议
2、跨语言 否 是
3、跨平台 否 是
4、model 提供两种消息模式p2p pub/sub 提供了五种消息模型 direct交换机/fanout交换机/topic交换机/headers交换机/system交换机,本质上,后4中与jms的pub/sub模型没有太大区别
5、支持消息类型 多种消息类型textMessage/MapMessage/BytesMessage/StreamMessage/ObjectMessage byte[] 当实际应用时,有复杂的消息,可以将消息序列化后发送
6、综合评价 jms定义了javaapi层面的标准,在java体系中,多个client均可以通过jms进行交互,不需要修改代码,但是其对跨平台的支持较差;
AMQP定义wire-level层的协议标准,天然具有跨平台、跨语言特性。
TCC编程模式(两阶段提交的变形)将业务逻辑分为三块:try预留业务资源(类似DML锁定资源),confirm确认执行业务操作(类似commit),cancel取消执行业务操作(类似rollback)以下单为例:try阶段会扣除库存,confirm阶段更新订单状态,如果更新订单失败,会进入cancel阶段,恢复库存。TCC开源框架:tcc-transaction (补偿性分布式事务框架)
后续补充该知识点
activeMQ的幂等
接收端一定要控制消息的幂等性(某一消息的多次执行的结果与一次执行的结果没有差别,即是幂等)
保序的解决方案:
就是让订阅端先保存消息,之后再处理
后续补充该知识点
消息发送:发送同步消息,发送异步消息,单向发送消息
消费消息:负载均衡模式,广播模式
导入mq客户端依赖
<dependency>
<groupId>org.apache.rocketmq groupId>
<aritfactId>rocketmq-clientaritfactId>
<version>4.4.0version>
dependency>
消息发送者步骤分析
1、创建消息生产者producer,并制定生产者组名
2、指定nameserver地址
3、启动producer
4、创建消息对象,指定主题topic,tag和消息体
5、发送消息,
6、关闭生产者producer
消息消费者步骤分析
1、创建消费者consumer,制定消费者组名;
2、制定namespace地址;
3、启动producer;
4、创建消息对象,指定主题topic,tag和消息体;
5、发送消息;
6、关闭生产者producer。
单向发送消息
消息重试
消息重试配置方式
普通消息
单向发送消息:不用关心发送结果的场景,例如:日志发送
消费消息:
1、负载均衡模式
多个消费者共同消费队列消息,每个消费者处理的消息不同
2、广播模式
每个消费者消费的消息都是相同的
(无序消息)普通,定时,延时,事务消息
默认允许每条消息最多重试16次(时间:4小时46分钟)
messageID不会被改变
想重试,三种方式:
1、返回Action.ReconsumeLater(推荐)
2、返回NUll
3、抛出异常
不想重试:
Action.CommitMessage
顺序消息
保证的是局部顺序,业务相关的消息放在一个队列,然后消费者针对一个队列的消息使用单线程来处理。
(顺序消息)不断进行消息重试,每次间隔1s,会出现消息消费被阻塞的情况 关注
死信队列
超过最大重试次数后还未被正常消费,进入死信队列(3天之内处理)
处理方式:排查造成死信队列的原因后,在消息队列RocketMq控制台重新发送该消息,让消费者重新消费
延时消息
RocketMQ并不支持任意时间的延迟,需要设置几个固定的延时等级(1s到2h)
技巧:使用system.in.read()可以保持线程处于运行状态,让子线程能正常运行
批量消息
批量发送消息能显著提高传递小消息的性能,限制是这些批量消息应该有相同的topic,相同的waitStoreMsgOK,而且不能是延时消息。
总大小不应超过4MB,超过4MB时,最好把消息进行分割
过滤消息
1、由tag过滤
2、根据sql过滤
事务消息
事务消息的流程:
正常事务消息的发送和提交,
1、发送消息(half消息)
2、服务端响应消息写入结果,
3、根据发送结果执行本地事务(如果写入失败,此时half消息对业务不可见,本地逻辑不执行)
4、根据本地事务状态执行commit或者rollback(commit操作生成消息索引,消息对消费者可见)
事务消息的补偿流程
1、对没有commit,rollback的事务消息(pending状态的消息,从服务端发起一次”回查“)
2、producer收到回查消息,检查回查消息对应的本地事务的状态
3、根据本地事务状态,重新commit或者rollback
补偿阶段用于解决消息commit或rollback发生超时或失败的情况。
事务消息状态
transactionStatus.commitTransaction:提交事务,它允许消费者消费此消息
transactionStatus.rollbackTransaction 回滚事务,它代表该消息将被删除,不允许被消费
transactionStatus.unknown:中间状态,它代表需要检查消息队列来确定状态
事务消息核心流程如上图所示。
事务消息的使用限制:
在这里插入代码片
项目实战(下单,支付)(使用技术:dubbo,RocketMQ,zookeeper,Springboot,MySql)
springboot整合rocketMQ
1、消息生产者
添加依赖
配置文件
启动类
测试类
rocketMQTemplate.convertAndSend(“springboot-rocketmq”,”hello springboot”,111);
2、消息消费者
消息监听器
@RocketMQMessageListener(topic=“springboot-mq”,consumerGoup= “”)
Public class Consumer implements RocketMQListener<String>{
}
3 ZMQ调试技巧
后续补充
Kafka 一个最基本的架构认识:由多个 broker 组成,每个 broker 是一个节点;你创建一个 topic,这个 topic 可以划分为多个 partition,每个 partition 可以存在于不同的 broker 上,每个 partition 就放一部分数据。
这就是天然的分布式消息队列,就是说一个 topic 的数据,是分散放在多个机器上的,每个机器就放一部分数据。
实际上 RabbmitMQ 之类的,并不是分布式消息队列,它就是传统的消息队列,只不过提供了一些集群、HA(High Availability, 高可用性) 的机制而已,因为无论怎么玩儿,RabbitMQ 一个 queue 的数据都是放在一个节点里的,镜像集群下,也是每个节点都放这个 queue 的完整数据。
Kafka 0.8 以前,是没有 HA 机制的,就是任何一个 broker 宕机了,那个 broker 上的 partition 就废了,没法写也没法读,没有什么高可用性可言。比如说,我们假设创建了一个 topic,指定其 partition 数量是 3 个,分别在三台机器上。但是,如果第二台机器宕机了,会导致这个 topic 的 1/3 的数据就丢了,因此这个是做不到高可用的。
Kafka 0.8 以后,提供了 HA 机制,就是 replica(复制品) 副本机制。每个 partition 的数据都会同步到其它机器上,形成自己的多个 replica 副本。所有 replica 会选举一个 leader 出来,那么生产和消费都跟这个 leader 打交道,然后其他 replica 就是 follower。写的时候,leader 会负责把数据同步到所有 follower 上去,读的时候就直接读 leader 上的数据即可。只能读写 leader?很简单,要是你可以随意读写每个 follower,那么就要 care 数据一致性的问题,系统复杂度太高,很容易出问题。Kafka 会均匀地将一个 partition 的所有 replica 分布在不同的机器上,这样才可以提高容错性。
这么搞,就有所谓的高可用性了,因为如果某个 broker 宕机了,没事儿,那个 broker上面的 partition 在其他机器上都有副本的。如果这个宕机的 broker 上面有某个 partition 的 leader,那么此时会从 follower 中重新选举一个新的 leader 出来,大家继续读写那个新的 leader 即可。这就有所谓的高可用性了。
写数据的时候,生产者就写 leader,然后 leader 将数据落地写本地磁盘,接着其他 follower 自己主动从 leader 来 pull 数据。一旦所有 follower 同步好数据了,就会发送 ack 给 leader,leader 收到所有 follower 的 ack 之后,就会返回写成功的消息给生产者。(当然,这只是其中一种模式,还可以适当调整这个行为)
消费的时候,只会从 leader 去读,但是只有当一个消息已经被所有 follower 都同步成功返回 ack 的时候,这个消息才会被消费者读到。
所有的消息队列都会有这样重复消费的问题,因为这是不MQ来保证,而是我们自己开发保证的,我们使用Kakfa来讨论是如何实现的。
其实重复消费不可怕,可怕的是你没考虑到重复消费之后,怎么保证幂等性。
举个例子吧。假设你有个系统,消费一条消息就往数据库里插入一条数据,要是你一个消息重复两次,你不就插入了两条,这数据不就错了?但是你要是消费到第二次的时候,自己判断一下是否已经消费过了,若是就直接扔了,这样不就保留了一条数据,从而保证了数据的正确性。一条数据重复出现两次,数据库里就只有一条数据,这就保证了系统的幂等性。幂等性,通俗点说,就一个数据,或者一个请求,给你重复来多次,你得确保对应的数据是不会改变的,不能出错。
所以第二个问题来了,怎么保证消息队列消费的幂等性?
其实还是得结合业务来思考,这里给几个思路:
MQ的基本原则就是数据不能多一条,也不能少一条,不能多其实就是我们前面重复消费的问题。不能少,就是数据不能丢,像计费,扣费的一些信息,是肯定不能丢失的。
数据的丢失问题,可能出现在生产者、MQ、消费者中。
消费者丢数据
唯一可能导致消费者弄丢数据的情况,就是说,你消费到了这个消息,然后消费者那边自动提交了 offset,让 Kafka 以为你已经消费好了这个消息,但其实你才刚准备处理这个消息,你还没处理,你自己就挂了,此时这条消息就丢咯。
这不是跟 RabbitMQ 差不多吗,大家都知道 Kafka 会自动提交 offset,那么只要关闭自动提交 offset,在处理完之后自己手动提交 offset,就可以保证数据不会丢。但是此时确实还是可能会有重复消费,比如你刚处理完,还没提交 offset,结果自己挂了,此时肯定会重复消费一次,自己保证幂等性就好了。
生产环境碰到的一个问题,就是说我们的 Kafka 消费者消费到了数据之后是写到一个内存的 queue 里先缓冲一下,结果有的时候,你刚把消息写入内存 queue,然后消费者会自动提交 offset。然后此时我们重启了系统,就会导致内存 queue 里还没来得及处理的数据就丢失了。
Kafka丢数据
我们生产环境就是按照上述要求配置的,这样配置之后,至少在 Kafka broker 端就可以保证在 leader 所在 broker 发生故障,进行 leader 切换时,数据不会丢失。
给 topic 设置 replication.factor 参数:这个值必须大于 1,要求每个 partition 必须有至少 2 个副本。
在 Kafka 服务端设置 min.insync.replicas 参数:这个值必须大于 1,这个是要求一个 leader 至少感知到有至少一个 follower 还跟自己保持联系,没掉队,这样才能确保 leader 挂了还有一个 follower 吧。
在 producer 端设置 acks=all:这个是要求每条数据,必须是写入所有 replica 之后,才能认为是写成功了。
在 producer 端设置 retries=MAX(很大很大很大的一个值,无限次重试的意思):这个是要求一旦写入失败,就无限重试,卡在这里了。
生产者丢数据
如果按照上述的思路设置了 acks=all,一定不会丢,要求是,你的 leader 接收到消息,所有的 follower 都同步到了消息之后,才认为本次写成功了。如果没满足这个条件,生产者会自动不断的重试,重试无限次。
我举个例子,我们以前做过一个 mysql binlog 同步的系统,压力还是非常大的,日同步数据要达到上亿,就是说数据从一个 mysql 库原封不动地同步到另一个 mysql 库里面去(mysql -> mysql)。常见的一点在于说比如大数据 team,就需要同步一个 mysql 库过来,对公司的业务系统的数据做各种复杂的操作。
你在 mysql 里增删改一条数据,对应出来了增删改 3 条 binlog 日志,接着这三条 binlog 发送到 MQ 里面,再消费出来依次执行,起码得保证人家是按照顺序来的吧?不然本来是:增加、修改、删除;你楞是换了顺序给执行成删除、修改、增加,不全错了么。
本来这个数据同步过来,应该最后这个数据被删除了;结果你搞错了这个顺序,最后这个数据保留下来了,数据同步就出错了。
Kafka:比如说我们建了一个 topic,有三个 partition。生产者在写的时候,其实可以指定一个 key,比如说我们指定了某个订单 id 作为 key,那么这个订单相关的数据,一定会被分发到同一个 partition 中去,而且这个 partition 中的数据一定是有顺序的。消费者从 partition 中取出来数据的时候,也一定是有顺序的。到这里,顺序还是 ok 的,没有错乱。接着,我们在消费者里可能会搞多个线程来并发处理消息。因为如果消费者是单线程消费处理,而处理比较耗时的话,比如处理一条消息耗时几十 ms,那么 1 秒钟只能处理几十条消息,这吞吐量太低了。而多个线程并发跑的话,顺序可能就乱掉了。
Kafka解决方案
一个 topic,一个 partition,一个 consumer,内部单线程消费,单线程吞吐量太低,一般不会用这个。
写 N 个内存 queue,具有相同 key 的数据都到同一个内存 queue;然后对于 N 个线程,每个线程分别消费一个内存 queue 即可,这样就能保证顺序性。
大量消息在 mq 里积压了几个小时了还没解决
一个消费者一秒是 1000 条,一秒 3 个消费者是 3000 条,一分钟就是 18 万条。所以如果你积压了几百万到上千万的数据,即使消费者恢复了,也需要大概 1 小时的时间才能恢复过来。
一般这个时候,只能临时紧急扩容了,具体操作步骤和思路如下:
假设你用的是 RabbitMQ,RabbtiMQ 是可以设置过期时间的,也就是 TTL。如果消息在 queue 中积压超过一定的时间就会被 RabbitMQ 给清理掉,这个数据就没了。那这就是第二个坑了。这就不是说数据会大量积压在 mq 里,而是大量的数据会直接搞丢。
这个情况下,就不是说要增加 consumer 消费积压的消息,因为实际上没啥积压,而是丢了大量的消息。我们可以采取一个方案,就是批量重导,这个我们之前线上也有类似的场景干过。就是大量积压的时候,我们当时就直接丢弃数据了,然后等过了高峰期以后,比如大家一起喝咖啡熬夜到晚上12点以后,用户都睡觉了。这个时候我们就开始写程序,将丢失的那批数据,写个临时程序,一点一点的查出来,然后重新灌入 mq 里面去,把白天丢的数据给他补回来。也只能是这样了。
假设 1 万个订单积压在 mq 里面,没有处理,其中 1000 个订单都丢了,你只能手动写程序把那 1000 个订单给查出来,手动发到 mq 里去再补一次。
如果消息积压在 mq 里,你很长时间都没有处理掉,此时导致 mq 都快写满了,咋办?这个还有别的办法吗?没有,谁让你第一个方案执行的太慢了,你临时写程序,接入数据来消费,消费一个丢弃一个,都不要了,快速消费掉所有的消息。然后走第二个方案,到了晚上再补数据吧。
写数据:过内存S大小(S可设置)的数据后,直接刷进磁盘,追加写入文件;
读数据:根据offset读取位置之后S大小的数据,进内存;
删数据:直接删磁盘文件(segment file),先删老文件(可设置)。
《寒山子诗集》:寒山与拾得两位大师是佛教史上著名的诗僧。唐代天台山国清寺隐僧寒山与拾得,行迹怪诞,相传是文殊菩萨与普贤菩萨的化身。寒山问曰:“世间有人谤我、欺我、辱我、笑我、轻我、贱我、恶我、骗我,该如何处之乎?”拾得答曰:“只需忍他、让他、由他、避他、耐他、敬他、不要理他、再待几年,你且看他。”这个绝妙的问答,蕴含了面对人我是非的处世之道,因此虽经一千多年,至今仍然脍炙人口。