kafka——随笔

注:仅代表个人随笔,如有错误还请见谅

kafka一认识

大数据中有两个主要挑战,第一个挑战是如何手机大量数据,第二个挑战是分析收集的数据,这就会用到消息传递系统

什么是消息系统?

消息系统负责将数据从一个应用程序传输到另一个应用程序,因此应用程序可以专注于数据,但不必担心如何共享数据。 分布式消息传递基于可靠消息队列的概念。 消息在客户端应用程序和消息传递系统之间异步排队。 有两种类型的消息传递模式可用 - 一种是点对点的,另一种是发布 - 订阅(pub-sub)消息传递系统。 大多数消息传递模式遵循pub-sub

点对点消息系统

在点对点系统中,消息被保存在一个队列中。 一个或多个消费者可以消费队列中的消息,但是特定的消息只能由最多一个消费者消费。 一旦消费者在队列中读取消息,消息就从该队列中消失。 这个系统的典型例子是一个订单处理系统,其中每个订单将由一个订单处理器处理,但是多订单处理器也可以同时工作。

 

发布-订阅消息系统

在发布-订阅系统中,消息被保存在一个主题中。 与点对点系统不同,消费者可以订阅一个或多个主题并使用该主题中的所有消息。 在发布-订阅系统中,消息生产者称为发布者,消息消费者称为订阅者。 一个真实的例子是Dish TV,它发布体育,电影,音乐等不同的频道,任何人都可以订阅他们自己的一套频道,并在他们的订阅频道可用时获得内容。

 

kafka

kafka全程Apache kafka是一个分布式的发布订阅消息传递系统和一个强大的队列,可以处理大量的数据,并且能将消息从一个端点传递到另一个端点,kafka适合离线和在线消息消费,Kafka消息被保存在磁盘上并在集群内复制以防止数据丢失。 Kafka建立在ZooKeeper同步服务之上。 它与Apache Storm和Spark完美集成用于实时流数据分析

优点

  • 可靠性:kafka是分布式,分区,复制和容错

  • 可扩展性:kafka消息系统无序停机即可轻松扩展

  • 耐用行:kafka使用分布式提交日志,这意味着消息尽可能快的保留在磁盘上,因此他是持久的

  • 性能:kafka对于发布和订阅消息都有很高的吞吐量,他保持稳定的性能,即使储存了许多TB数据量的消息

kafka的速度非常快,可确保零停机时间和零数据丢失

用例

  • 指标:Kafka通常用于运营监控数据。 这涉及从分布式应用程序汇总统计数据以生成操作数据的集中式提要。

  • 日志聚和解决方案: Kafka可以在整个组织中使用,从多个服务中收集日志,并以标准格式向多个消费者提供。

  • 流处理:流行的框架(如Storm和Spark Streaming)可以从主题读取数据,对其进行处理,并将处理后的数据写入新主题,以供用户和应用程序使用。 Kafka的强耐久性在流加工方面也非常有用。

Kafka是处理所有实时数据馈送的统一平台。 Kafka支持低延迟消息传送并在出现机器故障时保证容错。 它有能力处理大量不同的消费者。 Kafka速度非常快,每秒执行200万次写入。 Kafka将所有数据保留在磁盘上,这意味着所有写入都会进入操作系统(RAM)的页面缓存。 这使得从页面缓存向网络套接字传输数据非常高效。

kafka的具体术语:

四个:topics, brokers, producersconsumers

 

在上图中,主题(topic)被配置为三个分区。 分区1(Partition 1)具有两个偏移因子01。分区2(Partition 2)具有四个偏移因子0,1,23,分区3(Partition 3)具有一个偏移因子0replica(副本,一个Topic中的Partition可以有多个副本,每个副本都是一个独立的存储单元。一个Partition的Leader副本负责所有客户端对该Partition的读写请求,而Follower副本则通过与Leader副本同步数据来实现数据的复制和冗余,以提供更高的可靠性和容错性。Kafka通过副本的机制来保证数据的可靠性,即在某些特定情况下数据仍然可以被恢复。例如,当Leader副本宕机时,Kafka会自动选举某个Follower副本作为新的Leader,保证数据的可用性。同时,Kafka还支持多级副本复制机制,即一个Follower副本可以成为另一个Follower副本的Leader副本,从而保证数据的多设备备份,避免了单点故障的影响。) 的id与托管它的服务器的id相同。

假设,如果该主题的复制因子设置为3,则Kafka将为每个分区创建3个相同的副本,并将它们放入群集中以使其可用于其所有操作。 为了平衡集群中的负载,每个代理存储一个或多个这些分区。 多个生产者和消费者可以同时发布和检索消息。

  • Topics - 属于特定类别的消息流被称为主题(Topics),数据存储在主题中。主题分为多个分区。 对于每个主题,Kafka都保留一个分区的最小范围。 每个这样的分区都以不可变的有序顺序包含消息。 分区被实现为一组相同大小的段文件。

  • Partition - 主题可能有很多分区,所以它可以处理任意数量的数据。

  • Partition offset - 每个分区消息都有一个称为偏移量的唯一序列标识。

  • Replicas of partition - 副本只是分区的备份。 副本从不读取或写入数据。 它们用于防止数据丢失。

  • Brokers

    • 一个独立的 Kafka 服务器被称为 Broker。Broker 接收来自生产者的消息,为消息设置偏移量,并提交消息到磁盘保存。Broker 为消费者提供服务,对读取分区的请求做出响应,返回已经提交到磁盘的消息。

    • Broker 是集群 (Cluster) 的组成部分。每一个集群都会选举出一个 Broker 作为集群控制器 (Controller),集群控制器负责管理工作,包括将分区分配给 Broker 和监控 Broker。

    • 在集群中,一个分区 (Partition) 从属一个 Broker,该 Broker 被称为分区的首领 (Leader)。一个分区可以分配给多个 Brokers,这个时候会发生分区复制。这种复制机制为分区提供了消息冗余,如果有一个 Broker 失效,其他 Broker 可以接管领导权。

  • Kafka Cluster - Kafka拥有多个经纪人称为Kafka集群。 Kafka集群可以在无需停机的情况下进行扩展。 这些集群用于管理消息数据的持久性和复制。

  • Producers - 生产者负责创建消息。一般情况下,生产者在把消息均衡地分布到在主题的所有分区上,而并不关心消息会被写到哪个分区。如果我们想要把消息写到指定的分区,可以通过自定义分区器来实现。

  • Consumers - 消费者是消费者群组的一部分,消费者负责消费消息。消费者可以订阅一个或者多个主题,并按照消息生成的顺序来读取它们。消费者通过检查消息的偏移量 (offset) 来区分读取过的消息。偏移量是一个不断递增的数值,在创建消息时,Kafka 会把它添加到其中,在给定的分区里,每个消息的偏移量都是唯一的。消费者把每个分区最后读取的偏移量保存在 Zookeeper 或 Kafka 上,如果消费者关闭或者重启,它还可以重新获取该偏移量,以保证读取状态不会丢失。一个分区只能被同一个消费者群组里面的一个消费者读取,但可以被不同消费者群组中所组成的多个消费者共同读取。多个消费者群组中消费者共同读取同一个主题时,彼此之间互不影响。

  • Leader - Leader是负责所有分区读写的节点。 每个分区都有一台服务器充当领导者。

  • Follower - 遵循领导者(Leader)指示的节点称为追随者(Follower)。 如果领导失败,其中一个追随者将自动成为新领导。 追随者扮演正常的消费者角色,拉动消息并更新自己的数据存储

kafka二、使用Zookeeper搭建kafka高可用集群

