4.3-中间件之Kafka
一、初识Kafka
什么是Kafka?
Kafka是一种分布式的,基于发布 / 订阅的一个消息系统。在没有Kafka之前,系统架构是一个“蜘蛛网”模型,用户A在网站上发表了一条动态,这条数据需要被送到无数个地方:
- 写入主数据库
- 更新搜索索引,让其他人能搜到
- 更新用户画像,用于推荐系统
- 发送通知给他的粉丝
- 生成数据报表,给运营看
这样设计数据库会被系统不停的轮询,压力巨大,此外数据容易不一致且耦合严重,要修改一个接口需要牵一发动全身。
所以Kafka的设计目标就是:
目标一:高吞吐量
含义:每秒钟能处理海量的消息(目标是百万级/秒)。
必要性:这是 Kafka 诞生的首要原因。要作为整个公司数据的“中枢神经系统”,必须能承受所有系统产生的数据洪流。
如何实现:
顺序读写磁盘:即使使用廉价的机械硬盘,也能获得极高的吞吐。
批量处理:生产/消费都采用批量操作,减少网络I/O和系统调用次数。
零拷贝技术:优化数据在内核态和用户态之间的传输路径。
目标二:可扩展性
含义:系统容量可以通过增加普通的硬件来线性提升。
必要性:业务是增长的,数据量是指数级增加的,系统必须能轻松地水平扩展。
如何实现:
分区模型:每个 Topic 可以被划分为多个 Partition,这些 Partition 可以分散到不同的服务器上。增加服务器就能增加整体容量。
无状态 Broker:Kafka 的 broker(kafka集群中的一台服务器) 本身不记录消费者的状态,使得增删 broker 变得简单。
目标三:持久化与可靠性
含义:消息一旦被写入 Kafka,就不能丢失,并且可以被多次、长时间地消费。
必要性:Kafka 的定位不仅是消息队列,更是实时的、分布式的提交日志。数据是公司的资产,绝对不能丢。
如何实现:
消息直接持久化到磁盘:而不是先存内存,再刷盘,避免了断电丢失消息的风险。
多副本机制:每个 Partition 的数据都有多个副本,分散在不同机器上,防止单点故障。
目标四:松耦合与实时性
含义:让数据生产者无需关心谁用数据、何时用数据,同时保证数据能被实时消费。
必要性:这是解耦系统的关键。生产方和消费方独立发展、独立伸缩,互不影响。
如何实现:
发布-订阅模型:生产者发送消息到 Topic,多个消费者组可以独立订阅同一个 Topic。
消费者拉取模式:消费者自己控制消费节奏,不会因为消费能力慢而拖垮生产者和 Kafka 服务本身。
消息保留策略:数据会持久保存一段时间(可配置),允许新的消费者随时接入并消费历史数据。
二、kafka高吞吐量的实现
kafka的划分很细,即使对 TB 级以上数据也能保证常数时间复杂度的访问性能,依赖于其中的各个部分:有Topic Partition Segment索引文件 Segment数据文件 offset值
各部分的理解:
- Kafka是一个图书馆
- Topic是一个图书大类(如“少儿图书”类)
- partition是这个大类下的书架(如 一个作者一个书架)
- segment是书架的一层(关键),也是kafka文件存储的最小单位
- 索引文件和数据文件就是书架里面存放的书,一个负责查找,一个负责记录
- offset即书的页码,也是查找的关键。
| --topic1-0 topic1的0号分区| --00000000000000000000.index segment 文件| --00000000000000000000.log 后缀分表表示 Segment 索引文件和数据文件。| --00000000000000368769.index| --00000000000000368769.log| --00000000000000737337.index| --00000000000000737337.log| --00000000000001105814.index| --00000000000001105814.log
| --topic2-0
| --topic2-1
查找流程:假设要查找页码(offset)为368772
的内容(消息)
- 在根据查找消息的key确定他的“书架分区”partition
- 到指定书架前,每一层的编号,用的是这一层存放的起始页码,比如第一层存放(1-10000)第二层(10001-20000)...
- 利用二分查找定位
368772
所在的层号 - 取出对应层里面的目录(索引文件),这个目录是稀疏的,只稀疏记录了页码(offset)1从第0个字符开始读,页码(offset)3从第497个字符开始读....(而这条消息的全局页码(offset)是
368769 + 3 = 368772
) - 直接从这一层的数据文件的497字符开始读取
总结:找索引文件利用二分查找,找文件中对应的消息,利用偏移量和索引文件中的稀疏索引。
Kafka 高性能很重要的原因:
- 顺序磁盘 IO 存储设计,写在log数据文件里面的消息都是顺序的(kafka将生产者发送的消息攒成一批,顺序追加到数据文件末尾),这种顺序磁盘IO快于随机读写内存。
- PageCache,进程准备读写磁盘前,OS会先查看内容是否读取到PageCache上,如果命中,则可以避免磁盘IO
- 零拷贝,从磁盘读取消息,然后原封不动地通过网络发送给消费者。这样可以减少一半的拷贝操作,避免了内核缓冲区读到用户缓冲区,然后用户缓冲区再写回到内核缓冲区 的两次拷贝操作。只需要从磁盘读出到内核缓冲区,然后直接从内核缓冲区发送到网卡。(DMA负责)
三、kafka可扩展性的实现
Kafka一个很重要的特性就是,只需写入一次消息,可以支持任意多的应用读取这个消息。一个应用如果看作一个消费组,然后消费组里对应多个消费者,每一个消费者可以与一个Topic里面的分区对应。如果该应用消费能力不足,那么可以考虑在这个消费组里增加消费者。如果应用需要读取全量消息,那么就为该应用设置一个消费组。
消费者可以1:n个分区,但是如果消费者数量>分区数量,多余的消费者会空闲,因为一个分区不支持同一个消费组里的多个消费者消费,所以对应的Topic里面的分区应该多一些。
zookeeper的作用:注册服务发现,标识各broker的ip。
四、kafka可靠性保证
1.保证消息不丢失:生产者确认 + 服务端多副本冗余 + 消费者手动提交
- 生产者设置
acks
参数:acks=all
:需分区的 leader 以及 follower的确认。
acks=0,则不等broker返回,提供了最低延迟,但是最不安全。
acks=1,等待分区的leader落盘成功后返回ACK - broker采用多副本机制,消费者什么时候能读取消息?LEO和HW
HW标识了消费者可以读取的消息的位置,也就是主副分区中,更新消息最慢的分区的位置
LEO标记的是主分区的最新消息的位置
- 消费者在消费完毕后,再提交偏移量offset
如果是自动提交offset,比如1s一次,可能会导致丢失数据,因为可能1s还没有处理完数据
如果是手动提交offset,丢数据情况对于消费者一般不存在,但是无法避免重复消费(拉取)的情况,即消费者消费完但是没有提交offset就宕机了,需要重复消费。最大限度避免重复消费的办法就是 同步提交消息,这样最多重复消费一条消息。
五、kafka的策略
生产者消息的分区分配策略:
- 如果消息有指明分区号,之前归入对应分区。
- 如果没有指明分区号,但是消息带有对应的key值,Hash(key) % 分区数后,放入对应分区
- 没有指明分区也没有key,顺序轮询各个分区均匀放入。
消费者的分区分配策略:(消费组数量<分区)
- 整除分配,分区数/消费者数为消费者分配到的分区数,但是当无法整除的时候,不能实现均匀分配
- 轮询分配,把分区号按字典排序后,顺序轮询消费者分配。但是当各个消费者订阅的Topic不同的时候,无法实现均匀分配
- 粘性分配,首先按轮询分配对分区进行分配,但有一个关键改进:它严格遵循消费者的订阅信息。而且在出现有消费者C1脱离了消费组的情况下,进行分区重定向的时候,按照轮询只把C1的分区分给其余消费者,遵循尽量少的移动。
kafka的rebalance策略
当消费组的成员发生变更,有消费者加入或者有消费者宕机,或者消费者无法在规定时间内完成消费,会触发rebalance机制。
session.timeout.ms表示消费者向broker发生心跳的超时时间,一般大于3*发送心跳的时间间隔
max.poll.interval.ms表示每两次消费的时间间隔,也就是一次消费时长,如果超过这个时间,会认为消费者死了,会进行rebalance