Kafka架构深入——Kafka消费者

 

目录

Kafka架构深入——Kafka消费者

消费者定义

消费方式

分区分配策略

RoundRobin分配策略(面向分区)

Range strategy分配策略(面向主题)

消费者Offset的存储

Kafka 的高效读写机制

顺序写磁盘

零复制技术

Zookeeper在Kafka中的作用

Broker注册

Topic注册

参考

Kafka架构深入——Kafka消费者

消费者定义

简单说一下什么是消费者。

其实消费者就是从Kafka集群当中消费数据的客户端。当然,一个消费者的消费能力肯定是有限的,所以,当生产者产生数据过快时,单个消费者的消费能力达到上限,导致了Kafka集群消费数据的堆压。从而也就产生了消费者组的概念(一个不够,一群来凑....)。一个消费者组具有若干个消费者(具有相同的group.id),我们可以将消费者组看成是一个大的消费者,同一个topic的分区只能被组内的一个消费者消费一次,不允许多次消费。

注意点:

一个topic可以被 多个消费者组消费,但是每个消费者组消费的数据是 互不干扰 的,也就是说,每个消费者组消费的都是 完整的数据 。

一个分区只能被 同一个消费组内 的一个消费者组消费,而 不能给多个消费者 消费,也就是说如果你某个消费者组内的消费者数比该Topic的分区数还多,那么多余的消费者是不起作用的,即空闲状态。

消费方式

consumer 采用 pull(拉取)模式从 broker 中读取数据。

push(推)模式很难适应消费速率不同的消费者,因为消费发送速率是由broker决定的。它的目的是尽可能以最快速度传递消息,但是这样很容易造成consumer来不及处理消息,典型的表现就是拒绝服务以及网络拥塞。而pull模式则可以根据consumer的消费能力以适当的速率进行消费消息。

pull模式不足之处:如果kafka没有数据,消费者可能会陷入循环中,一直返回空数据。针对这一点,Kafka的消费者在消费数据时会传入一个时长参数timeout,如果当前没有数据可供消费,consumer会等待一段时间之后再返回,这段时长即为timeout。

分区分配策略

一个 consumer group 中有多个 consumer,一个 topic 有多个 partition,所以必然会涉及到 partition 的分配问题,即确定哪个 partition 由哪个 consumer 来消费。
Kafka 有两种分配策略,一个是 RoundRobin,一个是 Range默认为range,当消费者组内消费者发生变化时,会触发分区分配策略(方法重新分配)。

目前我们还不能自定义分区分配策略,只能通过partition.assignment.strategy参数选择 range 或 roundrobin。partition.assignment.strategy参数默认的值是range。

RoundRobin分配策略(面向分区)

RoundRobin的大致策略:将所有主题的分区组成TopicAndPartition列表,即主题+分区的命名,然后对于这个列表按照hashCode进行排序,然后轮询交给消费者组内的消费者。源码是这样的:

val allTopicPartitions = ctx.partitionsForTopic.flatMap { case(topic, partitions) =>
  info("Consumer %s rebalancing the following partitions for topic %s: %s"
       .format(ctx.consumerId, topic, partitions))
  partitions.map(partition => {
    TopicAndPartition(topic, partition)
  })
}.toSeq.sortWith((topicPartition1, topicPartition2) => {
  /*
   * Randomize the order by taking the hashcode to reduce the likelihood of all partitions of a given topic ending
   * up on one consumer (if it has a high enough stream count).
   */
  topicPartition1.toString.hashCode < topicPartition2.toString.hashCode
})

RoundRobin 轮询方式将所有主题的分区作为一个整体进行 hash 排序,消费者组内分配分区个数最大差别为1,是按照组来分的,可以解决多个消费者消费数据不均衡的问题。

假如存在一个topic  T1,且这个topic有10个分区,消费者组内存在两个消费者C1、C2,每个消费者有两个消费线程。

按照 hashCode 排序完的topic-partitions组依次为T1-5, T1-3, T1-0, T1-8, T1-2, T1-1, T1-4, T1-7, T1-6, T1-9,我们的消费者线程排序为C1-0, C1-1, C2-0, C2-1,最后分区分配的结果为:

C1-0 将消费 T1-5, T1-2, T1-6 分区;

C1-1 将消费 T1-3, T1-1, T1-9 分区;

C2-0 将消费 T1-0, T1-4 分区;

C2-1 将消费 T1-8, T1-7 分区;

这里顺便放一段官方的解释,大家可以理解一下:

RoundRobin:
The round-robin partition assignor lays out all the available partitions and all the available consumer threads. It then proceeds to do a round-robin assignment from partition to consumer thread. If the subscriptions of all consumer instances are identical, then the partitions will be uniformly distributed. (i.e., the partition ownership counts will be within a delta of exactly one across all consumer threads.)

注意!!!!

