kafka总结

简介

Kafka是由Apache软件基金会开发的一个开源流处理平台

Kafka是一种高吞吐量的分布式发布订阅消息系统

Kafka 是linkedin 公司用于日志处理的分布式消息队列,同时支持离线和在线日志处理。kafka 对消息保存时根据Topic进行归类,发送消息者称为Producer,消息接受者成为Consumer,此外kafka 集群有多个kafka 实例组成,每个实例(server)称为broker。无论是kafka集群,还是producer和consumer 都依赖于zookeeper 来保证系统可用性,为集群保存一些meta 信息。

 

官网地址:

http://kafka.apache.org/

https://kafka.apachecn.org/,正文文档有点滞后

特点

编程语言

由Scala和Java编写。

吞吐量

kafaka可以轻松达到百万级的TPS

不能保证顺序的场景

  • 多分区生产, 单线程消费
  • 多分区生产,多线程消费

消费数据 方式

由消费者主动拉取进行消费

持久性

kafka使用文件存储消息,这就直接决定了kafka在性能上严重依赖于文件系统本身的性能。而且,无论在任何OS下,对文件系统本身的优化几乎没有了改进的可能。文件缓存/直接内存映射是常用的提高文件系统性能的手段。因为kafka是对日志文件进行append操作,因此磁盘检索的开销还是比较少的,同时,为了减少磁盘写入的次数,broker会暂时将消息buffer起来,当消息数量达到了一个阈值,在写入到磁盘,这样就可以减少磁盘IO的次数。

性能

我们需要考虑网络IO,这直接关系到kafka的吞吐量问题。对于producer端而言,可以将消息buffer起来,当消息数量达到一定的阈值时,批量发送给broker;对于consumer端而言,也可以批量的fatch消息,不过消息量的大小是可以通过配置文件进行配置的。对于kafka broker端,sendfile系统调用可以潜在的提高网络IO性能:将文件数据映射到系统内存中,socket直接读取相应的内存区域即可,而不需要进程再次进行copy和交换。对于producer、consumer、broker而言,CPU的开支都不大,因此启用消息压缩机制是一个很好的策略;压缩需要消耗少量的CPU资源,不过对于kafka而言,网络IO应该更为重要。可以通过将任何在网路上传输的消息进行压缩来提高网络IO性能。kafka支持多种压缩方式(gzip/snappy)。

应用场景

日志收集:

可以用Kafka收集各种服务的日志信息,消费端来统一将日志进行管理或进行分析处理。

系统解耦

减少服务之间的互相影响

异步

作为异步的一种方式来使用,比如发送注册邮件和短信

流量削峰

利用消息队列特性对消息进行逐步处理,避免大量请求对系统造成太大压力

 

名词解释

broker

kafka集群中的服务器都叫做broker

controller:kafka 集群中的其中一个服务器,用来进行 leader选举以及各种 failover。

Topics

一个topic可以认为是一类消息

kafka对消息保存时根据Topic进行归类

一个Topic可以分布在多个Broker上。

partition

每个topic可以被划分成多个partition(区),至少被切分为1个Partition。

每个partition在存储层面是append log文件。

partiton命名规则为topic名称+有序序号,第一个partiton序号从0开始,序号最大值为partitions数量减1。

一个topic的多个partition被分配在kafka集群的多个broker上,每个broker负责partitions中消息的读写操作

任何发布到此partition的消息都会被直接追加到log文件的尾部,每条消息在文件中的位置称为offset(偏移量),offset为一个Long型数字,他是log中一条消息的唯一标识。kafka并没有提供其他额外的索引机制来存储offset,因为kafka的消息读写机制是顺序读写,保证kafka的吞吐率,几乎不允许(可以)对消息进行随机读取。

 

partition的设计目的有多个,最根本的原因是kafka基于文件存储,通过分区,可以将日志内容分布到多个broker上,避免文件尺寸达到单机的存储上限。可以将一个topic划分成多个partitions,这样既可以降低对单机磁盘容量的要求,又可以提高系统消息的读写速率。此外,越多的partition意味着可以容纳更多的consumer,更有效的提升并发性能。

Offset

消息在Partition中的编号,编号顺序不跨Partition。

消费者在消费的过程中需要记录自己消费了多少数据,即消费位置信息。在Kafka中这个位置信息有个专门的术语:位移(offset)。

很多消息引擎都把这部分信息保存在服务器端(broker端)。这样做的好处当然是实现简单,但会有三个主要的问题:1. broker从此变成有状态的,会影响伸缩性;2. 需要引入应答机制(acknowledgement)来确认消费成功。3. 由于要保存很多consumer的offset信息,必然引入复杂的数据结构,造成资源浪费。而Kafka选择了不同的方式:每个consumer group保存自己的位移信息,那么只需要简单的一个整数表示位置就够了;同时可以引入checkpoint机制定期持久化,简化了应答机制的实现。

Replication