为保证集群高可用,Zookeeper 集群的节点数最好是奇数,最少有三个节点,所以搭建了一个三个节点的集群。

1.1下载解压
# 下载
wget https://archive.apache.org/dist/zookeeper/zookeeper-3.4.14/zookeeper-3.4.14.tar.gz
# 解压
tar -zxvf zookeeper-3.4.14.tar.gz
1.2 修改配置

拷贝三份 zookeeper 安装包。分别进入安装目录的 conf 目录,拷贝配置样本 zoo_sample.cfgzoo.cfg 并进行修改,修改后三份配置文件内容分别如下:

zookeeper01 配置:

tickTime=2000
initLimit=10
syncLimit=5
dataDir=/usr/local/zookeeper-cluster/data/01
dataLogDir=/usr/local/zookeeper-cluster/log/01
clientPort=2181
# server.1 这个1是服务器的标识,可以是任意有效数字,标识这是第几个服务器节点,这个标识要写到dataDir目录下面myid文件里
# 指名集群间通讯端口和选举端口
server.1=127.0.0.1:2287:3387
server.2=127.0.0.1:2288:3388
server.3=127.0.0.1:2289:3389
如果是多台服务器,则集群中每个节点通讯端口和选举端口可相同,IP 地址修改为每个节点所在主机 IP 即可。

zookeeper02 配置,与 zookeeper01 相比,只有 dataLogDirdataLogDir 不同:

tickTime=2000
initLimit=10
syncLimit=5
dataDir=/usr/local/zookeeper-cluster/data/02
dataLogDir=/usr/local/zookeeper-cluster/log/02
clientPort=2182
server.1=127.0.0.1:2287:3387
server.2=127.0.0.1:2288:3388
server.3=127.0.0.1:2289:3389

zookeeper03 配置,与 zookeeper01,02 相比,也只有 dataLogDirdataLogDir 不同:

tickTime=2000
initLimit=10
syncLimit=5
dataDir=/usr/local/zookeeper-cluster/data/03
dataLogDir=/usr/local/zookeeper-cluster/log/03
clientPort=2183
server.1=127.0.0.1:2287:3387
server.2=127.0.0.1:2288:3388
server.3=127.0.0.1:2289:3389

配置参数说明:

  • tickTime:用于计算的基础时间单元。比如 session 超时:N*tickTime;

  • initLimit:用于集群,允许从节点连接并同步到 master 节点的初始化连接时间,以 tickTime 的倍数来表示;

  • syncLimit:用于集群, master 主节点与从节点之间发送消息,请求和应答时间长度(心跳机制);

  • dataDir:数据存储位置;

  • dataLogDir:日志目录;

  • clientPort:用于客户端连接的端口,默认 2181

1.3 标识节点

分别在三个节点的数据存储目录下新建 myid 文件,并写入对应的节点标识。Zookeeper 集群通过 myid 文件识别集群节点,并通过上文配置的节点通信端口和选举端口来进行节点通信,选举出 leader 节点。

创建存储目录

# dataDir
mkdir -vp /usr/local/zookeeper-cluster/data/01
# dataDir
mkdir -vp /usr/local/zookeeper-cluster/data/02
# dataDir
mkdir -vp /usr/local/zookeeper-cluster/data/03

创建并写入节点标识到 myid 文件:

#server1
echo "1" > /usr/local/zookeeper-cluster/data/01/myid
#server2
echo "2" > /usr/local/zookeeper-cluster/data/02/myid
#server3
echo "3" > /usr/local/zookeeper-cluster/data/03/myid
1.4 启动集群

分别启动三个节点:

# 启动节点1
/usr/app/zookeeper-cluster/zookeeper01/bin/zkServer.sh start
# 启动节点2
/usr/app/zookeeper-cluster/zookeeper02/bin/zkServer.sh start
# 启动节点3
/usr/app/zookeeper-cluster/zookeeper03/bin/zkServer.sh start
1.5集群验证

使用 jps 查看进程,并且使用 zkServer.sh status 查看集群各个节点状态。如图三个节点进程均启动成功,并且两个节点为 follower 节点,一个节点为 leader 节点。

Kafka集群搭建

2.1 下载解压

Kafka 安装包官方下载地址:http://kafka.apache.org/downloads ,本用例下载的版本为 2.2.0,下载命令:

# 下载
wget https://www-eu.apache.org/dist/kafka/2.2.0/kafka_2.12-2.2.0.tgz
# 解压
tar -xzf kafka_2.12-2.2.0.tgz

这里 j 解释一下 kafka 安装包的命名规则:以 kafka_2.12-2.2.0.tgz 为例,前面的 2.12 代表 Scala 的版本号(Kafka 采用 Scala 语言进行开发),后面的 2.2.0 则代表 Kafka 的版本号。

2.2 拷贝配置文件

进入解压目录的 config 目录下 ,拷贝三份配置文件:

# cp server.properties server-1.properties
# cp server.properties server-2.properties
# cp server.properties server-3.properties
2.3 修改配置

分别修改三份配置文件中的部分配置,如下:

server-1.properties:

# The id of the broker. 集群中每个节点的唯一标识
broker.id=0
# 监听地址
listeners=PLAINTEXT://hadoop001:9092
# 数据的存储位置
log.dirs=/usr/local/kafka-logs/00
# Zookeeper连接地址
zookeeper.connect=hadoop001:2181,hadoop001:2182,hadoop001:2183

server-2.properties:

broker.id=1
listeners=PLAINTEXT://hadoop001:9093
log.dirs=/usr/local/kafka-logs/01
zookeeper.connect=hadoop001:2181,hadoop001:2182,hadoop001:2183

server-3.properties:

broker.id=2
listeners=PLAINTEXT://hadoop001:9094
log.dirs=/usr/local/kafka-logs/02
zookeeper.connect=hadoop001:2181,hadoop001:2182,hadoop001:2183

这里需要说明的是 log.dirs 指的是数据日志的存储位置,确切的说,就是分区数据的存储位置,而不是程序运行日志的位置。程序运行日志的位置是通过同一目录下的 log4j.properties 进行配置的。

2.4 启动集群

分别指定不同配置文件,启动三个 Kafka 节点。启动后可以使用 jps 查看进程,此时应该有三个 zookeeper 进程和三个 kafka 进程。

bin/kafka-server-start.sh config/server-1.properties
bin/kafka-server-start.sh config/server-2.properties
bin/kafka-server-start.sh config/server-3.properties
2.5 创建测试主题

创建测试主题:

bin/kafka-topics.sh --create --bootstrap-server hadoop001:9092 \
                  --replication-factor 3 \
                  --partitions 1 --topic my-replicated-topic

创建后可以使用以下命令查看创建的主题信息:

bin/kafka-topics.sh --describe --bootstrap-server hadoop001:9092 --topic my-replicated-topic

可以看到分区 0 的有 0,1,2 三个副本,且三个副本都是可用副本,都在 ISR(in-sync Replica 同步副本) 列表中,其中 1 为首领副本,此时代表集群已经搭建成功。

kafka三、生产者详解消费者详解

生产者详解

  • Kafka 会将发送消息包装为 ProducerRecord 对象, ProducerRecord 对象包含了目标主题和要发送的内容,同时还可以指定键和分区。在发送 ProducerRecord 对象前,生产者会先把键和值对象序列化成字节数组,这样它们才能够在网络上传输。

  • 接下来,数据被传给分区器。如果之前已经在 ProducerRecord 对象里指定了分区,那么分区器就不会再做任何事情。如果没有指定分区 ,那么分区器会根据 ProducerRecord 对象的键来选择一个分区,紧接着,这条记录被添加到一个记录批次里,这个批次里的所有消息会被发送到相同的主题和分区上。有一个独立的线程负责把这些记录批次发送到相应的 broker 上。

  • 服务器在收到这些消息时会返回一个响应。如果消息成功写入 Kafka,就返回一个 RecordMetaData 对象,它包含了主题和分区信息,以及记录在分区里的偏移量。如果写入失败,则会返回一个错误。生产者在收到错误之后会尝试重新发送消息,如果达到指定的重试次数后还没有成功,则直接抛出异常,不再重试。

