kafka

  虽然一直在使用kafka,但是还没有系统的整理过kafka的原理,学习每个框架都要掌握其应用、原理和设计理念,这样才能举一反三,学为所用。今天就整理一些kafka相关的知识,一是为了加强记忆,二是通过整理再次回顾一下作者的设计思想。

kafka的定位:消息中间件、分布式实时流处理平台:(1)结合hadoop和hbase离线数据库做数据集成(2)实时流计算。

这里先说下kafka几个重要参数:

              (1)heartbeat.interval.ms——consumer每隔多长时间向coordinator发送心跳

              (2)session.timeout.ms——consumer最大检测失效时间,如果在配置时间内coordinator没有收到该consumer的心跳,

       则将该consumer从group中移除,从而触发消费组对消费分区的重新分配(rebalance)

              (3)max.poll.records——一次poll的最大消息条数

              (4)max.poll.interval.ms——两次poll之间的最大间隔时间,如果两次poll超过配置时间,coordinator会将将该consumer从group中移除,

       从而触发消费组对消费分区的重新分配(rebalance)

1、kafka整体设计

                            

   借用官网一张图来说明各个角色的定义:

  Producer:Producer即生产者,消息的产生者,是消息的入口。

  Broker:Broker是kafka实例,每个服务器上有一个或多个kafka的实例。每个kafka集群内的broker都有一个唯一的编号。
  Topic:消息的主题,可以理解为消息的分类,kafka的数据就保存在topic。在每个broker上都可以创建多个topic。
  Partition:Topic的分区,每个topic可以有多个分区,分区的作用是做负载,提高kafka的吞吐量。同一个topic在不同的分区的数据是不重复的,partition的表现形式就是一个一个的文件夹。文件夹中包括消息文件,还有两个索引文件:一个以消息在磁盘的偏移量(offset)为索引,一个以消息发送时间为索引。

  Segment:也叫段,是对partition的进一步细化,根据文件大小将分区进行拆分,一个为了进一步提高对同一个分区的读写能力,二是为了方便根据索引文件进行查找。

                                                         

   Replication:每一个分区都有多个副本,副本的作用是做备胎。当主分区(Leader)故障的时候会选择一个备胎(Follower)上位,成为Leader。在kafka中默认副本的最大数量是10个,且副本的数量不能大于Broker的数量,follower和leader绝对是在不同的机器,同一机器对同一个分区也只可能存放一个副本(包括自己)。只有leader分区可以支持读写,也就是生成者只能向leader分区写入,消费者只能从leader分区读取,其他副本只是备份(高可用),这样设计避免了复杂的主从同步导致的读写不一致问题。

  Message:消息存储在log文件中,包含消息体、消息大小、offset、压缩类型等。

    Offset:一个占8byte的有序id号,唯一标识消息的位置。

    消息大小:消息大小占4byte,标识消息的大小。

    消息体:被压缩过的具体消息内容。
  Consumer:消费者,即消息的消费方,是消息的出口。
  Consumer Group:可以将多个消费组组成一个消费者组,同一个分区的数据只能被消费者组中的某一个消费者消费。同一个消费者组的消费者可以消费同一个topic的不同分区的数据,这也是为了提高kafka的吞吐量。(举个例子:一个消费者组中3个消费者,一个topic中3个分区,则3个消费者各自消费一个分区,如果该组中4个消费者,则有一个消费者不会被分配任何分区,如果该组中2个消费者,则有一个消费者会消费两个分区,这么设计也是为了避免争抢产生的并发问题)。消费者的设计主要有两个作用:一是一个分区只能被同一个组中的某一个消费者消费,避免竞争;二是以消费者组维度来记录分区被消费的位置,避免重复消费。
  Zookeeper:kafka集群依赖zookeeper来保存集群的的元信息,来保证系统的可用性。