Kafka支持以Partition为单位对Message进行冗余备份,每个Partition都可以配置至少1个Replication(当仅1个Replication时即仅该Partition本身)。

每个partition将会被备份到多个broker中,这样就增强了系统的可靠性。

 

基于replicated方案,那么就意味着需要对多个备份进行调度,每个partition都有一个broker为“leader”,其余都为“follower”。leader负责所有的读写操作,如果leader失效,那么将会有其他的follower被选举为新的leader;follower只是单纯的和leader进行消息同步即可。由此可见,部署leader的broker承载了partition全部的请求压力,因此,从集群的整体角度考虑,有多少个partition,就有多少个leader,kafka将会将这些leader均衡的分配在broker上,来确保集群整体的吞吐量和稳定性。

 

Producer

kafka有两类客户端,一类叫producer(消息生产者),一类叫做consumer(消息消费者),客户端和broker服务器之间采用tcp协议连接

Producer将消息发布到指定的topic中,同时,producer还需要指定该消息属于哪个partition

Consumer

本质上kafka只支持topic,每一个consumer属于一个consumer group,每个consumer group可以包含多个consumer。发送到topic的消息只会被订阅该topic的每个group中的一个consumer消费。

如果所有的consumer都具有相同的group,这种情况和queue很相似,消息将会在consumer之间均衡分配;

如果所有的consumer都在不同的group中,这种情况就是广播模式,消息会被发送到所有订阅该topic的group中,那么所有的consumer都会消费到该消息。

kafka的设计原理决定,对于同一个topic,同一个group中consumer的数量不能多于partition的数量,否则就会有consumer无法获取到消息。

同一个consumer可以消费多个topic.

 

Guarantees

发送到partition中的消息将会按照它的接受顺序追加到日志中;

对于消费者而言,它的消息消费顺序是和日志中的顺序一致的;

如果partition的replicationfactor为N,那么就允许N-1个broker失效。

Consumer Group

Consumer Group,逻辑上的概念,是Kafka实现单播和广播两种消息模型的手段。同一个topic的数据,会广播给不同的group;同一个group中的worker,只有一个worker能拿到这个数据。换句话说,对于同一个topic,每个group都可以拿到同样的所有数据,但是数据进入group后只能被其中的一个worker消费。group内的worker可以使用多线程或多进程来实现,也可以将进程分散在多台机器上,worker的数量通常不超过partition的数量,且二者最好保持整数倍关系,因为Kafka在设计时假定了一个partition只能被一个worker消费(同一group内)。

组内必然可以有多个消费者或消费者实例(consumer instance),它们共享一个公共的ID,即group ID

组内的所有消费者协调在一起来消费订阅主题(subscribed topics)的所有分区(partition)。当然,每个分区只能由同一个消费组内的一个consumer来消费。

 

1,以consumer group为单位订阅 topic,每个consumer一起去消费一个topic;

2,consumer group 通过zookeeper来消费kafka集群中的消息(这个过程由zookeeper进行管理);

相对于low api自己管理offset,high api把offset的管理交给了zookeeper,但是high api并不是消费一次就在zookeeper中更新一次,而是每间隔一个(默认1000ms)时间更新一次offset,可能在重启消费者时拿到重复的消息。此外,当分区leader发生变更时也可能拿到重复的消息。因此在关闭消费者时最好等待一定时间(10s)然后再shutdown。

  • consumer group 设计的目的之一也是为了应用多线程同时去消费一个topic中的数据。

 

Consumer Group怎么分

rebalance

rebalance本质上是一种协议,规定了一个consumer group下的所有consumer如何达成一致来分配订阅topic的每个分区。比如某个group下有20个consumer,它订阅了一个具有100个分区的topic。正常情况下,Kafka平均会为每个consumer分配5个分区。这个分配的过程就叫rebalance。

什么时候rebalance

rebalance的触发条件有三种:

1、组成员发生变更(新consumer加入组、已有consumer主动离开组或已有consumer崩溃了——这两者的区别后面会谈到)

2、订阅主题数发生变更——这当然是可能的,如果你使用了正则表达式的方式进行订阅,那么新建匹配正则表达式的topic就会触发rebalance

3、订阅主题的分区数发生变更

 

如何进行组内分区分配

group下的所有consumer都会协调在一起共同参与分配,这是如何完成的?Kafka新版本consumer默认提供了两种分配策略:range和round-robin。当然Kafka采用了可插拔式的分配策略,你可以创建自己的分配器以实现不同的分配策略。实际上,由于目前range和round-robin两种分配器都有一些弊端,Kafka社区已经提出第三种分配器来实现更加公平的分配策略,只是目前还在开发中。我们这里只需要知道consumer group默认已经帮我们把订阅topic的分区分配工作做好了就行了。

 

ISR

isr 的全称是:In-Sync Replicas isr 是一个副本的列表

与leader副本保持一定程度同步的副本(包括Leader)组成ISR,当follower副本落后太多或者失效时,leader副本会吧它从ISR集合中剔除。