生产者发送数据时并不是直接就发送到Broker中,其发送的大致过程如下图:

img

send()方法发送数据

这里的send过程画的比较简单,中间省略了拦截器、序列化器、分区器等组件的操作,这些组件有默认的处理逻辑,也可以允许被自定义并替换掉默认的处理逻辑,感兴趣的也可以自己下去了解下。这里想表达的是数据使用send方法发送之后并不是直接就发送到节点服务器Broker中了,而是会发送到本地的消息队列(MQ)中了,而将数据从消息队列中发送到Broker中的实际是sender线程。另外一点值得注意的是,这个send方法有同步和异步两种方式,即是否阻塞当前线程,或者说是否等待发送的结果响应,具体使用方法见后面第2小节的示例。

消息队列(MQ)

由图中可以看到消息队列不止一个,具体的消息队列数量由分区数来决定,这里的消息队列和Broker中的分区数(Leader)是对应的,或者说是数据进入到哪一个消息队列(本地分区)其实由分区器计算好的。这些消息队列占用内存的最大值( RecordAccumulator )默认是32M,当需要处理的数据量比较大时,可以适当调整这个值(推荐调整为2的倍数),比如64M。

sender线程

sender线程在这里也省略一些内容,比如NetworkClient、Selector等,如果想要深入研究其原理(比如请求的缓存机制、失败重试机制等),也可以自己下去阅读下源码。sender线程主要在后台负责从消息队列中拉取数据并通过请求的方式发送到Broker中(注意,在这一步之前,生产者发送的数据都还存在本地中),而sender线程从消息队列中拉取数据的时机由两个参数来决定:

  • batch.size: 当消息队列中的数据量大小累积到该值后,sender才会从消息队列中拉取数据并发送到Broker中,这个值默认是16K,如果要增大这个值的话,建议调整为2的倍数,比如32K。

  • linger.ms: 当消息队列中的数据等待指定时间后(单位ms),即使数据量没有达到 batch.size 指定的大小,也会拉取消息队列中的数据发送到Broker中,但是这个参数的默认值是0ms,也就是说数据到达消息队列后就会马上被发送到Broker中。实际生产中会根据使用场景灵活调整这个值,毕竟默认值0ms, batch.size 参数其实就没起作用了。

  • 应答acks

    Broker接收到生产者发送来的数据后,需要给生产者返回应答(acks),该应答有三个值:

    • 0: Broker收到数据后立即返回应答成功,不需要等待数据持久化到硬盘中(即写入到log文件中)。

    • 1: Broker收到数据后,且Leader分区确认收到数据并将数据持久化到硬盘中(即写入到log文件中)后才会返回应答成功。

    • -1: Broker收到数据后,且Leader分区和所有Follower分区都收到数据并将数据持久化硬盘中(即写入到log文件中)后才会返回应答成功。

    注: acks这个参数主要用于保证数据的可靠性,比如配置为-1,则哪怕中途出现了一些意外,也能保证发送成功的数据被丢失的概率很小。

同步和异步发送数据

异步发送数据

指的是将消息发送到队列之后就直接返回结果了,不需要等待队列中的消息发送到集群节点上再返回结果,异步发送在指定参数的时候也有两种方式:带回调函数和不带回调函数。

创建 Kafka 生产者时,以下三个属性是必须指定的:

  • bootstrap.servers :指定 broker 的地址清单,清单里不需要包含所有的 broker 地址,生产者会从给定的 broker 里查找 broker 的信息。不过建议至少要提供两个 broker 的信息作为容错;

  • key.serializer :指定键的序列化器;

  • value.serializer :指定值的序列化器。

import time
from kafka import KafkaProducer
from kafka.errors import KafkaError

# 设置Kafka客户端的属性
properties = {
   'bootstrap_servers': 'host001:9092,host002:9092',
   'key_serializer': str.encode,
   'value_serializer': str.encode
}

# 创建一个Kafka生产者客户端
kafka_producer = KafkaProducer(**properties)

# 异步发送方式一:发送5条数据,无回调操作
for i in range(5):
   kafka_producer.send('topic_test001', 'hello')

# 异步发送方式二:发送5条数据,并执行回调方法
for i in range(5):
   # 如果指定了callback参数,每次发送消息之后,都会执行一个回调方法on_completion,
   # 方法的参数record_metadata中存放了此次发送消息的结果信息,包括消息发送到了哪个topic和partition,
   # 如果发送过程中发生了异常,异常信息则保存在参数exception中
   kafka_producer.send('topic_test001', 'hello').add_callback(
       lambda record_metadata: print(f"主题:{record_metadata.topic} 分区:{record_metadata.partition}")
  ).add_errback(
       lambda exception: print(f"发送失败:{exception}")
  )

# 等待所有消息发送完成
kafka_producer.flush()

# 关闭客户端
kafka_producer.close()
同步发送数据
import time
from kafka import KafkaProducer
from kafka.errors import KafkaError

# 设置Kafka客户端的属性
properties = {
'bootstrap_servers': 'host001:9092,host002:9092',
'key_serializer': str.encode,
'value_serializer': str.encode
}

# 创建一个Kafka生产者客户端
kafka_producer = KafkaProducer(**properties)

# 同步发送:发送5条数据
for i in range(5):
# 相比于异步发送就多了个get()方法,正是由于这个get()方法,才会阻塞当前操作,
# 直到确认消息已经到节点服务器中才会继续发送下一条消息(下一次循环)
kafka_producer.send('topic_test001', 'hello').get()

# 关闭客户端
kafka_producer.close()
自定义分区器

在send方法发送数据之后,会经过分区器将数据划分到不同的分区,再存储到对应的消息队列中,在某些特殊的场景下可能需要按照特定的要求将数据发送到不同的分区,这时候就可以使用自定义分区器,将数据发送到指定的分区。自定义的分区器,只要实现Partitioner接口即可

在Python中,Kafka提供了一个Partitioner类来自定义分区逻辑。下面是一个示例代码,展示如何在Python中使用自定义分区器:

from kafka import KafkaProducer
from kafka.errors import KafkaError

class MyPartitioner:
def __init__(self):
pass

def partition(self, topic, key, value, partitions):
# 获取数据,并按照自定义的规则将数据发送到指定的分区
msg = value.decode('utf-8')
if 'yun' in msg:
partition = 0
else:
partition = 1
return partition

# 设置Kafka客户端的属性
properties = {
'bootstrap_servers': 'host001:9092,host002:9092',
'key_serializer': str.encode,
'value_serializer': str.encode,
'partitioner': MyPartitioner()
}

# 创建一个Kafka生产者客户端
kafka_producer = KafkaProducer(**properties)

# 发送消息
for i in range(5):
kafka_producer.send('topic_test001', value='hello')

# 关闭客户端
kafka_producer.close()

在此示例中,我们创建了一个名为MyPartitioner的自定义分区器类。该类中有一个partition方法,用于根据消息的内容决定将消息发送到哪个分区。然后,我们通过在KafkaProducer的属性中指定partitioner参数来使用自定义分区器。

请注意,在Python中,我们无法直接实现Java中的Partitioner接口,因此我们需要自己实现分区逻辑,并将其作为一个实例传递给KafkaProducer的partitioner参数。