2、生产者

  Kafka将发送的消息先存储在缓冲区(其实就是一个ConcurrentHashMap)中,然后批量发送,由两个参数控制:(1)消息达到n条发送(2)如果没达到n条但是达到指定时间发送。

1、发送流程

      

(1)拦截器,实现ProducerInterceptor,可以针对消息发送的生命周期实现特定逻辑

(2)累加器就是一个ConcurrentHashMap(也有叫页缓存),用来在内存中缓存消息。

2、如何确认消息发送成功

       leader分区向Producer返回ACK即确认消息发送成功,可以设置ack属性。

(1)pros.put(“ack”,0),ack=0,不需返回ACK

(2)pros.put(“ack”,1),ack=1,只要leader落盘就返回ACK

(3)pros.put(“ack”,-1),ack=-1或all,leader和所有有效follower全部落盘才返回ACK,设置ACK时要注意重试次数会不会导致消息重复问题,pros.put(“retries”,n)

       什么是有效副本呢?

(1)ISR:in-sync replica set(有效副本集合)

(2)replica.lag.time.max.ms(超过该时间没有向leader同步数据,则会被移除ISR,等恢复了再加入到ISR)

3、消息存储

    

  一个topic中各个leader分区均匀地分布在集群中的各个broker上,这样做的目的:

  (1)均衡broker的请求压力,因为leader分区承担读写任务,follower副本只同步leader的数据。

       (2)防止leader分区过度集中在某个broker上,一旦该broker出现故障,会出现多个leader分区选举行为。

       具体算法:所有broker组成brokerList(同一个集群中的每个broker有一个唯一编号),第一个leader分区从brokerList随机选一个broker,第二个leader分区选择顺序下一个broker,以此类推,follower副本是随机分布的。例如:3分区,2副本,3broker

    

   Part0-leader随机选择了broker2,则part1-leader会放在broker3上,part2-leader会轮回放在broker1上,其他副本会随机分配。

 分区的拆分:

  为防止一个log文件过大,导致索引查找和读取较慢,分区会拆分成segment(段),拆分规则:

    (1)按时间拆分(默认1周):log.roll.hour/ms

    (2)按大小拆分(默认1G):log.segment.bytes

    (3)按索引大小拆分:log.index.size.max.bytes

      

  偏移量就是消息在log文件中的具体位置,偏移量索引采用的稀疏索引,如:

       稀疏度设置:log.index.interval.bytes(默认4kB),稀疏度越小索引越密集,检索越快,但是会消耗更多存储空间。

       时间索引中的消息时间可以使用producer发送的时间或是消息的落盘时间。

消息清理策略:

       开关:log.cleaner.enable=true

       策略:log.cleanup.policy=delete(默认)/compact——压缩

    compact的做法:产出重复的key,只保留key对应的最新的值。

       周期:log.retention.check.interval.ms=300000(默认5分钟),定时任务每5分钟检查一次,清理过期消息。

  清理条件:

         (1)基于时间:默认168小时(一周),即一周前的日志为过期日志

         (2)基于文件大小:默认不限制大小

 4、高可用之leader副本选举

  早期kafka使用zk进行leader选举(使用zk的watch机制和临时有序节点实现)。

       后期放弃的原因:当分区和副本较多时,会在zk上创建大量的节点,一旦某个broker挂掉,会触发大量的watch事件,产生惊群效应。

1、Broker Controller(控制器)

       分区副本的选举由Broker Controller(也是一个broker)处理,那么集群中哪个broker可以成为Controller呢?这就涉及到Controller的选举:

       集群中的broker在启动时会到zk上创建临时节点/controller,创建成功的则会成为Controller(一般第一个启动的broker会成为Controller)。其他的broker创建节点失败,会注册watch事件,一旦当前Controller出现故障就会触发所有broker注册的watch事件,其他节点又会争抢着创建/controller节点,创建成功的则成为新的Controller。

       集群中每选举一次控制器,就会通过zookeeper创建一个controller epoch,每一个选举都会创建一个更大,包含最新信息的epoch,如果有broker收到比这个epoch旧的数据,就会忽略它们,kafka也通过这个epoch来防止集群产生“脑裂”。