ISR集合中的副本才有资格被选举为leader,而在OSR集合中的副本则没有机会(这个原则可以通过修改对应的参数配置来改变)

 

条件1:根据副本和leader 的交互时间差,如果大于某个时间差 就认定这个副本不行了,就把此副本从isr 中剔除,此时间差根据

配置参数rerplica.lag.time.max.ms=10000 决定 单位ms

 

条件2:根据leader 和副本的信息条数差值决定是否从isr 中剔除此副本,此信息条数差值根据配置参数rerplica.lag.max.messages=4000 决定 单位条

 

isr 中的副本删除或者增加都是通过一个周期调度来管理的,ISR在ZooKeeper中维护

 

__consumer_offsert

__consumer_offsert 有50个分区,通过将group的id哈希值%50的值来确定要保存到那一个分区.  这样也是为了考虑到zookeeper不擅长大量读写的原因

内部保存的消费者的偏移量信息

 

关系

一个topic 可以配置几个partition

produce发送的消息分发到不同的partition中

consumer接受数据的时候是按照group来接受,kafka确保每个partition只能同一个group中的同一个consumer消费,如果想要重复消费,那么需要其他的组来消费。Zookeerper中保存这每个topic下的每个partition在每个group中消费的offset

新版kafka把这个offsert保存到了一个__consumer_offsert的topic下

 

Zookeeper在kafka的作用

Kafka有自带zookeeper,我们可以自己决定用它自带的还是使用我们已经有的zookeeper

1、broker的leader选举,broker的节点存活状态监控,topic注册,topic和broker关系维护

2、作为其分布式协调框架,将消息生产、消息存储、消息消费的过程结合在一起。

3、实现生产者与消费者的负载均衡。

 

早期版本的kafka用zk做集群元数据存储,消费者的消费状态(offset)订阅的topic等,group的管理。考虑到zk本身的一些因素以及整个架构较大概率存在单点问题,新的版本中减少了对zk的依赖,zk就主要负责broker的leader选举,检查broker的存活等

offset在0.1之前保存在zk中,0.1之后保存在一个topic中,由于zk写的性能不高,会影响消息消费的效率;

原理

生产者生产消息过程

  负载均衡:生产者将会和topic下的所有partition leader保持socket连接;消息由生产者直接通过socket发送个broker,中间不会经过任何“消息路由”,实际上,消息被发送给哪个broker由生产者端决定。如果一个消息有多个partitions,那么在producer端实现消息“均衡分发”是很有必要的。

  其中partition leader位置(host:port)保存在zookeeper中,生产者作为zookeeper client,已经注册了watch用来监听partition leader的变更事件。

  异步发送:将多条消息暂且保存在producer的buffer中,当达到一定的数量阈值时,将他们一起批量发送给broker,延迟批量发送实际上是提高了网络效率。不过也存在一些隐患,比如当producer失效时,那些尚未发送出去的消息将会丢失。

消费者消费消息过程

  consumer端向broker发送fatch请求,并告诉其获取消息的offset,此后,consumer会获得一定数量的消息,consumer端也可以通过重置offset来重新获取想要的消息。

在kafka中,采用的是pull模型,即consumer在和broker建立连接后,主动去pull(也就是fatch)消息;这种模式有自己的优点,首先,consumer可以根据自己的消费需求去fatch合适的消息并进行处理,此外,消费者可以良好的控制消费消息的数量。

  在kafka中,partition中的消息只有一个consumer在消费,切不存在消息状态的控制,也没有复杂的消息确认机制,可见,kafka broker是相当轻量级的。当消息被consumer接收后,consumer在本地保存最后消费消息的offset,并间歇性的向zookeeper注册offset。由此可见,consumer也是轻量级的。

kafka安全机制:partition复制备份

kafka将每个partition数据复制到多个broker(server)上,任何一个partition都有一个leader和多个follower(可以没有)。备份的个数可以通过broker的配置文件进行配置。leader处理所有的read-write请求,follower只需要和leader保持信息同步即可,leader负责跟踪所有的follower状态信息,如果follower落后太多或者失效,leader将会把它从replicas同步列表中删除。即使只有一个replicas存活,仍然可以保证消息的正常发送和接受,只要zookeeper集群存活即可。(不同于其他分布式存储,需要多数派存活)

 

  当leader失效时,需要在follower中选择一个新的leader(此选举并非通过zookeeper进行选举的),可能此时的follower落后于leader,因此需要一个“up-to-date”的follower。选择新的leader时也需要同时兼顾一个问题,那就是broker上leader的数量,如果一个server上有多个leader,意味着此service将承受更多的IO压力,所以在选举时,需要考虑leader的“负载平衡”。

 

对于consumer而言,他需要保存消费消息的offset,对于offset的保存和使用,由consumer来控制;当consumer正常消费消息时,offset将会“线性的”向前驱动,即,消息将依照顺序被消费。实际上,consumer也可以通过指定offset来消费特定的消息(offset将会保存在zookeeper中)

 

