ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Kafka日志刷盘策略:sync.ms/flush.messages调优与性能取舍

Kafka日志刷盘策略:sync.ms/flush.messages调优与性能取舍 上个月有位做后端的朋友给我发了份压测报告说他们的Kafka集群吞吐死活上不去CPU不高、磁盘看着也不忙但就是卡在几万条每秒。我让他把server.properties发过来扫了一眼看到两行配置log.flush.interval.ms10log.flush.interval.messages100。典型的刷新恐慌式配置。对分区数很少的测试环境来说这组参数等于把每次写入都变成了fsync的等待吞吐上不去才是正常的。后来把刷盘参数调回默认什么都没动峰值直接翻了五倍。这个经历让我想认真聊聊Kafka日志刷盘策略。市面上讲Kafka性能调优的文章很多但真正把sync.ms、flush.messages这两个参数讲透的很少——很多人甚至不知道Kafka官方配置里根本没有一个叫sync.ms的项。这篇东西不是概念科普而是把我自己踩过的坑、验证过的方法、不同场景下的配置组合完整梳理一遍给正在调参、或者搞不清刷盘和副本关系的朋友做个参考。1. 日志从生产到落盘Kafka到底把数据写到哪里去了1.1 一次producer写入背后发生了什么先回到最基础的问题一条消息从producer发出到最终躺在磁盘上中间经历了什么producer把消息发到leader brokerleader broker接住之后把消息追加到对应分区日志段的文件里。注意追加到文件这四个字——在Linux系统里这一步操作的是文件描述符数据先被拷贝到内核空间的PageCache页缓存然后写入调用就返回了。也就是说从Kafka进程的角度看写入成功了但从物理磁盘的角度看数据可能还在内存里躺着。接下来是follower拉取数据、ISR确认、返回ack给producer。整个链路走到这一步很多人认为数据已经存好了其实只是存到了内存被副本接收了。之前在某电商系统的压测环境里做过一次测试producer配置acksall同步发送100万条消息在leader节点上执行sync命令强制把缓存刷到磁盘发现磁盘上实际落盘的字节数明显少于producer统计的已发送字节数。差距就是那些还在PageCache里、还没来得及被内核回写的部分。1.2 PageCache、脏页回写与fsync三个关键时间点数据进入PageCache之后就不再归Kafka管了而是由操作系统决定什么时候写回磁盘。内核处理脏页回写主要有三个触发条件脏页数量占内存比例达到阈值内核会启动后台回写默认dirty_ratio大约是20%dirty_background_ratio约10%脏页在内存中驻留超过一定时间默认约30秒dirty_expire_centisecs内存压力大需要回收页面。也就是说如果Kafka完全不主动刷盘数据丢失窗口最坏可以接近30秒甚至更长。Kafka的主动刷盘本质上就是在操作系统的异步回写机制之外自己控制一个什么时候必须把脏页强制写回磁盘的节奏。这个主动动作在Kafka源码里最终落地的调用是FileChannel.force(true)也就是fsync系统调用。Kafka把这一步叫做flush。所以聊刷盘策略聊的就是什么条件下Kafka会主动去调用fsync。1.3 Kafka默认值为什么敢设得那么大先看Kafka的默认行为。在较新的版本里log.flush.interval.messages和log.flush.interval.ms的默认值都大得吓人——接近Long.MAX_VALUE说白了就是几乎不主动刷盘。老版本里log.flush.interval.ms曾经默认3000左右但后来官方也调整了策略。不同小版本有差异以实际broker加载的配置为准。为什么官方敢这么干因为Kafka的数据安全模型压根没押在本地刷盘上而是押在副本机制上。Kafka的日志段在每台broker上都有自己的副本只要ISR里还有别的broker存活即使某个节点的本地数据因为断电丢了集群依然可以从其他副本恢复数据。这和传统数据库的思路完全不同——比如MySQL的innodb_flush_log_at_trx_commit1会强制每次事务提交都刷盘因为单机数据库没有别的副本可以依靠。所以Kafka的设计逻辑是用跨机器的副本冗余来代替单机的高频刷盘换取了极高的吞吐。理解了这一点后面所有调参决策都有了一个基准判断框架。2. 先把参数认全sync.ms、flush.messages到底对应Kafka里的谁2.1 官方配置项和社区简称的对应关系先说一个很多人不知道的细节Kafka的broker配置里并没有sync.ms和flush.messages这两个原生命名。打开官方文档对应的配置名是log.flush.interval.ms和log.flush.interval.messages。但topic级别的动态配置里参数名确实就叫flush.ms和flush.messages不带log前缀。很多网上的文章把这两套名字混着写新手照着server.properties写了一个sync.ms结果broker启动时压根不识别配置静默失效。这里给大家一个对照表社区常用叫法broker配置项topic配置项默认行为单位sync.mslog.flush.interval.msflush.ms老版本约3000ms新版本基本不主动刷毫秒flush.messageslog.flush.interval.messagesflush.messages接近Long.MAX_VALUE按条数几乎不触发条注意虽然我下面为了行文方便也会说sync.ms但你在server.properties里写配置的时候请写log.flush.interval.ms。这个细节能帮你少踩一个静默失效的坑。2.2 两个触发条件是或不是且log.flush.interval.messages和log.flush.interval.ms是两个独立条件只要满足其中一个就会触发一次flush。这是Kafka源码里LogManager的flush检查逻辑决定的每次消息追加后计数器会增加一旦消息数达到阈值或者时间间隔达到阈值就调用一次flush。举个例子设置flush.messages10000flush.ms2000。假设每秒写入5000条消息那么大约2秒后消息数达到10000触发刷盘假设每秒只写入500条消息数很久都到不了10000但时间到了2秒同样会触发刷盘。所以两个参数同时设置时真正起决定作用的是那个更严格的条件。理解了或的关系你就不会再去纠结是不是两个条件同时满足才会刷盘这类问题了。2.3 设置后没生效先查topic级覆盖配置调试刷盘参数时最容易遇到的诡异情况是server.properties明明改了也确认重启了broker但行为就是不对。这时候十有八九是topic级配置覆盖了broker级配置。Kafka允许对单个topic设置动态配置参数名就是flush.messages和flush.ms。如果某个topic被手动设置过这些参数broker级配置对这个topic就不生效了。用这条命令可以查看topic当前生效的配置kafka-configs.sh --bootstrap-server broker-1:9092 \ --describe --entity-type topics --entity-name order-event输出结果里会明确标注这个topic是否有override配置。我遇到过不止一次有人在测试环境给topic设置了flush.messages100后来做性能验证时发现该topic吞吐只有其他topic的几分之一最后查出来就是这条覆盖配置在作怪。2.4 与刷盘纠缠不清的三个概念acks、HW/LEO、多副本聊刷盘策略绕不开和它长得很像的几个概念必须说清楚。acksall只是保证ISR里的所有副本都接收了这条消息接收的定义是写入PageCache不代表落盘。follower同样先把数据写进自己的PageCache再返回确认。HW和LEO描述的是副本之间同步的进度水位它和磁盘上落了多少数据没有直接关系。副本数只解决集群里还有没有别的机器存着这条数据不解决这台机器断电后本地未刷盘数据会不会丢。一句话总结在Kafka里复制和落盘是两件独立的事。副本冗余解决的是机器故障下的可用性主动刷盘解决的是单机断电或操作系统崩溃下的本地持久性。这两者的关系不是替代而是互补。3. 为什么刷得越勤不等于数据越安全性能三角与代价3.1 一次fsync到底在做什么fsync的成本比大多数人想象的高得多。它不只是把脏页写回磁盘还要等待磁盘硬件真的完成写入——包括等待磁盘控制器缓存里的数据落到物理介质上。磁盘控制器通常会先把数据放进自身的缓存再异步写盘fsync必须确认这一步也完成了才会返回。拿餐厅打个比方Kafka接单相当于服务员在点单系统里记了一笔写PageCachefsync相当于每来一桌客人你都要求服务员立刻跑到后厨看着厨师把菜做出来才算订单成功。如果生意火每秒钟来五千桌后厨门口自然就堵死了。机械硬盘上一次随机fsync的延迟可能高达几十毫秒SSD虽然快不少但高频fsync会放大写放大效应缩短闪存寿命。更麻烦的是fsync是一个同步等待操作它直接把内核和磁盘的耗时暴露给了Kafka的写入线程。3.2 分区数量对刷盘成本的放大效应很多人会想我设置10000条刷一次频率不算高啊。但他们忽略了一个重要细节Kafka的LogManager在触发flush检查时是按目录遍历所有日志的。如果集群里有几百个分区某个时刻恰好都有数据达到刷新条件就会同时发起大量fsync调用形成明显的IO尖峰。我在一个约200个分区的集群上观察过把log.flush.interval.ms从默认改成1000之后平均延迟没有明显变化但P99延迟的周期性毛刺非常明显。用iostat看每隔一秒左右磁盘w_await会突然飙高对应就是那一波集中flush。分区数量越多这种周期性io尖刺越明显。这也是为什么我不建议在分区数很多的集群上把sync.ms设得特别小的原因——你可能只是想更安全一点结果却引入了全集群范围内的延迟抖动。3.3 用一次简单压测说明差距我在本地测试集群上做过一次控制变量的实验三节点单topic6分区副本数3producer用同步发送1KB消息体。刷盘参数分别设了三组结果如下配置策略峰值吞吐P99延迟备注默认几乎不主动刷约25万条/s约3ms内核异步回写兜底flush.ms100约8万条/s约12ms带宽明显下降flush.messages1 flush.ms100约1.2万条/s约70ms每条消息都在等fsync需要说明的是不同硬件、不同磁盘类型得出的绝对数字会差很多但相对关系是一致的刷盘越频繁吞吐掉得越狠。尤其是按条数刷的策略在高吞吐场景下几乎是性能杀手。所以我的观点是在有多副本保障的生产集群上你优先要做的不是调大刷盘频率而是保证副本数、ISR、acks这些可靠性参数是健康的。本机刷盘作为最后一道兜底设置到一个能接受故障恢复期少量丢失的程度就够了。4. 按场景给配置从日志采集到交易支付参数该怎么选4.1 三类典型场景的推荐配置不同业务对数据丢失的容忍度完全不同配置策略也必须跟着场景走。我给三类典型场景列个速查表业务场景典型容忍度推荐配置理由日志采集、埋点数据分钟级丢失可接受保持默认不主动刷盘数据量大丢了可以重新采集副本兜底足够订单、支付、交易流水秒级丢失可接受flush.messages10000flush.ms1000双条件兜底性能损耗可控单节点测试环境、单副本topic严格不丢flush.messages1flush.ms100牺牲吞吐换取本机安全仅限测试第一类场景不用多说默认配置就是最优解。第二类场景需要解释一下为什么不是把flush.messages设得越小越好交易类topic通常消息量不大每秒可能只有几百上千条flush.messages10000可能半小时都触发不了所以必须有flush.ms兜底。1000毫秒意味着最坏情况下丢失窗口在一秒左右配合副本机制已经能满足绝大多数交易系统的安全要求。第三类场景属于特殊情况的妥协真正的生产环境不应该依赖单副本。4.2 server.properties配置与topic级动态调整如果你要改的是broker级参数直接在server.properties里加# 集群范围内所有topic默认生效 log.flush.interval.messages10000 log.flush.interval.ms1000改完需要重启broker才生效。但生产环境中为一个topic重启整个集群通常不可接受这时候用topic级动态配置更合理kafka-configs.sh --bootstrap-server broker-1:9092 \ --alter --entity-type topics --entity-name order-event \ --add-config flush.messages10000,flush.ms1000生效是即时的不需要重启。想取消覆盖、恢复继承broker默认值kafka-configs.sh --bootstrap-server broker-1:9092 \ --alter --entity-type topics --entity-name order-event \ --delete-config flush.messages,flush.ms我个人的经验是不要把刷盘参数在broker级一刀切。日志类topic保持默认交易类topic用topic级配置单独覆盖这样既不会影响核心业务的性能也不会让那些不需要高可靠性的topic为不必要的fsync买单。4.3 一个反直觉的建议先把副本配置好再调刷盘每次有人来问我这个topic怎么配刷盘参数我都会先反问一句你的replication.factor是多少min.insync.replicas是多少producer的acks设的什么如果replication.factor1那再怎么调刷盘参数机器坏了数据照样丢如果acks0或1那producer根本不知道消息有没有真正被集群接受。这时候你有两种选择要么靠高频刷盘降低单机断电的丢失概率要么把副本和acks配置改好。前者治标不治本后者才是正道。我自己见过最夸张的一次是某内部系统topic副本数只有1却把sync.ms设成10ms导致整个broker的IO被打满其余正常topic的延迟也跟着飙到秒级。把刷盘参数调回默认、给关键topic加上副本并配置acksall之后问题迎刃而解。调刷盘参数之前先把副本这条腿站牢。5. 验证与踩坑怎么确认刷盘策略真的在按你的预期工作5.1 用JMX和系统指标做观察配置调整之后不能只看文档就完事必须用数据确认刷盘行为符合预期。Kafka的LogManager暴露了一组JMX指标名称是kafka.log:typeLogFlushStats里面有AvgFlushMs、MaxFlushMs、FlushCount这些属性。通过jconsole或者任何JMX客户端都可以观察。AvgFlushMs能告诉你一次flush平均耗时多少如果这个值持续很高说明磁盘了承受压力。操作系统层面用iostat -dx 1观察w_await和%util能很直观地看到刷盘触发时的IO尖峰。配合free命令看cache占用——Kafka进程起来之后内存大量被PageCache吃掉这是正常现象不是内存泄漏不要误判。我确认配置是否生效的标准流程是先describe一下topic的实际配置确认没有override干扰然后观察FlushCount如果过了一段时间这个数字在涨说明刷盘在按预期发生最后用iostat确认磁盘IO没有异常毛刺。5.2 用故障演练验证丢数据窗口想验证当前的刷盘策略到底在故障时丢多少数据我的做法是做个专门的测试topic副本数设为1打开自动创建持续灌数据然后执行一次故障注入。如果你只在测试环境模拟进程崩溃用kill -9杀掉broker进程再重启是不够的——因为PageCache还活着数据可能依然在内存里Kafka重启后照样能读到。这会给你的测试结果造成严重误判。真正能模拟断电场景的做法是在虚拟机上直接强制断电或者用云主机控制台的强制重启功能。这样PageCache会完全丢失Kafka重启后你对比producer发送的条数和topic里实际能消费到的条数差距就是这次故障的丢失窗口。我在测试环境做过一次默认刷盘配置单副本topic连续写入5分钟强制断电重启后发现最后约20秒的数据不可见和内核30秒回写周期的预期基本吻合。这个数字能帮你建立对默认配置的直观感受。5.3 我踩过的几个坑第一个坑是flush.messages1就是每条都落盘的误解。实际不是。Kafka的写入流程是先写PageCache再返回ackflush的触发和消息写入是异步的。就算你把flush.messages设成1也只是意味着每追加一条消息后Kafka会发起一次fsync调用但这条消息的ack可能早在fsync完成之前就已经返回给producer了。你无法通过刷盘参数让ack和落盘变成严格的同步关系。第二个坑是试图用刷盘频率来弥补副本设计缺陷。有一阵子某个内部系统的磁盘性能很差有人想通过降低sync.ms减少丢失风险结果IO彻底被打满原本能用的服务直接不可用了。后来改成加副本、优化producer端ack策略同一块磁盘跑得好好的。第三个坑就是topic级覆盖配置。我自己排查过一个改了server.properties没生效的案例花了一个多小时最后发现是之前测试时给topic手动设置了flush.messages100broker级参数被覆盖了。所以任何调参之前先describe确认当前实际生效的配置长什么样这个习惯能省你大量时间。现在我的做法很简单默认配置打底关键topic用topic级动态配置单独覆盖上线前先describe确认再跑一轮压测观察FlushStats和磁盘指标。这套流程用了很久没有出过大问题。希望这篇关于日志刷盘策略的梳理能让你少走我走过的那些弯路。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进