1. 先搞懂Kafka broker高可用到底在解决什么问题
1.1 一个broker宕机之后会发生什么
做后端开发的应该都绕不过消息队列,Kafka更是队列里被用最多、踩坑也最多的那个。我接触Kafka这么多年,见过不少团队在生产环境把Kafka当成"文件版Redis"来用,topic建好了、消息能发能收就完事,压根没想过"broker如果挂了怎么办"。结果就是有一天某个节点的磁盘满了或者进程被OOM干掉,等我们发现的时候,业务方已经在群里喊了十几分钟"消息不动了"。
先说说没有高可用时一个broker宕机到底会带来什么后果。Kafka的基础模型是:生产者把消息写入到某个topic的某个partition,消费者从这个partition拉数据。如果一个broker进程停了,这个broker上所有partition的leader都会变成不可用状态。对于生产者来说,写入请求会失败;对于消费者来说,拉取请求也没有leader可以响应,整个业务链路瞬间卡住。如果这台机器直接物理损坏,比如磁盘阵列坏了、机房断电,那上面的数据物理上讲就没了,单纯重启已经救不回来。
我在之前的团队就经历过一次典型事故。那套Kafka集群一共三台机器,topic的副本数用的是默认值1,也就是说一个partition只有一份数据。某天晚上一台机器操作系统异常强制重启,Kafka进程起不来,跟这个broker相关的所有partition全部不可读写。因为副本数是1,根本没有别的机器上有这份数据的拷贝,等到运维把系统修好、Kafka进程拉起来、数据从磁盘里恢复,已经过去了一个多小时。这一个小时内所有依赖这组topic的业务数据全部积压,消费者整个挂在那里,下游同步任务全部阻塞。事后复盘结论很简单:副本数这个参数不是性能调优选项,是高可用拓扑的基础,生产环境绝不能按默认值来。
1.2 高可用到底要解决哪几件事
所谓的broker高可用,说白了就是三件事:消息不能丢、服务不能断、故障能自愈。消息不能丢,指生产者已经确认写入的数据,在broker故障后仍然可以被消费到;服务不能断,指某个broker挂掉之后,其余的broker能继续对外提供读写服务,业务无感知或者只有秒级抖动;故障能自愈,指不需要人工"手动切换主节点",Kafka自己能选出一个新的可用副本继续工作。
很多刚接触Kafka的朋友会误以为部署了集群就算高可用。实际上,集群只是必要条件,不是充分条件。一个三节点的Kafka集群,如果topic的副本因子是1,那跟三台各自独立的单机Kafka没有任何区别——每份数据还是只有一份,任何一台机器挂了,对应数据就不可用了。高可用的核心是"数据有冗余 + 角色能切换"。数据冗余靠副本机制,角色切换靠leader选举和controller协调,这两个机制配合起来,才是一个真正的高可用broker集群。
这套机制最大的优势在于,故障切换是自动的、分布式的。不像很多传统中间件需要配一个独立的主备切换组件来探活、选主、通知客户端,Kafka把故障检测和选举逻辑做进了broker自身。当然,为了做到这一点,Kafka内部设计了副本同步、ISR维护、Controller选主等一系列机制,这些机制本身是理解Kafka高可用的钥匙。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 高可用的地基:副本机制与ISR设计
2.1 副本是怎么分布到broker上的
Kafka的高可用不是整台broker级别的"双机热备",而是partition级别的冗余。一个topic可以分成多个partition,每个partition可以配置多个副本(replica)。这些副本会分布在不同的broker上,其中一个被选为leader,其余的都是follower。
这里有一个容易被忽略的点:replica的单位是partition,不是topic。同一个topic的不同partition可能有不同的leader,因此同一个broker上,一部分partition你是leader,另一部分partition你是follower。这也意味着集群里没有固定的"主节点broker",职责是按partition分散开来的。比如一个topic有6个partition、3个broker,每个broker上可能有2个partition的leader。某个broker挂了,只影响它担任leader的那几个partition,其余partition完全不受影响。
副本分布还有一个细节值得注意:尽量保证副本分布在不同的broker上。比如replication.factor=3,那么一个partition的3个副本应该在3台不同的机器上,这样任何一台机器挂了,其他两台还有数据。Kafka的副本分配器会默认做这个事,但它并不知道你的机器在物理机架上怎么排布的。所以如果你的集群分布在多个机架或者多个机房,建议配置broker.rack参数,这样Kafka分配副本时会参考rack信息,尽量让副本跨机架分布,避免一个机架断电导致两个副本同时不可用。
我在实际部署时会把rack配置写成物理机架号,比如rack=rack-a。一个3机架的生产集群,topic分配副本时就会自动避开"同一rack两个副本"的情况。别小看这一步,真遇到机房机柜跳闸的时候,这个配置可能就避免了一次整体不可用。
2.2 ISR到底在维护什么
ISR全称是in-sync replicas,即"同步中的副本集合"。这个集合由leader维护,里面装的是与leader数据保持同步的follower副本。生产环境排查Kafka问题时,大家经常在命令行里看到类似"leader=1, isr=[1,2]"的输出,这里的ISR就是高可用能"切换"的前提。
ISR的判定标准不是"活着"就行,而是跟随进度要跟得上。Kafka中follower会主动向leader拉取消息,只要在replica.lag.time.max.ms时间内一直保持拉取,并且拉到的消息offset能追上leader的HW(高水位),这个follower就留在ISR里。默认这个时间是30秒。如果一个follower因为GC暂停、网络抖动、磁盘繁忙导致超过30秒没有赶上进度,leader就会把它从ISR里踢出去。
这个机制非常关键,因为它保证了ISR里的副本都是"数据足够新"的副本。当一个leader挂了需要选新leader时,只能从ISR中选——这确保新leader手里的数据不会落后太多,不会出现"选出一个leader,结果它缺了一大段消息"的情况。
需要特别注意的是ISR为空的情况。如果ISR集合空掉了,说明所有follower都跟不上leader,只有leader自己在干活。这时如果leader也挂了,整个partition就不可用了。为了避免这种情况,生产环境配置里通常会把min.insync.replicas设置成大于1的值,意思是写入时至少要保证ISR里有这么多副本才允许写入,否则直接拒绝。这等于用一部分可用性换取数据安全,后面我会专门说这个权衡。
2.3 acks、min.insync.replicas和复制因子怎么配合
如果说ISR是Kafka高可用的"内部机制",那对外最直观的三个参数就是replication.factor、acks和min.insync.replicas。这三个参数配置不好,什么副本机制都是空谈。
replication.factor是topic创建时的副本总数,比如3就表示每个partition有3份数据。这个参数属于"决定冗余度"的顶层设计,一般建议3,小规模集群2也行,但1是绝对不行的。有个容易踩的坑是,很多团队用了Kafka自带的默认配置,结果默认的replication.factor=1,这就是灾难的开始。创建一个topic后可以用kafka-topics.sh --describe检查一下副本分布,确保每个partition的Replicas列有3个broker id。
acks是生产端的参数,表示生产者要求broker在什么条件下才返回写入成功。启动Kafka producer时没配过的朋友可能都不知道这个参数,但跟高可用关系极大。acks=0表示发出就完事,不等任何确认,消息可能直接丢;acks=1表示leader写入成功就返回,但如果leader在数据还没复制给follower时挂了,消息就丢了;acks=all表示所有ISR中的副本都写入成功才返回,这才符合高可用的"不丢消息"标准。
min.insync.replicas是broker端的参数,控制leader接受写入时最少需要的同步副本数量。配合acks=all使用时,如果ISR数量低于这个阈值,broker会直接拒绝写入(返回NotEnoughReplicasException)。比如设置min.insync.replicas=2,那ISR里只有leader一个副本时,所有写请求都会被拒。这看起来是制造麻烦,实际上是保护机制:宁可让业务写入失败,也不要让"已确认的数据"只有一份拷贝。
这三个参数配合起来的一个标准生产配置是:
- replication.factor=3
- min.insync.replicas=2
- acks=all
也就是说,每个partition有3份副本,写入时必须至少2个副本确认成功才返回。这样即使某个broker挂了,还是有一个副本的数据全在。而如果极端情况只剩1个副本在线,写入会被拒绝,不产生"假成功"。这套组合我实测下来,既保证了消息不丢,也不会把吞吐压得太低,是一个性价比很高的配置。
3. 核心机制:Leader选举与故障转移
3.1 新leader是怎么选出来的
broker高可用能不能落地的关键一步,就是当leader副本失效后,Kafka能不能快速、正确地从存活副本中选出一个新的leader。这个动作不是随机的,也不是选ISR里最"老"的,而是有一套严格逻辑。
正常情况下,新leader只从ISR中选择。原因上文说过,ISR中的副本数据与旧leader同步度最高,选它们出来不会丢已提交的消息。如果ISR列表里恰好没有可用的副本(比如ISR中的其他副本所在的broker也挂了),Kafka就会面临一个选择:是让partition继续不可用,还是从"不同步但活着"的副本里选一个出来。
这时候unclean.leader.election.enable参数就派上用场了。这个参数默认是false,也就是说,如果ISR里没有存活副本了,Kafka宁可让partition在短时间内不可读写,也不会选一个数据不全的副本当leader。把值设为true,则允许从"非ISR"副本中选举leader,代价是可能出现消息丢失或重复。生产环境我的建议是保持默认的false,除非你的业务能接受数据不一致。
选举过程本身由cluster的controller协调。具体来说,当某个broker下线,controller会感知到,并扫描该broker上作为leader的所有partition,从各自的ISR里挑选新的leader。整个过程的耗时取决于partition数量,但单partition级别是非常快的,通常毫秒到几十毫秒就能完成。
3.2 Controller角色与它的高可用
Kafka集群里有一个特殊的角色叫controller,负责全局管理:处理broker上线/下线事件、为partition分配副本、触发leader选举、管理topic元数据。你可以理解成它是一个"集群总管"。这个角色在一个集群里同一时刻只有一个controller在工作,其他broker处于待命状态。
controller的高可用方式很有意思,它不走"双活"那一套,而是通过集群内的选举机制保证controller挂了以后能快速产生新的controller。在依赖ZooKeeper的老版本里,controller通过ZK的临时节点来注册,谁创建成功谁就是controller。ZK节点消失触发其他broker竞争成为新controller。在KRaft模式下,controller和broker在同一个进程里,通过Raft协议选出活跃controller,逻辑上类似但不再依赖外部ZK组件。
我实际遇到过的controller问题多半出现在broker重启时。如果一次同时重启多个broker,可能触发多轮controller选举,期间集群的元数据操作会有短暂抖动。所以生产环境我会建议滚动重启,逐个broker重启而不是全部同时搞,这样能减少controller切换的震荡。
另外一个新手容易踩的坑:controller是全局单点角色,如果集群元数据操作很频繁且controller所在的broker本身负载很高,可能导致整个集群的partition分配、leader选举响应变慢。因此在大规模集群里,有人会把controller和业务broker分开部署,避免"总管"业务压力过大。小规模集群一般不必要,但监控上要留意controller所在broker的CPU和网络指标。
3.3 Leader切换时消息会不会丢
这是很多人在面试和实际生产里都会问的问题:leader切换的时候,消息会不会丢?要分两种情况回答。
第一种,消息已经返回给生产者"写入成功"(acks=all,且确认时数据已同步到min.insync.replicas个副本)。那么这种情况下,消息的副本数至少是2份,一个挂了,另一个还存在,新leader选出来之后数据不会丢。
第二种,消息还没有完成同步就发生了leader切换。比如数据只写到了老leader的内存或本地磁盘,follower还没来得及拉取,老leader就挂了。这种情况下,这部分消息会丢失。Kafka官方把这种"已写入但未同步"的数据叫uncommitted data,在高可用语义中,它们不属于"已确认"的消息,所以严格来说不违反"已确认消息不丢"的承诺。
生产环境里,如果你对消息丢失极度敏感,可以启动acks=all并设置min.insync.replicas=2。这样未同步的窗口会非常小,只有"leader本地写入但还没来得及复制给任何follower"的极短窗口,而且这种写入在返回给生产者之前,如果触发故障,生产端会收到"写入失败"而不是"成功",生产端可以通过重试补偿来避免最终丢失。
实际运维中我见过一次"消息丢失"误报,排查了半天发现是消费者提交offset的时机问题,不是broker丢消息。所以遇到疑似丢失,先别急着怪Kafka,梳理清楚生产端确认、broker复制、消费端提交三个环节,通常能找到真正的问题。
4. 实操:从零搭建一个高可用Kafka集群
4.1 环境准备与版本选择
讲完原理,咱们实际操作一遍,用三台机器搭一个高可用Kafka集群。环境我这里用的是三台Linux虚拟机,CPU和内存不用太大,2核4G就能跑起来做验证。操作系统是CentOS环境,但下面的配置对Ubuntu基本通用。
版本选择上,我建议直接用较新的Kafka稳定版。当前阶段我推荐Kafka 3.7或者3.8以后的版本,并且使用KRaft模式——这已经是社区明确的方向,Kafka 4.0已经从代码层面彻底移除对ZooKeeper的支持。如果你还在用老版本ZooKeeper架构,了解原理没问题,但新搭建环境建议直接上KRaft,少维护一套ZK集群,也少踩很多"ZK脑裂"的坑。
另外需要准备JDK。Kafka 3.7及以上版本要求JDK 8或者JDK 11都可以,但如果你要跑新特性或者比较新的版本,我这里直接用JDK 17,稳定性挺好。下载Kafka二进制包时找个你信任的镜像,我这边直接把包解压到/opt/kafka_xxx目录下。
三台机器的hostname和IP我这样规划:
| 节点 | IP | 角色 |
|---|---|---|
| kafka1 | 10.0.0.11 | broker + controller |
| kafka2 | 10.0.0.12 | broker + controller |
| kafka3 | 10.0.0.13 | broker + controller |
KRaft模式下,每个节点既是broker也是controller候选者,配置上会同时设置process.roles=broker,controller。
4.2 三个节点的KRaft配置详解
KRaft模式的配置核心在config/kraft/server.properties。每个节点的配置大部分相同,差异主要是node.id、advertised.listeners和log.dirs。我先以kafka1为例写一份关键配置:
properties复制process.roles=broker,controller
node.id=1
controller.quorum.voters=1@10.0.0.11:9093,2@10.0.0.12:9093,3@10.0.0.13:9093
listeners=PLAINTEXT://10.0.0.11:9092,CONTROLLER://10.0.0.11:9093
advertised.listeners=PLAINTEXT://10.0.0.11:9092
controller.listener.names=CONTROLLER
listener.security.protocol.map=CONTROLLER:PLAINTEXT,PLAINTEXT:PLAINTEXT
log.dirs=/data/kafka/kraft-combined-logs
num.partitions=3
default.replication.factor=3
min.insync.replicas=2
这里有几点要说明。node.id是节点唯一标识,三台机器分别是1、2、3。controller.quorum.voters必须是三台机器地址和端口的完整列表,这个跟ZK的集群配置很像,三个节点互相都知道对方存在。listeners里我一共配了两个端口,9092用于broker之间和客户端通信,9093是controller间的Raft通信端口。
log.dirs要特别留意。原来Kafka推荐这个目录用独立的块设备(磁盘),因为Kafka写入特别频繁,如果和系统盘混在一起,要么磁盘I/O互相干扰,要么日志占满系统盘导致节点崩了。我这里先用/data/kafka挂载了一块独立的磁盘,格式化后记得修改目录属主为当前运行用户。
副本相关的配置我直接写在集群默认配置里了:num.partitions=3表示新建topic默认3个分区,default.replication.factor=3表示默认3个副本,min.insync.replicas=2保证至少两个副本同步。这几个默认值加起来就是"新topic自动高可用"的关键。后面无论谁用命令行创建topic,都不用担心忘了加副本参数。
kafka2和kafka3的配置只需要改node.id(2和3)、listeners、advertised.listeners里的IP,其余保持一致。还有一个容易漏的地方:controller.quorum.voters在三个文件中保持一致,不要抄错IP。
4.3 生成集群ID并启动验证
KRaft模式启动前必须先生成一个集群ID,并把这个ID写入所有节点的元数据目录。在Kafka根目录执行:
bash复制bin/kafka-storage.sh random-uuid
会输出一串UUID,比如7L0k3xB9TfG3sG9GjG0cHg。然后在每台机器上执行同样的格式化命令,注意把UUID替换成你实际得到的那串,并且三台机器要用同一个UUID:
bash复制bin/kafka-storage.sh format -t 7L0k3xB9TfG3sG9GjG0cHg -c config/kraft/server.properties
这一步会在每个节点上初始化元数据目录。这里有一个踩过的坑:如果在第一台机器格式化之后,第二台机器格式化了两次或者不小心指定了不同的log.dirs,集群里节点之间的元数据会不一致,后面启动会报各种错。所以格式化之前确认好log.dirs路径,格式化命令在每台机器上只执行一次。
三台机器全部格式化完成后,分别启动:
bash复制bin/kafka-server-start.sh -daemon config/kraft/server.properties
启动后可以用日志或jps确认进程在跑。然后要验证集群状态,最直接的方式是用kafka-metadata-quorum命令:
bash复制bin/kafka-metadata-quorum.sh --bootstrap-server 10.0.0.11:9092 describe --status
这个命令会返回当前集群的活跃controller和节点信息。看到"Leader: 1"这类输出,说明Controller已经选出来了。用kafka-topics.sh创建一个测试topic:
bash复制bin/kafka-topics.sh --bootstrap-server 10.0.0.11:9092 --create --topic test-ha --partitions 3 --replication-factor 3
bin/kafka-topics.sh --bootstrap-server 10.0.0.11:9092 --describe --topic test-ha
如果配置正确,输出里能看到Topic: test-ha的3个partition,每个partition的Replicas和Isr列都有3个副本id。例如:
code复制Topic: test-ha Partition: 0 Leader: 1 Replicas: 1,2,3 Isr: 1,2,3
Topic: test-ha Partition: 1 Leader: 2 Replicas: 2,3,1 Isr: 2,3,1
Topic: test-ha Partition: 2 Leader: 3 Replicas: 3,1,2 Isr: 3,1,2
看到这个输出,说明高可用的基础配置已经就位。这时候再起一个生产者和一个消费者,发送几条测试消息,确认消息能正常收发。到这里,一个三节点的高可用Kafka集群就搭起来了。
5. 故障演练:模拟宕机实测主备切换
5.1 演练方案怎么设计
集群搭好不等于高可用,你得亲眼验证过故障切换才放心。我自己每次搭完新集群都会做一轮故障演练,模拟broker宕机、网络中断、进程被杀几种常见故障。下面这个演练方案我一直在用,脚本化和预期都写好,简单有效。
第一步,先起一个持续生产消息的脚本,让它一直写入test-ha这个topic。第二步,起一个消费者,记录消费到的消息数和最新offset。第三步,找到test-ha某个partition的leader所在的broker(比如partition 0的leader原来是broker 1),然后用kill命令模拟broker 1宕机:
bash复制kill -9 <kafka1进程PID>
这时候观察生产者和消费者的日志。理论上生产者会遇到短暂的通信异常,但由于我们配置了重试机制,它应该会在重试后恢复写入。消费者同理,在rebalance完成之后继续消费。等集群恢复稳定后,再查看test-ha的leader分布,你就能看到partition 0的leader已经换到了别的broker上。
5.2 实测过程记录
我按上面的流程实测过很多遍,以下是常见的现象记录。
杀掉broker 1进程后,大约3到5秒内,kafka-topics.sh --describe就能看到partition 0的Leader从1变成了2或3。Controller感知节点下线并触发选举的动作非常快。但同时生产者会有明显的瞬时错误,日志里会出现类似"Connection to node -1 could not be established. Broker may not be available."的报错。
如果生产者未配置重试,这些报错就会直接变成"发送失败",业务方立刻感知到写入异常。但如果配置了retries=10和retry.backoff.ms=300,重试后会继续成功发送,只是这一波消息的总延迟会大一些。这一点在生产环境特别重要:高可用不是完全无感知,而是把中断时间压缩到秒级,并且上层业务能通过重试消化抖动。
消费者端也会有类似体验。由于partition的leader切换,消费者group会触发rebalance,该group下的consumer会暂停消费,等分区重新分配完成后才继续。我观察到的默认rebalance耗时通常在一秒到几秒之间,取决于这个group的consumer数量和partition数量。
消息丢失的检测,我用的是"消费者最终消费到的消息总数"对比"生产者发送成功的消息总数"的方式。只要两者一致,说明在测试期间没有消息丢失。acks=all配合min.insync.replicas=2的配置下,kill掉一个broker不会丢已确认消息,这是我在多次演练中验证过的结论。
5.3 问题排查速查表
演练过程中难免遇到各种问题,我整理了日常运维和高可用排查中最常见的一批:
| 现象 | 可能原因 | 处理思路 |
|---|---|---|
| 生产者报NotEnoughReplicasException | min.insync.replicas高于ISR数量 | 检查broker副本是否都正常,等ISR恢复或调低参数 |
| 消费者rebalance后一直卡住 | 消费逻辑卡死或fetch请求超时 | 检查consumer心跳线程、GC情况,看是否有阻塞操作 |
| 某台broker频繁加入/离开集群 | 网络抖动或磁盘IO高导致follower被踢出ISR | 检查网卡丢包率、磁盘IO等待时间、GC日志 |
| controller在节点间频繁切换 | 节点网络分区或元数据写入慢 | 排查网络,确认quorum节点间端口连通性 |
| 消息延迟突然升高 | 生产端重试风暴或broker磁盘IO瓶颈 | 结合broker的请求队列、磁盘延迟、CPU指标分析 |
| 新建topic副本数不是3 | 使用了旧版默认配置或自定义创建参数 | 检查server.properties的default.replication.factor |
这几个问题我在不同客户环境基本都碰到过。特别想提一下第二个"rebalance卡住",有段时间线上偶发"消费者没有消费但也没有报错",排查到最后是某些consumer在回调里写了重量级数据库操作,导致session超时被判定死亡,频繁触发rebalance又反复重试,形成恶性循环。这种问题跟broker高可用本身无关,但真出事的时候,容易把锅甩给Kafka集群,所以排查时要先把自己的消费逻辑摘干净。
6. 高可用集群的日常运维经验与避坑指南
6.1 配置层面的几个关键细节
高可用集群搭好之后,日常运维里还有很多细节决定稳定性。
首先要检查JVM堆配置。Kafka启动脚本里默认的堆内存对大规模集群是不够的,但也不是越大越好。我常用的配置是KAFKA_HEAP_OPTS="-Xms4g -Xmx4g",如果机器内存富余,6到8G也行。堆太大反而会导致FGC的停顿时间变长,影响ISR同步。如果broker上partition特别多,堆太小会频繁GC,导致follower从ISR中掉队。这个参数需要结合partition数量综合评估,不能照搬别人的值。
其次是文件描述符和网络缓冲区。Kafka的高并发IO依赖操作系统的文件描述符上限,建议把ulimit -n设置到65535以上。生产环境我见过因为默认1024导致"Too many open files"的,这个问题一般不会在高可用测试时暴露,而是并发上来之后突然出现。
磁盘的使用率要纳入最高优先级监控。Kafka写入量大的时候,磁盘空间消耗速度很快。正常情况下我建议磁盘使用率超过70%就触发告警,超过85%必须介入。因为磁盘写满后,broker会报"Disk usage exceeded high watermark"并且分区只读,这相当于部分高可用能力直接失效。如果topic有副本,那副本同步也会因为磁盘空间不足卡住,ISR会缩成只有leader一个,数据就有了丢失风险。
还有一个经常被忽略的点:log.retention.hours和log.segment.bytes这两个参数决定日志保留多久和segment大小。保留时间太短,数据删得快;segment太小,会产生大量小文件,影响读写性能。我一般用默认的168小时,但容量规划时会把每天写入量乘以保留天数估算出来,确保磁盘容量足够,而不是等告警了才开始算。
6.2 监控与可视化工具推荐
高可用不是"配置完就完事",你得能实时看到集群的健康状态。Kafka本身提供JMX指标,但裸看JMX不直观,我推荐两条路线的工具组合。
监控指标收集与告警方面,用Prometheus加kafka_exporter是主流方案。kafka_exporter能从broker的JMX里拉出under replicated partitions、active controller count、offline partitions、request handler平均耗时、ISR变化等关键指标。把这些指标导入Prometheus,再配合Grafana面板,就能看到整个集群的实时健康状态。特别是kafka_controller_kafkacontroller_offlinepartitionscount,只要这个值大于0,说明有partition处于没有leader的状态,这通常就是故障的开始。
集群管理界面方面,我习惯用Kafka UI(基于Web的可视化工具)。它能直观展示broker列表、topic列表、partition的leader和ISR情况,还能直接查看消费组积压了多少消息。排查"消息为什么卡住"的时候特别好用,拉出来一眼就能看到消费者的lag。另外一个常用的是Kafka Manager,不过这个项目更新较慢,新版本Kafka兼容性一般,Kafka UI是新一点的方案。
我实际排查故障的习惯是:先看Grafana有没有under replicated partitions告警,再看Kafka UI里对应partition的ISR状态,最后才去翻broker日志。这样定位问题的速度比去服务器上一条条敲命令快得多。
6.3 扩容与日常巡检建议
高可用集群不是建好就不动了,业务量增长后总要做容量扩容。broker扩容相对简单:新机器配置好KRaft并加入controller.quorum.voters后,启动就会自动加入集群。但注意,新broker加入后,已有的partition不会自动把副本迁过去。你需要手动执行分区重分配,让部分partition的副本分布到新broker上。Kafka提供了具体的reassignment配置文件,用kafka-reassign-partitions.sh可以执行。这个操作是动态的、不中断业务的,但会占用网络和磁盘IO,建议在低峰期执行。
日常巡检建议标准化成一个清单:
- 检查所有broker进程是否存活,是否有僵尸进程
- 查看每个broker的磁盘使用率,确认没有超过70%阈值的节点
- 用kafka-topics.sh检查关键topic的ISR是否完整
- 查看under replicated partitions指标是否大于0
- 查看consumer group的lag情况,确认消费侧没有持续积压
- 检查最近是否有异常GC日志,确认JVM停顿时间可控
这套巡检流程配合监控告警,基本可以在用户发现异常之前提前处理掉大部分隐患。我一般安排每周一次巡检,每次大概10到15分钟,总比出了故障再拉通宵复盘强。
7. 最后聊几个高可用的进一步扩展方向
前面讲的都是broker层面的高可用,实际架构里你还可以继续扩展。比如生产者和消费者的高可用:生产者侧配置重试、幂等、消息确认,消费者侧配置多副本消费组,这些都能提升整条链路的可靠性。还有跨机房容灾,Kafka通过MirrorMaker在两个集群之间同步数据,这属于另一个级别的"高可用",但思路是一样的——数据冗余越多,故障后能选择的空间越大。
还有一点想提醒的是,Kafka高可用和"强一致性"是两件事。Kafka默认提供的是"至少一次"或"至多一次"的语义,看你怎么配合acks和幂等措施。如果你需要精确一次语义,需要用Kafka的事务API和生产端的幂等配置,那是另外一套体系。别把所有问题都抛给broker高可用来解决,定位清楚哪一层的能力,才能设计出匹配业务需求的架构。
我在实际使用中最大的体会就是:高可用是要花钱的——机器成本、运维成本、性能损耗。三副本意味着三倍磁盘空间,ISR同步意味着额外的网络带宽,acks=all意味着写入延迟变高。所以在设计之初就要想清楚,你的业务对"丢消息"和"短暂不可用"的容忍度到底是多少。如果是日志采集类,丢几条能接受,副本因子2可能就够;如果是订单支付类,那3副本+acks=all就是底线。
另外一个小技巧是,新topic创建时尽量通过服务化平台或者标准化脚本,而不要直接给人手敲命令行。这样能保证所有topic的副本参数都符合规范,不会出现有人建了一个副本数1的topic,自己还完全不知情的情况。我在团队里就把topic创建封装成了一个带参数校验的脚本,replication-factor必须大于等于3,否则直接拒绝创建。这个小工具帮我挡掉了不少事故。
Kafka的高可用不是一个"配完就安心"的功能,它是一整套需要持续关注、演练和调优的工程实践。希望这篇文章能从原理到实操帮你把这条路走通。