kafka集群几乎不需要维护任何producer和consumer的状态消息,这些消息由zookeeper来维护保存,因此,producer和consumer的客户端非常轻量级,他们可以随意的加入或者离开,不会对集群造成额外的影响。

kafka 消息传送机制

对于JMS实现,消息传输担保非常直接:有且只有一次(exactly once).在kafka中稍有不同:

  • at most once: 消息不会被重复发送,但是可能丢失
  • at least once: 消息可能会被重复发送,但是不会漏发送,消息至少发送一次,如果消息未能接受成功,可能会重发,直到接收成功.

    3) exactly once:不会少发送也不会重复发送,只会发送一次 .

at most once: 消费者fetch消息,然后保存offset,然后处理消息;当client保存offset之后,但是在消息处理过程中出现了异常,导致部分消息未能继续处理.那么此后"未处理"的消息将不能被fetch到,这就是"at most once".

    at least once: 消费者fetch消息,然后处理消息,然后保存offset.如果消息处理成功之后,但是在保存offset阶段zookeeper异常导致保存操作未能执行成功,这就导致接下来再次fetch时可能获得上次已经处理过的消息,这就是"at least once",原因offset没有及时的提交给zookeeper,zookeeper恢复正常还是之前offset状态.

    exactly once: kafka中并没有严格的去实现(基于2阶段提交,事务),我们认为这种策略在kafka中是没有必要的.

通常情况下"at-least-once"是我们首选.(相比at most once而言,重复接收数据总比丢失数据要好).

消息存储机制

    如果一个topic的名称为"my_topic",它有2个partitions,那么日志将会保存在my_topic_0和my_topic_1两个目录中;日志文件中保存了一序列"log entries"(日志条目),每个log entry格式为"4个字节的数字N表示消息的长度" + "N个字节的消息内容";每个日志都有一个offset来唯一的标记一条消息,offset的值为8个字节的数字,表示此消息在此partition中所处的起始位置..每个partition在物理存储层面,有多个log file组成(称为segment).segmentfile的命名为"最小offset".kafka.例如"00000000000.kafka";其中"最小offset"表示此segment中起始消息的offset.

其中每个partiton中所持有的segments列表信息会存储在zookeeper中.

    当segment文件尺寸达到一定阀值时(可以通过配置文件设定,默认1G),将会创建一个新的文件;当buffer中消息的条数达到阀值时将会触发日志信息flush到日志文件中,同时如果"距离最近一次flush的时间差"达到阀值时,也会触发flush到日志文件.如果broker失效,极有可能会丢失那些尚未flush到文件的消息.因为server意外实现,仍然会导致log文件格式的破坏(文件尾部),那么就要求当server启东是需要检测最后一个segment的文件结构是否合法并进行必要的修复.

    获取消息时,需要指定offset和最大chunk尺寸,offset用来表示消息的起始位置,chunk size用来表示最大获取消息的总长度(间接的表示消息的条数).根据offset,可以找到此消息所在segment文件,然后根据segment的最小offset取差值,得到它在file中的相对位置,直接读取输出即可.

    日志文件的删除策略非常简单:启动一个后台线程定期扫描log file列表,把保存时间超过阀值的文件直接删除(根据文件的创建时间).为了避免删除文件时仍然有read操作(consumer消费),采取copy-on-write方式.

 

producer 发布消息

写入方式

producer 采用 push 模式将消息发布到 broker,每条消息都被 append 到 patition 中,属于顺序写磁盘(顺序写磁盘效率比随机写内存要高,保障 kafka 吞吐率)。

消息路由

producer 发送消息到 broker 时,会根据分区算法选择将其存储到哪一个 partition。其路由机制为:

  1. 生产者指定了 patition,则直接使用;
  2. 未指定 patition 但指定消息的key,通过对 key 的 值进行hash 对patition数量取模选出一个 patition
  3. patition 和 key 都未指定,使用轮询选出一个 patition。

附上 java 客户端分区源码,一目了然:

查看代码

