先说下结论,2.4版本以前是不支持的,支持从副本进行数据读取,是2.4版本的新特性。
为了更好地去解释路转粉的原因,我们先了解一下kafka一些内部工作原理。
跟随者读取消息
Kafka的跟随者副本会从首领那里读取消息,是为了保持与首领同步的状态。而同步副本的认定,则是通过跟随者副本和首领副本通信的最大时延(replica.lag.time.max.ms)来进行判定的,默认为10秒。如果首领发生崩溃,其中一个同步的跟随者副本会被提升为新首领副本。
这里有几个名词:
ISR:保持同步的副本
OSR:不同步的副本。最开始所有的副本都在ISR中,在kafka工作的过程中,如果某个副本同步速度慢于replica.lag.time.max.ms指定的阈值,则被踢出ISR存入OSR,如果后续速度恢复可以回到ISR中
AR:包括所有的分区的副本,AR=ISR+OSR
还有两个重要的参数:
极端情况下,设置不完全的首领选举,unclean.leader.election.enable(默认为false)= true,可以从OSR集合中选举leader副本,但会导致数据丢失。
min.insync.replicas,这个参数设定ISR中的最小副本数是多少,在acks = -1 or all 时生效,默认值为1。
举个例子,如果一个topic包含3个副本,设置min.insync.replicas = 2,那么至少要存在两个同步副本才能向分区写入数据,否则broker就会停止接受生产者的请求。尝试发送数据的生产者会收到NotEnoughReplicasException异常。消费者仍然可以继续读取已有的数据。直到两个不可用分区中的一个重新变为可用的,才会恢复。
消费者读取消息
ISR中的跟随者副本都要从首领副本中读取消息,只有都同步完成的消息才能供消费者访问。在这个同步的过程中,数据即使已经写入也不能被消费者访问,这个过程是通过LEO-HW机制来实现的。
LogStartOffset:表示每个partition log的第一条消息的位置
LW:LowWatermark的缩写,低水位,代表AR列表中最小的LogStartOffset值
LEO:LogEndOffset的缩写,表示每个partition log的最新一条消息的位置
HW:HighWatermark的缩写,高水位,代表ISR列表中最小的那个LogEndOffset值,消费者可见的最新partition log的位置
还有一种更加复杂的场景,即带事务的场景
LSO:LastStableOffset的缩写,kafka消费端的参数——isolation.level,用来配置消费者的事务隔离级别,默认情况下为 “read_uncommitted”(读未提交的数据),即可以消费到HW(HighWatermark)处的位置。
如果设置为“read_committed”(读已提交的数据),那么消费者就会忽略事务未提交的消息,即只能消费到 LSO(LastStableOffset)的位置。
HW的实现原理
跟随者副本从首领副本读取数据的时候,也会把自己的LEO带上,首领副本收到的时候会更新自己的跟随者副本LEO的数据结构,同时,它也会从自己管理的这些ISR跟随者副本中找到最小的LEO值,即为HW,进而可以判断哪些消息是可以消费的,哪些消息是不可以消费的。
为什么最初Kafka不支持读写分离
毫无疑问,读写分离可以承载更多的系统流量请求,尤其是近实时的读流量请求,用数据库的从库一扛,简直完美。同样,用Kafka的跟随者副本,一样也可以做这个事情。
但有一点,这种读写分离的方式,适用于读多写少,且多维度读取的场景。Kafka的话,如果排除跟随者副本从首领副本读取消息,进行主从同步的场景,基本上只有生产者写和消费者读了,基本上算是读写比例1:1,是真正意义上的读写均衡,且读取维度也很单一,基本上算是顺序写再顺序读,并不符合于适用场景。
那如果消息多了,导致负载高了怎么办?
其实很简单,加broker,加分区,加消费者,无脑横向扩容,提升并行能力呗。反正也没有数据库查询完各个分表,再将结果集进行归并的场景和苦恼,怕什么?
如果支持读写分离了,会引入什么问题?
从以前的消费者和跟随者副本都从首领副本读取消息,变成了跟随者副本从首领副本读取消息,然后消费者再从跟随者副本读取消息。
数据的读取链路变长了,数据时延肯定也变长了,虽然消息队列本身就是最终一致性的,但我们肯定也不希望时延太久不是吗?
消费者是从跟随者副本读取消息的,但如果这个跟随者副本最初是在ISR里面,后来由于同步时延高了,被踢到OSR里面怎么处理?肯定是有处理方案的,但确实又增加了复杂度。
也就是说,Kafka本身具备很好的横向扩容的能力,能够把系统负载和单分区的消息处理量降下来,那么再开发副本读写分离的就有些鸡肋了,同时还会增加消息的到达时延以及代码复杂度,算起来投入产出比不高。
为什么后来路转粉了
Kafka在2.4版本的时候,推出了新特性,KIP-392: 允许消费者从最近的跟随者副本获取数据。这样,就等于给了我们读写分离的选择。
那为什么路转粉了呢?
因为有了这样的适用场景,kafka存在多个数据中心,而数据中心存在于不同机房,当其中一个数据中心需要向另一个数据中心同步数据的时候,如果只能从首领副本进行数据读取的话,需要跨机房来完成,而这些流量带宽又比较昂贵,而利用本地跟随者副本进行消息读取就成了比较明智的选择。
所以kafka推出这一个功能,目的并不是降低broker的系统负载,分摊消息处理量,而是为了节约流量资源。
具体配置
在broker端,需要配置参数 replica.selector.class,其默认配置为LeaderSelector,意思是:消费者从首领副本获取消息,改为RackAwareReplicaSelector,即消费者按照指定的rack id上的副本进行消费。还需要配置broker.rack参数,用来指定broker在哪个机房。
在consumer端,需要配置参数client.rack,且这个参数和broker端的哪个broker.rack匹配上,就会从哪个broker上去获取消息数据。
全文完。