若需要使用RoundRobin策略需要满足两个前提条件

  • 同一个Consumer Group里面的所有消费者的num.streams(消费线程数)必须相等。这是因为如果线程数量不一致,导致不同消费者消费的数量不一致,违背了消费者组内分配分区个数最大差别为1,依旧是导致了数据分配不均匀问题;
  • 每个消费者订阅的主题必须相同(如果主题不同,可能导致消费错乱的问题,因为经过hashCode,无法将按照主题进行消费)。

如下图所示,consumer0 订阅主题A,consumer1 订阅主题B,将 A、B主题的分区排序后分配给消费者组,TopicB 分区中的数据可能分配到 consumer0 中。

Apache Kafka 原理与架构

Range strategy分配策略(面向主题)

range 方式是按照主题来分的,不会产生轮询方式的消费混乱问题。

Range策略是对每个主题而言的,首先对同一个主题里面的分区按照序号进行排序,并对消费者按照字母顺序进行排序。在我们的例子里面,排完序的分区将会是0, 1, 2, 3, 4, 5, 6, 7, 8, 9;消费者线程排完序将会是C1-0, C2-0, C2-1。然后将partitions的个数除于消费者线程的总数来决定每个消费者线程消费几个分区。如果除不尽,那么前面几个消费者线程将会多消费一个分区。

举个例子,我们有10个分区,3个消费者线程, 10 / 3 = 3,而且除不尽,那么消费者线程 C1-0 将会多消费一个分区,所以最后分区分配的结果看起来是这样的:

C1-0 将消费 0, 1, 2, 3 分区

C2-0 将消费 4, 5, 6 分区

C2-1 将消费 7, 8, 9 分区

假如我们有11个分区,那么最后分区分配的结果看起来是这样的:

C1-0 将消费 0, 1, 2, 3 分区

C2-0 将消费 4, 5, 6, 7 分区

C2-1 将消费 8, 9, 10 分区

假如我们有2个主题(T1和T2),分别有10个分区,那么最后分区分配的结果看起来是这样的:

C1-0 将消费 T1主题的 0, 1, 2, 3 分区以及 T2主题的 0, 1, 2, 3分区

C2-0 将消费 T1主题的 4, 5, 6 分区以及 T2主题的 4, 5, 6分区

C2-1 将消费 T1主题的 7, 8, 9 分区以及 T2主题的 7, 8, 9分区

可以看出,C1-0 消费者线程比其他消费者线程多消费了2个分区,这就是Range strategy的一个很明显的弊端。总结一下就是:当消费者组内订阅的主题越多,分区分配可能就越不均匀,轮询排在前面的消费者可能消费的分区会远远大于后面的消费者。 顺便放个图。

Apache Kafka 原理与架构

顺便给大家放一段官方的解释:

  • Range:
    Range partitioning works on a per-topic basis. For each topic, we lay out the available partitions in numeric order and the consumer threads in lexicographic order. We then divide the number of partitions by the total number of consumer streams (threads) to determine the number of partitions to assign to each consumer. If it does not evenly divide, then the first few consumers will have one extra partition. For example, suppose there are two consumers C1 and C2 with two streams each, and there are five available partitions (p0, p1, p2, p3, p4). So each consumer thread will get at least one partition and the first consumer thread will get one extra partition. So the assignment will be: p0 -> C1-0, p1 -> C1-0, p2 -> C1-1, p3 -> C2-0, p4 -> C2-1

消费者Offset的存储

由于 consumer 在消费过程中可能会出现断电宕机等故障,consumer 恢复后,需要从故障前的位置继续消费。那么,我们可以考虑一下,宕机的consumer是从头开始消费,还是继续上次的offset消费。若是继续消费,那么,offset的存储位置在哪?

答案是:

consumer 需要实时记录自己消费到了哪个 offset,以便故障恢复后继续消费。Kafka 0.9 版本之前,consumer 默认将 offset 保存在 Zookeeper 中,从 0.9 版本开始,consumer 默认将 offset 保存在 Kafka 一个内置的 topic 中,该 topic 为 __consumer_offsets。并且,由于分区会进行rebalance重新分配的情况,因此offset是按照消费者组单位来存储的。而,根据消费者组ConsumerGroup、主题Topic、分区Partition三者确定唯一一个的offset。当消费者组内触发rebalance机制时,消费者组内的其他消费者也可以根据记录的<ConsumerGroup - Topic - Partition>来定位前一个消费者消费到的offset进而接着上次的Offset继续消费,来保证消费端的一致性。

Kafka 的高效读写机制

顺序写磁盘

Kafka的producer生产数据,要写入到log文件中,写的过程是一直追加到文件末端,为顺序写。官网有数据表明,同样的磁盘,顺序写能到600M/s,而随机写只有100K/s。这与磁盘的机械机构有关,顺序写之所以快,是因为其省去了大量磁头寻址的时间。