//创建消息实例
	public ProducerRecord(String topic, Integer partition, Long timestamp, K key, V value) {
	     if (topic == null)
	          throw new IllegalArgumentException("Topic cannot be null");
	     if (timestamp != null && timestamp < 0)
	          throw new IllegalArgumentException("Invalid timestamp " + timestamp);
	     this.topic = topic;
	     this.partition = partition;
	     this.key = key;
	     this.value = value;
	     this.timestamp = timestamp;
	}

	//计算 patition,如果指定了 patition 则直接使用,否则使用 key 计算
	private int partition(ProducerRecord<K, V> record, byte[] serializedKey , byte[] serializedValue, Cluster cluster) {
	     Integer partition = record.partition();
	     if (partition != null) {
	          List<PartitionInfo> partitions = cluster.partitionsForTopic(record.topic());
	          int lastPartition = partitions.size() - 1;
	          if (partition < 0 || partition > lastPartition) {
	               throw new IllegalArgumentException(String.format("Invalid partition given with record: %d is not in the range [0...%d].", partition, lastPartition));
	          }
	          return partition;
	     }
	     return this.partitioner.partition(record.topic(), record.key(), serializedKey, record.value(), serializedValue, cluster);
	}

	// 使用 key 选取 patition
	public int partition(String topic, Object key, byte[] keyBytes, Object value, byte[] valueBytes, Cluster cluster) {
	     List<PartitionInfo> partitions = cluster.partitionsForTopic(topic);
	     int numPartitions = partitions.size();
	     if (keyBytes == null) {
	          int nextValue = counter.getAndIncrement();
	          List<PartitionInfo> availablePartitions = cluster.availablePartitionsForTopic(topic);
	          if (availablePartitions.size() > 0) {
	               int part = DefaultPartitioner.toPositive(nextValue) % availablePartitions.size();
	               return availablePartitions.get(part).partition();
	          } else {
	               return DefaultPartitioner.toPositive(nextValue) % numPartitions;
	          }
	     } else {
	          //对 keyBytes 进行 hash 选出一个 patition
	          return DefaultPartitioner.toPositive(Utils.murmur2(keyBytes)) % numPartitions;
	     }
	}

写入流程

  1. producer 先从 zookeeper 的 "/brokers/.../state" 节点找到该 partition 的 leader
  2. producer 将消息发送给该 leader
  3. leader 将消息写入本地 log
  4. followers 从 leader pull 消息,写入本地 log 后 leader 发送 ACK(确认字符)
  5. leader 收到所有 ISR 中的 replica 的 ACK 后,增加 HW(high watermark,最后 commit 的 offset) 并向 producer 发送 ACK

 

broker 保存消息

存储方式

物理上把 topic 分成一个或多个 patition(对应 server.properties 中的 num.partitions=3 配置),每个 patition 物理上对应一个文件夹(该文件夹存储该 patition 的所有消息和索引文件),一个partition里理论上可以包含任意多个segment,每个segment对应一个索引文件一个数据文件,如下:

消息清除策略

kafka的config/server.properties文件中进行配置

提供了两种清除策略:删除(delete)、压缩(compact)

默认采取的删除策略

无论消息是否被消费,都会被执行配置的清除策略

删除策略

配置:

log.cleanup.policy = delete 所有数据启用删除策略

1 ) 基于时间:默认打开 。默认7天,检查周期5分钟, 以 segment 中所有记录中的最大时间戳作为该文件时间戳。
2) 基于大小:默认关闭 。超过设置的所有日志总大小,删除最早的 segment 。

  1. 基于时间:log.retention.hours=168
  2. 基于大小:log.retention.bytes=1073741824

Kafka的segment数据段清除不是及时的,他更像JVM垃圾回收那样,先打上deleted清除标记,在下一次清除的时候一起回收。例如下面的偏移量从00000000开始的segment便宜量就被标记为清除,再次消费读取就不允许从这个segment里面读取了。

 

压缩策略

配置:

在broker的配置中设置log.cleaner.enable=true启用cleaner,这个默认是关闭的。在Topic的配置中设置log.cleanup.policy=compact启用压缩策略。

将数据压缩,只保留每个key最后一个版本的数据。压缩后的offset可能是不连续的,将会拿到比这个offset大于等于的offset对应的消息

这种策略只适合特殊场景,比如消息的key是用户ID,消息体是用户的资料,通过这种压缩策略,整个消息集里就保存了所有用户最新的资料。

 

topic 创建

创建 topic 的序列图如下所示:

流程说明:

  1. controller 在 ZooKeeper 的 /brokers/topics 节点上注册 watcher,当 topic 被创建,则 controller 会通过 watch 得到该 topic 的 partition/replica 分配。
  2. controller从 /brokers/ids 读取当前所有可用的 broker 列表,对于 set_p 中的每一个 partition:

2.1 从分配给该 partition 的所有 replica(称为AR)中任选一个可用的 broker 作为新的 leader,并将AR设置为新的 ISR

2.2 将新的 leader 和 ISR 写入 /brokers/topics/[topic]/partitions/[partition]/state

  1. controller 通过 RPC 向相关的 broker 发送 LeaderAndISRRequest。

删除 topic

删除 topic 的序列图如下所示:

流程说明:

  1. controller 在 zooKeeper 的 /brokers/topics 节点上注册 watcher,当 topic 被删除,则 controller 会通过 watch 得到该 topic 的 partition/replica 分配。
  2. 若 delete.topic.enable=false,结束;否则 controller 注册在 /admin/delete_topics 上的 watch 被 fire,controller 通过回调向对应的 broker 发送 StopReplicaRequest。

kafka HA(高可用)

replication