生产者的其他属性:

上面生产者的创建都仅指定了服务地址,键序列化器、值序列化器,实际上 Kafka 的生产者还有很多可配置属性,如下

1. acks

acks 参数指定了必须要有多少个分区副本收到消息,生产者才会认为消息写入是成功的:

  • acks=0 : 消息发送出去就认为已经成功了,不会等待任何来自服务器的响应;

  • acks=1 : 只要集群的首领节点收到消息,生产者就会收到一个来自服务器成功响应;

  • acks=all :只有当所有参与复制的节点全部收到消息时,生产者才会收到一个来自服务器的成功响应。

2. buffer.memory

设置生产者内存缓冲区的大小。

3. compression.type

默认情况下,发送的消息不会被压缩。如果想要进行压缩,可以配置此参数,可选值有 snappy,gzip,lz4。

4. retries

发生错误后,消息重发的次数。如果达到设定值,生产者就会放弃重试并返回错误。

5. batch.size

当有多个消息需要被发送到同一个分区时,生产者会把它们放在同一个批次里。该参数指定了一个批次可以使用的内存大小,按照字节数计算。

6. linger.ms

该参数制定了生产者在发送批次之前等待更多消息加入批次的时间。

7. clent.id

客户端 id,服务器用来识别消息的来源。

8. max.in.flight.requests.per.connection

指定了生产者在收到服务器响应之前可以发送多少个消息。它的值越高,就会占用越多的内存,不过也会提升吞吐量,把它设置为 1 可以保证消息是按照发送的顺序写入服务器,即使发生了重试。

9. timeout.ms, request.timeout.ms & metadata.fetch.timeout.ms
  • timeout.ms 指定了 borker 等待同步副本返回消息的确认时间;

  • request.timeout.ms 指定了生产者在发送数据时等待服务器返回响应的时间;

  • metadata.fetch.timeout.ms 指定了生产者在获取元数据(比如分区首领是谁)时等待服务器返回响应的时间。

10. max.block.ms

指定了在调用 send() 方法或使用 partitionsFor() 方法获取元数据时生产者的阻塞时间。当生产者的发送缓冲区已满,或者没有可用的元数据时,这些方法会阻塞。在阻塞时间达到 max.block.ms 时,生产者会抛出超时异常。

11. max.request.size

该参数用于控制生产者发送的请求大小。它可以指发送的单个消息的最大值,也可以指单个请求里所有消息总的大小。例如,假设这个值为 1000K ,那么可以发送的单个最大消息为 1000K ,或者生产者可以在单个请求里发送一个批次,该批次包含了 1000 个消息,每个消息大小为 1K。

12. receive.buffer.bytes & send.buffer.byte

这两个参数分别指定 TCP socket 接收和发送数据包缓冲区的大小,-1 代表使用操作系统的默认值。

消费者详解

在 Kafka 中,消费者通常是消费者群组的一部分,多个消费者群组共同读取同一个主题时,彼此之间互不影响。Kafka 之所以要引入消费者群组这个概念是因为 Kafka 消费者经常会做一些高延迟的操作,比如把数据写到数据库或 HDFS ,或者进行耗时的计算,在这些情况下,单个消费者无法跟上数据生成的速度。此时可以增加更多的消费者,让它们分担负载,分别处理部分分区的消息,这就是 Kafka 实现横向伸缩的主要手段。需要注意的是:同一个分区只能被同一个消费者群组里面的一个消费者读取,不可能存在同一个分区被同一个消费者群里多个消费者共同读取的情况,可以看到即便消费者 Consumer5 空闲了,但是也不会去读取任何一个分区的数据,这同时也提醒我们在使用时应该合理设置消费者的数量,以免造成闲置和额外开销。

分区再均衡

因为群组里的消费者共同读取主题的分区,所以当一个消费者被关闭或发生崩溃时,它就离开了群组,原本由它读取的分区将由群组里的其他消费者来读取。同时在主题发生变化时 , 比如添加了新的分区,也会发生分区与消费者的重新分配,分区的所有权从一个消费者转移到另一个消费者,这样的行为被称为再均衡。正是因为再均衡,所以消费费者群组才能保证高可用性和伸缩性。

消费者通过向群组协调器所在的 broker 发送心跳来维持它们和群组的从属关系以及它们对分区的所有权。只要消费者以正常的时间间隔发送心跳,就被认为是活跃的,说明它还在读取分区里的消息。消费者会在轮询消息或提交偏移量时发送心跳。如果消费者停止发送心跳的时间足够长,会话就会过期,群组协调器认为它已经死亡,就会触发再均衡。

在创建消费者的时候以下以下三个选项是必选的:

  • bootstrap.servers :指定 broker 的地址清单,清单里不需要包含所有的 broker 地址,生产者会从给定的 broker 里查找 broker 的信息。不过建议至少要提供两个 broker 的信息作为容错;

  • key.deserializer :指定键的反序列化器;

  • value.deserializer :指定值的反序列化器。

除此之外你还需要指明你需要想订阅的主题,可以使用如下两个 API :

  • consumer.subscribe(Collection\ topics) :指明需要订阅的主题的集合;

  • consumer.subscribe(Pattern pattern) :使用正则来匹配需要订阅的集合

最后只需要通过轮询 API(poll) 向服务器定时请求数据。一旦消费者订阅了主题,轮询就会处理所有的细节,包括群组协调、分区再均衡、发送心跳和获取数据,这使得开发者只需要关注从分区返回的数据,然后进行业务处理。

以下是Python版本的代码,并解释了每一步的含义:

from kafka import KafkaConsumer, TopicPartition

# 定义主题和消费者组
topic = 'Hello-Kafka'
group = 'group1'

# 设置Kafka客户端属性
props = {
'bootstrap.servers': 'hadoop001:9092', # Kafka服务端的信息,多个节点信息之间可以用逗号连接
'group.id': group, # 指定消费者组
'key.deserializer': 'org.apache.kafka.common.serialization.StringDeserializer', # key的反序列化器
'value.deserializer': 'org.apache.kafka.common.serialization.StringDeserializer' # value的反序列化器
}

# 创建一个Kafka消费者客户端
consumer = KafkaConsumer(**props)

# 订阅主题
consumer.subscribe(topics=[topic])

try:
# 轮询获取数据
while True:
records = consumer.poll(timeout_ms=100) # 发起一次轮询,获取所有主题的数据
for tp, records in records.items(): # 遍历每个主题的数据
for record in records: # 遍历主题中的每条数据
# 输出消费的数据的相关信息
print(f'topic = {record.topic}, partition = {record.partition}, \
key = {record.key}, value = {record.value}, offset = {record.offset}')
finally:
# 关闭消费者客户端
consumer.close()

在这份代码中,我们通过KafkaConsumer类创建了一个Kafka消费者客户端。bootstrap.servers属性指定了Kafka服务端的信息,group.id属性指定了消费者组。

调用subscribe()方法订阅主题,然后在一个死循环中调用poll()方法轮询主题获取数据,传递超时时间timeout_ms参数,如果在轮询的时间内没有获取到数据,就返回空值。

获取到数据并遍历输出相关信息,最后在finally块中关闭消费者客户端。

消费者偏移量

  • 偏移量的重要性:

Kafka 的每一条消息都有一个偏移量属性,记录了其在分区中的位置,偏移量是一个单调递增的整数。消费者通过往一个叫作 _consumer_offset 的特殊主题发送消息,消息里包含每个分区的偏移量。 如果消费者一直处于运行状态,那么偏移量就没有 什么用处。不过,如果有消费者退出或者新分区加入,此时就会触发再均衡。完成再均衡之后,每个消费者可能分配到新的分区,而不是之前处理的那个。为了能够继续之前的工作,消费者需要读取每个分区最后一次提交的偏移量,然后从偏移量指定的地方继续处理。 因为这个原因,所以如果不能正确提交偏移量,就可能会导致数据丢失或者重复出现消费,比如下面情况:

  • 如果提交的偏移量小于客户端处理的最后一个消息的偏移量 ,那么处于两个偏移量之间的消息就会被重复消费;

  • 如果提交的偏移量大于客户端处理的最后一个消息的偏移量,那么处于两个偏移量之间的消息将会丢失。

自动提交偏移量

Kafka 支持自动提交和手动提交偏移量两种方式。这里先介绍比较简单的自动提交:

只需要将消费者的 enable.auto.commit 属性配置为 true 即可完成自动提交的配置。 此时每隔固定的时间,消费者就会把 poll() 方法接收到的最大偏移量进行提交,提交间隔由 auto.commit.interval.ms 属性进行配置,默认值是 5s。

使用自动提交是存在隐患的,假设我们使用默认的 5s 提交时间间隔,在最近一次提交之后的 3s 发生了再均衡,再均衡之后,消费者从最后一次提交的偏移量位置开始读取消息。这个时候偏移量已经落后了 3s ,所以在这 3s 内到达的消息会被重复处理。可以通过修改提交时间间隔来更频繁地提交偏移量,减小可能出现重复消息的时间窗,不过这种情况是无法完全避免的。基于这个原因,Kafka 也提供了手动提交偏移量的 API,使得用户可以更为灵活的提交偏移量。

在Python中,Kafka消费者默认是自动提交偏移量的。以下是一个示例代码,展示如何使用Python自动提交偏移量:

from kafka import KafkaConsumer

topic = "Hello-Kafka"
group = "group1"

# 设置Kafka消费者的属性
properties = {
'bootstrap_servers': 'hadoop001:9092',
'group_id': group,
'key_deserializer': str.decode,
'value_deserializer': str.decode,
'enable_auto_commit': True, # 开启自动提交偏移量
'auto_commit_interval_ms': 5000 # 自动提交偏移量的间隔时间(毫秒)
}

# 创建一个Kafka消费者客户端
consumer = KafkaConsumer(topic, **properties)

try:
for message in consumer:
print(f"topic = {message.topic}, partition = {message.partition}, key = {message.key}, value = {message.value}, offset = {message.offset}")
except KeyboardInterrupt:
pass
finally:
consumer.close()

在此示例中,我们在创建Kafka消费者客户端时,通过设置enable_auto_commit参数为True来开启自动提交偏移量。同时,我们可以通过auto_commit_interval_ms参数来指定自动提交偏移量的间隔时间。

for循环中,我们使用consumer对象迭代获取消息,并打印消息的相关信息。由于开启了自动提交偏移量,消费者会在每次拉取到消息后自动提交偏移量。

最后,在结束前一定要调用consumer.close()方法来关闭消费者客户端,以释放资源。

手动提交偏移量

用户可以通过将 enable.auto.commit 设为 false,然后手动提交偏移量。基于用户需求手动提交偏移量可以分为两大类:

  • 手动提交当前偏移量:即手动提交当前轮询的最大偏移量;

  • 手动提交固定偏移量:即按照业务需求,提交某一个固定的偏移量。

而按照 Kafka API,手动提交偏移量又可以分为同步提交和异步提交。

  • 同步提交:

    • 在Python中,可以通过调用consumer.commit()方法进行同步提交偏移量。以下是一个示例代码:

      from kafka import KafkaConsumer

      topic = "Hello-Kafka"
      group = "group1"

      # 设置Kafka消费者的属性
      properties = {
      'bootstrap_servers': 'hadoop001:9092',
      'group_id': group,
      'key_deserializer': str.decode,
      'value_deserializer': str.decode,
      'auto_offset_reset': 'earliest', # 设置消费者从最早的偏移量开始消费
      'enable_auto_commit': False # 关闭自动提交偏移量
      }

      # 创建一个Kafka消费者客户端
      consumer = KafkaConsumer(topic, **properties)

      try:
      for message in consumer:
      print(f"topic = {message.topic}, partition = {message.partition}, key = {message.key}, value = {message.value}, offset = {message.offset}")
      consumer.commit() # 同步提交偏移量
      except KeyboardInterrupt:
      pass
      finally:
      consumer.close()

      在此示例中,我们在创建Kafka消费者客户端时,通过设置enable_auto_commit参数为False来关闭自动提交偏移量。

      for循环中,我们使用consumer对象迭代获取消息,并打印消息的相关信息。然后,在每次处理完一条消息后,调用consumer.commit()方法进行同步提交偏移量。

      需要注意的是,默认情况下,commit()方法会提交当前轮询的最大偏移量。如果需要指定提交特定的偏移量,可以在调用commit()方法时传递参数,例如consumer.commit(offsets=[TopicPartition(topic, partition, offset)])

      最后,在结束前一定要调用consumer.close()方法来关闭消费者客户端,以释放资源。

如果某个提交失败,同步提交还会进行重试,这可以保证数据能够最大限度提交成功,但是同时也会降低程序的吞吐量。基于这个原因,Kafka 还提供了异步提交的 API。

  • 异步提交

    在Python中,可以通过调用consumer.commit_async()方法进行异步提交偏移量。以下是一个示例代码:

    from kafka import KafkaConsumer

    topic = "Hello-Kafka"
    group = "group1"

    # 设置Kafka消费者的属性
    properties = {
    'bootstrap_servers': 'hadoop001:9092',
    'group_id': group,
    'key_deserializer': str.decode,
    'value_deserializer': str.decode,
    'auto_offset_reset': 'earliest', # 设置消费者从最早的偏移量开始消费
    'enable_auto_commit': False # 关闭自动提交偏移量
    }

    # 创建一个Kafka消费者客户端
    consumer = KafkaConsumer(topic, **properties)

    try:
    for message in consumer:
    print(f"topic = {message.topic}, partition = {message.partition}, key = {message.key}, value = {message.value}, offset = {message.offset}")
    consumer.commit_async() # 异步提交偏移量
    except KeyboardInterrupt:
    pass
    finally:
    consumer.close()

    在此示例中,我们在创建Kafka消费者客户端时,通过设置enable_auto_commit参数为False来关闭自动提交偏移量。

    for循环中,我们使用consumer对象迭代获取消息,并打印消息的相关信息。然后,在每次处理完一条消息后,调用consumer.commit_async()方法进行异步提交偏移量。

    需要注意的是,commit_async()方法会在后台异步提交偏移量,不会阻塞程序执行。这样可以提高程序的吞吐量,因为此时可以继续请求数据,而不用等待Broker的响应。

    最后,在结束前一定要调用consumer.close()方法来关闭消费者客户端,以释放资源。

异步提交存在的问题是,在提交失败的时候不会进行自动重试,实际上也不能进行自动重试。假设程序同时提交了 200 和 300 的偏移量,此时 200 的偏移量失败的,但是紧随其后的 300 的偏移量成功了,此时如果重试就会存在 200 覆盖 300 偏移量的可能。同步提交就不存在这个问题,因为在同步提交的情况下,300 的提交请求必须等待服务器返回 200 提交请求的成功反馈后才会发出。基于这个原因,某些情况下,需要同时组合同步和异步两种提交方式。

