Kakfa MirrorMaker是Kafka 官方提供的跨数据中心的流数据同步方案。其实现原理,其实就是通过从Source Cluster消费消息然后将消息生产到Target Cluster,即普通的消息生产和消费。用户只要通过简单的consumer配置和producer配置,然后启动Mirror,就可以实现准实时的数据同步。
Kafka Mirror的基本特性有:
Kafka MirrorMaker的官方文档一直没有更新,因此新版Kafka为MirrorMaker增加的一些参数、特性等在文档上往往找不到,需要看Kafka MirrorMaker的源码。Kafka MirrorMaker的主类位于 kafka.tools.MirrorMaker
,尤其是一些参数的解析逻辑和主要的执行流程,会比较有助于我们理解、调试和优化Kafka MirrorMaker。
从Kafka 0.9版本开始引入了new consumer API。相比于普通的old consumer api,new Conumser API有以下主要改变:
Kakfa MirrorMaker同时提供了对新旧版本的Consumer API的支持。
默认是旧版API,当添加 --new.consumer
参数,MirrorMaker将使用新的Consumer API进行消息消费:
// Create consumers
val mirrorMakerConsumers = if (!useNewConsumer) {//如果用户没有配置使用new consumer,则使用旧的consumer
val customRebalanceListener = {
val customRebalanceListenerClass = options.valueOf(consumerRebalanceListenerOpt)
if (customRebalanceListenerClass != null) {
val rebalanceListenerArgs = options.valueOf(rebalanceListenerArgsOpt)
if (rebalanceListenerArgs != null) {
Some(CoreUtils.createObject[ConsumerRebalanceListener](customRebalanceListenerClass, rebalanceListenerArgs))
} else {
Some(CoreUtils.createObject[ConsumerRebalanceListener](customRebalanceListenerClass))
}
} else {
None
}
}
if (customRebalanceListener.exists(!_.isInstanceOf[ConsumerRebalanceListener]))
throw new IllegalArgumentException("The rebalance listener should be an instance of kafka.consumer.ConsumerRebalanceListener")
createOldConsumers(//创建旧的consumer
numStreams,
options.valueOf(consumerConfigOpt),
customRebalanceListener,
Option(options.valueOf(whitelistOpt)),
Option(options.valueOf(blacklistOpt)))
} else {//用户指定使用new consumer
val customRebalanceListener = {
val customRebalanceListenerClass = options.valueOf(consumerRebalanceListenerOpt)
if (customRebalanceListenerClass != null) {
val rebalanceListenerArgs = options.valueOf(rebalanceListenerArgsOpt)
if (rebalanceListenerArgs != null) {
Some(CoreUtils.createObject[org.apache.kafka.clients.consumer.ConsumerRebalanceListener](customRebalanceListenerClass, rebalanceListenerArgs))
} else {
Some(CoreUtils.createObject[org.apache.kafka.clients.consumer.ConsumerRebalanceListener](customRebalanceListenerClass))
}
} else {
None
}
}
if (customRebalanceListener.exists(!_.isInstanceOf[org.apache.kafka.clients.consumer.ConsumerRebalanceListener]))
throw new IllegalArgumentException("The rebalance listener should be an instance of" +
"org.apache.kafka.clients.consumer.ConsumerRebalanceListner")
createNewConsumers(//创建new consumer
numStreams,
options.valueOf(consumerConfigOpt),
customRebalanceListener,
Option(options.valueOf(whitelistOpt)))
}
这是我启动Kakfa MirrorMaker 的命令:
nohup ./bin/kafka-mirror-maker.sh --new.consumer --consumer.config config/mirror-consumer.properties --num.streams 40 --producer.config config/mirror-producer.properties --whitelist 'ABTestMsg|AppColdStartMsg|BackPayMsg|WebMsg|GoldOpenMsg|BoCaiMsg' &
mirror-consumer.properties
配置文件如下:
#新版consumer摈弃了对zookeeper的依赖,使用bootstrap.servers告诉consumer kafka server的位置
bootstrap.servers=ip-188-33-33-31.eu-central-1.compute.internal:9092,ip-188-33-33-32.eu-central-1.compute.internal:9092,ip-188-33-33-33.eu-central-1.compute.internal:9092
#如果使用旧版Consumer,则使用zookeeper.connect
#zookeeper.connect=ip-188-33-33-31.eu-central-1.compute.internal:2181,ip-188-33-33-32.eu-central-1.compute.internal:2181,ip-188-33-33-33.eu-central-1.compute.internal:2181
1.compute.internal:2181
#change the default 40000 to 50000
request.timeout.ms=50000
#hange default heartbeat interval from 3000 to 15000
heartbeat.interval.ms=30000
#change default session timeout from 30000 to 40000
session.timeout.ms=40000
#consumer group id
group.id=africaBetMirrorGroupTest
partition.assignment.strategy=org.apache.kafka.clients.consumer.RoundRobinAssignor
#restrict the max poll records from 2147483647 to 200000
max.poll.records=20000
#set receive buffer from default 64kB to 512kb
receive.buffer.bytes=524288
#set max amount of data per partition to override default 1048576
max.partition.fetch.bytes=5248576
#consumer timeout
#consumer.timeout.ms=5000
mirror-producer.properties的配置文件如下:
bootstrap.servers=10.120.241.146:9092,10.120.241.82:9092,10.120.241.110:9092
# name of the partitioner class for partitioning events; default partition spreads data randomly
#partitioner.class=
# specifies whether the messages are sent asynchronously (async) or synchronously (sync)
producer.type=sync
# specify the compression codec for all data generated: none, gzip, snappy, lz4.
# the old config values work as well: 0, 1, 2, 3 for none, gzip, snappy, lz4, respectively
compression.codec=none
# message encoder
serializer.class=kafka.serializer.DefaultEncoder
同时,我使用 kafka-consumer-groups.sh
循环监控消费延迟:
bin/kafka-consumer-groups.sh --bootstrap-server ip-188-33-33-31.eu-central-1.compute.internal:9092,ip-188-33-33-32.eu-central-1.compute.internal:9092,ip-188-33-33-33.eu-central-1.compute.internal:9092 --describe --group africaBetMirrorGroupTest --new-consumer
当我们使用new KafkaConsumer进行消息消费,要想通过kafka-consumer-groups.sh获取整个group的offset、lag延迟信息,也必须加上–new-consumer,告知kafka-consumer-groups.sh,这个group的消费者使用的是new kafka consumer,即group中所有consumer的信息保存在了Kafka上的一个名字叫做 __consumer_offsets
的特殊topic上,而不是保存在zookeeper上。我在使用kafka-consumer-groups.sh的时候就不知道还需要添加 --new-consumer
,结果我启动了MirrorMaker以后,感觉消息在消费,但是就是在zookeeper的/consumer/ids/上找不到group的任何信息。后来在stack overflow上问了别人才知道。
在我的另外一篇博客 《Kafka为Consumer分派分区:RangeAssignor和RoundRobinAssignor》中,介绍了Kafka内置的分区分派策略:RangeAssignor和RoundRobinAssignor。由于RangeAssignor是早期版本的Kafka的唯一的分区分派策略,因此,默认不配置的情况下,Kafka使用RangeAssignor进行分区分派,但是,在MirrorMaker的使用场景下,RoundRobinAssignor更有利于均匀的分区分派。甚至在 KAFKA-3831中有人建议直接将MirrorMaker的默认分区分派策略改为RoundRobinAssignor。那么,它们到底有什么区别呢?我们先来看两种策略下的分区分派结果。在我的实验场景下,有6个topic: ABTestMsg|AppColdStartMsg|BackPayMsg|WebMsg|GoldOpenMsg|BoCaiMsg
,每个topic有两个分区。由于MirrorMaker所在的服务器性能良好,我设置 --num.streams 40
,即单台MirrorMaker会用40个线程,创建40个独立的Consumer进行消息消费,两个MirrorMaker加起来80个线程,80个并行Consumer。由于总共只有 6 * 2=12
个TopicPartition,因此最多也只有12个Consumer会被分派到分区,其余Consumer空闲。
我们来看基于RangeAssignor分派策略,运行kafka-consumer-groups.sh观察到的分区分派的结果:
TOPIC PARTITION CURRENT-OFFSET LOG-END-OFFSET LAG CONSUMER-ID HOST CLIENT-ID
ABTestMsg 0 780000 820038 49938 africaBetMirrorGroupTest-4-cf330e66-1319-4925-9605-46545df13453/114.113.198.126 africaBetMirrorGroupTest-0
ABTestMsg 1 774988 820038 55000 africaBetMirrorGroupTest-19-c77523e7-7b87-472b-9a26-cd902888944d/114.113.198.126 africaBetMirrorGroupTest-1
AppColdStartMsg 0 774000 820039 55938 africaBetMirrorGroupTest-19-674d8ad4-39d2-40cc-ae97-f4be9c1bb154/114.113.198.126 africaBetMirrorGroupTest-0
AppColdStartMsg 1 774100 820045 56038 africaBetMirrorGroupTest-15-91c67bf8-0c1c-42ac-97f0-5369794c2d1b/114.113.198.126 africaBetMirrorGroupTest-1
BackPayMsg 0 780000 820038 49938 africaBetMirrorGroupTest-4-cf330e66-1319-4925-9605-46545df13453/114.113.198.126 africaBetMirrorGroupTest-0
BackPayMsg 1 774988 820038 55000 africaBetMirrorGroupTest-19-c77523e7-7b87-472b-9a26-cd902888944d/114.113.198.126 africaBetMirrorGroupTest-1
WebMsg 0 774000 820039 55938 africaBetMirrorGroupTest-19-674d8ad4-39d2-40cc-ae97-f4be9c1bb154/114.113.198.126 africaBetMirrorGroupTest-0
WebMsg 1 774100 820045 56038 africaBetMirrorGroupTest-15-91c67bf8-0c1c-42ac-97f0-5369794c2d1b/114.113.198.126 africaBetMirrorGroupTest-1
GoldOpenMsg 0 780000 820038 49938 africaBetMirrorGroupTest-4-cf330e66-1319-4925-9605-46545df13453/114.113.198.126 africaBetMirrorGroupTest-0
GoldOpenMsg 1 774988 820038 55000 africaBetMirrorGroupTest-19-c77523e7-7b87-472b-9a26-cd902888944d/114.113.198.126 africaBetMirrorGroupTest-1
BoCaiMsg 0 774000 820039 55938 africaBetMirrorGroupTest-19-674d8ad4-39d2-40cc-ae97-f4be9c1bb154/114.113.198.126 africaBetMirrorGroupTest-0
BoCaiMsg 1 774100 820045 56038 africaBetMirrorGroupTest-15-91c67bf8-0c1c-42ac-97f0-5369794c2d1b/114.113.198.126 africaBetMirrorGroupTest-1
- - - - - africaBetMirrorGroupTest-6-ae373364-2ae2-42b8-8a74-683557e315bf/114.113.198.126 africaBetMirrorGroupTest-6
- - - - - africaBetMirrorGroupTest-9-0e346b46-1a2c-46a2-a2da-d977402f5c5d/114.113.198.126 africaBetMirrorGroupTest-9
- - - - - africaBetMirrorGroupTest-7-f0ae9f31-33e6-4ddd-beac-236fb7cf20d5/114.113.198.126 africaBetMirrorGroupTest-7
- - - - - africaBetMirrorGroupTest-7-e2a9e905-57c1-40a6-a7f3-4aefd4f1a30a/114.113.198.126 africaBetMirrorGroupTest-7
- - - - - africaBetMirrorGroupTest-8-480a2ef5-907c-48ed-be1f-33450903ec72/114.113.198.126 africaBetMirrorGroupTest-8
- - - - - africaBetMirrorGroupTest-8-4206bc08-58a5-488a-b756-672fb4eee6e0/114.113.198.126 africaBetMirrorGroupTest-8
.....后续更多空闲consumer省略不显示
当没有在 mirror-consumer.properties
中配置分区分派策略,即使用默认的RangeAssignor的时候,我们发现,尽管我们每一个MirrorMaker有40个Consumer,整个Group中有80个Consumer,但是,一共 6 * 2 = 12
个TopicPartition竟然全部聚集在2-3个Consumer上,显然,这完全浪费了并行特性,被分配到一个consumer上的多个TopicPartition只能串行消费。
因此,通过 partition.assignment.strategy=org.apache.kafka.clients.consumer.RoundRobinAssignor
显式指定分区分派策略为RoundRobinAssignor,重启MirrorMaker,重新通过 kafka-consumer-groups.sh
命令观察分区分派和消费延迟结果:
TOPIC PARTITION CURRENT-OFFSET LOG-END-OFFSET LAG CONSUMER-ID HOST CLIENT-ID
ABTestMsg 0 819079 820038 959 africaBetMirrorGroupTest-4-cf330e66-1319-4925-9605-46545df13453/114.113.198.126 africaBetMirrorGroupTest-1
ABTestMsg 1 818373 820038 1665 africaBetMirrorGroupTest-19-c77523e7-7b87-472b-9a26-cd902888944d/114.113.198.126 africaBetMirrorGroupTest-5
AppColdStartMsg 0 818700 818907 1338 africaBetMirrorGroupTest-19-674d8ad4-39d2-40cc-ae97-f4be9c1bb154/114.113.198.126 africaBetMirrorGroupTest-20
AppColdStartMsg 1 818901 820045 1132 africaBetMirrorGroupTest-15-91c67bf8-0c1c-42ac-97f0-5369794c2d1b/114.113.198.126 africaBetMirrorGroupTest-18
BackPayMsg 0 819032 820038 959 africaBetMirrorGroupTest-4-cf330e66-1319-4925-9605-46545df13453/114.113.198.126 africaBetMirrorGroupTest-5
BackPayMsg 1 818343 820038 1638 africaBetMirrorGroupTest-19-c77523e7-7b87-472b-9a26-cd902888944d/114.113.198.126 africaBetMirrorGroupTest-8
WebMsg 0 818710 818907 1328 africaBetMirrorGroupTest-19-674d8ad4-39d2-40cc-ae97-f4be9c1bb154/114.113.198.126 africaBetMirrorGroupTest-7
WebMsg 1 818921 820045 1134 africaBetMirrorGroupTest-15-91c67bf8-0c1c-42ac-97f0-5369794c2d1b/114.113.198.126 africaBetMirrorGroupTest-9
GoldOpenMsg 0 819032 820038 959 africaBetMirrorGroupTest-4-cf330e66-1319-4925-9605-46545df13453/114.113.198.126 africaBetMirrorGroupTest-12
GoldOpenMsg 1 818343 820038 1638 africaBetMirrorGroupTest-19-c77523e7-7b87-472b-9a26-cd902888944d/114.113.198.126 africaBetMirrorGroupTest-14
BoCaiMsg 0 818710 818907 1322 africaBetMirrorGroupTest-19-674d8ad4-39d2-40cc-ae97-f4be9c1bb154/114.113.198.126 africaBetMirrorGroupTest-14
BoCaiMsg 1 818921 820045 1189 africaBetMirrorGroupTest-15-91c67bf8-0c1c-42ac-97f0-5369794c2d1b/114.113.198.126 africaBetMirrorGroupTest-117
- - - - - africaBetMirrorGroupTest-6-ae373364-2ae2-42b8-8a74-683557e315bf/114.113.198.126 africaBetMirrorGroupTest-6
- - - - - africaBetMirrorGroupTest-9-0e346b46-1a2c-46a2-a2da-d977402f5c5d/114.113.198.126 africaBetMirrorGroupTest-9
- - - - - africaBetMirrorGroupTest-7-f0ae9f31-33e6-4ddd-beac-236fb7cf20d5/114.113.198.126 africaBetMirrorGroupTest-7
- - - - - africaBetMirrorGroupTest-7-e2a9e905-57c1-40a6-a7f3-4aefd4f1a30a/114.113.198.126 africaBetMirrorGroupTest-7
- - - - - africaBetMirrorGroupTest-8-480a2ef5-907c-48ed-be1f-33450903ec72/114.113.198.126 africaBetMirrorGroupTest-8
- - - - - africaBetMirrorGroupTest-8-4206bc08-58a5-488a-b756-672fb4eee6e0/114.113.198.126 africaBetMirrorGroupTest-8
.....后续更多空闲consumer省略不显示
对比RangeAssingor,消息延迟明显减轻,而且,12个TopicPartition被均匀分配到了不同的consumer上,即单个Consumer只负责一个TopicPartition的消息消费,不同的TopicPartition之间实现了完全并行。
之所以出现以上不同,原因在于两个分区分派方式的策略不同:
对所有的Consumer进行排序,排序后的结果为Consumer-0,Consumer-1,Consumer-2 ....Consumer-79
对ABTestMsg进行分区分派:
ABTestMsg-0分配给Consumer-0
ABTestMsg-1分配各Consumer-1
对AppColdStartMsg进行分区分派:
AppColdStartMsg-0分配各Consumer-0
AppColdStartMsg-1分配各Consumer-1
#后续TopicParition的分派以此类推
可见,RangeAssingor 会导致多个TopicPartition被分派在少量分区上面。
- RoundRobinAssignor:与RangeAssignor最大的区别,是不再逐个Topic进行分区分派,而是先将Group中的所有TopicPartition平铺展开,再一次性对他们进行一轮分区分派。
将Group中的所有TopicPartition展开,展开结果为:
ABTestMsg-0,ABTestMsg-1,AppColdStartMsg-0,AppColdStartMsg-1,BackPayMsg-0,BackPayMsg-1,WebMsg-0,WebMsg-1,GoldOpenMsg-0,GoldOpenMsg-1,BoCaiMsg-0,BoCaiMsg-1
对所有的Consumer进行排序,排序后的结果为 Consumer-0
, Consumer-1
, Consumer-2
, Consumer-79
。
开始讲平铺的TopicPartition进行分区分派
ABTestMsg-0分配给Consumer-0
ABTestMsg-1分配给Consumer-1
AppColdStartMsg-0分配给Consumer-2
AppColdStartMsg-1分配给Consumer-3
BackPayMsg-0分配给Consumer-4
BackPayMsg-1分配给Consumer-5
#后续TopicParition的分派以此类推
由此可见,RoundRobinAssignor平铺式的分区分派算法是让我们的Kafka MirrorMaker能够无重叠地将TopicParition分派给Consumer的原因。
网络带宽本身也会限制Kafka Mirror的吞吐量。进行压测的时候,我分别在我们的在线环境和测试环境分别运行Kafka MirrorMaker,均选择两台服务器运行MirrorMaker,但是在线环境是实体机环境,单台机器通过SCP方式拷贝Source Cluster上的大文件,平均吞吐量是600KB-1.5MB之间,但是测试环境的机器是同一个host主机上的多台虚拟机,SCP吞吐量是100KB以下。经过测试,测试环境消息积压会逐渐增多,在线环境持续积压,但是积压一直保持稳定。这种稳定积压是由于每次 poll()
的间隙新产生的消息量,属于正常现象。
通过Kafka MirrorMaker运行时指定的consumer配置文件(在我的环境中为 $MIRROR_HOME/config/mirror-consumer.properties
)来配置consumer。其中,通过以下配置,可以控制单次 poll()
的消息体量(数量和总体大小)
max.poll.records
:单次 poll()
操作最多消费的消息总量,这里说的poll是单个consumer而言的。无论过大过小,都会发生问题:
heartbeat.interval.ms
)没有收到对应Consumer的心跳,则GroupCoordinator会判定这个Server已经挂掉,因此将这个Consumer负责的partition分派给其它Consumer,即触发rebalance。rebalance操作的影响范围是整个Group,即Group中所有的Consumer全部暂停消费直到Rebalance完成。而且,TopicPartition越长,这个过程会越长。其实,一个正常消费的环境,应该是任何时候都不应该发生rebalance的(一个新的Consumer的正常加入也会引起Rebalance,这种情况除外)。虽然Kafka本身是非常稳定的,但是还是应该尽量避免rebalance的发生。在某些极端情况下触发一些bug,rebalance可能永远停不下来了。。。如果单次 max.poll.records
消费太多消息,这些消息produce到Target Cluster的时间可能会较长,从而可能触发Rebalance。在不稳定的网络环境下,应该增加部分超时时间配置,如 request.timeout.ms
或者 session.timeout.ms
,一方面可以避免频繁的超时导致大量不必要的重试操作,同时,通过增加如上文所讲,通过增加 heartbeat.interval.ms
时间,可以避免不必要的rebalance操作。当然,在网络环境良好的情况下,上述配置可以适当减小以增加Kafka Server对MirrorMaker出现异常情况下的更加及时的响应。
总之,Kafka MirrorMaker作为跨数据中心的Kafka数据同步方案,绝对无法允许数据丢失以及数据的传输速度低于生产速度导致数据越积累越多。因此,唯有进行充分的压测和精准的性能调优,才能综合网络环境、服务器性能,将Kafka MirrorMaker的性能发挥到最大。