如图.1所示,同一个 partition 可能会有多个 replica(对应 server.properties 配置中的 default.replication.factor=N)。没有 replica 的情况下,一旦 broker 宕机,其上所有 patition 的数据都不可被消费,同时 producer 也不能再将数据存于其上的 patition。引入replication 之后,同一个 partition 可能会有多个 replica,而这时需要在这些 replica 之间选出一个 leader,producer 和 consumer 只与这个 leader 交互,其它 replica 作为 follower 从 leader 中复制数据。

Kafka 分配 Replica 的算法如下:

  1. 将所有 broker(假设共 n 个 broker)和待分配的 partition 排序
  2. 将第 i 个 partition 分配到第(i mod n)个 broker 上
  3. 将第 i 个 partition 的第 j 个 replica 分配到第((i + j) mode n)个 broker上

 

leader failover(故障转移)

当 partition 对应的 leader 宕机时,需要从 follower 中选举出新 leader。在选举新leader时,一个基本的原则是,新的 leader 必须拥有旧 leader commit 过的所有消息。

 

kafka 在 zookeeper 中(/brokers/.../state)动态维护了一个 ISR(in-sync replicas),由节的写入流程可知 ISR 里面的所有 replica 都跟上了 leader,只有 ISR 里面的成员才能选为 leader。对于 f+1 个 replica,一个 partition 可以在容忍 f 个 replica 失效的情况下保证消息不丢失。

 

当所有 replica 都不工作时,有两种可行的方案:

  1. 等待 ISR 中的任一个 replica 活过来,并选它作为 leader。可保障数据不丢失,但时间可能相对较长。
  2. 选择第一个活过来的 replica(不一定是 ISR 成员)作为 leader。无法保障数据不丢失,但相对不可用时间较短。

kafka 0.8.* 使用第二种方式。

broker failover

kafka broker failover 序列图如下所示:

流程说明:

  1. controller 在 zookeeper 的 /brokers/ids/[brokerId] 节点注册 Watcher,当 broker 宕机时 zookeeper 会 fire watch
  2. controller 从 /brokers/ids 节点读取可用broker
  3. controller决定set_p,该集合包含宕机 broker 上的所有 partition
  4. 对 set_p 中的每一个 partition

    4.1 从/brokers/topics/[topic]/partitions/[partition]/state 节点读取 ISR

    4.2 决定新 leader(如4.3节所描述)

    4.3 将新 leader、ISR、controller_epoch 和 leader_epoch 等信息写入 state 节点

  1. 通过 RPC 向相关 broker 发送 leaderAndISRRequest 命令

 

controller failover

 当 controller 宕机时会触发 controller failover。每个 broker 都会在 zookeeper 的 "/controller" 节点注册 watcher,当 controller 宕机时 zookeeper 中的临时节点消失,所有存活的 broker 收到 fire 的通知,每个 broker 都尝试创建新的 controller path,只有一个竞选成功并当选为 controller。

 

当新的 controller 当选时,会触发 KafkaController.onControllerFailover 方法,在该方法中完成如下操作:

  1. 读取并增加 Controller Epoch。
  2. 在 reassignedPartitions Patch(/admin/reassign_partitions) 上注册 watcher。
  3. 在 preferredReplicaElection Path(/admin/preferred_replica_election) 上注册 watcher。
  4. 通过 partitionStateMachine 在 broker Topics Patch(/brokers/topics) 上注册 watcher。
  5. 若 delete.topic.enable=true(默认值是 false),则 partitionStateMachine 在 Delete Topic Patch(/admin/delete_topics) 上注册 watcher。
  6. 通过 replicaStateMachine在 Broker Ids Patch(/brokers/ids)上注册Watch。
  7. 初始化 ControllerContext 对象,设置当前所有 topic,“活”着的 broker 列表,所有 partition 的 leader 及 ISR等。
  8. 启动 replicaStateMachine 和 partitionStateMachine。
  9. 将 brokerState 状态设置为 RunningAsController。
  10. 将每个 partition 的 Leadership 信息发送给所有“活”着的 broker。
  11. 若 auto.leader.rebalance.enable=true(默认值是true),则启动 partition-rebalance 线程。
  12. 若 delete.topic.enable=true 且Delete Topic Patch(/admin/delete_topics)中有值,则删除相应的Topic。

consumer 消费消息

consumer API

kafka 提供了两套 consumer API:

  1. The high-level Consumer API
  2. The SimpleConsumer API

其中 high-level consumer API 提供了一个从 kafka 消费数据的高层抽象,而 SimpleConsumer API 则需要开发人员更多地关注细节。

 

The high-level consumer API

high-level consumer API 提供了 consumer group 的语义,一个消息只能被 group 内的一个 consumer 所消费,且 consumer 消费消息时不关注 offset,最后一个 offset 由 zookeeper 保存。

 

使用 high-level consumer API 可以是多线程的应用,应当注意:

  1. 如果消费线程大于 patition 数量,则有些线程将收不到消息
  2. 如果 patition 数量大于线程数,则有些线程多收到多个 patition 的消息
  3. 如果一个线程消费多个 patition,则无法保证你收到的消息的顺序,而一个 patition 内的消息是有序的