2、分区副本的选举

       ISR:同步正常的副本集合

       OSR:同步异常的副本集合

    

   只有ISR集合中副本可以参与选举,如果ISR为空,此时如果“允许非正常副本参与选举”开关打开(默认false),也会从OSR中进行选择,但可能会造成数据丢失。

选举算法的选择:

  paxos——>由paxos演变而来的raft,zab(zk使用),这类算法概括一下:先到先得,少数服从多数。

       Kafka没有使用类似raft这种算法,原因:这种算法依赖于节点间的通信,一旦节点间不能正常通信,可能发生脑裂,出现多个leader副本。

       了解kafka选举算法要先了解几个概念:

         LEO(Log End Offset):下一条等待写入的消息的偏移量offset(最新的offset+1)

         HW(High Watermark):ISR中最小的LEO

         HW的作用:消费者消费offset小于HW的消息(确保主从消息一致,但是可能会丢消息)。

      

    使用HW限制消费区间的原因:为了防止leader副本挂掉,消息被超前消费。如果不限制,如果leader故障,消费者已经消费了offset=7的消息,但是其他的replica还没来得及同步offset=7的消息,当replica0或replica1成为新的leader就会出现消息被超前消费的问题,这也是kafka保证主从同步一致的一种手段。

主从如何保持同步:

       (1)follower节点向leader节点发送一个fetch请求,leader向follower发送数据

       (2)follower接收到数据响应后,依次写入消息并更新自己的LEO

       (3)leader更新HW(ISR中最小的LEO)

故障处理:

  (1)follower故障:

        follower故障恢复后,开始从leader同步消息,此时它首先会截掉本地记录的HW之后的消息,避免消息不一致。比如replica0故障恢复后,会截掉offset=5 和6的消息,再从5开始从leader同步,等追上之后又会加入到ISR集合中。

  (2)leader故障:

           选取ISR中顺位第一的follower成为新leader(类似于微软的pacificA算法)

5、消费者

  kafka消息拉取:只支持pull方式,不支持push方式(因为kafka的定位就是处理大量数据)。kafka提供了一个简洁的poll方法来拉取消息,但是其实现了协作、分区重平衡、心跳、数据拉取等功能。

消费者如何记录当前消费位置:

  消息在.log文件中都是顺序存储的,每个消息都有一个偏移量(offset),每个消费者组消费到当前分区的哪个位置都会记录下来,避免消费者重启消息又从头开始消费。消费者组在各个分区的消费位置会被存储在一个特定的topic中(_consumer_offsets_*),该topic默认50个分区,消费者组与分区位置具体放在哪个这50个分区中的哪个分区呢?是将topic名称进行hash后对50取模,所以如果将消费者组换个名称,则整个组将重新消费。

  消费组可以选择从当前位置或从头开始消费消息

  位移提交commit:更新消费的offset

       由消费端enable.auto,commit属性设置:

    true:调用poll方法后每个5秒(有auto.commit.interval.ms指定)提交一次位移

    false:手动提交,consumer.commitAsync()异步提交,consumer.commitSync()同步提交。

      同步提交:发起提交时调用方会阻塞,如果服务器返回提交失败会重试,直到成功或抛异常(适合消息量不太大,且对重复消费限制较严格的场景)

      异步提交:调用方不会阻塞,提交失败不会重试,但可以通过callback判断本次提交成功/异常(适合消息量大,允许重复消费的场景)

  指定位移消费Seek,可以追踪之前的消费或回溯消费

