Kafka 的保留策略不是“到期删除”,而是一份需要被运维兑现的宽松上界
Kafka 的持久化叙事通常由两个数字支撑:
retention.ms决定数据保留多久,acks=all决定写入有多可靠。这两个承诺都是真的,但它们兑现的方式和大多数人的直觉不同。保留策略不是一个到点执行的删除指令,而是一个周期性检查 + 多层豁免 + 延迟释放的宽松上界;持久性保证的成立条件,则写在 ISR 的动态收缩规则里。把 Kafka 的官方设计文档、Netdata 的排障记录和 KIP-928 放在一起读,会看到一条一致的线索:Kafka 的承诺不是“保证发生”,而是“在条件成立时不发生失败”。
保留检查是一张时间表,不是一条删除线
Kafka 回收磁盘的方式不是“消息写入时启动一个倒计时,到期触发删除”,而是由一个后台线程按固定周期扫描关闭的日志段。log.retention.check.interval.ms 的默认值是 300000 ms,即 5 分钟[reference:0]。每 5 分钟,broker 才会评估一次哪些关闭的段已经满足保留条件。
满足条件之后,删除也不是立即发生的。段文件会先被标记,然后需要等待 file.delete.delay.ms,默认 60000 ms,即 1 分钟,操作系统才会真正释放磁盘空间[reference:1]。Netdata 的排障指南对此的总结很直接:因为 5 分钟的检查间隔和 1 分钟的删除延迟,retention.ms 是一个下界,而不是实时保证;一个低流量、保留 10 分钟的主题,如果活跃段尚未滚动,数据可能实际保留 20 分钟甚至更久[reference:2]。
这两个延迟叠加的工程含义是:磁盘用量的峰值不由 retention.ms 单独决定,而由写入速率乘以“保留时间 + 检查周期 + 删除延迟”共同决定。在规划容量时把 retention.ms 当作精确的磁盘回收时刻,是一种常见的误算。
活跃段:保留策略永远够不到的那一块
保留策略只作用于关闭的日志段。当前正在接收写入的活跃段,无论它的数据有多老、无论 retention.ms 是否早已过期,都不会被保留策略删除[reference:3]。
一个段什么时候关闭?取决于三个条件中最先触发的那个:segment.bytes(默认 1 GiB)、segment.ms(默认 7 天),或者索引文件大小达到上限。索引条目大约每 log.index.interval.bytes(默认 4 KiB)一条,因此 10 MiB 的索引文件实际上把单个段的大小限制在约 5 GiB,即使 segment.bytes 设为更大的值[reference:4]。
这组默认值的组合产生了一个容易被低估的边界:低吞吐主题的活跃段可能长时间不滚动。如果一个主题每天只写入几 MB,segment.bytes 永远到不了 1 GiB,段会在 segment.ms 的 7 天后才关闭。在这 7 天里,保留策略对它完全无能为力;而一旦关闭,它还需要再经过检查周期和删除延迟才能真正释放。对于分区数量多、单分区流量低的集群,“活跃段豁免”乘以“分区数”会变成一个可观的固定磁盘占用,与 retention.bytes 的设定值无关。
磁盘满:保留策略来不及兑现时,发生的是写入拒绝
保留策略的宽松性在磁盘正常增长时只是“回收比预期慢一点”。但当磁盘在下一个检查周期到来之前就被写满时,性质发生了变化。
Kafka 对日志目录满的处理方式是:broker 捕获到 No space left on device 引发的 IOException,然后停止该日志目录上的所有日志操作——移除 fetcher、停止 compaction,保留策略本身仍会继续执行,但这个目录已经不再接受新的写入[reference:5]。在只有一个日志目录的部署中,这意味着 broker 的写入路径整体失效;生产者会收到 NotEnoughReplicasException 或 DiskException,而旧的日志段仍然可以被读取[reference:6]。
KIP-928 对此的描述触及了问题的结构:当前用户在日志目录满时根本无法与 broker 交互,提案的目标是预留约 40 MB 的空间,以便在饱和状态下仍然能够执行删除主题或收紧保留配置这类管理操作[reference:7]。KIP 里有一句话值得单独拎出来:问题不在于边界设在哪里,而在于边界处的行为是否被明确定义[reference:8]。保留策略的“宽松”本身不是缺陷;缺陷是这种宽松在磁盘满时以“写入被冻结”的形式一次性结算,而没有给运维留出在饱和状态下修正配置的余地。
ISR 收缩:持久性保证的成立条件也在被侵蚀
磁盘满不只是保留策略的问题,它同时会传导到 Kafka 的持久性承诺上。
Kafka 官方设计文档对持久性的表述有一个精确的限定:已提交的消息不会丢失,只要在任意时刻至少有一个同步副本存活。acks=all 等待所有当前 ISR 确认,min.insync.replicas 设定写入被接受所需的最小 ISR 数量。这套机制的正确性依赖于 ISR 本身是健康的。
当一个日志目录因磁盘满或 I/O 错误被标记为 offline,该目录上的分区副本随之失去可用性,ISR 相应收缩[reference:10]。如果收缩后的 ISR 仍然不低于 min.insync.replicas,写入会继续,但持久性已经在一个更薄的副本集合上兑现;一旦 ISR 跌破阈值,acks=all 的写入就会直接收到 NotEnoughReplicasException,可用性先于数据丢失发生[reference:11]。
如果此时打开 unclean.leader.election.enable=true,一个不在 ISR 中的副本可以被选为 leader,以永久数据丢失为代价换取分区重新可用[reference:12]。这个开关的默认值是 false,官方建议大多数场景保持禁用;它的存在本身就是在声明:Kafka 的持久性边界不是“永远不丢”,而是“在至少一个同步副本存活且不被越过的前提下不丢”。
边界不在保留策略的配置值里
把这条线索串起来,Kafka 的保留与持久化呈现出一种一致的结构:保留策略提供的是一个由检查周期、删除延迟和活跃段豁免共同定义的上界,而不是一个精确的回收时刻;持久性保证的兑现,依赖于 ISR 在磁盘满、副本故障和 leader 选举等事件中不被侵蚀到阈值以下。
retention.ms 调到 7 天,不意味着第 7 天的数据会被准点清除;acks=all 打开,不意味着任何情况下写入都不会丢。两者都是条件承诺:前者要求磁盘的写入速率没有在回收兑现之前撞上物理容量,后者要求 ISR 的收缩没有越过 min.insync.replicas 的底线。Netdata 的容量规划建议给出了一个可操作的检验方式:用 BytesInPerSec 乘以 retention.ms 再乘以副本因子,算出理论上的磁盘“跑道”,如果实际占用显著高于这个值,说明保留或 compaction 已经在某个环节偏离了预期[reference:13]。
判断 Kafka 的保留策略是否“够用”,比调大 retention.bytes 更有用的,是先确认三件事:活跃段在预期的写入速率下多久滚动一次,磁盘在下一个 5 分钟检查周期到来之前还有多少余量,以及 ISR 在副本丢失时的收缩空间是否仍然高于 min.insync.replicas。承诺的兑现条件,写在这些动态的运维边界里,而不是写在 broker 的保留配置项里。
- 点赞
- 收藏
- 关注作者
评论(0)