零复制技术

传统的读取文件数据并发送到网络的步骤如下:
(1)操作系统将数据从磁盘文件中读取到内核空间的页面缓存;
(2)应用程序将数据从内核空间读入用户空间缓冲区;
(3)应用程序将读到数据写回内核空间并放入socket缓冲区;
(4)操作系统将数据从socket缓冲区复制到网卡接口,此时数据才能通过网络发送。

其中能实现零复制技术的重点前提:出现了DMA技术

DMA(Direct Memory Access,直接内存存取) 是所有现代电脑的重要特色,它的出现就是为了解决批量数据的输入/输出问题。它允许不同速度的硬件装置来沟通,而不需要依赖于 CPU 的大量中断负载。否则,CPU 需要从来源把每一片段的资料复制到暂存器,然后把它们再次写回到新的地方。在这个时间中,CPU 对于其他的工作来说就无法使用。 当 DMA 技术的出现,数据文件在各个层之间的传输,则可以直接绕过CPU,使得外围设备可以通过DMA控制器直接访问内存。

有了 DMA 技术的,通过网卡直接去访问系统的内存,就可以实现现绝对的零拷贝了。这样就可以最大程度提高传输性能。通过“零拷贝”技术,我们可以去掉那些没必要的数据复制操作, 同时也会减少上下文切换次数。

Java的零拷贝由 FileChannel.transferTo() 方法实现。transferTo() 方法将数据从 FileChannel 对象传送到可写的字节通道(如Socket Channel等)。在内部实现中,由 native 方法 transferTo() 来实现,它依赖底层操作系统的支持。在 UNIX 和 Linux系统中,调用这个方法将会引起 sendfile() 系统调用。

“零拷贝技术”只用将磁盘文件的数据复制到页面缓存中一次,然后将数据从页面缓存直接发送到网络中(发送给不同的订阅者时,都可以使用同一个页面缓存),避免了重复复制操作。

如果有10个消费者,传统方式下,数据复制次数为4*10=40次,而使用“零拷贝技术”只需要1+10=11次,一次为从磁盘复制到页面缓存,10次表示10个消费者各自读取一次页面缓存。

Zookeeper在Kafka中的作用

先放一张Kafka在zookeeper中的存储结构图(不过据说后续将会将Zookeeper从Kafka中移除...)。

大致作用有:配置管理、负载均衡、命名服务、分布式通知、分布式锁、Controller选举等。

Kafka集群中有一个broker会被选举为Controller,负责管理集群broker的上下线,所有topic的分区副本分配leader选举等工作。

Controller的管理工作都是依赖于Zookeeper的。

Broker注册

Broker是分布式部署并且相互之间相互独立,但是需要有一个注册系统能够将整个集群中的Broker管理起来,此时就使用到了Zookeeper。在Zookeeper上会有一个专门用来进行Broker服务器列表记录的节点:/brokers/ids

每个Broker在启动时,都会到Zookeeper上进行注册,即到/brokers/ids下创建属于自己的节点,如/brokers/ids/[0...N]。

Kafka使用了全局唯一的数字来指代每个Broker服务器,不同的Broker必须使用不同的Broker ID进行注册,创建完节点后,每个Broker就会将自己的IP地址和端口信息记录到该节点中去。其中,Broker创建的节点类型是临时节点一旦Broker宕机,则对应的临时节点也会被自动删除

Topic注册

在Kafka中,同一个Topic的消息会被分成多个分区并将其分布在多个Broker上,这些分区信息及与Broker的对应关系也都是由Zookeeper在维护,由专门的节点来记录,如:/borkers/topics

Kafka中每个Topic都会以/brokers/topics/[topic]的形式被记录,如/brokers/topics/login和/brokers/topics/search等。Broker服务器启动后,会到对应Topic节点(/brokers/topics)上注册自己的Broker ID并写入针对该Topic的分区总数,如/brokers/topics/login/3->2,这个节点表示Broker ID为3的一个Broker服务器,对于"login"这个Topic的消息,提供了2个分区进行消息存储,同样,这个分区节点也是临时节点

Kafka是一个分布式系统,却需要Zookeeper这另一个分布式系统来进行管理,在大规模集群和云原生的背景下,使用ZookeeperKafka的运维和集群性能造成了很大的压力。去除Zookeeper的必然趋势,这也符合大道至简的架构思想。Kafaka计划在3.0版本会兼容Zookeeper ControllerQuorum Controller,这样用户可以进行灰度测试。

 

参考

https://www.iteblog.com/archives/9827.html#522_ISR

https://www.bilibili.com/video/BV1a4411B7V9?p=20

https://www.jianshu.com/p/835ec2d4c170

https://www.cnblogs.com/qingyunzong/p/9007107.html

 

 

 

 

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值