消费的分区:

  1、分区策略

  (1)RangeAssignor分配策略(默认分配策略)

    RangeAssignor策略的原理是按照消费者总数和分区总数进行整除运算来获得一个跨度,然后将分区按照跨度进行平均分配,以保证分区尽可能均匀地分配给所有的消费者。对于每一个topic,RangeAssignor策略会将消费组内所有订阅这个topic的消费者按照名称的字典序排序,然后为每个消费者划分固定的分区范围,如果不够平均分配,那么字典序靠前的消费者会被多分配一个分区。

    假设有10个分区,3个消费者,排完序的分区将会是0, 1, 2, 3, 4, 5, 6, 7, 8, 9;消费者线程排完序将 会是C1-0, C2-0, C3-0。然后将partitions的个数除于消费者线程的总数来决定每个消费者线程消费几个 分区。如果除不尽,那么前面几个消费者线程将会多消费一个分区。在我们的例子里面,我们有10个分 区,3个消费者线程,10 / 3 = 3,而且除不尽,那么消费者线程 C1-0 将会多消费一个分区的结果看起来是这样的:

          C1-0 将消费 0, 1, 2, 3 分区
          C2-0 将消费 4, 5, 6 分区
          C3-0 将消费 7, 8, 9 分区  

     这个分区也有一个缺点,如果topic特别多,那么第一个消费者可能会多消费很多个分区,压力会增加。

  (2)RoundRobinAssignor(轮询分区)

    轮询分区策略是把所有partition和所有consumer线程都列出来,然后按照hashcode进行排序。最后通 过轮询算法分配partition给消费线程。如果所有consumer实例的订阅是相同的,那么partition会均匀 分布。

    如果同一个消费组内所有的消费者的订阅信息都是相同的,那么RoundRobinAssignor策略的分区分配会是均匀的。举例,假设消费组中有2个消费者C0和C1,都订阅了主题t0和t1,并且每个主题都有3个分区,那么所订阅的所有分区可以标识为:t0p0、t0p1、t0p2、t1p0、t1p1、t1p2。最终的分配结果为:
          消费者C0:t0p0、t0p2、t1p1

          消费者C1:t0p1、t1p0、t1p2

    如果同一个消费组内的消费者所订阅的信息是不相同的,那么在执行分区分配的时候就不是完全的轮询分配,有可能会导致分区分配的不均匀。如果某个消费者没有订阅消费组内的某个topic,那么在分配分区的时候此消费者将分配不到这个topic的任何分区。

    比如消费组内有3个消费者C0、C1和C2,它们共订阅了3个主题:t0、t1、t2,这3个主题分别有1、2、3个分区,即整个消费组订阅了t0p0、t1p0、t1p1、t2p0、t2p1、t2p2这6个分区。具体而言,消费者C0订阅的是主题t0,消费者C1订阅的是主题t0和t1,消费者C2订阅的是主题t0、t1和t2,那么最终的分配结果为:

          消费者C0:t0p0

          消费者C1:t1p0

          消费者C2:t1p1、t2p0、t2p1、t2p2

    所以当消费组内的消费者订阅不同主题时,轮询分配不是很完美。

  (3)StrickyAssignor 分配策略(粘滞策略)

    它主要有两个目的:

      分区的分配尽可能的均匀

      分区的分配尽可能和上次分配保持相同

      当两者发生冲突时, 第一个目标优先于第二个目标。它的实现比前两种策略都更加复杂,但是分配结果更加优异。

  2、分区再平衡(Rebalance)

  (1)由谁来执行 Rebalance 操作?由谁来管理消费端 consumer 的 group ?

    Kafka 提供了一个 角色:Coordinator。Coordinator 来完成对消费端 group 的管理。当 consumer group 的第一个 consumer启动的时候,它会去和 Kafka Server 去确定到底谁是它们组的 Coordinator。之后该 group 组内的所有成员都会和该 Coordinator 进行通信。

  (2)consumer group 如何确定自己的 Coordinator是谁?

    当消费者向 kafka 集群中的任意一个 broker 发送一个 GroupCoordinatorRequest 请求,Kafka Server 服务端会返回一个当前负载最小的 broker 节点的 id,并将该 id 所对应的的 broker 节点设置为当前 consumer group 的 Coordinator。
  (3)Rebalance过程

    整个rebalance的过程分为两个步骤:①JoinGroup過程 和 ②Synchronizing Group State 阶段。

  第一步:确定 Coordinator

  第二步:JoinGroup过程,表示消费者加入到consumer group中的过程。

    当确定了 Coordinator 之后,所有的 Consumer 都会向 Coordinator 发送一个 JoinGroup 请求(只要启动,所有消费者都会发送该请求)。此时 Coordinator 会从 Consumer group 中选取一个 consumer 担任 leader 角色,并把组成员信息和订阅的消息发送给所有的消费组
      

  第三步:Synchronizing Group State 阶段 

    完成分区分配之后,就进入了 Synchronizing Group State阶段。该阶段主要完成 leader 将消费者对应的 partition 分配方案同步给consumer group 中的所有 consumer。每一个消费者,都会向 Coordinator 发送一个 SyncGroupRequest 请求。请求内容:包含group_id、member_id、generation_id 。在 leader 层面,还会有一个 member_assignment 内容。

    每个消费者,还会向 Coordinator 发送 SyncGroup 请求,不过只有 leader 节点会发送分配方案,其他消费者也会发送分配方案,不过发送内容都是空,只是打打酱油而已。当 leader 把方案发给 Coordinator 以后,Coordinator 会把结果设置到 SyncGroupResponse 中。这样所有成员都知道自己应该消费哪个分区。
      

    Kafka 每个客户端,在收到分发策略 SyncGroupResponse 后,会根据返回结果去执行。consumer group 的分区分配方案是在客户端执行的。Kafka 将这个权利下放给客户端主要是因为这样做可以有更好的灵活性。

