- 1 为什么要使用消息队列
- 消息队列可以理解为一个存储数据的中间件,用来转发,暂存大批量的数据
- 应用解耦
- 流量削峰
- 异步处理
- 消息通讯
- 2 kafka的相关概念,解释下
- Topic消息主题
- 消息的生产与消费,围绕消息主题进行生产、消费以及其他消息管理操作
- Topic也是消息队列的一种发布与订阅消息模型。生产者向消息主题发布消息,多个消费者订阅该消息主题的消息,生产者与消费者彼此并无直接关系
- 生产者(Producer)
- 向Topic(消息主题)发布消息的一方。发布消息的最终目的在于将消息内容传递给其他系统/模块,使对方按照约定处理该消息
- 消费者(Consumer)
- 从Topic(消息主题)订阅消息的一方。订阅消息最终目的在于处理消息内容,如日志集成场景中,监控告警平台(消费者)从主题订阅日志消息,识别出告警日志并发送告警消息/邮件
- 代理(Broker)
- 即Kafka集群架构设计中的单个Kafka进程,一个Kafka进程对应一台服务器,因此手册中描述的代理,还包括对应的存储、带宽等服务器资源
- 分区(Partition)
- 为了实现水平扩展与高可用,Kafka将Topic划分为多个分区,消息被分布式存储在分区中
- 副本(Replica)
- 消息的备份存储。为了确保消息可靠,Kafka创建Topic时,每个分区会分别从代理中选择1个或多个,对消息进行冗余存储
- 每个分区都随机挑选一个副本作为Leader,该分区所有消息的生产与消费都在Leader副本上完成,消息从Leader副本复制到其他副本(Follower)
- Topic消息主题
- 3 kafka的ISR 和 AR 怎么理解
- 分区中的所有副本统称为AR(Assigned Repllicas)
- 所有与leader副本保持一定程度同步的副本(包括Leader)组成ISR(In-Sync Replicas),ISR集合是AR集合中的一个子集。与leader副本同步滞后过多的副本(不包括leader)副本,组成OSR(Out-Sync Relipcas)
- Leader副本负责维护和跟踪ISR集合中所有的follower副本的滞后状态,当follower副本落后太多或者失效时,leader副本会把它从ISR集合中剔除。如果OSR集合中follower副本“追上”了Leader副本,之后再ISR集合中的副本才有资格被选举为leader
- 4 broker 和 follower 的同步机制
- LEO:LogEndOffset 的缩写,表示每个partition的log最后一条Message的位置。
- HW: HighWatermark 的缩写,是指consumer能够看到的此partition的位置。 取一个partition对应的ISR中最小的LEO作为HW,consumer最多只能消费到HW所在的位置
- 对于leader新写入的消息,consumer不能立刻消费,leader会等待该消息被所有ISR中的replicas同步后更新HW,此时消息才能被consumer消费
- 由此可见,Kafka的复制机制既不是完全的同步复制,也不是单纯的异步复制
- 5 kafka为啥那么快
- pagecache
- Broker 收到数据后,写磁盘时只是将数据写入 Page Cache,并不保证数据一定完全写入磁盘。从这一点看,可能会造成机器宕机时,Page Cache 内的数据未写入磁盘从而造成数据丢失。但是这种丢失只发生在机器断电等造成操作系统不工作的场景,而这种场景完全可以由 Kafka 层面的 Replication 机制去解决。如果为了保证这种情况下数据不丢失而强制将 Page Cache 中的数据 Flush 到磁盘,反而会降低性能。也正因如此,Kafka 虽然提供了 flush.messages 和 flush.ms 两个参数将 Page Cache 中的数据强制 Flush 到磁盘,但是 Kafka 并不建议使用
- 顺序读写
- Kafka 中每个分区是一个有序的,不可变的消息序列,新的消息不断追加到 partition 的末尾,这个就是顺序写
- 零拷贝
- Kafka 中存在大量的网络数据持久化到磁盘(Producer 到 Broker)和磁盘文件通过网络发送(Broker 到 Consumer)的过程。这一过程的性能直接影响 Kafka 的整体吞吐量
- 网络数据持久化到磁盘 (Producer 到 Broker) 使用 mmap 内存文件映射, mmap 将内核中读缓冲区(read buffer)的地址与用户空间的缓冲区(user buffer)进行映射。从而实现内核缓冲区与应用程序内存的共享,省去了将数据从内核读缓冲区(read buffer)拷贝到用户缓冲区(user buffer)的过程
- 磁盘文件通过网络发送(Broker 到 Consumer) 通过 NIO 的 transferTo/transferFrom 调用操作系统的 sendfile 实现零拷贝
- 消息批量处理和压缩
- Kafka 的客户端和 broker 还会在通过网络发送数据之前,在一个批处理中累积多条记录 (包括读和写)。记录的批处理分摊了网络往返的开销,使用了更大的数据包从而提高了带宽利用率
- Producer 可将数据压缩后发送给 broker,从而减少网络传输代价,目前支持的压缩算法有:Snappy、Gzip、LZ4。数据压缩一般都是和批处理配套使用来作为优化手段的
- 多路复用的网络模型
- 增强对客户端的网络连接的并发处理
- 利用 Partition 实现并行处理
- 由于不同 Partition 可位于不同机器,因此可以充分利用集群优势,实现机器间的并行处理。另一方面,由于 Partition 在物理上对应一个文件夹,即使多个 Partition 位于同一个节点,也可通过配置让同一节点上的不同 Partition 置于不同的磁盘上,从而实现磁盘间的并行处理
- pagecache
- 6 kafka 的可靠性
- 生产端 ack
- ack = 0,producer发送后即为成功,无需分区partition的leader确认写入成功
- ack = 1,producer发送后需要接收到 partition 的 leader发送确认收到的回复
- ack = 0,producer发送后,需要ISR中所有副本都成功写入成功才能收到成功响应
- broker
- 持久化存储
- 副本机制
- ISR commit
- 消息 consumer 端
- 在消费者消费数据的时候,需要消费者记录好offset值及提交,就能保证数据不丢失
- 再重复消息时,需要保障幂等性
- 生产端 ack
- 7 kafka 生产端的事务是怎样
- Kafka中幂等和事务(Kafka 0.11.0.0版本引入的两个特性)以此来实现 Exactly once
- 开启幂等性功能的方式很简单,只需显式地将生产者客户端参数 enable.idempotence=true(默认值为false)Kafka的幂等只能保证单个生产者会话(session)中单分区的幂等。幂等性不能跨多个分区运作,而事务可以弥补这个缺陷。事务可以保证对多个分区写入操作的原子性
- API 要求事务性 Producer 的第⼀个操作应该是在 Kafka 集群中显示注册 transactional.id。 当注册的时候,Kafka broker ⽤给定的 transactional.id 检查打开的事务并且完成处理。 Kafka也增加了⼀个与transactional.id 相关的epoch。Epoch存储每个transactional.id内部元数据。⼀旦epoch被触发,任何具有相同的 transactional.id 和旧的epoch的⽣产者被视为僵尸,Kafka拒绝来自这些生产者的后续事务性写入
- Kafka保证 consumer 最终只能消费非事务性消息或已提交事务性消息
- 7.1 kafka 生产端的幂等性是怎样实现
- kafka 为了实想幂等性,他在底层的设计架构中引入了Producer和SequenceNumber
- ProducerID:在每一个新的Producer初始化时,或被分配一个唯一的ProducerID,这个ProducerID对客户端使用者是不可见的。
- sequenceNumber:对于每个producerID,Producer发送数据的每个Topic和Partition都对饮一个从0开始递增的SequenceNumber值
- 8 kafka 怎么保证数据的一致性
- 一致性定义:若某条消息对client可见,那么即使Leader挂了,在新Leader上数据依然可以被读到
- 提交的消息并不是立马能被消费,需要被ISR同步记录后。每个分区会维护 HighWatermark,代表客户端可见的最大offset,取自 ISR 中最小的 LEO 作为HW。因此当leader挂机从ISR选举上来的新 leader 一定存在客户端曾经可见的数据
- leader发生故障之后,会从ISR中选举一个新的leader,为保证多个副本间的数据一致性,其余的follower会先将各自的log文件高于 HW 的部分截掉,然后从新的leader同步数据"保证存储一致性"
- follower发生故障后会被临时剔除ISR, 待该follower 恢复后,follower会读取本地磁盘记录的上次HW, 并将log文件高于HW的部分截取掉,从HW开始向leader进行同步
- 9 kafka 的顺序一致性
- 如果需要保证数据的一致性
- 发送端不能异步发送,异步发送在发送失败的情况下,就没办法保证消息顺序
- 消息不能分区。也就是1个topic,只能有1个队列
- 对于接收端,不能并行消费,也即不能开多线程或者多个客户端消费同1个队列
- kafka 默认保证同一个partition分区内的消息是有序的,则producer可以在发送消息时可以指定需要保证顺序的几条消息发送到同一个分区。这样当消费者消费时,消息就是有序
- 如果需要保证数据的一致性
- 10 kafka 的 consumer group 是什么
- consumer group是kafka提供的可扩展且具有容错性的消费者机制
- consumer group下可以有一个或多个consumer
- consumer group 订阅的 topic 下的每个分区只能分配给 group 中的其中一个 consumer
- group.id是一个字符串,唯一标识一个consumer group
- 11 kafka 为啥不支持读写分离
- 数据一致性问题。数据从主节点转到从节点必定会有一个延时的时间窗口,这个时间窗口会导致主从节点之间的数据不一致
- 延时问题。类似 Redis 这种组件,数据从写入主节点到同步至从节点中的进程需求经 历网络→主节点内存→网络→从节点内存这几个阶段,整个进程会耗费必定的时间。而在 Kafka 中,主从同步会比 Redis 愈加耗时,它需求阅历网络→主节点内存→主节点磁盘→网络→从节 点内存→从节点磁盘这几个阶段。对延时要求高的服务,kafka的主写从读的功用并不好
- 12 消费端的幂等性怎么保证
- 唯一值 + 唯一索引
- 如果订单入口,判断根据唯一索引判断是否订单已存在
- 唯一值 + 防重表
- 防重表相当于一个日志操作表,当然防重表也是利用主键/索引的唯一性,如果插入防重表冲突即直接返回成功
- 唯一ID + 事务状态
- 根据订单状态判断是否已经处理过
- 版本号,乐观锁
- 判断数据库里的数据与消息队列里的版本号是否一致,如果一致则处理过
- 上面的处理需要保证原子性和隔离性
- 使用数据库事务 + insert/for update
- 使用 redis,先更新到 redis 再同步到数据库
- 唯一值 + 唯一索引
- 13 如何处理消息堆积问题
- 先修复consumer消费者的问题,以确保其恢复消费速度,然后将现有consumer 都停掉
- 新建一个 topic,partition 是原来的10倍(10 倍的 queue)
- 然后写一个临时的分发数据的 consumer 程序,这个程序部署上去消费积压的数据,消费之后不做耗时的处理,直接均匀轮询写入临时建立好的 10 倍数量的 queue
- 接着临时征用 10 倍的机器来部署 consumer,每一批 consumer 消费一个临时 queue 的数据。这种做法相当于是临时将 queue 资源和 consumer 资源扩大 10 倍,以正常的 10 倍速度来消费数据
- 等快速消费完积压数据之后,得恢复原先部署的架构,重新用原先的 consumer 机器来消费消息
- 14 设计一个高可用的消息队列
- 分片 + 副本冗余
- 集群同步
- 主题 + 订阅模式
- 持久化
- 多路复用 + 零拷贝 + pagecache
- 15 说下实际生产中用过的事务消息实现方案(非同步的事务)
- 本地消息事务表 + 消息队列
- 1 在同一个事务中完成订单数据和单独的消息表入库
- 通过 cancal 将增量消息放入消息队列
- 主动线程轮询查询增量消息放入消息队列,增量可以通过缓存已发送过的消息表最大ID筛选
- 2 消费者消费过后,标记事务消息的成功与失败
- 1 在同一个事务中完成订单数据和单独的消息表入库
- 可以使用 kafka 的事务API,范围大于数据库的事务操作即可
- 本地消息事务表 + 消息队列
// 初始化事务,需要注意确保transation.id属性被分配
void initTransactions();
// 开启事务
void beginTransaction() throws ProducerFencedException;
// 为Consumer提供的在事务内Commit Offsets的操作
void sendOffsetsToTransaction(Map<TopicPartition, OffsetAndMetadata> offsets,
String consumerGroupId) throws ProducerFencedException;
// 提交事务
void commitTransaction() throws ProducerFencedException;
// 放弃事务,类似于回滚事务的操作
void abortTransaction() throws ProducerFencedException;
- RocketMQ事务消息也是类似kafka,但是更加完善,采用了2PC(两阶段提交)+ 补偿机制(事务状态回查)的思想来实现了提交事务消息,同时增加一个补偿逻辑来处理二阶段超时或者失败的消息
- 正常事务消息的发送及提交
- a 生产者发送half消息到Broker服务端(半消息)半消息是一种特殊的消息类型,该状态的消息暂时不能被Consumer消费
- b、Broker服务端将消息持久化之后,给生产者响应消息写入结果(ACK响应);
- c、生产者根据发送结果执行本地事务逻辑(如果写入失败,此时half消息对业务不可见,本地逻辑不执行);
- d、生产者根据本地事务执行结果向Broker服务端提交二次确认(Commit 或是 Rollback),Broker服务端收到 Commit 状态则将半事务消息标记为可投递,订阅方最终将收到该消息;Broker服务端收到 Rollback 状态则删除半事务消息,订阅方将不会接收该消息
- 事务消息的补偿流程
- a、在网络闪断或者是应用重启的情况下,可能导致生产者发送的二次确认消息未能到达Broker服务端,经过固定时间后,Broker服务端将会对没有Commit/Rollback的事务消息(pending状态的消息)进行“回查”;
- b、生产者收到回查消息后,检查回查消息对应的本地事务执行的最终结果;
- c、生产者根据本地事务状态,再次提交二次确认给Broker,然后Broker重新对半事务消息Commit或者Rollback;
- 正常事务消息的发送及提交
- 16 kafka的消息存储结构是怎样的
- 在存储结构上分区的每个副本对应一个Log对象,每个Log又划分为多个LogSegment,每个LogSegment包括一个日志文件和两个索引文件,其中两个索引文件分别为偏移量索引文件和时间戳索引文件。Log对象中维护了一个ConcurrentSkipListMap,底层是一个跳跃表,保存该主题所有分区对应的所有LogSegment。日志文件和索引文件与磁盘上的物理存储文件相对应。Kafka将日志文件封装为一个 FileMessageSet 对象,将两个索引文件封装为 OffsetIndex 和TimeIndex 对象
- 偏移量索引
- 时间索引,则是基于偏移量索引做搜索。通常先使用TimeIndex寻找满足时间戳要求的消息位移值,然后再利用OffsetIndex定位该位移值所在的物理文件位置
- 消息存储的日志结构,东西太多,一般不会问
- 17 kafka不使 Zookeeper
- Zab协议自身的限制导致了zookeeper的很多瓶颈,比如,单leader瓶颈,切主时服务不可用、系统存储的内容有限,可扩展性不足
- Kakfa Without ZooKeeper 减少了服务配置的复杂度,不再需要通过去配置zookeeper来协调Kafka。整个系统也变得更轻量级了