为什么要使用消息队列?
作用1: 削峰填谷(突发大请求量问题)
case1:
- 聊天(比如 QQ ,微信): 在过年的前后的短时间内QPS差距非常大
- 过年前(23:59:59): QPS 只有1万
- 过年后(00:00:00): QPS 会超过 10 万;所有人都在疯狂的发送"新年快乐"这样的新年祝福
- 这时服务器不可能承受瞬间超过平时 10 倍的压力
这时就可以使用消息队列:
- 将消息存在消息队列中
- 按照服务能够消费的速度进行消费
- K8S的弹性伸缩,当压力太大时会自动拓展服务器,加快处理能力;当压力降低会收缩服务器降低成本(k8s 的弹性伸缩也需要时间,如果没有消息队列做削峰填谷,还没有拓展服务器服务就被打爆了)
- 当压力变小后: 将拓展的服务器杀掉,节约成本
作用2: 解耦(单一原则)
case1:
- 用户登录: 关联的功能非常多(token 颁发,消息推送,活动的推荐,计算在线时长等)
- 如果全部做在登录的api 中:登录的代码可能超过几千行,开发困难,并且难以维护.
原则是一次只做一件事
- 登录就只颁发 token
- 将登录的消息存在消息队列中
- 与登录相关的其他服务(消息推送,活动的推荐,计算在线时长等)订阅登录的消息,然后做进一步的操作
作用3: 异步(减少处理时间)
- 用户登录: 关联的功能非常多(token 颁发,消息推送,活动的推荐,计算在线时长等)
- 要自信的任务非常多,执行时间久(用户等待时间长)
使用消息队列进行异步处理
- 完成关键业务(获取 token,消息放入消息队列)就可以返回
- 服务器后台再逐渐执行其他业务(消息推送,活动的推荐,计算在线时长等),不需要等所有服务执行完成,减少用户等待时间.
如何选择消息队列(kafka&RocketMQ)
成本
按阿里云的计费标准
kafka:( 标准版 20MB/s 读写; 500GB 磁盘,1000 分区 ) 1,189.75 /月
RocketMQ: ((按每条消息 100 字节算)15000TPS) 61,710/月
两个吞吐量性能相似,价格相差 50 多倍
功能
- kafka: 提供基本的消息队列的功能(生产与消费)
具体可以查阅 kafka 官方文档或者 sdk 的 api
RocketMQ: 提供了更加丰富的功能
- 消息的种类: 提供普通消息;顺序消息,延时消息,事务消息 4 种
- 生产: 提供普通的 send 与 request 两种模式
- 消费: 提供了 push 与 pull 两种模式,同时提供消息过滤功能
具体可以查阅 RocketMQ 官方文档或者 sdk 的 api
性能
- kafka
写
读
- RocketMQ
写
读
根据测试结果来看
kafka : 发送 25MB/s; 接收400MB/s(最高情况)
RocketMQ: 发送:12MB/s;接收 20MB/s
总结:
- kafka 可以达到 100w QPS
- RocketMQ 大概是 20w QPS
选择
- 首先考虑 kafka(普通消息传输:case 日志,大数据搜集, IM): 成本低,性能好等诸多优势
- 如果kafka无法满足使用的功能需求(比如需要事务,或者延时发布等功能):再考虑使用 RocketMQ.
- case: 支付/订单等功能: 在操作中需要额外的 DB 层的操作,需要保证所有操作的原子性
rocketMQ是参考kafka进行实现的为什么rocketMQ与kafka性能差距很大呢?
kafka 的底层数据储存实现
- kafka 的数据是进行分区储存的,每一个分区对应单独的文件夹(数据,索引都是单独的),这样在并发(多生产者与消费者)读写的情况下性能更好(特别是读)
- 可以减少并发读写的冲突问题
- 在读的情况下,可以更好的使用批量读取的方式获取数据,提升读取性能
rocketMQ 的底层数据储存实现
- rocketMQ 的 index 的分开储存的,但是数据(message)是混合储存的,所有 topic 的所有分区的数据都是储存一起的.
- 这样使得 rocketMQ 在并发的条件下读写性能略逊一筹
- 并发写会有更多冲突
- 在读取的时候,每次需要通过索引映射数据,更加麻烦,不利于批量读取
零拷贝实现的差异
原始的发送流程
-
磁盘数据->内核空间->用户空间->socket 发送缓冲区->网卡
-
数据经历了 4 次拷贝
kafka使用的sendfile模式
-
磁盘数据->内核空间->网卡
-
数据经历了 2 次拷贝
rocketMQ的mmap模式
- 磁盘数据->内核空间->socket 发送缓冲区->网卡
- 数据经历了 3 次拷贝
mmap与sendfile的差异
-
mmap:数据会先存在 socket 发送缓冲区中,再发送;方便做重试等功能
-
sendfile: 只关心发送了几个字节,不关心发送的内容
-
rocketMQ 需要做重试与二次消费,所以需要将发送的具体数据,所以选择了 mmap 的模式
为什么rocketMQ不使用分区分开储存而要使用混合储存的方式?
我没有在文档与书籍中看到为什么 rocketMQ 要使用混合储存的方式,但是我在探究他们的不同的时候发现 kafka 存在分区数量庞大(10000+)会导致性能下降的问题.
-
原因 1: zookeeper 维护了每一个分区的元数据,当分区非常大的时候 zookeeper 压力很大
-
原因 2:操作系统持有文件句柄数量是有限的,文件数量太多会导致性能下降
rocketMQ针对大topic支持的调整
-
调整中心节点: rocketMQ 使用的无状态的 nameserv 的模式,只储存他们的端口消息,不储存元数据,在大 topic 的情况下对中心节点的影响很小
-
采用混合储存的方式减少文件的数量(文件数量减少 1/3)
实际上 rocketMQ 为了能够支持大 topic 的场景而采用了混合储存的模式减少文件的数量
kafka如何缓解大量 topic 的问题
-
选择合适的分区: 分区与并发的 IO 性能是挂钩的,分区少会降低一些并发读写性能,但是同时会减少中心节点压力与文件数量,根据业务的需要与 kafka 的实际情况选择合适的分区数
-
增加机器(集群):
举个例子:
- 现在有 10000 个 topic 部署到一个集群上,那么集群的中心节点压力非常大,持有文件句柄数量非常多
- 现在增加一个集群,平衡一半的 topic;那么集群的压力就会减半
其他问题
消息丢失/重复消费("刚好一次"语义的实现)
消息丢失
拆分为三种情况:
- 生产者丢失
- brocker服务器(消息队列) 丢失
- 消费者丢失
- 什么情况下会导致生产者消息丢失
- 客户端问题: 客户端故障
- 服务端(broker)问题: 服务端故障
- 网络问题: 网络不可用
首先这是一个低概率的事件,无论是服务器还是消息队列,都是集群化部署的,即使一个节点意外宕机,其他节点会立即的接替工作
所以针对生产者的丢失
- 客户端故障: 向用户展示操作失败,由用户自行重试
- 服务端(broker)故障 & 网络问题 : 客户端进行重试,设定超时时间与重试次数
- 记录错误日志,监控与告警
如何知道生产者的消息发送成功还是失败?如何保证发送之后 brocker 消息不丢失?
- kafka 的刷盘机制: kafka为了提高性能,数据会先储存在 page Cache 中,然后异步定时刷盘的
kafka 数据不丢失需要对生产者的request.required.acks
进行配置
0: 不接收结果的返回
1: 当 leader 节点成功,就认为成功
-1(all): leader 与所有 ISR (可以理解为冗余节点或者从节点)节点都返回 ack 才算成功
要保证数据不丢失,
- 数据有多个冗余节点
- 需要配置
request.required.acks
为-1(all)模式
但是带来的会减少 kafka 的吞吐量
- 而 rocketMQ 同时提供了同步刷盘(flushDiskType = SYNC_FLUSH)与异步刷盘(flushDiskType = ASYNC_FLUSH)两种模式(brocker配置)
在集群模式下要保证数据的完全不丢失需要将主从节点都配置为flushDiskType = SYNC_FLUSH,但是带来的将是性能的降低
具体配置
## master 节点配置
flushDiskType = SYNC_FLUSH
brokerRole=SYNC_MASTER
## slave 节点配置
brokerRole=slave
flushDiskType = SYNC_FLUSH
但是如果想要同时保证性能与可靠性也可以选择与 kafka 中的all,类似的方式
- 主节点写入缓冲区,从节点也写入缓冲区(不需要落盘),就认为发送成功了(因为如果是集群模式下主节点宕机,是选择从节点为主节点,而不是等待主节点恢复,只要保证从节点有最新数据就行了.)
总结
- 通过配置确保消息能够到达消息队列,并保证不丢失
- 通过重试的方式实现"最少一次"的语义;
- 每条消息在消息队列中都有唯一序号,保证消息会重复,实现了"刚好一次"的发送
消费者丢失消息
- rocketMQ 与 kafka 在消费上使用了类似的逻辑
- 通过 offset 偏移量进行消费的,提交偏移量的时机可以避免消息不丢失
“最多一次”&"最少一次"消费语义的实现
- 获取到消息立即提交偏移量: "最多一次"消费
- 消费结束后再提交偏移量: "最少一次"消费
要保证消息不丢失需要使用"最少一次"消费的逻辑
但是为了避免重复消费的问题,需要通过 消息的唯一 ID进行去重处理(比如订单 ID 这样的)从而实现"刚好一次"消费的语义
总结
- 通过控制偏移量的提交实现"最少一次"消费
- 通过唯一"键"进行去重,实现"刚好一次"消费
参考
https://kafka.apache.org/documentation/#introduction
https://rocketmq.apache.org/zh/docs/featureBehavior/01normalmessage
https://blog.youkuaiyun.com/m0_71513446/article/details/143386962
https://rocketmq-learning.com/faq/ons-user-question-history16752/
https://www.cnblogs.com/goodAndyxublog/p/12563813.html