6、kafka的有点

(1)高吞吐、低延迟

       从底层数据处理来看kafka能够快速处理海量数据的原因:

       首先理解一个硬件相关的概念,DMA,它是协处理器,主要是代理CPU处理数据传输,来减少CPU在数据I/O上的等待时间。

       来看一个kafka consumer消费数据的过程: 

          File.read(fileDesc, buf, len);

          Socket.send(socket, buf, len);

  在这个过程中,数据一共发生了四次传输的过程。其中两次是 DMA 的传输,另外两次,则是通过 CPU 控制的传输。

    第一次传输,是从硬盘上,读到操作系统内核的缓冲区里。这个传输是通过 DMA 搬运的。

    第二次传输,需要从内核缓冲区里面的数据,复制到我们应用分配的内存里面。这个传输是通过 CPU 搬运的。

    第三次传输,要从我们应用的内存里面,再写到操作系统的 Socket 的缓冲区里面去。这个传输,还是由 CPU 搬运的。

    最后一次传输,需要再从 Socket 的缓冲区里面,写到网卡的缓冲区里面去。这个传输又是通过 DMA 搬运的。

       Kafka通过调用java的NIO库,将4次传输减少到2次,具体过程如下:

    第一次,是通过 DMA,从硬盘直接读到操作系统内核的读缓冲区里面。

    第二次,则是根据 Socket 的描述符信息,直接从读缓冲区里面,写入到网卡的缓冲区里面。

   传统IO:

        

  kafkaIO:

        

(2)、高伸缩性

       分区partition机制,通过增加分区可以进行横向扩展。

(3)、持久性、可靠性

       副本机制,每个分区都会有副本,leader分区挂了可以使用副本(follower分区)保证数据不丢失。

(4)、容错性

       副本的选举功能,leader分区挂了,follower分区会重新选举出leader。

(5)、高并发

  (1)页缓存,消息先保存在kafka系统内存中,满足条件再一次性发送到broker

  (2)磁盘顺序写入

  (3)零拷贝(也就是第一点说的数据传输)

 

 

    

posted @ 2021-07-25 15:33  jingyi_up  阅读(434)  评论(0编辑  收藏  举报