Kafka数据可靠性深度解读
介绍
Kafka
起初是由LinkedIn
公司开发的一个分布式的消息系统,后成为Apache
的一部分,它使用Scala
编写,以可水平扩展和高吞吐率而被广泛使用。目前越来越多的开源分布式处理系统如Cloudera
、Apache Storm
、Spark
等都支持与Kafka
集成。
1. 概述
Kafka
与传统消息系统相比,有以下不同:
- 它被设计为一个分布式系统,易于向外扩展;
- 它同时为发布和订阅提供高吞吐量;
- 它支持多订阅者,当失败时能自动平衡消费者;
- 它将消息持久化到磁盘,因此可用于批量消费,例如
ETL
以及实时应用程序;
Kafka
凭借着自身的优势,越来越受到互联网企业的青睐,唯品会也采用Kafka
作为其内部核心消息引擎之一。Kafka
作为一个商业级消息中间件,消息可靠性的重要性可想而知。如何确保消息的精确传输?如何确保消息的准确存储?如何确保消息的正确消费?这些都是需要考虑的问题。本文首先从Kafka
的架构着手,先了解下Kafka
的基本原理,然后通过对Kafka
的存储机制、复制原理、同步原理、可靠性和持久性保证等等一步步对其可靠性进行分析,最后通过benchmark
来增强对Kafka高可靠性的认知。
2. Kafka体系架构
如上图所示,一个典型的Kafka
体系架构包括若干Producer
(可以是服务器日志,业务数据,页面前端产生的page view
等等),若干broker
(Kafka
支持水平扩展,一般broker
数量越多,集群吞吐率越高),若干Consumer (Group)
,以及一个Zookeeper
集群。Kafka
通过Zookeeper
管理集群配置,选举leader
,以及在consumer group
发生变化时进行rebalance
。Producer
使用push
(推)模式将消息发布到broker
,Consumer
使用pull
(拉)模式从broker
订阅并消费消息。
名词解释:
名称 | 解释 |
---|---|
Broker |
消息中间件处理节点,一个Kafka 节点就是一个broker ,一个或者多个Broker 可以组成一个Kafka 集群 |
Topic |
Kafka 根据topic 对消息进行归类,发布到Kafka 集群的每条消息都需要指定一个topic |
Producer |
消息生产者,向Broker 发送消息的客户端 |
Consumer |
消息消费者,从Broker 读取消息的客户端 |
ConsumerGroup |
每个Consumer 属于一个特定的Consumer Group ,一条消息可以发送到多个不同的Consumer Group ,但是一个Consumer Group 中只能有一个Consumer 能够消费该消息 |
Partition |
物理上的概念,一个topic 可以分为多个partition ,每个partition 内部是有序的 |
2.1 Topic
& Partition
一个topic
可以认为一个一类消息,每个topic
将被分成多个partition
,每个partition
在存储层面是append log
文件。任何发布到此partition
的消息都会被追加到log
文件的尾部,每条消息在文件中的位置称为offset
(偏移量),offset
为一个long
型的数字,它唯一标记一条消息。每条消息都被append
到partition
中,是顺序写磁盘,因此效率非常高(经验证,顺序写磁盘效率比随机写内存还要高,这是Kafka
高吞吐率的一个很重要的保证)。
每一条消息被发送到broker
中,会根据partition
规则选择被存储到哪一个partition
。如果partition
规则设置的合理,所有消息可以均匀分布到不同的partition
里,这样就实现了水平扩展。(如果一个topic
对应一个文件,那这个文件所在的机器I/O
将会成为这个topic
的性能瓶颈,而partition
解决了这个问题)。在创建topic
时可以在$KAFKA_HOME/config/server.properties
中指定这个partition
的数量(如下所示),当然可以在topic
创建之后去修改partition
的数量。
1
|
# The default number of log partitions per topic. More partitions allow greater
|
在发送一条消息时,可以指定这个消息的key
,producer
根据这个key
和partition
机制来判断这个消息发送到哪个partition
。partition
机制可以通过指定producer
的partition.class
这一参数来指定,该class
必须实现kafka.producer.Partitioner
接口。有关Topic
与Partition
的更多细节,可以参考下面的“Kafka文件存储机制”这一节。
3. 高可靠性存储分析
Kafka
的高可靠性的保障来源于其健壮的副本(replication
)策略。通过调节其副本相关参数,可以使得Kafka
在性能和可靠性之间运转的游刃有余。Kafka
从0.8.x
版本开始提供partition
级别的复制,replication
的数量可以在$KAFKA_HOME/config/server.properties
中配置(default.replication.refactor
)。
这里先从Kafka
文件存储机制入手,从最底层了解Kafka
的存储细节,进而对其的存储有个微观的认知。之后通过Kafka
复制原理和同步方式来阐述宏观层面的概念。最后从ISR
,HW
,leader
选举以及数据可靠性和持久性保证等等各个维度来丰富对Kafka
相关知识点的认知。
3.1 Kafka文件存储机制
Kafka
中消息是以topic
进行分类的,生产者通过topic
向Kafka broker
发送消息,消费者通过topic
读取数据。然而topic
在物理层面又能以partition
为分组,一个topic
可以分成若干个partition
,那么topic
以及partition
又是怎么存储的呢?partition
还可以细分为segment
,一个partition
物理上由多个segment
组成,那么这些segment
又是什么呢?下面我们来一一揭晓。
为了便于说明问题,假设这里只有一个Kafka
集群,且这个集群只有一个Kafka broker
,即只有一台物理机。在这个Kafka broker
中配置($KAFKA_HOME/config/server.properties
中)log.dirs=/tmp/kafka-logs
,以此来设置Kafka
消息文件存储目录,与此同时创建一个topic:topic_vms_test
,partition
的数量为4
($KAFKA_HOME/bin/kafka-topics.sh --create --zookeeper localhost:2181 --partitions 4 --topic topic_vms_test --replication-factor 4
)。那么我们此时可以在/tmp/kafka-logs
目录中可以看到生成了4
个目录:
1
|
drwxr-xr-x 2 root root 4096 Apr 10 16:10 topic_vms_test-0
|
在Kafka
文件存储中,同一个topic
下有多个不同的partition
,每个partiton
为一个目录,partition
的名称规则为:topic
名称+有序序号,第一个序号从0
开始计,最大的序号为partition
数量减1
,partition
是实际物理上的概念,而topic
是逻辑上的概念。
上面提到partition
还可以细分为segment
,这个segment
又是什么?如果就以partition
为最小存储单位,我们可以想象当Kafka producer
不断发送消息,必然会引起partition
文件的无限扩张,这样对于消息文件的维护以及已经被消费的消息的清理带来严重的影响,所以这里以segment
为单位又将partition
细分。每个partition
(目录)相当于一个巨型文件被平均分配到多个大小相等的segment
(段)数据文件中(每个segment
文件中消息数量不一定相等)这种特性也方便old segment
的删除,即方便已被消费的消息的清理,提高磁盘的利用率。每个partition
只需要支持顺序读写就行,segment
的文件生命周期由服务端配置参数(log.segment.bytes
,log.roll.ms
,log.roll.hours
等若干参数)决定。segment
文件由两部分组成,分别为.index
文件和.log
文件,分别表示segment
索引文件和数据文件。这两个文件的命令规则为:partition
全局的第一个segment
从0
开始,后续每个segment
文件名为上一个segment
文件最后一条消息的offset
值,数值大小为64
位,20
位数字字符长度,没有数字用0
填充,如下:
1
|
00000000000000000000.index
|
以上面的segment
文件为例,展示出segment
:00000000000000170410
的.index
文件和.log
文件的对应的关系,如下图:
如上图,.index
索引文件存储大量的元数据,.log
数据文件存储大量的消息,索引文件中的元数据指向对应数据文件中message
的物理偏移地址。其中以.index
索引文件中的元数据[3, 348]
为例,在.log
数据文件表示第3
个消息,即在全局partition
中表示170410 + 3 = 170413
个消息,该消息的物理偏移地址为348
。
那么如何从partition
中通过offset
查找message
呢?以上图为例,读取offset = 170418
的消息,首先查找segment
文件,其中00000000000000000000.index
为最开始的文件,第二个文件为00000000000000170410.index
(起始偏移为170410 + 1 = 170411
),而第三个文件为00000000000000239430.index
(起始偏移为239430 + 1 = 239431
),所以这个offset = 170418
就落到了第二个文件之中,其他后续文件可以依次类推。其实以偏移量命名并排列这些文件,然后根据二分查找法就可以快速定位到具体文件位置。其次根据00000000000000170410.index
文件中的[8,1325]
定位到00000000000000170410.log
文件中的1325
的位置进行读取。
要是读取offset = 170418
的消息,从00000000000000170410.log
文件中的1325
的位置进行读取,那么怎么知道何时读完本条消息,否则就读到下一条消息的内容了?这个就需要联系到消息的物理结构了,消息都具有固定的物理结构,包括:offset(8 Bytes)
、消息体的大小(4 Bytes)
、crc32(4 Bytes)
、magic(1 Byte)
、attributes(1 Byte)
、key length(4 Bytes)
、key(K Bytes)
、payload(N Bytes)
等等字段,可以确定一条消息的大小,即读取到哪里截止。
3.2 复制原理和同步方式
Kafka
中topic
的每个partition
有一个预写式的日志文件,虽然partition
可以继续细分为若干个segment
文件,但是对于上层应用来说可以将partition
看成最小的存储单元(一个有多个segment
文件拼接的“巨型”文件),每个partition
都由一些列有序的、不可变的消息组成,这些消息被连续的追加到partition
中。
上图中有两个新名词:HW
和LEO
。这里先介绍下LEO
,LogEndOffset
的缩写,表示每个partition
的log
最后一条Message
的位置。HW
是HighWatermark
的缩写,是指consumer
能够看到的此partition
的位置,这个涉及到多副本的概念,这里先提及一下,下节再详表。
言归正传,为了提高消息的可靠性,Kafka
每个topic
的partition
有N
个副本(replicas
),其中N(大于等于1)
是topic
的复制因子(replica fator
)的个数。Kafka
通过多副本机制实现故障自动转移,当Kafka
集群中一个broker
失效情况下仍然保证服务可用。在Kafka
中发生复制时确保partition
的日志能有序地写到其他节点上,N
个replicas
中,其中一个replica
为leader
,其他都为follower
, leader
处理partition
的所有读写请求,与此同时,follower
会被动定期地去复制leader
上的数据。
如下图所示,Kafka
集群中有4
个broker
, 某topic
有3
个partition
,且复制因子即副本个数也为3
:
Kafka
提供了数据复制算法保证,如果leader
发生故障或挂掉,一个新leader
被选举并被接受客户端的消息成功写入。Kafka
确保从同步副本列表中选举一个副本为leader
,或者说follower
追赶leader
数据。leader
负责维护和跟踪ISR
(In-Sync Replicas
的缩写,表示副本同步队列,具体可参考下节)中所有follower
滞后的状态。当producer
发送一条消息到broker
后,leader
写入消息并复制到所有follower
。消息提交之后才被成功复制到所有的同步副本。消息复制延迟受最慢的follower
限制,重要的是快速检测慢副本,如果follower
“落后”太多或者失效,leader
将会把它从ISR
中删除。
3.3 ISR
上节我们涉及到ISR (In-Sync Replicas)
,这个是指副本同步队列。副本数对Kafka
的吞吐率是有一定的影响,但极大的增强了可用性。默认情况下Kafka
的replica
数量为1
,即每个partition
都有一个唯一的leader
,为了确保消息的可靠性,通常应用中将其值(由broker
的参数offsets.topic.replication.factor
指定)大小设置为大于1
,比如3
。 所有的副本(replicas
)统称为Assigned Replicas
,即AR
。ISR
是AR
中的一个子集,由leader
维护ISR
列表,follower
从leader
同步数据有一些延迟(包括延迟时间replica.lag.time.max.ms
和延迟条数replica.lag.max.messages
两个维度, 当前最新的版本0.10.x
中只支持replica.lag.time.max.ms
这个维度),任意一个超过阈值都会把follower
剔除出ISR
, 存入OSR(Outof-Sync Replicas)
列表,新加入的follower
也会先存放在OSR
中。AR = ISR + OSR
。
Kafka 0.10.x
版本后移除了replica.lag.max.messages
参数,只保留了replica.lag.time.max.ms
作为ISR
中副本管理的参数。为什么这样做呢?replica.lag.max.messages
表示当前某个副本落后leaeder
的消息数量超过了这个参数的值,那么leader
就会把follower
从ISR
中删除。假设设置replica.lag.max.messages = 4
,那么如果producer
一次传送至broker
的消息数量都小于4
条时,因为在leader
接受到producer
发送的消息之后而follower
副本开始拉取这些消息之前,follower
落后leader
的消息数不会超过4
条消息,故此没有follower
移出ISR
,所以这时候replica.lag.max.message
的设置似乎是合理的。
但是producer
发起瞬时高峰流量,producer
一次发送的消息超过4
条时,也就是超过replica.lag.max.messages
,此时follower
都会被认为是与leader
副本不同步了,从而被踢出了ISR
。但实际上这些follower
都是存活状态的且没有性能问题。那么在之后追上leader
,并被重新加入了ISR
。于是就会出现它们不断地剔出ISR
然后重新回归ISR
,这无疑增加了无谓的性能损耗。而且这个参数是broker
全局的。设置太大了,影响真正“落后”follower
的移除;设置的太小了,导致follower
的频繁进出。无法给定一个合适的replica.lag.max.messages
的值,故此,新版本的Kafka
移除了这个参数。
注:
ISR
中包括:leader
和follower
。
上面一节还涉及到一个概念,即HW
。HW
俗称高水位,HighWatermark
的缩写,取一个partition
对应的ISR
中最小的LEO
作为HW
,consumer
最多只能消费到HW
所在的位置。另外每个replica
都有HW
,leader
和follower
各自负责更新自己的HW
的状态。对于leader
新写入的消息,consumer
不能立刻消费,leader
会等待该消息被所有ISR
中的replicas
同步后更新HW
,此时消息才能被consumer
消费。这样就保证了如果leader
所在的broker
失效,该消息仍然可以从新选举的leader
中获取。对于来自内部broker
的读取请求,没有HW
的限制。
下图详细的说明了当producer
生产消息至broker
后,ISR
以及HW
和LEO
的流转过程:
由此可见,Kafka
的复制机制既不是完全的同步复制,也不是单纯的异步复制。事实上,同步复制要求所有能工作的follower
都复制完,这条消息才会被commit
,这种复制方式极大的影响了吞吐率。而异步复制方式下,follower
异步的从leader
复制数据,数据只要被leader
写入log
就被认为已经commit
,这种情况下如果follower
都还没有复制完,落后于leader
时,突然leader
宕机,则会丢失数据。而Kafka
的这种使用ISR
的方式则很好的均衡了确保数据不丢失以及吞吐率。
Kafka
的ISR
的管理最终都会反馈到Zookeeper
节点上。具体位置为:/brokers/topics/[topic]/partitions/[partition]/state
。目前有两个地方会对这个Zookeeper
的节点进行维护:
Controller
来维护:Kafka
集群中的其中一个Broker
会被选举为Controller
,主要负责Partition
管理和副本状态管理,也会执行类似于重分配partition
之类的管理任务。在符合某些特定条件下,Controller
下的LeaderSelector
会选举新的leader
,ISR
和新的leader_epoch
及controller_epoch
写入Zookeeper
的相关节点中。同时发起LeaderAndIsrRequest
通知所有的replicas
。leader
来维护:leader
有单独的线程定期检测ISR
中follower
是否脱离ISR
,如果发现ISR
变化,则会将新的ISR
的信息返回到Zookeeper
的相关节点中。
3.4 数据可靠性和持久性保证
当producer
向leader
发送数据时,可以通过request.required.acks
参数来设置数据可靠性的级别:
1
(默认):这意味着producer
在ISR
中的leader
已成功收到的数据并得到确认后发送下一条message
。如果leader
宕机了,则会丢失数据。0
:这意味着producer
无需等待来自broker
的确认而继续发送下一批消息。这种情况下数据传输效率最高,但是数据可靠性确是最低的。-1
:producer
需要等待ISR
中的所有follower
都确认接收到数据后才算一次发送完成,可靠性最高。但是这样也不能保证数据不丢失,比如当ISR
中只有leader
时(前面ISR
那一节讲到,ISR
中的成员由于某些情况会增加也会减少,最少就只剩一个leader
),这样就变成了acks=1
的情况。
如果要提高数据的可靠性,在设置request.required.acks=-1
的同时,也要min.insync.replicas
这个参数(可以在broker
或者topic
层面进行设置)的配合,这样才能发挥最大的功效。min.insync.replicas
这个参数设定ISR
中的最小副本数是多少,默认值为1
,当且仅当request.required.acks
参数设置为-1
时,此参数才生效。如果ISR
中的副本数少于min.insync.replicas
配置的数量时,客户端会返回异常:org.apache.kafka.common.errors.NotEnoughReplicasExceptoin: Messages are rejected since there are fewer in-sync replicas than required。
接下来对acks=1
和-1
的两种情况进行详细分析:
-
request.required.acks=1
producer
发送数据到leader
,leader
写本地日志成功,返回客户端成功;此时ISR
中的副本还没有来得及拉取该消息,leader
就宕机了,那么此次发送的消息就会丢失。 -
request.required.acks=-1
同步(Kafka
默认为同步,即producer.type=sync
)的发送模式,replication.factor>=2
且min.insync.replicas>=2
的情况下,不会丢失数据。有两种典型情况。acks=-1
的情况下(如无特殊说明,以下acks
都表示为参数request.required.acks
),数据发送到leader
,ISR
的follower
全部完成数据同步后,leader
此时挂掉,那么会选举出新的leader
,数据不会丢失。
acks=-1
的情况下,数据发送到leader
后,部分ISR
的副本同步,leader
此时挂掉。比如follower1
和follower2
都有可能变成新的leader
, producer
端会得到返回异常,producer
端会重新发送数据,数据可能会重复。
当然上图中如果在leader crash
的时候,follower2
还没有同步到任何数据,而且follower2
被选举为新的leader
的话,这样消息就不会重复。
注:
Kafka
只处理fail/recover
问题,不处理Byzantine
问题。
3.5 关于HW
的进一步探讨
考虑上图(即acks=-1
,部分ISR
副本同步)中的另一种情况,如果在Leader
挂掉的时候,follower1
同步了消息4,5
,follower2
同步了消息4
,与此同时follower2
被选举为leader
,那么此时follower1
中的多出的消息5
该做如何处理呢?这里就需要HW
的协同配合了。如前所述,一个partition
中的ISR
列表中,leader
的HW
是所有ISR
列表里副本中最小的那个的LEO
。类似于木桶原理,水位取决于最低那块短板。
如上图,某个topic
的某partition
有三个副本,分别为A、B、C
。A
作为leader
肯定是LEO
最高,B
紧随其后,C
机器由于配置比较低,网络比较差,故而同步最慢。这个时候A
机器宕机,这时候如果B
成为leader
,假如没有HW
,在A
重新恢复之后会做同(makeFollower
)操作,在宕机时log
文件之后直接做追加操作,而假如B
的LEO
已经达到了A
的LEO
,会产生数据不一致的情况,所以使用HW
来避免这种情况。A
在做同步操作的时候,先将log
文件截断到之前自己的HW
的位置,即3
,之后再从B
中拉取消息进行同步。
如果失败的follower
恢复过来,它首先将自己的log
文件截断到上次checkpointed
时刻的HW
的位置,之后再从leader
中同步消息。leader
挂掉会重新选举,新的leader
会发送“指令”让其余的follower
截断至自身的HW
的位置然后再拉取新的消息。当ISR
中的某个副本的LEO
不一致时,如果此时leader
挂掉,选举新的leader
时并不是按照LEO
的高低进行选举,而是按照ISR
中的顺序选举。
3.6 Leader选举
一条消息只有被ISR
中的所有follower
都从leader
复制过去才会被认为已提交。这样就避免了部分数据被写进了leader
,还没来得及被任何follower
复制就宕机了,而造成数据丢失。而对于producer
而言,它可以选择是否等待消息commit
,这可以通过request.required.acks
来设置。这种机制确保了只要ISR
中有一个或者以上的follower
,一条被commit
的消息就不会丢失。
有一个很重要的问题是当leader
宕机了,怎样在follower
中选举出新的leader
,因为follower
可能落后很多或者直接crash
了,所以必须确保选择“最新”的follower
作为新的leader
。一个基本的原则就是,如果leader
不在了,新的leader
必须拥有原来的leader commit
的所有消息。这就需要做一个折中,如果leader
在表明一个消息被commit
前等待更多的follower
确认,那么在它挂掉之后就有更多的follower
可以成为新的leader
,但这也会造成吞吐率的下降。
一种非常常用的选举leader
的方式是“少数服从多数”,Kafka
并不是采用这种方式。这种模式下,如果我们2f + 1
个副本,那么在commit
之前必须保证有f+1
个replica
复制完消息,同时为了保证能正确选举出新的leader
,失败的副本数不能超过f
个。这种方式有个很大的优势,系统的延迟取决于最快的几台机器,也就是说比如副本数为3
,那么延迟就取决于最快的那个follower
而不是最慢的那个。
“少数服从多数”的方式也有一些劣势,为了保证leader
选举的正常进行,它所能容忍的失败的follower
数比较少,如果要容忍1
个follower
挂掉,那么至少要3
个以上的副本,如果要容忍2
个follower
挂掉,必须要有5
个以上的副本。也就是说,在生产环境下为了保证较高的容错率,必须要有大量的副本,而大量的副本又会在大数据量下导致性能的急剧下降。这种算法更多用在Zookeeper
这种共享集群配置的系统中而很少在需要大量数据的系统中使用的原因。HDFS
的HA
功能也是基于“少数服从多数”的方式,但是其数据存储并不是采用这样的方式。
实际上,leader
选举的算法非常多,比如Zookeeper
的Zab
、Raft
以及Viewstamped Replication
。而Kafka
所使用的leader
选举算法更像是微软的PacificA
算法。
Kafka
在Zookeeper
中为每一个partition
动态的维护了一个ISR
,这个ISR
里的所有replica
都跟上了leader
,只有ISR
里的成员才能有被选为leader
的可能(unclean.leader.election.enable=false
)。在这种模式下,对于f+1
个副本,一个Kafka topic
能在保证不丢失已经commit
消息的前提下容忍f
个副本的失败,在大多数使用场景下,这种模式是十分有利的。事实上,为了容忍f
个副本的失败,“少数服从多数”的方式和ISR
在commit
前需要等待的副本的数量是一样的,但是ISR
需要的总的副本的个数几乎是“少数服从多数”的方式的一半。
上文提到,在ISR
中至少有一个follower
时,Kafka
可以确保已经commit
的数据不丢失,但如果某一个partition
的所有replica
都挂了,就无法保证数据不丢失了。这种情况下有两种可行的方案:
等待ISR
中任意一个replica
“活”过来,并且选它作为leader
。选择第一个“活”过来的replica
(并不一定是在ISR
中)作为leader
这就需要在可用性和一致性当中作出一个简单的抉择。如果一定要等待ISR
中的replica
“活”过来,那不可用的时间就可能会相对较长。而且如果ISR
中所有的replica
都无法“活”过来了,或者数据丢失了,这个partition
将永远不可用。选择第一个“活”过来的replica
作为leader
,而这个replica
不是ISR
中的replica
,那即使它并不保障已经包含了所有已commit
的消息,它也会成为leader
而作为consumer
的数据源。默认情况下,Kafka
采用第二种策略,即unclean.leader.election.enable=true
,也可以将此参数设置为false
来启用第一种策略。unclean.leader.election.enable
这个参数对于leader
的选举、系统的可用性以及数据的可靠性都有至关重要的影响。下面我们来分析下几种典型的场景。
如果上图所示,假设某个partition
中的副本数为3
,replica-0
, replica-1
, replica-2
分别存放在broker0
, broker1
和broker2
中。AR=(0,1,2)
,ISR=(0,1)
。设置request.required.acks=-1
,min.insync.replicas=2
,unclean.leader.election.enable=false
。这里将broker0
中的副本也称之为broker0
。起初broker0
为leader
,broker1
为follower
。
当ISR
中的replica-0
出现crash
的情况时,broker1
选举为新的leader[ISR=(1)]
,因为受min.insync.replicas=2
影响,write
不能服务,但是read
能继续正常服务。此种情况恢复方案:
尝试恢复(重启)replica-0
,如果能起来,系统正常;如果replica-0
不能恢复,需要将min.insync.replicas
设置为1
,恢复write
功能。当ISR
中的replica-0
出现crash
,紧接着replica-1
也出现了crash
, 此时[ISR=(1),leader=-1]
,不能对外提供服务,此种情况恢复方案:尝试恢复replica-0
和replica-1
,如果都能起来,则系统恢复正常;
如果replica-0
起来,而replica-1
不能起来,这时候仍然不能选出leader
,因为当设置unclean.leader.election.enable=false
时,leader
只能从ISR
中选举,当ISR
中所有副本都失效之后,需要ISR
中最后失效的那个副本能恢复之后才能选举leader
,即replica-0
先失效,replica-1
后失效,需要replica-1
恢复后才能选举leader
。保守的方案建议把unclean.leader.election.enable
设置为true
,但是这样会有丢失数据的情况发生,这样可以恢复read
服务。同样需要将min.insync.replicas
设置为1
,恢复write
功能;replica-1
恢复,replica-0
不能恢复,这个情况上面遇到过,read
服务可用,需要将min.insync.replicas设置为
1,恢复write
功能;replica-0
和replica-1
都不能恢复,这种情况可以参考情形2
.
当ISR
中的replica-0
,replica-1
同时宕机,此时[ISR=(0,1)]
,不能对外提供服务,此种情况恢复方案:尝试恢复replica-0
和replica-1
,当其中任意一个副本恢复正常时,对外可以提供read
服务。直到2
个副本恢复正常,write
功能才能恢复,或者将min.insync.replicas
设置为1
。
3.7 Kafka
的发送模式
Kafka
的发送模式由producer
端的配置参数producer.type
来设置,这个参数指定了在后台线程中消息的发送方式是同步的还是异步的,默认是同步的方式,producer.type=sync
。如果设置成异步的模式,即producer.type=async
,可以是producer
以batch
的形式push
数据,这样会极大的提高broker
的性能,但是这样会增加丢失数据的风险。如果需要确保消息的可靠性,必须要将producer.type
设置为sync
。
对于异步模式,还有4个配套的参数,如下:
Property | Description |
---|---|
queue.buffering.max.ms |
默认值:5000 。启用异步模式时,producer 缓存消息的时间。比如我们设置成1000 时,它会缓存1s 的数据再一次发送出去,这样可以极大的增加broker 吞吐量,但也会造成时效性的降低。 |
queue.buffering.max.messages |
默认值:10000 。启用异步模式时,producer 缓存队列里最大缓存的消息数量,如果超过这个值,producer 就会阻塞或者丢掉消息。 |
queue.enqueue.timeout.ms |
默认值:-1 。当达到上面参数时producer 会阻塞等待的时间。如果设置为0 ,buffer 队列满时producer 不会阻塞,消息直接被丢掉;若设置为-1 ,producer 会被阻塞,不会丢消息。 |
batch.num.messages |
默认值:200 。启用异步模式时,一个batch 缓存的消息数量。达到这个数值时,producer 才会发送消息。(每次批量发送的数量) |
以batch
的方式推送数据可以极大的提高处理效率,kafka producer
可以将消息在内存中累计到一定数量后作为一个batch
发送请求。batch
的数量大小可以通过producer
的参数(batch.num.messages
)控制。通过增加batch
的大小,可以减少网络请求和磁盘IO
的次数,当然具体参数设置需要在效率和时效性方面做一个权衡。在比较新的版本中还有batch.size
这个参数。
4.高可靠性使用分析
4.1消息传输保障
前面已经介绍了Kafka
如何进行有效的存储,以及了解了producer
和consumer
如何工作。接下来讨论的是Kafka
如何确保消息在producer
和consumer
之间传输。有以下三种可能的传输保障(delivery guarantee
):
At most once
: 消息可能会丢,但绝不会重复传输At least once
:消息绝不会丢,但可能会重复传输Exactly once
:每条消息肯定会被传输一次且仅传输一次
Kafka
的消息传输保障机制非常直观。当producer
向broker
发送消息时,一旦这条消息被commit
,由于副本机制(replication
)的存在,它就不会丢失。但是如果producer
发送数据给broker
后,遇到的网络问题而造成通信中断,那producer
就无法判断该条消息是否已经提交(commit
)。虽然Kafka
无法确定网络故障期间发生了什么,但是producer
可以retry
多次,确保消息已经正确传输到broker
中,所以目前Kafka
实现的是at least once
。
consumer
从broker
中读取消息后,可以选择commit
,该操作会在Zookeeper
中存下该consumer
在该partition
下读取的消息的offset
。该consumer
下一次再读该partition
时会从下一条开始读取。如未commit
,下一次读取的开始位置会跟上一次commit
之后的开始位置相同。当然也可以将consumer
设置为autocommit
,即consumer
一旦读取到数据立即自动commit
。如果只讨论这一读取消息的过程,那Kafka
是确保了exactly once
, 但是如果由于前面producer
与broker
之间的某种原因导致消息的重复,那么这里就是at least once
。
考虑这样一种情况,当consumer
读完消息之后先commit
再处理消息,在这种模式下,如果consumer
在commit
后还没来得及处理消息就crash
了,下次重新开始工作后就无法读到刚刚已提交而未处理的消息,这就对应于at most once
了。
读完消息先处理再commit
。这种模式下,如果处理完了消息在commit
之前consumer crash
了,下次重新开始工作时还会处理刚刚未commit
的消息,实际上该消息已经被处理过了,这就对应于at least once
。
要做到exactly once
就需要引入消息去重机制。
4.2 消息去重
如上一节所述,Kafka
在producer
端和consumer
端都会出现消息的重复,这就需要去重处理。Kafka
文档中提及GUID(Globally Unique Identifier)
的概念,通过客户端生成算法得到每个消息的unique id
,同时可映射至broker
上存储的地址,即通过GUID
便可查询提取消息内容,也便于发送方的幂等性保证,需要在broker
上提供此去重处理模块,目前版本尚不支持。
针对GUID
, 如果从客户端的角度去重,那么需要引入集中式缓存,必然会增加依赖复杂度,另外缓存的大小难以界定。
不只是Kafka
, 类似RabbitMQ
以及RocketMQ
这类商业级中间件也只保障at least once
, 且也无法从自身去进行消息去重。所以我们建议业务方根据自身的业务点进行去重,比如业务消息本身具备幂等性,或者借助Redis
等其他产品进行去重处理。
4.3 高可靠性配置
Kafka
提供了很高的数据冗余弹性,对于需要数据高可靠性的场景,我们可以增加数据冗余备份数(replication.factor
),调高最小写入副本数的个数(min.insync.replicas
)等等,但是这样会影响性能。反之,性能提高而可靠性则降低,用户需要自身业务特性在彼此之间做一些权衡性选择。
要保证数据写入到Kafka
是安全的,高可靠的,需要如下的配置:
-
topic
的配置:replication.factor>=3
,即副本数至少是3
个;2<=min.insync.replicas<=replication.factor
-
broker
的配置:leader
的选举条件unclean.leader.election.enable=false
-
producer
的配置:request.required.acks=-1(all)
,producer.type=sync
5. 测试报告
Kafka
在唯品会有着很深的历史渊源,根据唯品会消息中间件团队(VMS
团队)所掌握的资料显示,在VMS
团队运转的Kafka
集群中所支撑的topic
数已接近2000
,每天的请求量也已达百亿级。这里就以Kafka
的高可靠性为基准点来探究几种不同场景下的行为表现,以此来加深对Kafka
的认知,为大家在以后高效的使用Kafka
时提供一份依据。
5.1 测试环境
Kafka broker
用到了4
台机器,分别为broker[0/1/2/3]
配置如下:
1
|
CPU: 24core/2.6GHZ
|
broker
端JVM
参数设置:
1
|
-Xmx8G -Xms8G -server -XX:+UseParNewGC -XX:+UseConcMarkSweepGC -XX:+CMSClassUnloadingEnabled -XX:+CMSScavengeBeforeRemark -XX:+DisableExplicitGC -Djava.awt.headless=true -Xloggc:/apps/service/kafka/bin/../logs/kafkaServer-gc.log -verbose:gc -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+PrintGCTimeStamps -Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false -Dcom.sun.management.jmxremote.port=9999
|
客户端机器配置:
1
|
CPU: 24core/2.6GHZ
|
5.2 不同场景测试
场景1
场景1:测试不同的副本数、min.insync.replicas
策略以及request.required.acks
策略(以下简称acks
策略)对于发送速度(TPS
)的影响。
具体配置:一个producer
;发送方式为sync
;消息体大小为1kB
;partition
数为12
。副本数为:1/2/4
;min.insync.replicas
分别为1/2/4
;acks
分别为-1(all)/1/0
。
具体测试数据如下表(min.insync.replicas
只在acks=-1
时有效):
acks | replicas | in.insync.replicas | retries | TPS |
---|---|---|---|---|
-1 | 1 | 1 | 0 | 28511.3 |
-1 | 2 | 1 | 0 | 22359.5 |
-1 | 2 | 2 | 0 | 22927.4 |
-1 | 4 | 1 | 0 | 16193.9 |
-1 | 4 | 2 | 0 | 16599.9 |
-1 | 4 | 4 | 0 | 16680.3 |
0 | 1 | N/A | 0 | 45353.8 |
0 | 2 | N/A | 0 | 46426.5 |
0 | 4 | N/A | 0 | 46764.2 |
1 | 1 | N/A | 0 | 3950.3 |
1 | 2 | N/A | 0 | 32192.2 |
1 | 4 | N/A | 0 | 32275.9 |
测试结果分析:
客户端的acks
策略对发送的TPS
有较大的影响,TPS
:acks_0 > acks_1 > ack_-1
;
副本数越高,TPS
越低;副本数一致时,min.insync.replicas
不影响TPS
;acks=0/1
时,TPS
与min.insync.replicas
参数以及副本数无关,仅受acks
策略的影响。下面将partition
的个数设置为1
,来进一步确认下不同的acks
策略、不同的min.insync.replicas
策略以及不同的副本数对于发送速度的影响,详细请看情景2和情景3。
场景2
场景2:在partition
个数固定为1
,测试不同的副本数和min.insync.replicas
策略对发送速度的影响。
具体配置:一个producer
;发送方式为sync
;消息体大小为1kB
;producer
端acks=-1(all)
。变换副本数:2/3/4
;min.insync.replicas
设置为:1/2/4
。
测试结果如下:
replicas | min.insync.replicas | TPS |
---|---|---|
2 | 1 | 9738.8 |
2 | 2 | 9701.6 |
3 | 1 | 8999.7 |
3 | 2 | 9243.1 |
4 | 1 | 9005.8 |
4 | 2 | 8216.9 |
4 | 4 | 9092.4 |
测试结果分析:副本数越高,TPS
越低(这点与场景1
的测试结论吻合),但是当partition
数为1
时差距甚微。min.insync.replicas
不影响TPS
。
场景3
场景3:在partition
个数固定为1
,测试不同的acks
策略和副本数对发送速度的影响。
具体配置:一个producer
;发送方式为sync
;消息体大小1kB
;min.insync.replicas=1
。topic
副本数为:1/2/4
;acks
:0/1/-1
。
测试结果如下:
replicas | acks | TPS |
---|---|---|
1 | 0 | 76696 |
2 | 0 | 57503 |
4 | 0 | 59367 |
1 | 1 | 19489 |
2 | 1 | 20404 |
4 | 1 | 18365 |
1 | -1 | 18641 |
2 | -1 | 9739 |
4 | -1 | 9006 |
测试结果分析(与情景1
一致):
副本数越多,TPS
越低;客户端的acks
策略对发送的TPS
有较大的影响,TPS
:acks_0 > acks_1 > ack_-1
。
场景4
场景4:测试不同partition
数对发送速率的影响
具体配置:一个producer
;消息体大小为1KB
;发送方式为sync
;topic
副本数为2
;min.insync.replicas=2
;acks=-1
。partition
数量设置为1/2/4/8/12
。
测试结果:
测试结果分析:partition
数量的不同会影响TPS
,随着partition
的个数的增长TPS
会有所增长,但并不是一直成正比关系,到达一定临界值时,partition
数量的增加反而会使TPS略微降低。
场景5
场景5:通过将集群中部分broker
设置成不可服务状态,测试对客户端以及消息落盘的影响。
具体配置:一个producer
;消息体大小1KB
;发送方式为sync
;topic
副本数为4
;min.insync.replicas
设置为2
;acks=-1
;retries=0/100000000
;partition
数为12
。
具体测试数据如下表:
acks | replicas | min.insync.replicas | retries | 测试方法 | TPS | 数据落盘 | 出现错误 |
---|---|---|---|---|---|---|---|
-1 | 4 | 2 | 0 | 发送过程中kill两台broker | 12840 | 一致(部分数据可落盘,部分失败) | 错误1 |
-1 | 4 | 2 | 100000000 | 发送过程中kill两台broker | 13870 | 一致(消息有重复落盘) | 错误2 |
-1 | 4 | 2 | 100000000 | 发送过程中kill三台broker,之后重启 | N/A | 一致(消息有重复落盘) | 错误2、3、4 |
出错信息:
1
|
错误1:客户端返回异常,部分数据可落盘,部分失败:
|
测试结果分析:kill
两台broker
后,客户端可以继续发送。broker
减少后,partition
的leader
分布在剩余的两台broker
上,造成了TPS
的减小;kill
三台broker
后,客户端无法继续发送。Kafka
的自动重试功能开始起作用,当大于等于min.insync.replicas
数量的broker
恢复后,可以继续发送;当retries
不为0
时,消息有重复落盘;客户端成功返回的消息都成功落盘,异常时部分消息可以落盘。
场景6
场景6:测试单个producer
的发送延迟,以及端到端的延迟。
具体配置:一个producer
;消息体大小1KB
;发送方式为sync
;topic
副本数为4
;min.insync.replicas
设置为2
;acks=-1
;partition
数为12
。
测试数据及结果(单位为ms):
发送端(avg) | 发送端(min) | 发送端(max) | 发送端(99%) | 发送端(99.99%) | 消费端(avg) | 消费端(min) | 消费端(max) | 消费端(99%) | 消费端(99.99%) |
---|---|---|---|---|---|---|---|---|---|
1.715 | 1 | 157 | 3 | 29 | 1.646 | 1 | 288 | 4 | 72 |
各场景测试总结:
- 当
acks=-1
时,Kafka
发送端的TPS
受限于topic
的副本数量(ISR
中),副本越多TPS越低; acks=0
时,TPS
最高,其次为1
,最差为-1
,即TPS
:acks_0 > acks_1 > ack_-1
;min.insync.replicas
参数不影响TPS
;partition
的不同会影响TPS
,随着partition
的个数的增长TPS
会有所增长,但并不是一直成正比关系,到达一定临界值时,partition
数量的增加反而会使TPS略微降低;Kafka
在acks=-1
,min.insync.replicas>=1
时,具有高可靠性,所有成功返回的消息都可以落盘。
参考:https://mp.weixin.qq.com/s/ExzSzf0ue7d-_Qv8q6p9bw?scene=25#wechat_redirect
原文作者:文/it_zzy(简书作者)
原文标题:kafka数据可靠性深度解读