这一篇简要总结一下kafka的原理和概念
1. kafka是一个消息队列,
对应的特性有
- 高吞吐量:吞吐量高达数十万
- 高并发:支持数千个客户端同时读写
- 低延迟:延迟最低只有几毫秒
- 消息持久性和可靠性:消息被持久化到本地磁盘,同时支持数据备份
- 集群容错性:允许部分节点失败
- 可扩展性:支持集群动态扩展
2. kafka中主要的术语
- broker
- topic
- partition
- consumer
- producer
- 集群
- 消息
kafka 是一个分布式的消息中间件,一条消息就是类似我们往MYSQL中存储中的任何一条数据一样,kafka服务以集群的方式存在,集群中的每一个节点叫broker。消息在kafka的归档方式是按照topic来进行归档的(就像是数据库中一个表一样),同时,每个topic又可以被分为一个或者多个partition(可以理解为MYSQL中的分表),用来提高并行度(生产者或者消费者的并行度)。
消息的生产者被称为producer,消息的消费者被称为consumer。生产者将消息发送到broker中的摸个topic的某个partition当中,consumer指定从某一个或者多个topic的partion当中拉取数据。kafka可以保证保证数据的生产和消费的有序性(当然如果想要严格保证的话还需要进一步了解和设置它)。kafka的一个简单图:
3. kafka broker
kafka broker层面要做的东西可以主要分为两个比较大的方面,一方面是集群的管理,一方面是数据的管理。
3.1 topic partition的数据冗余和leader partition
因为kafka是分布式的,所以他会通过一定的数据冗余来对抗集群中的部分节点失败的情况,通过设置每个topic的replica.factor=3(或其他),每个topic就会有3份存储,这样的话当其中的一个或者连个副本所在的broker宕机的话,服务依然能够提供(根据不同的配置,有可能导致少许数据丢失,后面会聊聊)。因为每个partition有多个副本,而kafka规定了多个副本中只能有一个leader 副本,所有的读写都是对这个leader进行的。
在整个kafka集群中有一个大部分分布式服务都有的master角色,他负责了整个集群的topic的partition的leader选举,叫controller。
3.2 集群中的controller
像所有的分布式集群都有master节点一样(zookeeper有master节点,es也有),kafka也有一个master角色,只是在kafka当中叫controller。controller可以认为承担了集群的可用性管理,他主要负责每个partition的leader选举工作。
那么kafka中的controller是如何选举的的呢,因为有了zookeeper,所以选举的功能得到了很大简化。在启动的时候大家都是去zookeeper那里抢占式的创建一个相同的临时节点(/controller)。zookeeper的分布式一致性保证了只有一个请求能够创建成功,那么创建成功的就成为了controller,其他失败的节点则在这个节点上添加监听,在当前controller发生故障失败的时候这个临时节点(/controller)会被zookeeper删除,然后大家就可以再进行一次竞争了。
controller在每次选举得到的新的控制器会通过zookeeper确认自己是新的controller,然后会把epoch+=1,这里的epoch是什么呢,就是一个数值,可以简单理解为皇帝的年号一样的东西,每个皇帝都有自己的epoch,然后下一任的皇帝的epoch比当前这一任的大。controller每次发消息的时候也会带上这个epoch,这样主要是为了防止脑裂,也就是新的controller选出来之后,旧的卡住的controller又活过来了,这个时候他可能还认为自己是controller,进而给其他的节点发号施令,但是他的epoch比较旧,这样的信息会被其他的节点忽略。
epoch是分布式中避免脑裂常用的一种手段,在zookeeper和raft当中都有应用。
3.3 partition中的leader,follower,in-sync-replica
leader,follower,in-sync-replica都是运行时的概念,而且会动态变化的,在静态的存储中一般都叫replica
在上面讲到,kafka为了提升容错能力,每个partition会有多个副本(replica)多个副本当中有一个会成为leader, 每次接收producer和consumer的请求总是由leader partition来处理。
在producer发送消息往leader时,leader会将message存入自己所在的partition当中,同时follower也在源源不断的从leader拉取数据,也就是同步leader中的数据。在这里面kafka允许部分副本比较慢,这样可以提升服务的服务性能,kafka维护了一个follower的子集,叫in-sync-replica
叫同步副本集,这个集合里面也包括leader本身,也就是leader会认为这些副本是一直在保持和leader进行同步的。
1. leader如何判断一个follower是否应该属于in-sync-replica 集合呢
- 在更早的版本中可以通过设置
replica.lag.max.messages=5(或其他)
来决定消息滞后leader中消息达到5条的follower将被踢出 in-sync-replica集合 - 从0.9开始,废弃了这个参数,转而用了
replica.lag.time.max.ms=10000(默认是10s)
,这个配置表示,如果in-sync-replica中的follower在10s内没有向leader发送拉取数据的请求,那么这个followe将被踢出in-sync-replica。 - 这样的一个优化的好处是,假如遇到消息瞬时流量高峰,in-sync-replica中的follower的数据很容易落后leader较多,然后会被踢出去,在跟上leader后又会加入回来,这种情况的踢出-加入本身是没有必要的,如果采用
replica.lag.time.max.ms
就可以有效避免这个问题
2. leader的选举
那么当一个leader挂掉之后,新的leader又是如何被controller选中的呢,他会从 in-sync-replica
中选取一个座位leader,是不是有点粗暴,而且,我们也可以看到,因为 in-sync-replica
中的follower是有可能落后于leader的,这样,新的leader选出来以后,其数据是有可能落后于原来的leader的,这就有可能造成数据的丢失(已经给producer返回了成功消息,但是最终消息却没有完成真正的持久化,consumer消费不到这条消息)。当然,我们通过一些配置是能够达到数据不丢失的,需要broker端和producer端的配合。
3. 最小ISR设置
在broker端还可以配置min.insync.replicas
,如果min.insync.replicas=2
,那么至少要存在两个同步副本才能向分区写入数据。这个时候如果只有一个同步副本,那么Broker就会停止接受生产者的请求,此时Broker变成了只读,尝试发送数据的生产者会收到NotEnoughReplicasException异常,但是消费者仍然可以继续读取已有的数据。
这是为了避免发生不完全选举时数据的写入和读取出现非预期的行为,可以看出来,这个参数也是实现高可用的重要一环,假如设置min.insync.replicas=1
,那么leader挂了,就无法选出leader了。
3.4 partition中消息的High-Watermark
1. 什么是HighWatermark,有什么用
kafka中的消息在partition当中是按照先来后到的顺序持续存入的,High-Watermark ,高水位,他标识了截止到哪条消息是consumer可以看到的。因为要尽可能满足数据的一致性,有可能消息只是在partition中的leader存在,还没有复制给其他的follower,这个时候让consumer看到这条消息是不安全的。因为这条消息有可能还没有答应producer已经成功持久化,这个时候如果leader宕机,也会导致数据的不一致,因为这个时候可能给producer返回的是fail,但是实际上consumer却消费到了这条数据。所以kafka就设计了high-water-mark,标识截止到哪条数据是consumer可以消费的。
2. HW的更新机制:
1. LEO ,log-end-offset
在聊HW的更新机制之前,需要先了解LEO(log-end-offset),这个是partition的每个replica中存储的日志的最后一条日志的offset(最后存进来的),offset其实就是日志的进来的顺序编号,可以理解为数组的下标。
producer每发进来一条消息,server端(broker)对应的都会放到某个指定的partition下,每个消息都会产生一个offset。
同时,leader当中不仅会有自己的LEO,也会有其他follower的LEO信息(这个信息也就是follower在拉取leader的数据的时候传入的fetch offset ),leader会根据这些LEO信息来完成HW的更新。HW=min(LEO)我们可以通以下几个问题来回答HW的更新
-
follower 何时更新LEO
- follower 副本专属线程不断地向leader副本所在broker发送FETCH请求会携带自己的fetch-offset数据(也就是是自己的LEO)。
- leader 副本发送 FETCH response 给follower副本。
- Follower 拿到response之后取出数据写入到本地底层日志中,在该过程中其LEO值会被更新。
-
leader 如何更新自己记录的follower的LEO
leader 端非自己副本对象 LEO值是在leader端处理follower的FETCH请求过程中被更新的。 -
follower 何时更新HW
- Follower 副本对象更新HW是在其更新本地LEO之后。
- 一旦follower向本地日志写完数据后它就会尝试更新其HW值。
- 算法为取本地LEO与FETCH response中leader的HW值的较小值,也就是说follower在fetch的时候leader会把自己的HW也传过去
-
leader 何时更新HW
- Leader 副本对象处理 Follower FETCH请求是在更新完leader 端非自己副本对象的LEO后将尝试更新其自己HW值
- producer 端写入消息会更新leader Replica的LEO
- 副本被踢出ISR时
- 某分区变更为leader副本后
-
leader 在正常同步时的更新机制 HW的更新过程
- leader会根据所有follower的LEO来更新自己的HW
4 producer
这里主要是要了解producer的发送机制,以及一些比较重要的配置。
1. producer如何直接连接对应的topic所在节点
Producers直接发送消息到broker上的leader partition,不需要经过任何中介一系列的路由转发。为了实现这个特性,
- kafka集群中的每个broker都可以响应producer的请求,并返回topic的一些元信息,这些元信息包括哪些机器是存活的,topic的leader partition都在哪,现阶段哪些leader partition是可以直接被访问的。
- Producer客户端自己控制着消息被推送到哪些partition。实现的方式可以是随机分配、实现一类随机负载均衡算法,或者指定一些分区算法。
- Kafka提供了接口供用户实现自定义的分区,用户可以为每个消息指定一个partitionKey,通过这个key来实现一些hash分区算法。比如,把userid作为partitionkey的话,相同userid的消息将会被推送到同一个分区。
- 以Batch的方式推送数据可以极大的提高处理效率,kafka Producer 可以将消息在内存中累计到一定数量后作为一个batch发送请求。Batch的数量大小可以通过Producer的参数控制,参数值可以设置为累计的消息的数量(如500条)、累计的时间间隔(如100ms)或者累计的数据大小(64KB)。通过增加batch的大小,可以减少网络请求和磁盘IO的次数,当然具体参数设置需要在效率和时效性方面做一个权衡。
2. 相关配置
acks:
- acks=0 ,生产者把消息发送到broker即认为成功,不等待broker的处理结果,这种情况下,下面的retries的配置也是无效的。这种方式的吞吐最高,但也是最容易丢失消息的
- acks=1:生产者会在该分区的群首(leader)写入消息并返回成功后,认为消息发送成功。如果群首写入消息失败,生产者会收到错误响应并进行重试。这种方式能够一定程度避免消息丢失,但如果群首宕机时该消息没有复制到其他副本,那么该消息还是会丢失。
- acks=all:生产者会等待所有副本成功写入该消息,这种方式是最安全的,能够保证消息不丢失,但是延迟也是最大的,这种一般是用在对数据的持久化和一致性比较高的场景,但是对数据吞吐量要求不是特别高。
retries
- 当生产者发送消息收到一个可恢复异常时,会进行重试,这个参数指定了重试的次数。在实际情况中,这个参数需要结合retry.backoff.ms(重试等待间隔)来使用,建议总的重试时间比集群重新选举群首的时间长,这样可以避免生产者过早结束重试导致失败
batch.size
- 当多条消息发送到一个分区时,生产者会进行批量发送,这个参数指定了批量消息的大小上限(以字节为单位)。当批量消息达到这个大小时,生产者会一起发送到broker;但即使没有达到这个大小,生产者也会有定时机制来发送消息,避免消息延迟过大。
linger.ms
- 这个参数指定生产者在发送批量消息前等待的时间,当设置此参数后,即便没有达到批量消息的指定大小,到达时间后生产者也会发送批量消息到broker。默认情况下,生产者的发送消息线程只要空闲了就会发送消息,即便只有一条消息。设置这个参数后,发送线程会等待一定的时间,这样可以批量发送消息增加吞吐量,但同时也会增加延迟。
buffer.memory
- 这个参数设置生产者缓冲发送的消息的内存大小
client.id
- 这个参数可以是任意字符串,它是broker用来识别消息是来自哪个客户端的。在broker进行打印日志、衡量指标或者配额限制时会用到。
max.in.flight.requests.per.connection
- 这个参数指定生产者可以发送多少消息到broker并且等待响应,设置此参数较高的值可以提高吞吐量,但同时也会增加内存消耗。如果想要保证消息的有序性,只能设置为1。2.0中默认为5,kafka保证了单个producer的严格exactly-once,也保证了有序性,比较牛叉
max.request.size
- 这个参数限制生产者发送数据包的大小,数据包的大小与消息的大小、消息数相关。如果我们指定了最大数据包大小为1M,那么最大的消息大小为1M,或者能够最多批量发送1000条消息大小为1K的消息。另外,broker也有message.max.bytes参数来控制接收的数据包大小。在实际中,建议这些参数值是匹配的,避免生产者发送了超过broker限定的数据大小。
5 consumer
kafka的消费者,消费者,顾名思义,就是从kafka broker 上拉取生产者producer产生的数据。消费的数据粒度可以到达topic.partition,同时,也可以指定的起始位置offset值,或者是按照时间查找offset,然后进行消费。
每个consumer会有一个group-id,多个consumer可以属于一个group-id,进而分享一个topic的多个partition(提高并行能力)
consumer可以使用自动提交消费点位offset,也可以使用手动提交的方式,他其实没有ack机制,每次提交offset,就是往broker的__consumer_offset__ 这个topic生产消息而已。然后下次启动的时候又默认会从这个topic中取到相关的offset信息,使用这个offset从broker中拉取数据。
1. consumer的rebalance过程
1. 发生rebalance的时机
1.正常情况
组成员个数发生变化。例如有新的 consumer 实例加入该消费组或者离开组。
订阅的 Topic 个数发生变化。
订阅 Topic 的分区数发生变化。
2.消费者意外情况
session 过期
max.poll.interval 到期,在这个时间值达到时,心跳线程会自动停止发送heartbeats 然后 发送leave-group request
这个时候会触发rebalance,
2. rebalance过程
下面以新增一个consumer来阐述
-
Consumer Client 发送 join-group 请求,如果 Group 不存在,创建该 Group,Group 的状态为 Empty;
-
由于 Group 的 member 为空,将该 member 加入到 Group 中,并将当前 member (client)设置为 Group 的 leader,进行 rebalance 操作,Group 的状态变为 preparingRebalance,等待 rebalance.timeout.ms 之后(为了等待其他 member 重新发送 join-group,如果 Group 的状态变为 preparingRebalance,Consumer Client 在进行 poll 操作时,needRejoin() 方法结果就会返回 true,也就意味着当前 Consumer Client 需要重新加入 Group),Group 的 member 更新已经完成,此时 Group 的状态变为 AwaitingSync,并向 Group 的所有 member 返回 join-group 响应;
-
client 在收到 join-group 结果之后,如果发现自己的角色是 Group 的 leader,就进行 assignment,该 leader 将 assignment 的结果通过 sync-group 请求发送给 GroupCoordinator,而 follower 也会向 GroupCoordinator 发送一个 sync-group 请求(只不过对应的字段为空);
-
当 GroupCoordinator 收到这个 Group leader 的请求之后,获取 assignment 的结果,将各个 member 对应的 assignment 发送给各个 member,而如果该 Client 是 follower 的话就不做任何处理,此时 group 的状态变为 Stable(也就是说,只有当收到的 Leader 的请求之后,才会向所有 member 返回 sync-group 的结果,这个是只发送一次的,由 leader 请求来触发)。
2. 消费者的一些配置
fetch.min.bytes
- 这个参数允许消费者指定从broker读取消息时最小的数据量。当消费者从broker读取消息时,如果数据量小于这个阈值,broker会等待直到有足够的数据,然后才返回给消费者。对于写入量不高的主题来说,这个参数可以减少broker和消费者的压力,因为减少了往返的时间。而对于有大量消费者的主题来说,则可以明显减轻broker压力。
fetch.max.wait.ms
- 上面的fetch.min.bytes参数指定了消费者读取的最小数据量,而这个参数则指定了消费者读取时最长等待时间,从而避免长时间阻塞。这个参数默认为500ms。
max.partition.fetch.bytes
-
这个参数指定了每个分区返回的最多字节数,默认为1M。也就是说,KafkaConsumer.poll()返回记录列表时,每个分区的记录字节数最多为1M。如果一个主题有20个分区,同时有5个消费者,那么每个消费者需要4M的空间来处理消息。实际情况中,我们需要设置更多的空间,这样当存在消费者宕机时,其他消费者可以承担更多的分区。
-
需要注意的是,max.partition.fetch.bytes必须要比broker能够接收的最大的消息(由max.message.size设置)大,否则会导致消费者消费不了消息。另外,在上面的样例可以看到,我们通常循环调用poll方法来读取消息,如果max.partition.fetch.bytes设置过大,那么消费者需要更长的时间来处理,可能会导致没有及时poll而会话过期。对于这种情况,要么减小max.partition.fetch.bytes,要么加长会话时间。
session.timeout.ms
- 这个参数设置消费者会话过期时间,默认为3秒。也就是说,如果消费者在这段时间内没有发送心跳,那么broker将会认为会话过期而进行分区重平衡。这个参数与heartbeat.interval.ms有关,heartbeat.interval.ms控制KafkaConsumer的poll()方法多长时间发送一次心跳,这个值需要比session.timeout.ms小,一般为1/3,也就是1秒。更小的session.timeout.ms可以让Kafka快速发现故障进行重平衡,但也加大了误判的概率(比如消费者可能只是处理消息慢了而不是宕机)。
auto.offset.reset
- 这个参数指定了当消费者第一次读取分区或者上一次的位置太老(比如消费者下线时间太久)时的行为,可以取值为latest(从最新的消息开始消费)或者earliest(从最老的消息开始消费)。
enable.auto.commit
- 这个参数指定了消费者是否自动提交消费位移,默认为true。如果需要减少重复消费或者数据丢失,你可以设置为false。如果为true,你可能需要关注自动提交的时间间隔,该间隔由auto.commit.interval.ms设置。
partition.assignment.strategy
-
我们已经知道当消费组存在多个消费者时,主题的分区需要按照一定策略分配给消费者。这个策略由PartitionAssignor类决定,默认有两种策略:
- 范围(Range):对于每个主题,每个消费者负责一定的连续范围分区。假如消费者C1和消费者C2订阅了两个主题,这两个主题都有3个分区,那么使用这个策略会导致消费者C1负责每个主题的分区0和分区1(下标基于0开始),消费者C2负责分区2。可以看到,如果消费者数量不能整除分区数,那么第一个消费者会多出几个分区(由主题数决定)。
- 轮询(RoundRobin):对于所有订阅的主题分区,按顺序一一的分配给消费者。用上面的例子来说,消费者C1负责第一个主题的分区0、分区2,以及第二个主题的分区1;其他分区则由消费者C2负责。可以看到,这种策略更加均衡,所有消费者之间的分区数的差值最多为1。
-
partition.assignment.strategy
设置了分配策略,默认为org.apache.kafka.clients.consumer.RangeAssignor
(使用范围策略),你可以设置为org.apache.kafka.clients.consumer.RoundRobinAssignor
(使用轮询策略),或者自己实现一个分配策略然后将partition.assignment.strategy
指向该实现类。
client.id
- 这个参数可以为任意值,用来指明消息从哪个客户端发出,一般会在打印日志、衡量指标、分配配额时使用。
max.poll.records
- 这个参数控制一个poll()调用返回的记录数,这个可以用来控制应用在拉取循环中的处理数据量。
max.poll.interval.ms
- 两次 poll 之间的最大时间间隔,设置大一点可以处理消息的时间,在到达这个时间没有进行poll()操作的话会自动停止发送心跳,并且发送一个leave-group的请求,
假如两次poll()之间处理请求比较大的话应该放到异步去做,因为服务器同时会使用这个参数作为等待其他consumer相应rejion的最大时长,假如其他consumer也把max.poll.interval.ms设置的比较长的话,那么整个rebalance可能耗时会很长。
receive.buffer.bytes、send.buffer.bytes
- 这两个参数控制读写数据时的TCP缓冲区,设置为-1则使用系统的默认值。如果消费者与broker在不同的数据中心,可以一定程度加大缓冲区,因为数据中心间一般的延迟都比较大。
partition.assignment.strategy
- 这个设置了consumer的partition在consumer中的分配机制
kafka的压缩和解压缩,理论上broker是不会再处理的,除非单独配置了,这样有可能会导致cpu升高
http://zhongmingmao.me/2019/08/02/kafka-compression/
6. 高性能读写的秘密
- 顺序读写
- 零拷贝
参考
https://juejin.im/post/5d9944e9f265da5b6a169271
https://juejin.im/post/5bf6b0acf265da612d18e931#heading-5
https://juejin.im/post/5c0683b1f265da614f701441
https://juejin.im/post/5c46e729e51d452c8e6d5679