#注释:虽然程序不能在失败时候进行自动重试,但是我们是可以手动进行重试的,你可以通过一个 Map offsets 来维护你提交的每个分区的偏移量,然后当失败时候,你可以判断失败的偏移量是否小于你维护的同主题同分区的最后提交的偏移量,如果小于则代表你已经提交了更大的偏移量请求,此时不需要重试,否则就可以进行手动重试。
  • 同步加异步:

    from kafka import KafkaConsumer

    topic = "Hello-Kafka"
    group = "group1"

    # 设置Kafka消费者的属性
    properties = {
    'bootstrap_servers': 'hadoop001:9092',
    'group_id': group,
    'key_deserializer': str.decode,
    'value_deserializer': str.decode,
    'auto_offset_reset': 'earliest', # 设置消费者从最早的偏移量开始消费
    'enable_auto_commit': False # 关闭自动提交偏移量
    }

    # 创建一个Kafka消费者客户端
    consumer = KafkaConsumer(topic, **properties)

    try:
    for message in consumer:
    print(f"topic = {message.topic}, partition = {message.partition}, key = {message.key}, value = {message.value}, offset = {message.offset}")

    # 同步提交偏移量
    consumer.commit()

    # 异步提交偏移量
    consumer.commit_async()
    except KeyboardInterrupt:
    pass
    finally:
    consumer.close()

    在此示例中,我们在创建Kafka消费者客户端时,通过设置enable_auto_commit参数为False来关闭自动提交偏移量。

    for循环中,我们使用consumer对象迭代获取消息,并打印消息的相关信息。在处理完一条消息后,我们同时调用consumer.commit()方法进行同步提交偏移量,以及consumer.commit_async()方法进行异步提交偏移量。

    需要注意的是,同步提交会阻塞程序执行,直到偏移量成功提交或发生错误。而异步提交则不会阻塞程序执行,并在后台异步提交偏移量。这样可以提高程序的吞吐量。

    最后,在结束前一定要调用consumer.close()方法来关闭消费者客户端,以释放资源。

在正常的轮询中使用异步提交来保证吞吐量,但是因为在最后即将要关闭消费者了,所以此时需要用同步提交来保证最大限度的提交成功。

  • 提交特定偏移量

    在上面同步和异步提交的 API 中,实际上我们都没有对 commit 方法传递参数,此时默认提交的是当前轮询的最大偏移量,如果你需要提交特定的偏移量,可以调用它们的重载方法。

    可以使用commit()方法来提交特定的偏移量。以下是一个示例代码:

    from kafka import KafkaConsumer, TopicPartition

    topic = "Hello-Kafka"
    group = "group1"

    # 设置Kafka消费者的属性
    properties = {
    'bootstrap_servers': 'hadoop001:9092',
    'group_id': group,
    'key_deserializer': str.decode,
    'value_deserializer': str.decode,
    'auto_offset_reset': 'earliest', # 设置消费者从最早的偏移量开始消费
    'enable_auto_commit': False # 关闭自动提交偏移量
    }

    # 创建一个Kafka消费者客户端
    consumer = KafkaConsumer(topic, **properties)

    try:
    for message in consumer:
    print(f"topic = {message.topic}, partition = {message.partition}, key = {message.key}, value = {message.value}, offset = {message.offset}")

    # 获取当前消息的分区和偏移量
    topic_partition = TopicPartition(message.topic, message.partition)
    offsets = {topic_partition: message.offset + 1} # 提交下一个偏移量

    # 提交特定的偏移量
    consumer.commit(offsets=offsets)
    except KeyboardInterrupt:
    pass
    finally:
    consumer.close()

    在此示例中,我们在创建Kafka消费者客户端时,通过设置enable_auto_commit参数为False来关闭自动提交偏移量。

    for循环中,我们使用consumer对象迭代获取消息,并打印消息的相关信息。在处理完一条消息后,我们获取当前消息的分区和偏移量,然后构建一个字典offsets,其中键是TopicPartition对象,值是下一个要提交的偏移量。

    最后,我们使用consumer.commit(offsets=offsets)方法来提交特定的偏移量。

    需要注意的是,提交的偏移量应该是下一个要消费的消息的偏移量,因此我们将当前消息的偏移量加1。

    最后,在结束前一定要调用consumer.close()方法来关闭消费者客户端,以释放资源。

  • 监听分区再均衡

    因为分区再均衡会导致分区与消费者的重新划分,有时候你可能希望在再均衡前执行一些操作:比如提交已经处理但是尚未提交的偏移量,关闭数据库连接等。此时可以在订阅主题时候,调用 subscribe 的重载方法传入自定义的分区再均衡监听器。

    在Python中,可以通过实现on_partitions_revokedon_partitions_assigned方法来监听分区的再均衡。以下是一个示例代码:

    from kafka import KafkaConsumer

    topic = "Hello-Kafka"
    group = "group1"

    # 设置Kafka消费者的属性
    properties = {
    'bootstrap_servers': 'hadoop001:9092',
    'group_id': group,
    'key_deserializer': str.decode,
    'value_deserializer': str.decode,
    'auto_offset_reset': 'earliest', # 设置消费者从最早的偏移量开始消费
    'enable_auto_commit': False, # 关闭自动提交偏移量
    }

    class MyConsumer:
    def __init__(self):
    self.consumer = KafkaConsumer(topic, **properties)

    def on_partitions_revoked(self, revoked):
    # 分区再均衡前执行的操作
    print("Partitions revoked:", revoked)

    def on_partitions_assigned(self, assigned):
    # 分区再均衡后执行的操作
    print("Partitions assigned:", assigned)

    def consume(self):
    try:
    self.consumer.subscribe(topics=[topic], on_assign=self.on_partitions_assigned, on_revoke=self.on_partitions_revoked)

    for message in self.consumer:
    print(f"topic = {message.topic}, partition = {message.partition}, key = {message.key}, value = {message.value}, offset = {message.offset}")

    # 处理消息

    # 手动提交偏移量
    self.consumer.commit()
    except KeyboardInterrupt:
    pass
    finally:
    self.consumer.close()

    # 创建一个自定义的消费者对象
    consumer = MyConsumer()

    # 启动消费者
    consumer.consume()

    在此示例中,我们首先定义了一个MyConsumer类,该类包含了on_partitions_revokedon_partitions_assigned方法。这两个方法分别用于在分区再均衡前和分区再均衡后执行操作。

    consume方法中,我们通过调用subscribe方法来订阅主题,并传递on_assignon_revoke参数,以指定分区再均衡时的回调方法。

    for循环中,我们使用consumer对象迭代获取消息,并打印消息的相关信息。在处理完一条消息后,我们手动提交偏移量,以确保消息被正确处理。

    最后,在结束前一定要调用consumer.close()方法来关闭消费者客户端,以释放资源。

  • 退出轮询

    Kafka 提供了 consumer.wakeup() 方法用于退出轮询,它通过抛出 WakeupException 异常来跳出循环。需要注意的是,在退出线程时最好显示的调用 consumer.close() , 此时消费者会提交任何还没有提交的东西,并向群组协调器发送消息,告知自己要离开群组,接下来就会触发再均衡 ,而不需要等待会话超时。

    可以使用wakeup()方法和多线程来实现优雅退出轮询。以下是一个示例代码:

    from kafka import KafkaConsumer
    import threading

    topic = "Hello-Kafka"
    group = "group1"

    # 设置Kafka消费者的属性
    properties = {
    'bootstrap_servers': 'hadoop001:9092',
    'group_id': group,
    'key_deserializer': str.decode,
    'value_deserializer': str.decode,
    'auto_offset_reset': 'earliest', # 设置消费者从最早的偏移量开始消费
    'enable_auto_commit': False # 关闭自动提交偏移量
    }

    # 创建一个Kafka消费者客户端
    consumer = KafkaConsumer(topic, **properties)

    # 创建一个线程用于监听退出指令
    def listen_for_exit():
    while True:
    command = input("Enter 'exit' to stop consuming: ")
    if command == 'exit':
    consumer.wakeup()
    break

    # 启动监听线程
    exit_thread = threading.Thread(target=listen_for_exit)
    exit_thread.start()

    try:
    while True:
    records = consumer.poll(timeout_ms=100)
    for topic_partition, record_list in records.items():
    for record in record_list:
    print(f"topic = {record.topic}, partition = {record.partition}, key = {record.key}, value = {record.value}, offset = {record.offset}")
    # 处理消息

    # 手动提交偏移量
    consumer.commit()
    except KeyboardInterrupt:
    pass
    finally:
    consumer.close()
    exit_thread.join()
    print("Consumer closed")

    在此示例中,我们创建了一个名为listen_for_exit的函数,用于监听退出指令。在一个单独的线程中,我们使用input()函数获取用户输入的指令,如果输入的是"exit",则调用consumer.wakeup()方法唤醒消费者,然后退出循环。

    在主线程中,我们使用while循环来轮询获取消息。当捕获到KeyboardInterrupt异常时,我们会进入finally块,关闭消费者并等待退出线程结束。

    需要注意的是,在结束前一定要调用consumer.close()方法来关闭消费者客户端,以释放资源。最后,通过调用exit_thread.join()方法,等待退出线程执行完毕。

    这样,当用户输入"exit"时,消费者会优雅地退出轮询并关闭。

  • 独立的消费者

    在Kafka中,独立的消费者是指每个消费者实例都具有唯一的消费者组ID,这样每个消费者实例可以独立地消费主题中的消息,而不会共享分区。以下是一个示例代码:

    from kafka import KafkaConsumer

    topic = "Hello-Kafka"
    group = None # 设置为None表示使用独立的消费者

    # 设置Kafka消费者的属性
    properties = {
    'bootstrap_servers': 'hadoop001:9092',
    'group_id': group,
    'key_deserializer': str.decode,
    'value_deserializer': str.decode,
    'auto_offset_reset': 'earliest', # 设置消费者从最早的偏移量开始消费
    'enable_auto_commit': False # 关闭自动提交偏移量
    }

    # 创建一个独立的Kafka消费者客户端
    consumer = KafkaConsumer(topic, **properties)

    try:
    for message in consumer:
    print(f"topic = {message.topic}, partition = {message.partition}, key = {message.key}, value = {message.value}, offset = {message.offset}")

    # 处理消息

    # 手动提交偏移量
    consumer.commit()
    except KeyboardInterrupt:
    pass
    finally:
    consumer.close()

    在此示例中,我们将group设置为None,表示使用独立的消费者。每个消费者实例都具有唯一的消费者组ID,因此它们可以独立地消费主题中的消息。

    其他部分与常规消费者相同,我们设置了Kafka消费者的属性,并创建了一个独立的Kafka消费者客户端。在循环中,我们使用consumer对象迭代获取消息,并打印消息的相关信息。在处理完一条消息后,我们手动提交偏移量,以确保消息被正确处理。

    最后,在结束前一定要调用consumer.close()方法来关闭消费者客户端,以释放资源。

