了解 Kafka 从《Kafka 权威指南》开始的,读着读着有点懵逼,所以我打算从 Kafka 设计上的一些点来总结一下。
不止于 Kafka 的各种细节,而是学习它的这种设计思想,如果能有十分之一用到自己设计的系统中,估计也会很牛逼了。
铭记于心:
- 要做什么;
- 这么做的设计思想是啥;
- 如何实现这种设计思想。
Kafka 主要目标
使用推送和拉去模型解耦生产者和消费者;
为消息传递系统中的消息提供数据持久化,以便支持多个消费者;
通过系统优化实现高吞吐量;
系统可以随着数据流的增长进行横向扩展。
发布与订阅系统:解耦
数据消息的发送者不会直接把消息发送给接收者。
发布者以某种方式对消息进行分类,接收者订阅它们,以便接受特定类型的消息。
发布与订阅系统一般会有一个 broker,也就是发布消息的中心点。
发布与订阅系统的演进
持续的数据流
Kafka 持续的数据流,是一个流平台。以数据流为中心。
一个主题的数据可以看成一个流,不管它有多少个分区。
流是一组从生产者移动到消费者的数据。
主题:对消息进行分类
Kafka 的消息通过主题进行分类。
分区:实现数据冗余和伸缩性
主题被分为若干个分区,一个分区就是一个提交日志。
日志以追加的方式写入分区,然后以先入先出的顺序读取。
一个主题一般包含几个分区,因此无法在整个主题范围保证消息的顺序,但可以保证单个分区的顺序。
Kafka 通过分区来实现数据冗余和伸缩性。
分区可以分布在不同的服务器上,也就是一个主题可以横跨多个服务器,以此提供比单个服务器更强大的能力。
ProducerRecord 对象包含了目标主题、键和值。
Kafka 的消息是一个个键值对,ProducerRecord 对象可以只包含目标主题和值,键可以设置为默认的 null,不过大多数应用会用到键。
键:控制消息写入不同的分区
消息可以有一个可选的元数据,也就是键。
键也是一个字节数组,和消息一样对 Kafka 也没有特别的含义。
消息以一种可控的方式写入不同的分区时,会用到键。
例如,可以为键生成一个一致性散列值,然后使用散列值对主题分区数进行取模,为消息选取分区。
这样可以保证具有相同键的消息总是被写到相同的分区上。
键的两个用途:可以作为消息的附加信息,也可以用来决定消息被写到主题的哪个分区。
拥有相同的键被写到同一分区。
如果键值为 null,并且使用了默认的分区器,那么记录将被随机地发送到主题内各个可用的分区上。
分区器使用轮询算法将消息均衡地分布到各个分区上。
如果键值不为空,并且使用了默认的分区器,那么 Kafka 会对键进行散列(使用 Kafka 自己的散列算法,即使升级 Java 版本,散列值也不会发生变化),然后根据散列值把消息映射到特定的分区上。
同一个键总是被映射到同一个分区上,所以进行映射时,会使用主题的所有分区,而不仅仅是可用的分区。
如果写入数据的分区不可用,那么就会发生错误。
当然你也可以实现自定义的分区策略。
分区再均衡:添加新分区后的操作
在主题发生改变时,如管理员添加了新的分区,会发生分区重分配。
分区的所有权从一个消费者转移到另一个消费者,这种行为被称为再平衡。
再平衡非常重要,它为消费者群组带来了高可用性和伸缩性(可以放心地添加或移除消费者)。
但是再均衡期间,消费者无法消费消息,造成整个群组一小段时间的不可用。
另外,当分区被重新分配给另一个消费者时,消费者当前的读取状态会丢失,它可能还需要去刷新缓存。
群组协调器:负责分区分配,为消费者分配分区
消费者的线程安全
批次:减少网络开销
批次就是一组消息,这些消息属于同一个主题和分区。
如果每一个消息都单独穿行于网络,会导致大量的网络开销,把消息分成批次可以减少网络开销。
不过这是在时间延迟和吞吐量之间作出权衡:批次越大,单位时间处理的消息就越多,单个消息处理的时间就越长。
压缩
批次数据会被压缩,这样可以提高数据的传输和存储能力,但是要做更多的计算处理。
模式:保证数据格式的一致性
JSON 和 XML 缺乏强类型的处理能力,不同版本之间的兼容性也不是很好。
Kafka 使用了 Avro(Hadoop 开发的一款序列化框架)。
Avro 提供了一种紧凑的序列化格式,模式和消息体是分开的,当模式发生变化时,不需要重新生成代码。
序列化器:推荐使用 Avro 序列化器作为备选方案
创建一个生产者必须指定序列化器,当然也可以使用默认的字符串序列化器,Kafka 还提供了整型和字节数组序列化器,当然也可以使用自定义序列化器。
不同版本的序列化器和反序列化器之间可能还存在兼容性问题。
所以不建议使用自定义的序列化器,而是使用已有的序列化器和反序列化器,如 JSON、Avro、Thrift 或 Protobuf。
生产者
Kafka 的客户端分类两种基本类型:生产者和消费者。
一般情况下,一个消息会被发布到一个特定的主题上。
生产者默认把消息均衡地分布到主题的所有分区上,而并不关心特定消息会被写到哪个分区。
在某些情况下,生产者会把消息直接写到指定的分区。
这通常是通过消息键和分区器来实现的,分区器为键生成一个散列值,并将其映射到指定的分区上。
这样可以保证同一个键的消息会被写到同一个分区上。
消费者
消费者订阅一个或者多个主题,并按消息生成的顺序读取它们。
Kafka 支持多个消费者从一个单独的消息流上读取数据,而且消费者之间互不影响。
这与其他消息队列不同,其他消息队列的消息一旦被一个客户端读取,其他客户端就无法再读取它。
如果生产者往主题写入的速度超过了应用程序消费数据的速度,只使用单个消费者处理消息,应用程序永远跟不上消息生成的速度。
我们可以使用多个消费者从同一个主题读取消息,对消息进行分流。
消费群组:多个消费者共同读取一个主题
群组保证每个分区只能被一个消费者使用。
下图群组中有 3 个消费者同时读取一个主题。其中两个消费者各自读取一个分区,另外一个消费者读取其他两个分区。
消费者与分区之间的映射通常被称为消费者对分区的所有权关系。
消费者群组从主题读取消息
每个消费者只处理部分分区的消息,这就是横向伸缩的主要手段。
我们有必要为主题创建大量的分区,在负载增长时可以加入更多的消费者。
不过要注意不要让消费者的数量超过主题分区的数量,多余的消费者只会被闲置。
多个消费者群组消费同一个主题:
一个新的消费者加入群组时,它读取的时原本由其他消费者读取的消息。
当一个消费者被关闭或者崩溃时,它就离开群组,原本由它读取的分区将由群组里的其他消费者来读取。
提交和偏移量
Kafka 不会像其他 JMS 队列那样需要得到消费者的确认,这是 Kafka 的一个独特之处。
相反,消费者可以使用 Kafka 来追踪消息在分区里的位置(偏移量)。
把更新分区当前位置的操作叫做提交。
消费者往一个叫做 _consumer_offset 的特殊主题发送消息,消息里面包含每个分区的偏移量。
如果消费者一直处于运行状态,那么偏移量就没什么用。
如果消费者发生崩溃或者有新的消费者加入群组,就会触发再平衡,完成再平衡后,每个消费者可能分配到新的分区,而不是之前处理的那个。
如果要继续完成工作,消费者需要读取每个分区最后一次提交的偏移量,然后从指定偏移量指定的地方继续处理。
如果提交的偏移量小于客户端处理的最后一个消息的偏移量,那么处于两个偏移量之间的消息就会被重复处理。
如果提交的偏移量大于客户端处理的最后一个消息的偏移量,那么处于两个偏移量之间的消息将会丢失。
再均衡监听器:为消费者分配新分区或者移除旧分区时,执行一些应用程序代码
比如失去分区的所有权之前,需要处理在缓冲区积累下来的记录,可能还要关闭文件句柄、数据库连接等。
偏移量:同步提交,异步提交,提交当前,提交特定
broker 和集群:复制、冗余和扩展
一个独立的 Kafka 服务器被称为 broker。
broker 接受来自生产者的消息,为消息设置偏移量,并提交消息到磁盘保存。
broker 为消费者提供服务,对读取分区的请求作出响应,返回已经提交到磁盘的消息。
broker 是集群的组成部分。
集群控制器:负责管理工作
broker 是集群的组成部分。每个集群都有一个 broker 同时充当了集群控制器的角色(自动从集群的活跃成员中选举出来)。
控制器负责管理工作,包括将分区分配给 broker 和监控 broker。
在集群中,一个分区从属于一个 broker,该 broker 被称为分区的首领。
分区复制:为分区提供了消息冗余
一个分区可以分配给多个 broker,这个时候会发生分区复制。
这种复制机制提供了消息冗余,如果有一个 broker 失效,其他 broker 可以接管领导权。
不过相关的生产者和消费者都要重新连接到新的首领。
集群里的分区复制
保留消息:持久化
Kafka 可以按照要求保存数据,多久都可以。
Kafka 按照一定的顺序持久化保存的。
Kafka 的数据分布在整个系统里,具备数据故障保护能力和性能伸缩能力。
Kafka broker 默认的消息保留策略是这样的:要么保留一段时间(比如 7 天),要么保留到消息达到一定大小的字节数(比如 1GB)。
当消息达到这些上限时,旧消息就会过期并被删除,所以在任何时刻,可用消息的总量都不会超过配置参数所指定的大小。
主题可以配置自己的保留策略。
基于磁盘的存储:保证数据不会丢失
允许消费者非实时地读取消息。
消息被提交到磁盘,根据设置的保留规则进行保存。
每个主题可以设置单独的保留规则,以便满足不同的消费者的需求。
持久化数据可以保证数据不会丢失。
消费者可以被关闭,但是消息会继续保留在 Kafka 里,消费者可以继续从上次中断的地方继续处理消息。
多集群:数据更加安全
使用多个集群的原因:
- 数据类型分离
- 安全需求隔离
- 多数据中心(灾难恢复)
偏移量:控制读取状态
消费者通过检查消息的偏移量来区分已经读取过的消息。
在给定的分区里,每个消息的偏移量都是唯一的。
消费者把每个分区最后读取的消息偏移量保存到 Zookeeper 或 Kafka 上,如果消费者关闭或者重启,它的读取状态不会丢失。
可复制
零复制
复制协议
控制器
存储层
伸缩性
开发阶段可以先使用单个 broker,再扩展到包含 3 个 broker 的小型开发集群,然后随着数据不断增长,部署到生产环境可能包含上百个 broker。
对在线集群进行扩展丝毫不影响整体系统的可用性。
要提高集群的容错能力,需要配置较高的复制系数。
高性能
通过横向扩展生产者、消费者和 broker,Kafka 可以轻松处理巨大的消息流。
在处理大量数据的同时,还能保证亚秒级的消息延迟。
消息发送:同步和异步
同步发送消息需要等待 Kafka 集群的回应。假设一个消息在应用程序和 Kafka 集群之间来回要 10ms,那么发送 100 个消息需要 1s。
如果只发送消息而不等待响应,那么发送 100 个消息的时间会少很多。
大多数时候,我们并不需要等待响应——尽管 Kafka 会把目标主题、分区信息和消息的偏移量发送回来,但对于应用程序来说并不是必须的。