《Kafka权威指南》学习笔记
深入kafka
集群成员关系
Kafka使用Zookeeper来维护集群成员的信息。
每个broker都有一个唯一标识符,这个标识符可以在配置文件里指定,也可以自动生成(强烈建议写在配置文件中)
。
在broker启动的时候,它通过创建临时节点把自己的ID注册到Zookeeper(/brokers/ids
).
-brokers
-ids
-0
-1
-2
....
-topics
-topic1
-topicX
....
-seqid
每个brokers里的内容如下:
{
"endpoints":["PLAINTEXT://s159:9092"],
"host":"s159",
"jmx_port":-1,
"listener_security_protocol_map":{
"PLAINTEXT":"PLAINTEXT"
},
"port":9092,
"timestamp":"1539157821283",
"version":4
}
Kafka组件订阅Zookeeper的/brokers/ids
路径,当有broker加入集群或退出集群时,这些组件就可以获得通知。
如果你要启动另一个具有相同ID的broker
,会得到一个错误,新broker会试着进行注册,但不会成功,因为Zookeeper里已经有一个具有相同ID的broker。
在broker停机、出现网络分区或长时间垃圾回收停顿时,broker会从Zookeeper上断开连接,此时broker在启动时创建的临时节点会自动从Zookeeper上移除。监听broker列表的Kafka组件会被告知该broker已移除。
在关闭broker时,它对应的节点也会消失,不过它的ID会继续存在于其他数据结构中。
例如,主题的副本列表(下面会介绍)里就可能包含这些ID。在完全关闭一个broker之后,如果使用相同的broker-id启动另一个全新的broker,它会立即加入集群,并拥有与旧broker相同的分区和主题。
控制器
控制器其实就是一个broker,只不过它除了具有一般broker的功能之外,还负责分区首领的选举.
集群里第一个启动的broker通过在Zookeeper里创建一个临时节点/controller
让自己成为控制器。
**例:**它的内容如下:
{
"brokerid":1,
"timestamp":"1538038397695",
"version":1
}
上述内容说明:broker列表中id=1的为leader
。
其他broker在启动时也会尝试创建这个节点,不过它们会收到一个“节点已存在”的异常,然后“意识”到控制器节点已存在,也就是说集群里已经有一个控制器了。
其他broker在控制器节点上创建ZookeeperWatcher对象,这样它们就可以收到这个节点的变更通知。这种方式可以确保集群里一次只有一个控制器存在。
控制器负责在节点加入或离开集群时进行分区首领选举。控制器使用epoch来避免“脑裂”。`“脑裂”是指两个节点同时认为自己是当前的控制器。
复制
复制功能是Kafka架构的核心。
在Kafka的文档里,Kafka把自己描述成“一个分布式的、可分区的、可复制的提交日志服务”。复制之所以这么关键,是因为它可以在个别节点失效时仍能保证Kafka的可用性和持久性。Kafka使用主题来组织数据,每个主题被分为若干个分区,每个分区有多个副本,副本被保存在broker上
。
副本有以下两种类型。
- Leader副本:每个分区都有一个Leader副本。为了保证一致性,所有生产者请求和消费者请求都会经过这个副本。
- Follower副本:Leader以外的副本都是跟Follower副本。Follower副本不处理来自客户端的请求,它们唯一的任务就是
从Leader副本那里复制消息
,保持与Follower副本一致的状态。如果Follower发生崩渍,其中的一个Follower会被选为为新Leader。
如果一个Follower副本无法与Leader副本保持一致,在Leader发生失效时,它就不可能成为新Leader一一毕竟它没有包含全部的消息。
相反,持续请求得到的最新悄息副本被称为同步的副本(会在Isr列表中)
。在Leader发生失效时,只有同步副本才有资格被选为新Leader。
Follower的正常不活跃时间或在成为不同步副本
之前的时间是通过replica.lag.time.max.ms
参数来配置的。
每个分区都有一个首选Leader
,创建主题时选定的Leader就是分区的首选Leader。
默认情况下,Kafka的auto.leader.rebalance.enable
被设为true,它会检查首选Leader是不是当前Leader,如果不是,并且该副本是同步的,那么就会触发Leader选举,让首选Leader成为当前Leader。
**例:**集群有3个broker(0,1,2),
使用命令./kafka-topics.sh--create--zookeepers159:2181,s162:2181,s163:2181--replication-factor3--partitions6--topicckxx
创建topic.
Topic:ckxx PartitionCount:6 ReplicationFactor:3 Configs:
Topic:ckxx Partition:0 Leader:1 Replicas:1,2,0 Isr:1,2,0
Topic:ckxx Partition:1 Leader:2 Replicas:2,0,1 Isr:2,0,1
Topic:ckxx Partition:2 Leader:0 Replicas:0,1,2 Isr:0,1,2
Topic:ckxx Partition:3 Leader:1 Replicas:1,0,2 Isr:1,0,2
Topic:ckxx Partition:4 Leader:2 Replicas:2,1,0 Isr:2,1,0
Topic:ckxx Partition:5 Leader:0 Replicas:0,2,1 Isr:0,2,1
处理请求
broker的大部分工作是处理客户端、分区副本和控制器发送给分区Leader
的请求。
Kafka提供了一个二进制协议(基于TCP),指定了请求消息的格式以及broker如何对请求作出响应一一包括成功处理请求或在处理请求过程中遇到错误。
标准消息头
请求消息都包含一个标准消息头
,格式如下:
- Requesttype(也就是APIkey)
- Requestversion(broker可以处理不同版本的客户端请求,井根据客户端版本作出不同的响应)
- CorrelationID-一个具有唯一性的数字,用于标识请求消息,同时也会出现在响应消息和错误日志里(用于诊断问题)
- ClientID-用于标识发送请求的客户端
生产请求和获取请求都必须发送给分区的Leader副本
。
- 如果broker收到一个针对特定分区的请求,而该分区的首领在另一个broker上,那么发送请求的客户端会收到一个“非分区首领”的错误响应。
- 当针对特定分区的获取请求被发送到一个不含有该分区首领的broker上,也会出现同样的错误。
元数据请求
Kafka客户端要自己负责把生产请求和获取请求发送到正确的broker上。
客户端怎么知道该往哪里发送请求呢?客户端使用了另一种请求类型,也就是元数据请求
。
这种请求包含了客户端感兴趣的主题列表
。服务器端的响应消息里指明了这些主题所包含的分区、每个分区都有哪些副本,以及哪个副本是Leader(首领)
。元数据请求可以发送给任意一个broker,因为所有broker都缓存了这些信息。
一般情况下,客户端会把这些信息缓存起来,并直接往目标broker上发送生产请求和获取请求。它们需要时不时地通过发送元数据请求来刷新这些信息(刷新的时间间隔通过
metadata.max.age.ms
参数来配置),从而知道元数据是否发生了变更
生产请求
配置生产者的时候,有一个重要的acks配置参数,通过配置该参数可以指定了需要多少个broker确认才可以认为一个消息写入是成功的。
acks=1
:只要Leader收到消息就认为是成功的acks=all
:所有的同步副本
收到消息才算成功acks=0
:生产者把消息发送出去之后,完全无需等待broker的响应。
包含首领副本的broker在收到生产请求时,会对请求做一些验证。
- 发送数据的用户是否有主题写入权限?
- 请求里包含的acks值是否有效(只允许出现。、1或all)?
- 如果acks=all,是否有足够多的罔步副本保证消息已经被安全写入?
之后,悄息被写入本地磁盘。在Linux系统上,消息会被写到文件系统缓存里,并不保证它们何时会被刷新到磁盘上。Kafka不会一直等待数据被写到磁盘上一一它依赖复制功能来保证消息的持久性。
在消息被写入分区的Leader之后,broker开始检查acks配置参数
- 如果acks被设为0或1,那么broker立即返回响应;
- 如果acks被设为all,那么请求会被保存在一个叫作炼狱的缓冲区里,直到首领发现所有跟随者副本都复制了消息,晌应才会被返回给客户端。
获取请求
broker处理获取请求的方式与处理生产请求的方式很相似。
客户端发送请求,向broker请求主题分区里具有特定偏移量的消息,好像在说:“请把主题Test分区0、偏移量从53开始的消息以及主题Test分区3、偏移量从64开始的消息发给我。”
客户端还可以指定broker最多可以从一个分区里返回多少数据。
这个限制是非常重要的,因为客户端需要为broker返回的数据分配足够的内存。如果没有这个限制,broker返回的大量数据有可能耗尽客户端的内存。
我们之前讨论过,请求需要先到达指定的分区首领上,然后客户端通过查询元数据来确保请求的路由是正确的。
- 首领在收到请求时,它会先检查请求是否有效一一比如,指定的偏移量在分区上是否存在?
- 如果客户端请求的是已经被删除的数据,或者请求的偏移量不存在,那么broker将返回一个错误。
- 如果请求的偏移量存在,broker将按照客户端指
定的数量上限
从分区里读取消息,再把消息返回给客户端。
Kafka使用
零复制
技术向客户端发送消息一一也就是说,Kafka直接把消
息从文件(或者更确切地说是Linux文件系统缓存)里发送到网络通道,而不需要经过任何中间缓冲区。
客户端除了可以设置broker返回数据的上限,也可以设置下限。
例如,如果把下限设置为10kb,就好像是在告诉broker:“等到有l0kb数据的时候再把它们发送给我。”
在主题消息流量不是很大的情况下,这样可以减少CPU和网络开销。
当然,我们不会让客户端一直等待broker累积数据。在等待了一段时间之后,就可以把可用的数据拿回处理,而不是一直等待下去。
所以,客户端可以定义一个超时时间,告诉broker:“如果你无法在X毫秒内累积满足要求的数据量,那么就把当前这些数据返回给我。”
有意思的是,
并不是所有保存在分区首领上的数据都可以被客户端读取
。大部分客户端只能读取已经被写入所有同步副本的消息
.
在消息还没有被写入所有同步副本之前,是不会发送给消费者的。尝试获取这些消息的请求会得到空的响应,而不是错误。
因为还没有被足够多副本复制的消息被认为是“不安全”
的。如果首领发生崩溃,另一个副本成为新首领,那么这些消息就丢失了。如果我们允许消费者读取这些消息,可能就会破坏一致性。
如果broker间的消息复制因为某些原因变慢,那么消息到达消费者的时间也会随之变长(因为我们会先等待消息复制完毕)。
延迟时间可以通过参数replica.lag.time.max.ms
来配置,它指定了副本在复制消息时可被允许的最大延迟时间。
其他请求
我们讨论了Kafka最为常见的几种请求类型
- 元数据请求
- 生产请求
- 获取请求
另还有其他一些常见的请求类型:
- 增加请求类型
以前的Kafka消费者使用Zookeeper来跟踪偏移量,在消费者启动的时候,它通过检查保存在Zookekeper上的偏移量就可以知道从哪里开始处理悄息。
因为各种原因,我们决定不再使用Zookeeper来保存偏移量,而是把偏移量保存在特定的Kafka主题上。为了达到这个目的,我们不得不往协议里增加几种请求类型:OffsetCommitRequest,OffsetFetchRequest和ListOffsetsRequest
。现在,在应用程序调用commitOffset()
方法时,客户端不再把偏移量写入Zookeeper,而是往Kafka发送OffsetFetchRequest
请求。 - 修改已有消息类型
除了往协议里增加新的请求类型外,我们也会通过修改已有的请求类型来给它们增加新功能。例如, - 从Kafka0.9.0到Kafka 0.10.0,我们希望能够让客户端知道谁是当前的控制器,于是把控制器信息添加到元数据响应消息里。
- 在元数据请求消息和响应消息里添加了一个新的version字段。(0.9.0版本客户端的
version是0
,0.10.0版本客户端version是1
(向下兼容
)) - 在0.10.0版本的Kafka里加入了
ApiVersionRequest
,客户端可以询问broker支持哪些版本的请求,然后使用正确的版本与broker通信。
物理存储
Kafka的基本存储单元是分区。在配置Kafka的时候,管理员指定了一个用于存储分区的目录清单一一也就是log.dir
参数的值.
分区分配
在创建主题时,Kafka首先会决定如何在broker间分配分区。
假设你有6个broker,打算创建一个包含10个分区的主题,并且复制系数为3。
那么Kafka就会有30个分区副本,它们可以被分配给6个broker。
在进行分区分配时,我们要达到如下的目标。
- 在broker间平均地分布分区副本。对于我们的例子来说,就是要保证每个broker可以分到5个副本。
- 确保每个分区的每个副本分布在不同的broker上。
即:同一个broker上不会出现一个分区的多个副本。
- 如果为broker指定了机架
(通过broker.rack=my-rack-id配置)
信息,那么尽可能把每个分区的副本分配到不同机架的broker上。这样做是为了保证一个机架的不可用不会导致整体的分区不可用。
文件管理
保留数据是Kafka的一个基本特性,Kafka不会一直保留数据,也不会等到所有消费者都读取了消息之后才删除消息。
Kafka管理员为每个主题配置了数据保留期限,有如下两种方式
- a.规定数据被删除之前可以保留多长时间
- b.清理数据之前可以保留的数据量大小。
因为在一个大文件里查找和删除消息是很费时的,也很容易出错,所以我们把分区分成若干个片段(也叫日志段:LogSegment)
。
默认情况下,每个片段包含lGB或一周的数据,以较小的那个为准
。在broker往分区写入数据时,如果达到片段上限,就关闭当前文件,井打开一个新文件。
当前正在写入数据的片段叫作活跃片段。活动片段永远不会被删除
文件格式
我们把Kafka的消息和偏移量保存在文件里。保存在磁盘上的数据格式与从生产者发送过来或者发送给消费者的悄息格式是一样的。
因为使用了相同的消息格式进行磁盘存储和网络传输,Kafka可以使用零复制技术给消费者发送消息,同时避免了对生产者已经压缩过的消息进行解压和再压缩。
索引
消费者可以从Kafka的任意可用偏移量位置开始读取消息。
假设消费者要读取从偏移量100开始的lMB消息,那么broker必须立即定位到偏移量100),然后开始从这个位置读取消息。
为了帮助broker更快地定位到指定的偏移量,Kafka为每个分区维护了一个索引。索引把偏移量映射到片段文件
和偏移量在文件里的位置
。
索引也被分成片段,所以在删除消息时,也可以删除相应的索引。Kafka不维护索引的校验和。如果索引出现损坏,Kafka会通过重新读取消息并录制偏移量和位置来重新生成索引。如果有必要,管理员可以删除索引,这样做是绝对安全的,Kafka会自动重新生成这些索引。