因为 Kafka 的设计目标是高吞吐和低延迟,所以在 Kafka 中,消费者通常都是从属于某个群组的,这是因为单个消费者的处理能力是有限的。但是某些时候你的需求可能很简单,比如可能只需要一个消费者从一个主题的所有分区或者某个特定的分区读取数据,这个时候就不需要消费者群组和再均衡了, 只需要把主题或者分区分配给消费者,然后开始读取消息井提交偏移量即可。在这种情况下,就不需要订阅主题, 取而代之的是消费者为自己分配分区。 一个消费者可以订阅主题(井加入消费者群组),或者为自己分配分区,但不能同时做这两件事情。

kafka消费者可选属性

  1. fetch.min.byte

消费者从服务器获取记录的最小字节数。如果可用的数据量小于设置值,broker 会等待有足够的可用数据时才会把它返回给消费者。

2. fetch.max.wait.ms

broker 返回给消费者数据的等待时间,默认是 500ms。

3. max.partition.fetch.bytes

该属性指定了服务器从每个分区返回给消费者的最大字节数,默认为 1MB。

4. session.timeout.ms

消费者在被认为死亡之前可以与服务器断开连接的时间,默认是 3s。

5. auto.offset.reset

该属性指定了消费者在读取一个没有偏移量的分区或者偏移量无效的情况下该作何处理:

  • latest (默认值) :在偏移量无效的情况下,消费者将从最新的记录开始读取数据(在消费者启动之后生成的最新记录);

  • earliest :在偏移量无效的情况下,消费者将从起始位置读取分区的记录。

6. enable.auto.commit

是否自动提交偏移量,默认值是 true。为了避免出现重复消费和数据丢失,可以把它设置为 false。

7. client.id

客户端 id,服务器用来识别消息的来源。

8. max.poll.records

单次调用 poll() 方法能够返回的记录数量。

9. receive.buffer.bytes & send.buffer.byte

这两个参数分别指定 TCP socket 接收和发送数据包缓冲区的大小,-1 代表使用操作系统的默认值。

深入理解kafka副本机制

kafka集群

Kafka 使用 Zookeeper 来维护集群成员 (brokers) 的信息。每个 broker 都有一个唯一标识 broker.id,用于标识自己在集群中的身份,可以在配置文件 server.properties 中进行配置,或者由程序自动生成。下面是 Kafka brokers 集群自动创建的过程:

  • 每一个 broker 启动的时候,它会在 Zookeeper 的 /brokers/ids 路径下创建一个 临时节点,并将自己的 broker.id 写入,从而将自身注册到集群;

  • 当有多个 broker 时,所有 broker 会竞争性地在 Zookeeper 上创建 /controller 节点,由于 Zookeeper 上的节点不会重复,所以必然只会有一个 broker 创建成功,此时该 broker 称为 controller broker。它除了具备其他 broker 的功能外,还负责管理主题分区及其副本的状态

  • 当 broker 出现宕机或者主动退出从而导致其持有的 Zookeeper 会话超时时,会触发注册在 Zookeeper 上的 watcher 事件,此时 Kafka 会进行相应的容错处理;如果宕机的是 controller broker 时,还会触发新的 controller 选举。

副本机制

为了保证高可用,kafka 的分区是多副本的,如果一个副本丢失了,那么还可以从其他副本中获取分区数据。但是这要求对应副本的数据必须是完整的,这是 Kafka 数据一致性的基础,所以才需要使用 controller broker 来进行专门的管理。下面将详解介绍 Kafka 的副本机制。

2.1 分区和副本

Kafka 的主题被分为多个分区 ,分区是 Kafka 最基本的存储单位。每个分区可以有多个副本 (可以在创建主题时使用 replication-factor 参数进行指定)。其中一个副本是首领副本 (Leader replica),所有的事件都直接发送给首领副本;其他副本是跟随者副本 (Follower replica),需要通过复制来保持与首领副本数据一致,当首领副本不可用时,其中一个跟随者副本将成为新首领。

image-20230619152015343

2.2 ISR机制

每个分区都有一个 ISR(in-sync Replica) 列表,用于维护所有同步的、可用的副本。首领副本必然是同步副本,而对于跟随者副本来说,它需要满足以下条件才能被认为是同步副本:

  • 与 Zookeeper 之间有一个活跃的会话,即必须定时向 Zookeeper 发送心跳;

  • 在规定的时间内从首领副本那里低延迟地获取过消息。

如果副本不满足上面条件的话,就会被从 ISR 列表中移除,直到满足条件才会被再次加入。