The SimpleConsumer API

如果你想要对 patition 有更多的控制权,那就应该使用 SimpleConsumer API,比如:

  1. 多次读取一个消息
  2. 只消费一个 patition 中的部分消息
  3. 使用事务来保证一个消息仅被消费一次

但是使用此 API 时,partition、offset、broker、leader 等对你不再透明,需要自己去管理。你需要做大量的额外工作:

  1. 必须在应用程序中跟踪 offset,从而确定下一条应该消费哪条消息
  2. 应用程序需要通过程序获知每个 Partition 的 leader 是谁
  3. 需要处理 leader 的变更

使用 SimpleConsumer API 的一般流程如下:

  1. 查找到一个“活着”的 broker,并且找出每个 partition 的 leader
  2. 找出每个 partition 的 follower
  3. 定义好请求,该请求应该能描述应用程序需要哪些数据
  4. fetch 数据
  5. 识别 leader 的变化,并对之作出必要的响应

 

以下针对 high-level Consumer API 进行说明。

consumer group

kafka 的分配单位是 partition。每个 consumer 都属于一个 group,一个 partition 只能被同一个 group 内的一个 consumer 所消费(也就保障了一个消息只能被 group 内的一个 consuemr 所消费),但是多个 group 可以同时消费这个 partition。

 

kafka 的设计目标之一就是同时实现离线处理和实时处理,根据这一特性,可以使用 spark/Storm 这些实时处理系统对消息在线处理,同时使用 Hadoop 批处理系统进行离线处理,还可以将数据备份到另一个数据中心,只需要保证这三者属于不同的 consumer group。如下图所示:

消费方式

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

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

 

对于 Kafka 而言,pull 模式更合适,它可简化 broker 的设计,consumer 可自主控制消费消息的速率,同时 consumer 可以自己控制消费方式——即可批量消费也可逐条消费,同时还能选择不同的提交方式从而实现不同的传输语义。

consumer delivery guarantee

如果将 consumer 设置为 autocommit,consumer 一旦读到数据立即自动 commit。如果只讨论这一读取消息的过程,那 Kafka 确保了 Exactly once。

 

但实际使用中应用程序并非在 consumer 读取完数据就结束了,而是要进行进一步处理,而数据处理与 commit 的顺序在很大程度上决定了consumer delivery guarantee:

 

1.读完消息先 commit 再处理消息。

    这种模式下,如果 consumer 在 commit 后还没来得及处理消息就 crash 了,下次重新开始工作后就无法读到刚刚已提交而未处理的消息,这就对应于 At most once

2.读完消息先处理再 commit。

    这种模式下,如果在处理完消息之后 commit 之前 consumer crash 了,下次重新开始工作时还会处理刚刚未 commit 的消息,实际上该消息已经被处理过了。这就对应于 At least once。

3.如果一定要做到 Exactly once,就需要协调 offset 和实际操作的输出。

精典的做法是引入两阶段提交。如果能让 offset 和操作输入存在同一个地方,会更简洁和通用。这种方式可能更好,因为许多输出系统可能不支持两阶段提交。比如,consumer 拿到数据后可能把数据放到 HDFS,如果把最新的 offset 和数据本身一起写到 HDFS,那就可以保证数据的输出和 offset 的更新要么都完成,要么都不完成,间接实现 Exactly once。(目前就 high-level API而言,offset 是存于Zookeeper 中的,无法存于HDFS,而SimpleConsuemr API的 offset 是由自己去维护的,可以将之存于 HDFS 中)

总之,Kafka 默认保证 At least once,并且允许通过设置 producer 异步提交来实现 At most once。而 Exactly once 要求与外部存储系统协作,幸运的是 kafka 提供的 offset 可以非常直接非常容易得使用这种方式。

consumer rebalance

当有 consumer 加入或退出、以及 partition 的改变(如 broker 加入或退出)时会触发 rebalance。consumer rebalance算法如下:

  1. 将目标 topic 下的所有 partirtion 排序,存于PT
  2. 对某 consumer group 下所有 consumer 排序,存于 CG,第 i 个consumer 记为 Ci
  3. N=size(PT)/size(CG),向上取整
  4. 解除 Ci 对原来分配的 partition 的消费权(i从0开始)
  5. 将第i*N到(i+1)*N-1个 partition 分配给 Ci

 

在 0.8.*版本,每个 consumer 都只负责调整自己所消费的 partition,为了保证整个consumer group 的一致性,当一个 consumer 触发了 rebalance 时,该 consumer group 内的其它所有其它 consumer 也应该同时触发 rebalance。这会导致以下几个问题:

1.Herd effect

  任何 broker 或者 consumer 的增减都会触发所有的 consumer 的 rebalance

2.Split Brain

  每个 consumer 分别单独通过 zookeeper 判断哪些 broker 和 consumer 宕机了,那么不同 consumer 在同一时刻从 zookeeper 看到的 view 就可能不一样,这是由 zookeeper 的特性决定的,这就会造成不正确的 reblance 尝试。

  1. 调整结果不可控

