某日,生产有个模块发送kafka消息失败,经排查发现是有台broker挂了。当时就给紧急重启了。但事后想想不对劲,不是高可用吗?挂一台broker就撂挑子了还怎么好意思说高可用?想想应该是我们使用的方式不对,现在来复现并排查下当时的问题。
我们的kafka版本是2.0版本,生产上部署的只有2个broker,并且分区对应的副本也是只有1个。
找个测试环境模拟当时的生产上的情况。启动 2台broker:
./bin/zkServer.sh start conf/zoo.cfg
bin/kafka-server-start.sh -daemon config/server.properties
删除原有的topic:
bin/kafka-topics.sh --zookeeper 10.107.4.xxx:2181 --delete --topic cftest_demo_topic1
重新建一个topic,并且配置分区数为6,分区副本为1:
bin/kafka-topics.sh \
--create \
--topic cftest_demo_topic1 \
--partitions 6 \
--replication-factor 1 \
--zookeeper 10.107.4.xxx:2181
发送消息,能够正常消费:
强制删除其中一台broker,模拟生产一台broker挂掉的情况:
可以看到原先在broker 1的分区的leader状态都边成了-1。
再手动发送消息试试:
不管我发多少次,都正常,与预想的部分发送失败不一样。经过调试,发现可用分区列表已经更新了,只有可用的0,2,4这3个分区了,所以发送的数据会一直往这3个可用的分区发送数据:
那为什么当时生产上的会有部分发送失败?按道理应该也不会失败才对,有一台broker是正常的,那所有的数据应该会发送到正常的那台broker上才对。
针对这个问题我一开始猜测会不会是更新元数据那一块有问题,给卡住了,导致的元数据未更新,然后可用分区列表里面还有已经失效的分区存在,这样才会有可能是发送数据给发送到了不可用的分区上去。但是这个我没有证据来证明这一点。后面发现我们应用有指定分区的发送方式:kafkaTemplate.send(topic, partition, null, msg); 就是这种指定了具体的分区号的发送方式。问题应该就是这种指定分区的方式给导致的发送到了不可用的分区上了。模拟一把看看是不是与猜测的一样:
从上图可以看见重现了生产的报错了。所以问题的原因是发送消息发送到了不可用的分区了。
其实如果是不指定分区的话,按上面的配置也没多大问题(发送会找可用的分区发送,但是这个是有前提的,继续往下看),但是如果一台broker挂了的话,那台broker上的数据就消费不了了(有种伪高可用的意思了)。但是生产上已经有指定分区这种方式了,怎么能实现真正的高可用?
首先broker得是3台吧(别问我为什么是3这个数字,我也没细究,按我的理解非要用2台应该也是可以的,但是我没去验证),这个我们已经通知运维同学加了一台broker了,但是只是单纯的加一台broker的话也没用,因为我们的分区副本数为1,想一想,每个分区都只分布在其中一个broker上,如果某个broker又挂了,这个分区想选举新的分区leader都选举不了了。所以分区副本数怎么也得是2个吧。分区副本数调整这个东西还得手动写json调整。
现在模拟下3台broker,并且每个分区副本数为2的场景能不能实现高可用:
先模拟将当前的分区副本数调整为2,这个需要确认当前broker对应的id号及手动编写json进行调整。
查看kafka集群有几台并且对应的broker id是多少:
bin/kafka-broker-api-versions.sh --bootstrap-server 10.107.4.xxx:9092
或者登录zk查看对应的broker id:
./zkCli.sh -server 10.107.4.xxx:2181
ls /brokers/ids
手动编写increase-replication-factor.json内容(replicas 的值就是对应分区要分布的broker编号):
{
"version": 1,
"partitions": [
{"topic": "cftest_demo_topic1", "partition": 0, "replicas": [0, 1]},
{"topic": "cftest_demo_topic1", "partition": 1, "replicas": [1, 3]},
{"topic": "cftest_demo_topic1", "partition": 2, "replicas": [3, 0]},
{"topic": "cftest_demo_topic1", "partition": 3, "replicas": [0, 3]},
{"topic": "cftest_demo_topic1", "partition": 4, "replicas": [1, 0]},
{"topic": "cftest_demo_topic1", "partition": 5, "replicas": [3, 1]}
]
}
使用上面编辑的json文件进行手动调整分区:
bin/kafka-reassign-partitions.sh \
--zookeeper 10.107.4.xxx:2181 \
--reassignment-json-file /data/applog/increase-replication-factor.json \
--execute
执行完后查看对应topic的分区情况:
可以看到replicas及isr都变化了,修改完后再次发送消息看看正不正常:
可以看到发送成功,而且消费者也是能够正常消费的。
现在在手动的关闭一台broker,因为我的代码中调整为了发往partition 3,我们上面配置的3号分区在broker 0、3 这2台服务上,并且这个分区对应的leader是在broker 0,所以我们手动kill掉broker 0这台。
先确认下broker 0这台对应的是哪台服务:
使用bin/kafka-broker-api-versions.sh --bootstrap-server 10.107.4.xxx:9092 来查看对应的broker 对应的服务:
然后到对应的服务上手动kill掉kafka进程。
再到另一台服务上查看现在对应的topic分区情况:
./kafka-topics.sh --describe --zookeeper 10.107.4.xxx:2181 --topic cftest_demo_topic1
可以看到分区已经重新分配了,之前所有在broker 0上的leader分区都已经变到其他broker了,而且所有的isr中也都没有broker 0 了。
再次发送消息试试,测试的结果是能够正常发送消息,但是不会正常的消费消息。为什么???这是几个意思?我使用的姿势还是不对吗?
经过多次测试,发现只有在kill掉broker 0的情况下会出现上面那个情况(能发送消息,但是消费不了消息),kill掉另外2台是能够正常高可用的。
在网上一顿搜索,发现应该是__consumer_offsets这个topic分区的问题,这个topic是记录消费者提交的偏移量的信息,如果是这个topic分区有问题的话那消费者就读不到对应的偏移量是多少了,也就不知道从哪消费了。这正好和上面消费者不消费对应起来了。
先查看下__consumer_offsets分区的情况:
没有kill broker, __consumer_offsets分区分配情况:
Kill掉broker0后__consumer_offsets分区分配情况:
Kill掉broker1后__consumer_offsets分区分配情况:
从上面3张图可以看到__consumer_offsets分区副本数也是只有1个,在kill掉broker0的时候分区有一半的分区都使用不了。奇怪的是第三张图,按道理kill掉broker 1后,对应的原先leader为1的都会变为-1才对(就和第二张图一样),但是这里没有变化,有哪位大佬知道的请赐教一下。这个问题先不管,从这三张图大概可以知道为什么我们在kill broker0的时候消费者不消费了,kill另外2台broker的时候就能正常消费。因为只有在kill broker0的时候__consumer_offsets分区有变化了,有一半的分区都使用不了了。
怎么让__consumer_offsets分区也高可用?调整分区副本数呗。一个是删除这个topic,然后调整kafka里server.properties配置里面的offsets.topic.replication.factor值再重启kafka。还有一种方法我们上面刚使用,就是手动调整分区副本数。我们仍然是采用手动调整分区副本数这个方式:
查看调整后的分区分布情况:
可以看到都有3个副本了。
然后再试下kill掉broker0试试:
从__consumer_offsets分区分布情况及测试情况来看已经实现高可用了,能够正常发送消息及消费消息了。再分别kill掉broker 1和broker 3测试了下,也是正常的。至此,我们的kafka应该算是高可用的了。
总结一下:
1、在生产者发送消息的时候如果没有指定分区号的情况下,会默认使用可用分区进行发送数据。在有broker挂掉的情况下,客户端会更新元数据,可用分区列表会更新。
2、新增一台broker并不会影响已存在的topic分区的分布情况
3、如果业务topic的分区副本数为1,那么在有broker挂掉并且生产者有指定分区号方式发送的情况下会有发送失败的情况。如果kafka内部 __consumer_offsets的分区副本数为1,那么在有broker挂掉的情况下是有可能会导致消费者不消费消息。
4、如果需要实现高可用,一定要设置分区副本数大于2,对于我们的业务topic如此,对于kafka内部的 __consumer_offsets亦是如此。对于已经存在的topic,如果需要调整分区副本数的话,需要手动编辑json进行调整。