这里给出一个主题创建的示例:使用 --replication-factor 指定副本系数为 3,创建成功后使用 --describe 命令可以看到分区 0 的有 0,1,2 三个副本,且三个副本都在 ISR 列表中,其中 1 为首领副本。

2.3 不完全的首领选举

对于副本机制,在 broker 级别有一个可选的配置参数 unclean.leader.election.enable,默认值为 fasle,代表禁止不完全的首领选举。这是针对当首领副本挂掉且 ISR 中没有其他可用副本时,是否允许某个不完全同步的副本成为首领副本,这可能会导致数据丢失或者数据不一致,在某些对数据一致性要求较高的场景 (如金融领域),这可能无法容忍的,所以其默认值为 false,如果你能够允许部分数据不一致的话,可以配置为 true。

2.4 最少同步副本

ISR 机制的另外一个相关参数是 min.insync.replicas , 可以在 broker 或者主题级别进行配置,代表 ISR 列表中至少要有几个可用副本。这里假设设置为 2,那么当可用副本数量小于该值时,就认为整个分区处于不可用状态。此时客户端再向分区写入数据时候就会抛出异常 org.apache.kafka.common.errors.NotEnoughReplicasExceptoin: Messages are rejected since there are fewer in-sync replicas than required。

2.5 发送确认

Kafka 在生产者上有一个可选的参数 ack,该参数指定了必须要有多少个分区副本收到消息,生产者才会认为消息写入成功:

  • acks=0 :消息发送出去就认为已经成功了,不会等待任何来自服务器的响应;

  • acks=1 : 只要集群的首领节点收到消息,生产者就会收到一个来自服务器成功响应;

  • acks=all :只有当所有参与复制的节点全部收到消息时,生产者才会收到一个来自服务器的成功响应。

数据请求

3.1 元数据请求机制

在所有副本中,只有领导副本才能进行消息的读写处理。由于不同分区的领导副本可能在不同的 broker 上,如果某个 broker 收到了一个分区请求,但是该分区的领导副本并不在该 broker 上,那么它就会向客户端返回一个 Not a Leader for Partition 的错误响应。 为了解决这个问题,Kafka 提供了元数据请求机制。

首先集群中的每个 broker 都会缓存所有主题的分区副本信息,客户端会定期发送发送元数据请求,然后将获取的元数据进行缓存。定时刷新元数据的时间间隔可以通过为客户端配置 metadata.max.age.ms 来进行指定。有了元数据信息后,客户端就知道了领导副本所在的 broker,之后直接将读写请求发送给对应的 broker 即可。

如果在定时请求的时间间隔内发生的分区副本的选举,则意味着原来缓存的信息可能已经过时了,此时还有可能会收到 Not a Leader for Partition 的错误响应,这种情况下客户端会再次求发出元数据请求,然后刷新本地缓存,之后再去正确的 broker 上执行对应的操作,过程如下图:

 

3.2 数据可见性

需要注意的是,并不是所有保存在分区首领上的数据都可以被客户端读取到,为了保证数据一致性,只有被所有同步副本 (ISR 中所有副本) 都保存了的数据才能被客户端读取到。

 

3.3 零拷贝

Kafka 所有数据的写入和读取都是通过零拷贝来实现的。传统拷贝与零拷贝的区别如下:

传统模式下的四次拷贝与四次上下文切换

以将磁盘文件通过网络发送为例。传统模式下,一般使用如下伪代码所示的方法先将文件数据读入内存,然后通过 Socket 将内存中的数据发送出去。

buffer = File.read
Socket.send(buffer)

这一过程实际上发生了四次数据拷贝。首先通过系统调用将文件数据读入到内核态 Buffer(DMA 拷贝),然后应用程序将内存态 Buffer 数据读入到用户态 Buffer(CPU 拷贝),接着用户程序通过 Socket 发送数据时将用户态 Buffer 数据拷贝到内核态 Buffer(CPU 拷贝),最后通过 DMA 拷贝将数据拷贝到 NIC Buffer。同时,还伴随着四次上下文切换,如下图所示:

 

sendfile和transferTo实现零拷贝

Linux 2.4+ 内核通过 sendfile 系统调用,提供了零拷贝。数据通过 DMA 拷贝到内核态 Buffer 后,直接通过 DMA 拷贝到 NIC Buffer,无需 CPU 拷贝。这也是零拷贝这一说法的来源。除了减少数据拷贝外,因为整个读文件到网络发送由一个 sendfile 调用完成,整个过程只有两次上下文切换,因此大大提高了性能。零拷贝过程如下图所示:

 

从具体实现来看,Kafka 的数据传输通过 TransportLayer 来完成,其子类 PlaintextTransportLayertransferFrom 方法通过调用 python NIO 中 FileChannel 的 transferTo 方法实现零拷贝,如下所示:

def transfer_from(file_channel, position, count, socket_channel):
return file_channel.transfer_to(position, count, socket_channel)

注: transferTotransferFrom 并不保证一定能使用零拷贝。实际上是否能使用零拷贝与操作系统相关,如果操作系统提供 sendfile 这样的零拷贝系统调用,则这两个方法会通过这样的系统调用充分利用零拷贝的优势,否则并不能通过这两个方法本身实现零拷贝。

物理存储

4.1 分区分配

在创建主题时,Kafka 会首先决定如何在 broker 间分配分区副本,它遵循以下原则:

  • 在所有 broker 上均匀地分配分区副本;

  • 确保分区的每个副本分布在不同的 broker 上;

  • 如果使用了 broker.rack 参数为 broker 指定了机架信息,那么会尽可能的把每个分区的副本分配到不同机架的 broker 上,以避免一个机架不可用而导致整个分区不可用。

基于以上原因,如果你在一个单节点上创建一个 3 副本的主题,通常会抛出下面的异常:

Error while executing topic command : org.apache.kafka.common.errors.InvalidReplicationFactor   
Exception: Replication factor: 3 larger than available brokers: 1.
4.2 分区数据保留规则

保留数据是 Kafka 的一个基本特性, 但是 Kafka 不会一直保留数据,也不会等到所有消费者都读取了消息之后才删除消息。相反, Kafka 为每个主题配置了数据保留期限,规定数据被删除之前可以保留多长时间,或者清理数据之前可以保留的数据量大小。分别对应以下四个参数:

  • log.retention.bytes :删除数据前允许的最大数据量;默认值-1,代表没有限制;

  • log.retention.ms:保存数据文件的毫秒数,如果未设置,则使用 log.retention.minutes 中的值,默认为 null;

  • log.retention.minutes:保留数据文件的分钟数,如果未设置,则使用 log.retention.hours 中的值,默认为 null;

  • log.retention.hours:保留数据文件的小时数,默认值为 168,也就是一周。

因为在一个大文件里查找和删除消息是很费时的,也很容易出错,所以 Kafka 把分区分成若干个片段,当前正在写入数据的片段叫作活跃片段。活动片段永远不会被删除。如果按照默认值保留数据一周,而且每天使用一个新片段,那么你就会看到,在每天使用一个新片段的同时会删除一个最老的片段,所以大部分时间该分区会有 7 个片段存在。

4.3 文件格式

通常保存在磁盘上的数据格式与生产者发送过来消息格式是一样的。 如果生产者发送的是压缩过的消息,那么同一个批次的消息会被压缩在一起,被当作“包装消息”进行发送 (格式如下所示) ,然后保存到磁盘上。之后消费者读取后再自己解压这个包装消息,获取每条消息的具体信息。

 

posted @ 2023-06-19 15:55  未来的启示录  阅读(100)  评论(0)    收藏  举报