所有的 consumer 都并不知道其它 consumer 的 rebalance 是否成功,这可能会导致 kafka 工作在一个不正确的状态。

基于以上问题,kafka 设计者考虑在0.9.*版本开始使用中心 coordinator 来控制 consumer rebalance,然后又从简便性和验证要求两方面考虑,计划在 consumer 客户端实现分配方案。

 

kafka的partition扩容

数据需要rebalance,kafka提供了bin/kafka-reassign-partitions.sh工具,完成parttition的迁移。

1、增加partition数量

bin/kafka-topics.sh --zookeeper 10.1.11.6:2181 --alter --topic all --partitions 24

2、重新分配parttion(reassign partitions)

./bin/kafka-reassign-partitions.sh --broker-list "1,2,3" --topics-to-move-json-file move.json --zookeeper 10.1.11.6:2181 --generate

其中topics的json文件内容为:

{"topics": [{"topic": "logstash-product"}], "version":1 }

3、使用上一步生成的建议partition json内容进行完成迁移

“Proposed partition reassignment configuration”后面的内容保存到reassign.json文件中

bin/kafka-reassign-partitions.sh --broker-list "1,2,3" --reassignment-json-file reassign.json --zookeeper 10.1.11.6:2181 --execute

4、修改参数,重启kafka

sed -i 's/log.retention.hours=1/log.retention.hours=24/g' /apps/conf/kafka/server.properties

 

retries参数

retries参数的值决定了生产者可以重发消息的次数,如果达到这个次数,生产者会放弃重试并返回错误。

这里的重试发送是指消息写入失败的重试,kafka的消息是拉的,并不是控制发送给消费者的重试。

 

注意事项

最开始在本机搭建了kafka伪集群,本地 producer 客户端成功发布消息至 broker。随后在服务器上搭建了 kafka 集群,在本机连接该集群,producer 却无法发布消息到 broker(奇怪也没有抛错)

advertised.listeners 是 broker 给 producer 和 consumer 连接使用的,如果没有设置,就使用 listeners,而如果 host_name 没有设置的话,就使用 java.net.InetAddress.getCanonicalHostName() 方法返回的主机名。

 

修改方法:

  1. listeners=PLAINTEXT://121.10.26.XXX:9092
  2. advertised.listeners=PLAINTEXT://121.10.26.XXX:9092

 

问题处理

TimeoutException

org.apache.kafka.common.errors.TimeoutException: Failed to update metadata after 60000 ms.

连不上集群服务器:

看客户端配置ip及端口与kafka集群是否一致

看客户端版本与kafka集群版本是否兼容

添加副本数量

原来topic有一个分片一个副本(就是它自己)现在要增加副本数量

编写json文件

{
    "version": 1,
    "partitions": [
        {
            "topic": "ctfo_park_cloud_business_unit",
            "partition": 0,
            "replicas": [
                1,
                2,
                3
            ]
        }
    ]
}

json内容是给指定分片指定副本数量,如果哟多个分片要分别罗列出来

执行命令并指定json文件

/usr/local/kafka/bin/kafka-reassign-partitions.sh --zookeeper  172.20.72.91:2181 --reassignment-json-file /usr/local/updatetopic.json --execute

 

kafka的partition的state的leader为-1

原因分析:

broker shutdown的时候,partition的leader在此broker上,controller选主没有成功,移除此broker后,对应的partition的leader就被赋值成-1了。

出现这种情况需要重启该停掉的kafka服务来恢复

 

kafka扩容

修改kafka的配置文件,启动kafka,新加入的机器只能对新产生的topic起作用,对已有的topic在没有做处理前,是不会承担任何任务的

重新分区

假设有一个名为test的topic,只有1个partition,现在由于存储空间不足,需要重新分区。

修改topic的partitions

./bin/kafka-topics.sh --zookeeper 10.0.210.152:2181 --alter --topic test --partitions 6

现在topic有6个partition,但是数据还没有迁移过去

迁移数据:

使用kafka提供的工具kafka-reassign-partitions.sh来迁移数据。迁移数据需要分三步做

具体查看https://blog.csdn.net/gezilan/article/details/80412490

迁移操作会将指定Topic 的数据文件移动到新的节点目录下,这个过程可能需要等待很长时间

在生产的同时进行数据迁移会出现重复数据。所以迁移的时候避免重复生产数据

 

重新分区对消费的影响:

spring.kafka.auto-offset-reset值

earliest

当各分区下有已提交的offset时,从提交的offset开始消费;无提交的offset时,从头开始消费

latest

当各分区下有已提交的offset时,从提交的offset开始消费;无提交的offset时,消费新产生的该分区下的数据

none

topic各分区都存在已提交的offset时,从offset后开始消费;只要有一个分区不存在已提交的offset,则抛出异常

posted @ 2022-08-12 12:57  星光闪闪  阅读(103)  评论(0)    收藏  举报