基于Kafka时间粒度消息回溯设计方案

1.背景

当业务消费消息时,有时因为某些原因(bug、异常、依赖服务故障等)导致消费全部无效,需要回溯消息进行消费,比如消费者2个小时内的处理逻辑可能出现了问题,业务发现后,想回溯到2小时前offset位置重新消费补回相关消息。
基于Kafka时间粒度消息回溯设计方案_第1张图片

2.总体设计概述

时间粒度消息回溯特性,是基于Kafka源码扩展功能,需要修改Broker和consumer两端代码,要考虑与官方版本升级迁移等兼容性问题,尽量减少代码侵入式修改。

kafka目前已经支持按照offset粒度的回溯消费,如果我们能实现时间戳到offset 的查询也就完成按照时间维度的消息回溯功能,功能分解如下:

  • 根据时间戳查找offset
  • 根据offset进行消息的回溯功能

3.详细设计

2.1 Kafka文件存储基础知识

请参考:Kafka文件存储机制那些事

2.2 Kafka 消息消费流程

  • Consumer发送FetchRequest到Leader Partition所在的Broker
  • Broker获取消息的 start offset和size (获取消息的size大小),根据 .index 数据查找 offset 相关的 position 数据,由于 .index 并不存储所有的 offset,所以会首先查找到小于等于 start_offset 的数据, 然后定位到相应的 .log 文件,开始顺序读取到 start_offset 确定 position。
  • 根据 position 和 size 便可确认需要读取的消息范围,根据确定的消息文件范围,直接通过 sendfile 的方式将内容发送给消费者。

2.3 利用Kafka如何实现消息回溯设计

在Kafka文件存储机制那篇博客已经提到,每个topic被切分为一个或多个partition,一个partition有多个replicas,每个replica逻辑上相当于一个超大文件,物理上由无数个固定大小相同的段文件组成,每个段文件包一个数据文件和索引文件,Kafka索引文件设计比较精巧和简单,因为只有2个字段,利用mmap机制可以在内存中快速高效查找,但正由于设计过于简单,而限制了Kafka的相关功能扩展,如今要实现时间粒度消息回溯,则必须在broker端持久化时间戳,即broker收到消息之后为每条消息添加时间戳,以下有3种设计方案

2.3.1方案1

侵入式最小,简单粗暴:
partition的段数据文件中每条消息Key作为保留字段,并没有使用,此处可以利用起来,key设计为复合类型包含timestmap字段,segment log file存储消息格式修改为:

长度(bytes) 字段 类型
4 key size: key 长度, -1 代表没有 key Int32
key size key: key 的数据; 序列化为Map容易扩展,包含timestamp字段 Byte[]

查询流程(consumer设计):
consumer从每个partition初始位置开始回放消息,回放消息过程时,解析出Kafka中保留key的内容,并序列化为map,取出timestamp字段与输入timestamp进行比较,小于取出消息的timestamp,则继续不断读取下一条消息,直到大于取出消息的timestamp为止,停止消息回放,设置上一条消息为目标offset,最后根据目标offset为起点进行正常消费。

此设计方案
优点:

  • 实现较为简单,对Kafka侵入式小
  • 只需要修改broker源码增加时间戳字段
  • 通过timestamp查找offset的逻辑在Consumer端进行
  • Consuemr在应用层级开发就可以了

缺点:

  • broker网络开销大,如没有流控或限流机制,可能短时间内网卡打满
  • broker磁盘IO开销大,可能在线生产和消费服务有影响

2.3.2 方案2

性能优先:
segment log file存储格式同方案1,segment index file索引文件增加一个字段,存储格式如下:

长度(bytes) 字段 类型
4 relative_offset: offset - baseOffset, baseOffset为文件名的数,主要是降低内存占用考虑 Integer
4 Position: 对应的消息文件中消息的位置 Integer
8 Timestamp: 时间戳 Long

此设计方案
优点:

  • 复用segment index file逻辑和读写策略,包括二分查找等
  • 通过timestamp查找offset的逻辑在Broker端进行
  • 网络和IO开销极小

缺点:

  • segment index file占用大小是原理一倍左右的空间
  • segment index file采用mmap的方式映射到内存且访问频繁,大部分数据加载到内存,而时间粒度消息回溯功能使用频率低,如此设计有些浪费空间。

2.3.3 方案3

性能和内存占用最优:
segment log file存储格式同方案1,timestamp采用独立的索引文件进行存储, 读写策略同index相同,文件名称为baseOffset-baseTimestmap.timestamp_index, 存储格式如下:

长度(bytes) 字段 类型
4 relativeTimestamp相对值= timestamp - baseTimestmap,起始的baseTimestmap存储在文件名中,降低内存消耗 Integer
4 relativeOffset: offset - baseOffset, baseOffset即为xxx.log和xxx.index文件名的数字 Integer

此设计方案
优点:

  • 复用segment index file逻辑和读写策略,包括二分查找等
  • 通过timestamp查找offset的逻辑在Broker端进行
  • 网络和IO开销极小
  • 使用时才有内存开销

缺点:

  • 开发相对比较复杂,实现周期长,需要维护好xxx.timestamp_index生命周期以及与xxx.index衔接和匹配
  • 在极端情况下,xxx.timestamp_index数据并没有全部加载到内存,比原生多一次磁盘IO
  • 适配Consumer功能,Broker端需求新开发timestampRequest请求接口(timestampRequest和timestampReponse)

xxx.timestamp_index写入策略设计:
策略与xxx.index一样,不会每条消息写一条索引记录,而是采取稀疏索引的方式:

  • 消息 size 维度稀疏: 类似于xxx.index 根据消息的 bytesSize 维度来决定。 xxx.index文件是在消息超过一定的大小之后便写入一个 index,这样做的原因是在查找并未在xxx.index中的offset 的时候,后续顺序遍历 xxx.log文件的时间比较可控。
  • 时间 维度稀疏:根据要求的精度,比如 2s, 那么我们每隔 2s 左右的时间记录一下xxx.timestamp_index 即可。

查询流程:

  1. 根据给定的时间戳timestamp定位响应的 xxx.timestamp_index 文件
  2. 在 xxx.timestamp_index 内进行二分查找,找到第一个小于等于 timestamp 的 (timestamp、offset) 对。
  3. 根据 offset查找对应的position
  4. 从position开始遍历xxx.log文件找到第一个大于等于 timestamp 的消息,返回该消息的时间戳即可

3. 综述-方案选择

方案1:实现简单,但扩展性查,对Broker端性能影响较大
方案2:对内存资源消耗增加一倍,无论是否使用此功能都有开销
方案3:查找offset与timestamp逻辑分离,使用才有极小开销,功能扩展性好,后续可以实现基于timestamp的消息查询功能
综上所述,推荐选择方案3(美团此方案在线稳定运行了10+月)

你可能感兴趣的:(apache,kafka)