1 Kafka简介
1.1 Kafka定义
Kafka是一个分布式的基于发布/订阅模式的消息队列,主要应用于大数据实时处理领域,同时其也是一个分布式事件流平台 ,用于高性能数据管道、流分析、数据集成和关键任务应用。
1.2 Kafka基础架构
-
Producer:消息生产者,就是向 Kafka broker 发消息的客户端。
-
Consumer:消息消费者,订阅 Kafka broker 消息的客户端。
-
Consumer Group(CG):消费者组,由多个 consumer 组成。消费者组内每个消费者负责消费不同分区的数据,一个分区只能由一个组内消费者消费;消费者组之间互不影响。所有的消费者都属于某个消费者组,即消费者组是逻辑上的一个订阅者。
-
Broker:一台 Kafka 服务器就是一个 broker。一个集群由多个 broker 组成。一个broker 可以容纳多个 topic。
-
Topic:可以理解为一个队列,生产者和消费者面向的都是一个 topic。
-
Partition:为了实现扩展性,一个非常大的 topic 可以分布到多个 broker(即服器)上,一个 topic可以分为多个 partition,每个 partition 是一个有序的队列。
-
Replica:副本。一个 topic 的每个分区都有若干个副本,一个 Leader和若干个Follower。
-
Leader:“主”副本,生产者发送数据的对象,以及消费者消费数据的对象都是 Leader。
-
Follower:“从”副本,实时从 Leader 中同步数据,保持和Leader 数据的同步。Leader 发生故障时,某个 Follower 会成为新的 Leader。
-
ISR:和leader保持同步的follower+leader集合,如
leader:0,ISR:0,1,2
;如果Follower长时间未向Leader发送通信请求或同步数据,则该Follower将被踢出ISR。该时间阈值由replica.lag.time.max.ms
参数设定,默认30s。例如2超时,(leader:0, isr:0,1)。 -
AR:每个分区中的所有副本统称
1.3 Kafka-Kraft模式
1.3.1 Kraft架构
-
介绍
Kafka-Zk架构的元数据存储在zookeeper中,运行时动态选举controller,由controller进行Kafka集群的管理。Kafka-Kraft架构上移除了zk的依赖,用三台controller节点代替zk集群,元数据保存在 controller 中,由 controller 直接进行 Kafka 集群管理。
-
好处
-
Kafka不再依赖外部框架,能够独立运行,集群扩展时不再受zk读写能力限制。
-
controller直接管理Kafka集群,性能上升。
-
controller不再动态选举,而是由配置文件设置,可以针对性的加强controller节点的配置应对高负载。
-
1.3.2 Kraft内容
在 KRaft模式中,一部分 broker 被指定为控制器,这些控制器提供过去由 ZK 提供的共识服务。所有集群元数据都将存储在 Kafka 主题中并在内部进行管理。Kafka 集群可以以专用或共享模式运行。 在专用模式下,一些节点将其process.roles配置设置为controller,而其余节点将其设置为broker。对于共享模式,一些节点将process.roles设置为controller, broker并且这些节点将执行双重任务。采用哪种方式取决于集群的大小。 在 KRaft 模式集群中充当控制器的代理列在controller.quorum.voters每个代理上设置的配置属性中。这允许所有代理与控制器进行通信。这些控制器代理之一将是活动控制器,它将处理与其他代理通信对元数据的更改。 所有控制器代理都维护一个保持最新的内存元数据缓存,以便任何控制器都可以在需要时接管作为活动控制器。这是 KRaft 的特性之一,使其比基于 ZooKeeper 的控制平面高效得多。 集群元数据存储在名为__cluster_metadata,Kafka使用这个主题在控制器和代理节点之间同步集群状态更改。不是控制器将元数据更改广播给其他控制器或代理,而是它们各自获取更改。这使得保持所有控制器和代理同步非常有效,并且还缩短了代理和控制器的重启时间。
2 集群安装
2.1 集群规划
-
当前稳定版本:kafka_2.13-3.5.1.tgz,推荐使用kraft模式部署。
-
规划目录:/opt/modules
-
前置要求:有JDK配置,不推荐使用centos7默认的配置
-
服务器:192.168.23.128;192.168.23.129;192.168.23.130
2.2 集群安装
解压安装包并重命名
tar -zxvf kafka_2.13-3.5.1.tgz -C /opt/module/
mv kafka_2.13-3.5.1 kafka
修改配置文件
vim /opt/modules/kafka/config/kraft/server.properties
修改后的配置文件
############################# Server Basics #############################
# Kafka的角色 controller相当于主机(zk);controller相当于从机
process.roles=broker,controller
# 节点ID,集群中唯一
node.id=1
# 所有的Controller节点列表
controller.quorum.voters=1@192.168.23.128:9093,2@192.168.23.129:9093,3@192.168.23.130:9093
############################# Socket Server Settings #############################
# 不同节点绑定的端口
listeners=PLAINTEXT://:9092,CONTROLLER://:9093
# broker服务协议别名
inter.broker.listener.name=PLAINTEXT
# broker对外暴露的地址
advertised.listeners=PLAINTEXT://192.168.23.128:9092
# Controller服务协议的别名
controller.listener.names=CONTROLLER
# 协议别名到安全协议的映射
listener.security.protocol.map=CONTROLLER:PLAINTEXT,PLAINTEXT:PLAINTEXT,SSL:SSL,SASL_PLAINTEXT:SASL_PLAINTEXT,SASL_SSL:SASL_SSL
# 数据传输线程数,总核数的50%的 2/3
num.network.threads=3
# 处理磁盘IO的线程数量,占总核数的50%
num.io.threads=4
# 副本拉取线程数,总核数的 50%的 1/3
#num.replica.fetchers=
# 发送套接字的缓冲区大小
socket.send.buffer.bytes=102400
# 接收套接字的缓冲区大小
socket.receive.buffer.bytes=102400
# 请求套接字的缓冲区大小
socket.request.max.bytes=104857600
############################# Log Basics #############################
# kafka 运行日志(数据)存放的路径,路径不需要提前创建,kafka 自动帮你创建,可以配置多个磁盘路径,路径与路径之间可以用","分隔
log.dirs=/opt/modules/kafka/data
# 是否自动创建主题,默认true,为true当生产者向一个未创建的topic发送消息时会自动创建一个分区数为num.partitions、副本因子为default.replication.factor的topic,消费也是同理,这种创建是非预期的,增加topic的管理和维护难度,建议关闭
auto.create.topics.enable=fales
# topic在当前broker上的分区数
num.partitions=1
# 用来恢复和清理data下数据的线程数量
num.recovery.threads.per.data.dir=1
############################# Internal Topic Settings #############################
# 用于设置新创建的主题(包括系统主题)的默认副本数
default.replication.factor=3
# 每个 topic 创建时的副本数,默认1个副本
offsets.topic.replication.factor=3
# 每个Topic的事务状态日志分区的副本因子,事务状态日志分区记录了正在进行中的事务和已提交的事务的状态信息,为了确保数据的可靠性和高可用性,事务状态日志的副本会在 Kafka 集群中进行复制
transaction.state.log.replication.factor=3
# Topic事务状态日志的最小副本同步数,它决定了在进行事务状态日志的副本同步时,至少有多少个副本必须参与同步才认为同步成功。
# 不能大于 transaction.state.log.replication.factor,否则可能导致无法进行副本同步,从而影响数据的可用性。
transaction.state.log.min.isr=2
# 自动 Leader Partition 平衡
auto.leader.rebalance.enable=true
# 每个broker允许的不平衡的leader的比率,如果有broker不平衡leader超过该比例,控制器会触发再平衡策略
leader.imbalance.per.broker.percentage=10
# 检查leader负载是否平衡的时间间隔
leader.imbalance.check.interval.seconds=300
# 如果Follower指定时间内未向Leader发送通信请求或同步数据,则该Follower将被踢出ISR
replica.lag.time.max.ms=30000
# ISR队列中的应答的最小副本数量
min.insync.replicas=2
############################# Log Flush Policy #############################
#消息会立即写入文件系统,但默认情况下我们只使用fsync()来同步
#操作系统延迟缓存。以下配置控制将数据刷新到磁盘。
#这里有一些重要的权衡:
# 1。持久性:如果不使用复制,可能会丢失未刷新的数据。
# 2。延迟:当发生刷新时,非常大的刷新间隔可能导致延迟峰值,因为将有大量数据需要刷新。
# 3。吞吐量:刷新通常是开销最大的操作,较小的刷新间隔可能导致查找过多。
#下面的设置允许将刷新策略配置为在一段时间后刷新数据
#每N条消息(或两者)。这可以在全局范围内完成,并在每个主题的基础上覆盖。
# 强制页缓存刷新写到磁盘的记录数
#log.flush.interval.messages=10000
# 刷新数据到磁盘的时间间隔
#log.flush.interval.ms=1000
############################# Log Retention Policy #############################
# segment 文件保留的最长时间,7天,超时将被删除
log.retention.hours=168
# segment日志删除策略,7天内产生日志大于此配置将删除最早的,log.retention.hours相互独立
#log.retention.bytes=1073741824
# 每个 segment 文件的大小,默认最大 1G,超出这个范围将创建一个新的日志文件(KB)
log.segment.bytes=1073741824
# kafka 默认里面每当写入了 4kb 大小的日志(.log),然后就往 index 文件里面记录一个索引
log.index.interval.bytes=4096
# 检查过期数据的时间,默认 5 分钟检查一次是否数据过期
log.retention.check.interval.ms=300000
# 对于过期的的数据删除策略,delete清理,compact压缩
log.cleanup.policy=delete
另外两台服务器同样如此配置,需要更改
node.id
和advertised.listeners
生成存储目录唯一ID
/opt/modules/kafka/bin/kafka-storage.sh random-uuid
使用唯一ID格式化存储目录
bin/kafka-storage.sh format -t YOUR_RANDOM_UUID -c /opt/modules/kafka/config/kraft/server.properties
启动kafka集群
bin/kafka-server-start.sh -daemon config/kraft/server.properties
停止kafka集群
bin/kafka-server-stop.sh
3 Kafka命令行操作
-
查询原来存储在zk上的元数据摘要信息
bin/kafka-metadata-quorum.sh --bootstrap-server localhost:9092 describe --status
bin/kafka-metadata-quorum.sh --bootstrap-server localhost:9092 describe --replication
3.1 Topic相关命令行操作
-
查询集群中的所有topic
bin/kafka-topics.sh --bootstrap-server localhost:9092 --list
-
创建Topic
bin/kafka-topics.sh --bootstrap-server localhost:9092 --create --partitions 3 --replication-factor 3 --topic first
-
查询指定Topic
bin/kafka-topics.sh --bootstrap-server localhost:9092 --create --partitions 3 --replication-factor 3 --topic first
-
修改分区数(只能增加不能减少)
bin/kafka-topics.sh --bootstrap-server localhost:9092 --alter --topic first --partitions 3
-
删除Topic
bin/kafka-topics.sh --bootstrap-server localhost:9092 --delete --topic first
3.2 生产者相关命令行操作
向Topic发送消息
bin/kafka-console-producer.sh --bootstrap-server localhost:9092 --topic first
3.3 消费者相关命令行操作
实时消费Topic数据, --group *** 是指定的消费者组名称
bin/kafka-console-consumer.sh --bootstrap-server localhost:9092 --topic first <-- group ***>
消费Topic历史数据
bin/kafka-console-consumer.sh --bootstrap-server localhost:9092 --from-beginning --topic first <-- group ***> --
4 Kafka生产者
4.1 发送原理
生产者发送消息的时候涉及到main和sender两个线程,在 main 线程中创建了RecordAccumulator双端队列,默认32m,生产者发送的数据分批存储在这个队列中。main 线程将消息发送给 RecordAccumulator,Sender 线程不断从 RecordAccumulator 中拉取消息发送到 Kafka Broker。
当RecordAccumulator中的一批数据达到batch.size或者时间上到linger.ms后发送到sender线程,通过selector发送到Kafka集群,Kafka集群会根据应答级别acks向sender线程返回成功或者失败的信息,成功后会清理 RecordAccumulator 中的数据。
sender线程中维护着一个NetworkClient,默认每个broker最多缓存5个请求。
4.2 生产者重要参数
参数名称 | 描述 |
---|---|
bootstrap.servers | 生产者连接集群所需的broker地址清单,可以是一个或者多个,如:192.168.23.128:9092 |
key.serializer 和 value.serializer | 指定发送消息的 key 和 value 的序列化类型,一定要写全类名 |
buffer.memory | RecordAccumulator双端队列缓冲区总大小,默认 32m |
batch.size | 缓冲区一批数据最大值,默认 16k。适当增加该值,可以提高吞吐量,但是如果该值设置太大,会导致数据传输性能降低 |
linger.ms | 如果数据迟迟未达到 batch.size,sender 等待 linger.time之后就会发送数据。单位 ms,默认值是 0,表示无延迟,生产环境建议该值大小为 5-100ms 之间 |
acks | 0:生产者发送过来的数据,不需要等数据落盘应答;1:生产者发送过来的数据,Leader 收到数据后应答;-1(all):生产者发送过来的数据,Leader+和 isr 队列里面的所有节点收齐数据后应答。默认值是-1,-1 和 all 是等价的。 |
max.in.flight.requests.per.connection | 允许最多没有返回 ack 的次数,默认为 5,开启幂等性要保证该值是 1-5 的数字 |
retries | 当消息发送出现错误的时候,系统会重发消息,retries表示重试次数,默认值是int的最大值,如果设置了重试,还想保证消息的有序性,需要设置max.in.flight.requests.per.connection=1,否则,如果有其他消息也在同一个连接上进行发送,并且其中的一个重试请求比较耗时,可能会导致其他新消息请求先于重试请求发送成功。有序性和吞吐量之间需要具体权衡 |
retry.backoff.ms | 重试之间的时间间隔,默认是 100ms。 |
enable.idempotence | 是否开启幂等性,默认 true,开启幂等性 |
compression.type | 生产者发送的所有数据的压缩方式。默认是 none不压缩;支持压缩类型:none、gzip、snappy、lz4 和 zstd。 |
4.3 生产者分区
4.3.1 分区好处
-
分区就是把海量数据按照分区切割成块存储在多个broker上,合理控制分区能够实现负载均衡的效果;
-
生产者以分区为单位发送数据,消费者组中的各消费者以分区为单位消费数据,提高并行度
4.3.2 分区策略
默认分区器:DefaultPartitioner
分区原则
-
指明partiton的情况下,直接将指明的值作为分区,如partiton=0,所有数据都会写入0号分区;
-
没有指明partiton但有key的情况下,将key的hash值与topic的分区数取余的值作为partiton的值;
-
既没有partition值又没有key值的情况下,kafka采用粘性分区器,会随机选择并尽可能一直使用一个分区,直到该分区的batch已满或者已完成,才会再随机选择非当前的分区。(第一次调用时随机生成一个整数(后面每次调用在这个整数上自增),将这个值与 topic 可用的 partition 总数取余得到 partition值,即 round-robin 算法)
自定义分区器
import org.apache.kafka.clients.producer.Partitioner;
import org.apache.kafka.common.Cluster;
import java.util.Map;
public class MyPartitioner implements Partitioner {
/*
* 返回信息对应的分区
* @param topic 主题
* @param key 消息的 key
* @param keyBytes 消息的 key 序列化后的字节数组
* @param value 消息的 value
* @param valueBytes 消息的 value 序列化后的字节数组
* @param cluster 集群元数据可以查看分区信息
* @return
*/
@Override
public int partition(String topic, Object key, byte[] keyBytes, Object value, byte[] valueBytes, Cluster cluster) {
String msgValue = value.toString();
int partition;
if (msgValue.contains("haha")) {
partition = 1;
} else {
partition = 0;
}
return partition;
}
@Override
public void close() {
}
@Override
public void configure(Map<String, ?> map) {
}
}
生产者需要在属性配置中去指定自定义分区器
Properties.put(ProducerConfig.PARTITIONER_CLASS_CONFIG, MyPartitioner.class.getName());
4.4 生产者调优
4.4.1 提升吞吐量
batch.size: 批次大小,默认16k
linger.ms: sender线程等待时间,成产建议5-100ms
buffer.memory: 缓冲区大小,默认32m
compression.type: 消息压缩 生产建议snappy
4.4.2 数据可靠性
ACK应答级别
-
0:生产者发送过来的数据,不需要等数据落盘应答,可靠性低,效率高;
-
1:生产者发送过来的数据,Leader 收到数据后应答,应答后还没开始同步副本Leader挂了,新的Leader不会收到相同的消息,因为生产者已经认为消息发送成功了,一般用于传输允许丢失个别数据的场景;
-
-1(all):生产者发送过来的数据,Leader+和 isr 队列里面的所有节点收齐数据后应答。默认值是-1,-1 和 all 是等价的。一般用于对数据可靠性要求高的场景。
-
Leader收到数据,所有Follower都开始同步数据,但有一个Follower,因为某种故障,迟迟不能与Leader进行同步,那这个问题怎么解决呢?
-
Leader维护了一个动态的ISR队列,和leader保持同步的follower+leader集合;
-
如果Follower长时间未向Leader发送通信请求或同步数据,则该Follower将被踢出ISR。
-
该时间阈值由replica.lag.time.max.ms参数设定,默认30s。例如2超时,(leader:0, isr:0,1)。这样就不用等长期联系不上或者已经故障的节点。
-
-
数据可靠性分析
-
如果分区副本设置为1个,或 者ISR里应答的最小副本数量 min.insync.replicas 默认为1,和ack=1的效果是一样的,仍然有丢数的风险
-
数据完全可靠条件 = ACK级别设置为-1 + 分区副本大于等于2 + ISR里应答的最小副本数量大于等于2
-
-
数据重复分析
-
生产者重试: 如果消息发送过程中发生了网络波动或其他故障,导致部分副本已经成功写入而部分副本还未写入,生产者会继续重试发送消息,会导致消息在不同的副本中被多次写入,从而出现重复数据。
-
ISR 副本变更:如果在消息写入过程中,ISR 副本发生变更,如某些副本由于故障被移除出 ISR,导致之前已经成功写入消息的副本不再是 ISR 副本,那么在新的 ISR 副本中可能会再次写入该消息,从而出现重复数据。
-
网络分区:网络分区的情况下,导致 Leader 和 ISR 副本之间的消息同步受阻。在网络分区解除后,消息可能会在新的 Leader 和 ISR 副本中重新写入,从而出现重复数据。
-
-
数据传递语义
-
至少一次 = ACK为1 + 分区副本数>=2 + ISR应答最小副本数>=2;
-
最多一次 = ACK为0
-
精准一次 = 幂等性 + 至少一次
幂等性
幂等性就是指生产者无论向broker发送多少条相同数据,broker只会持久化一条数据,保证数据不重复。
重复数据的判断标准:具有<PID,Partition,SeqNumber>相同主键的消息提交时,broker只会持久化一条数据。kafka每次重启都会分配一个新的PID,Partition表示分区号,SeqNumber是单调自增的,所以幂等性只能保证单分区内不重复。
开启幂等性:enable.idempotence 默认为 true
生产者事务
开启事务必须开启幂等性。
事务是一种更强大的保证机制,它确保了生产者在多个分区上的一系列操作要么全部成功提交(产生事务性消息),要么全部失败回滚(不产生任何消息)。事务确保了多个消息在多个分区上的原子性,即要么全部成功,要么全部失败。
-
事务原理
-
生产者使用事务前必须先定义一个唯一事务ID,有了这个ID即便客户端挂掉重启后也能继续处理没有完成的事务
-
生产者请求kafka集群的TC(事务协调器)获取到producer_id;
-
生产者携带producer_id发送消息到Topic的leader分区,同时向TC(事务协调器)发送commit请求,TC收到后会将这次commit请求持久化到一个存储事务信息的特殊主题,这个特殊主题默认有50个分区,每个分区负责一部分事务。
-
持久化完成后TC会返回生产者成功信息,同时向topic发送commit请求,成功后TC会持久化事务信息到那个特殊主题。
-
生产者事务一旦回滚,就会清除所有在当前未完成事务中发送的消息,取消与当前事务相关联的未确认的发送请求,就好像它们从未发送过一样。这时,需要有专门逻辑去处理这些回滚的消息,不然就会被丢弃,而这是生产上无法容忍的。Kafka自身提供了重试机制,可以在配置中设置
retries
参数来指定发送失败时的重试次数,这在一定程度上可以解决发送过程中的临时性失败的问题,对于重试完依然失败的消息可以记录到关系型数据库中,定时处理。
-
API
// 0 设置事务ID
Properties.put(ProducerConfig.TRANSACTIONAL_ID_CONFIG, "transaction_id");
// 1 初始化事务
void initTransactions();
// 2 开启事务
void beginTransaction() throws ProducerFencedException;
// 3 在事务内提交已经消费的偏移量(主要用于消费者)
void sendOffsetsToTransaction(Map<TopicPartition, OffsetAndMetadata> offsets, String consumerGroupId) throws ProducerFencedException;
// 4 提交事务
void commitTransaction() throws ProducerFencedException;
// 5 放弃事务(类似于回滚事务的操作)
void abortTransaction() throws ProducerFencedException;
-
示例代码
import org.apache.kafka.clients.producer.KafkaProducer;
import org.apache.kafka.clients.producer.ProducerConfig;
import org.apache.kafka.clients.producer.ProducerRecord;
import org.apache.kafka.clients.producer.RecordMetadata;
import org.apache.kafka.common.errors.ProducerFencedException;
import org.apache.kafka.common.errors.OutOfOrderSequenceException;
import org.apache.kafka.common.errors.AuthorizationException;
import org.apache.kafka.common.errors.KafkaException;
import java.util.Properties;
import java.util.concurrent.ExecutionException;
public class KafkaTransactionalProducer {
public static void main(String[] args) {
// 配置生产者属性
Properties props = new Properties();
props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092");
props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.StringSerializer");
props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.StringSerializer");
// 设置事务ID
props.put(ProducerConfig.TRANSACTIONAL_ID_CONFIG, "my-transactional-id");
// 创建Kafka生产者
KafkaProducer<String, String> producer = new KafkaProducer<>(props);
// 初始化事务
producer.initTransactions();
try {
// 开启事务
producer.beginTransaction();
// 发送消息
ProducerRecord<String, String> record1 = new ProducerRecord<>("my-topic", "key1", "value1");
ProducerRecord<String, String> record2 = new ProducerRecord<>("my-topic", "key2", "value2");
producer.send(record1, (RecordMetadata metadata, Exception exception) -> {
if (exception != null) {
exception.printStackTrace();
} else {
System.out.printf("Sent record(key=%s value=%s) " +
"meta(partition=%d, offset=%d)\n",
record1.key(), record1.value(), metadata.partition(), metadata.offset());
}
});
producer.send(record2, (RecordMetadata metadata, Exception exception) -> {
if (exception != null) {
exception.printStackTrace();
} else {
System.out.printf("Sent record(key=%s value=%s) " +
"meta(partition=%d, offset=%d)\n",
record2.key(), record2.value(), metadata.partition(), metadata.offset());
}
});
// 提交事务
producer.commitTransaction();
} catch (ProducerFencedException | OutOfOrderSequenceException | AuthorizationException e) {
// 致命错误,需要关闭生产者
e.printStackTrace();
producer.close();
} catch (KafkaException e) {
// 非致命错误,回滚事务
e.printStackTrace();
producer.abortTransaction();
} finally {
producer.close();
}
}
}
4.4.3 单分区数据有序
-
未开启幂等性
max.in.flight.requests.per.connection需要设置为1。
-
开启幂等性
max.in.flight.requests.per.connection需要设置为小于等于5。
因为在kafka1.x版本之后,启用幂等后,kafka服务端会缓存producer发来的最近5个request的元数据,故无论如何,都可以保证最近5个request的数据都是有序的。
只能保证但分区数据有序,分区间数据无序。
5 Kafka Broker
5.1 Kafka Broker工作流程
-
broker启动后在controller节点上注册,controller节点中最先启动的为leader;
-
由controller leader节点监听各个broker节点的变化,决定leader副本的选举,并将节点元数据保存到
__cluster_metadata
主题中,controller 从节点会从主节点上同步元数据; -
假设某个broker的leader副本挂了,controller会监听到节点的变化,获取ISR队列,按照选举规则选举出新的Leader副本后更新Leader和ISR;
-
选举规则:在isr中存活为前提,按照AR中排在前面的优先。例如ar[1,0,2], isr [1,0,2],那么leader就会按照1,0,2的顺序轮询;
相关重要参数详见2.2中修改后的配置文件中。
5.2 服役新的Broker节点
-
准备工作
新的节点部署好Kafka Broker节点,关闭自动再均衡
-
创建要均衡的主题
vim topics-to-move.json
{
"topics": {
{
"topic": "first"
}
},
"version": 1
}
-
生成负载均衡计划
bin/kafka-reassign-partitions.sh --bootstrap-server hadoop102:9092 --topics-to-move-json-file topics-to-move.json --broker-list "0,1,2,3" --generate
-
创建副本存储计划(根据生成的负载均衡计划)
vim increase-replication-factor.json
{
"partitions": [
{
"topic": "first",
"partition": 0,
"replicas": [2,3,0],
"log_dirs": ["any","any","any"]
},
{
"topic": "first",
"partition": 1,
"replicas": [3,0,1],
"log_dirs": ["any","any","any"]
},
{
"topic": "first",
"partition": 2,
"replicas": [0,1,2],
"log_dirs": ["any","any","any"]
}
]
}
-
执行副本存储计划
bin/kafka-reassign-partitions.sh --bootstrap-server localhost:9092 --reassignment-json-file increase-replication-factor.json --execute
-
验证副本存储计划
bin/kafka-reassign-partitions.sh --bootstrap-server localhost:9092 --reassignment-json-file increase-replication-factor.json --verify
5.3 退役旧的Broker节点
按照服役新节点的操作重新制定计划,生成负载均衡计划时注意
--broker-list "0,1,2,3"
中去掉已退役的节点ID,然后再关闭要退役的节点即可。
6 Kafka副本
6.1 基本信息
-
副本作用:提高数据可靠性。
-
Kafka创建Topic时默认1个副本,但是可以通过
offsets.topic.replication.factor=N
指定副本数。 -
生产环境Topic副本数一般配置为2个,保证数据可靠性;太多副本会增加磁盘存储空间,降低网络传输效率。
-
副本分为Leader和Follower,生产者只会把数据发往Leader,然后Follower从Leader同步数据。
-
AR指所有副本;ISR表示和 Leader 保持同步的 Follower 集合;OSR表示 Follower 与 Leader 副本同步时,延迟过多的副本。
-
如果Follower长时间未向Leader发送通信或者同步请求会被踢出ISR。
6.2 选举流程
同Broker工作流程,选举规则:在isr中存活为前提,按照AR中排在前面的优先。例如ar[1,0,2], isr [1,0,2],那么leader就会按照1,0,2的顺序轮询。
6.3 故障细节
6.3.1 主要概念
-
LEO:每个副本的最后一个offset,即最新的offset + 1。
-
HW:所有副本中最小的LEO。
6.3.2 Follower故障处理细节
-
Follower发生故障后会被临时踢出ISR,这期间Leader和别的Follower会继续接收数据,记录offset。
-
Follower故障恢复后会读取本地磁盘记录的上次的HW,并将log文件高于HW的部分删除掉,并从HW开始从Leader同步数据。
-
等到故障恢复后的Follower的LEO大于等于该Partiton的HW,即Follwer追上Leader之后,就可以重新加入ISR了。
6.3.3 Leader故障处理细节
Leader发生故障后,会从ISR中选出一个新的Leader,为保证多个副本之间的数据一致性,其余的Follower会先将各自的log文件高于HW的部门截掉,然后从新的Leader同步数据。
注意:这只能保证副本之间的数据一致性,并不能保证数据不丢失或者不重复。
6.4 手动调整分区副本存储
-
创建副本存储计划
vim increase-replication-factor.json
{
"version": 1,
"partitions": [
{
"topic": "three",
"partition": 0,
"replicas": [0,1]
},
{
"topic": "three",
"partition": 1,
"replicas": [0,1]
},
{
"topic": "three",
"partition": 2,
"replicas": [1,0]
},
{
"topic": "three",
"partition": 3,
"replicas": [1,0]
}
]
}
-
执行副本存储计划
bin/kafka-reassign-partitions.sh --bootstrap-server localhost:9092 --reassignment-json-file increase-replication-factor.json --execute
-
验证副本存储计划
bin/kafka-reassign-partitions.sh --bootstrap-server localhost:9092 --reassignment-json-file increase-replication-factor.json --verify
6.5 Leader Partition自动再平衡
自动再平衡相关配置参数详见
修改后的配置文件
中的配置。
正常情况下,Kafka本身会自动把Leader Partition均匀分散在各个机器上,来保证每台机器的读写吞吐量都是均匀的。但是如果某些broker宕机,会导致Leader Partition过于集中在其他少部分几台broker上,这会导致少数几台broker的读写请求压力过高,其他宕机的broker重启之后都是follower partition,读写请求很低,造成集群负载不均衡。
6.6 增加副本因子
在生产环境当中,由于某个主题的重要等级需要提升,我们考虑增加副本。副本数的增加需要先制定计划,然后根据计划执行
-
创建副本存储计划
vim increase-replication-factor.json
{
"version": 1,
"partitions": [
{
"topic": "four",
"partition": 0,
"replicas": [0,1,2]
},
{
"topic": "four",
"partition": 1,
"replicas": [0,1,2]
},
{
"topic": "four",
"partition": 2,
"replicas": [0,1,2]
}
]
}
6.7 文件存储
6.7.1 Topic数据的存储机制
Topic是逻辑上的概念,而partition是物理上的概念,每个partition对应于一个log文件,该log文件中存储的就是Producer生产的数据。Producer生产的数据会被不断追加到该log文件末端,为防止log文件过大导致数据定位效率低下,Kafka采取了分片和索引机制,将每个partition分为多个segment。每个segment包括:“.index”偏移量水印文件、“.log”日志文件和.timeindex时间戳索引文件等。这些文件位于一个文件夹下,该文件夹的命名规则为:topic名称+分区序号,例如:first-0。
说明:index和log文件以当前segment的第一条消息的offset命名。
-
Topic数据存储位置
/opt/modules/kafka/data/topic-partition
-
通过工具查看里面的log日志文件、index索引文件
kafka-run-class.sh kafka.tools.DumpLogSegments --files ./00000000000000000000.index
-
index和log文件详解
index为稀疏索引,默认每往log文件写入4k数据才会往index文件中写入一条索引,可以通过调整log.index.interval.bytes
来配置默认大小。
index文件中保存的索引是相对offset,好处是保证offset的值所占空间不会太大,可以控制在固定大小。
kafka是如何在log文件中快速定位到指定offset的记录的?
首先,会根据这个offset去定位Segment文件,每个Segment文件都有自己对应的offset范围,定位到index文件和log文件;然后,找到小于等于目标offset的最大offset对应的索引项,获取到绝对的offset,最后通过这个offset去log文件中找比自己小的,向下遍历找到目标记录。
日志存储参数详见“修改后的配置参数“
6.7.2 文件清理策略
-
kafka中默认的日志保存时间默认为7天,可以通过调整
log.retention.hours/minutes/ms
调整存储时间;调整log.retention.check.interval.ms
参数来调整检查周期。 -
日志如果超出设置时间,可以通过设置
log.cleanup.policy=delete/compact
两种策略处理,通常这两种策略配合使用。-
delete:过期数据删除
-
默认基于时间(log.retention.hours/minutes/ms),以segment 中所有记录中的最大时间戳作为该文件时间戳。
-
基于大小(log.retention.bytes,默认等于-1,表示无穷大),超过设置的所有日志总大小,删除最早的 segment。
-
如果一个 segment 中有一部分数据过期,一部分没有过期,怎么处理?
-
时间保留策略:如果有部分消息的时间戳未超过过期时间,而另一部分消息的时间戳已超过过期时间,那么只有过期的消息会被删除,未过期的消息会被保留在该日志段中。
-
大小保留策略:如果消息未过期但日志段大小已达到限制,那么新的消息将被写入到新的日志段,而旧的日志段将被保留,供消费者读取已提交的消息。这样,未过期的消息会被分散存储在多个日志段中。
-
-
-
compact:日志压缩
-
对于相同key的不同value值只保留最新版本,需要注意的是,压缩后的offset可能是不连续的,可能会出现从比没有的offset大的位置开始消费的情况。
-
这种策略只适合特殊场景,比如消息的key是用户ID,value是用户的资料,通过这种压缩策略,整个消息集里就保存了所有用户最新的资料。
-
-
6.8 高效读写数据
-
Kafka本身就是一个分布式的基于发布/订阅模式的消息队列,可以采用分区技术,并行度高。
-
读数据时采用稀疏索引机制,可以快速定位到要消费的数据。
-
生产者发送数据是顺序写入到log文件中的,也就是在文件末尾一直追加;官方报告同样的机械磁盘顺序写为600M/s,随机写100K/s。
-
页缓存:Kafka重度依赖底层操作系统提供的页缓存功能。当生产者写数据时,操作系统只是将数据写入页缓存中。当消费者读数据时,先从页缓存中查找,如果找不到,再去磁盘中读取。实际上页缓存就是是把尽可能多的空闲内存都当做了磁盘缓存来使用。
-
零拷贝:Kafka的数据加工处理操作交由生产者和消费者端处理。Broker应用层不关心存储的数据,所以就不用走应用层,传输效率高。
-
需要注意的是,虽然kafka提供了
log.flush.interval.messages
和log.flush.interval.ms
去配置强制页缓存刷写到磁盘的条数和频率,但是都不建议修改,交由操作系统自己管理。
7 Kafka消费者
Kafka仅支持消费者pull(拉)模式,从broker中主动拉取数据。不足之处是,如果Kafka没有数据,消费者可能会陷入循环中,一直返回空数据。
7.1 消费者工作流程
-
每个分区的数据只能由消费者组中的一个消费者消费,但是一个消费者可以消费多个分区的数据。
-
每个消费者的offset由消费者提交到
_consumer_offsets
系统主题中保存。
7.2 消费者组原理
消费者组:由多个消费者组成,组内所有消费者group_id相同。
消费者组内每个消费者负责消费不同分区的数据,一个分区只能由一个组内消费者消费,消费者组之间互不影响。
消费者组内消费者大于主题分区数,超出部分的消费者会闲置。
消费者初始化原理
-
coordinator节点辅助实现消费者组的初始化和分区的分配。
-
coordinator节点选择=group_id % 50(_consumer_offsets系统主题的默认分区数),如groupid的hashcode值 = 1,1% 50 = 1,那么__consumer_offsets 主题的1号分区在哪个broker上,就选择这个节点的coordinator作为这个消费者组的Leader。消费者组下的所有的消费者提交offset的时候就往这个分区去提交offset,订阅Leader coordinator所在的broker的消费者为消费者组中的Leader。
消费者组初始化流程
-
每个消费者都会向coordinator节点发送JoinGroup请求,并会选出一个消费者成为Leader把要消费的Topic情况发送给它。
-
Leader消费者会负责制定消费方案,并把消费方案发送给coordinator节点。
-
coordinator节点会把方案下发给各个组内消费者,每个消费者都会和coordinator保持心跳(默认3秒),一旦超时(session.timeout.ms=45s),该消费者就会被移除并触发再平衡;或者消费者处理时间过长(max.poll.interval.ms5分钟)也会触发再平衡。
7.3 消费者重要参数
参数名称 | 描述 |
---|---|
bootstrap.servers | 向 Kafka 集群建立初始连接用到的 host/port 列表 |
key.deserializer / value.deserializer | 指定接收消息的 key 和 value 的反序列化类型。一定要写全类名。 |
group.id | 标记消费者所属的消费者组。 |
enable.auto.commit | 默认值为 true,消费者会自动周期性地向服务器提交偏移量。 |
auto.commit.interval.ms | 如果设置了 enable.auto.commit 的值为 true, 则该值定义了消费者偏移量向 Kafka 提交的频率,默认 5s。 |
auto.offset.reset | 当 Kafka 中没有初始偏移量或当前偏移量在服务器中被删除了的处理方式,earliest:自动重置到最早的偏移量; latest:默认自动重置到最新的偏移量;none:如果消费组原来的偏移量被删除就向消费者抛异常;anything:向消费者抛异常。 |
offsets.topic.num.partitions | __consumer_offsets(系统主题)的分区数,默认是 50 个分区。不建议修改 |
heartbeat.interval.ms | Kafka 消费者和 coordinator 之间的心跳时间,默认 3s。该条目的值必须小于 session.timeout.ms ,也不应该高于session.timeout.ms 的 1/3。不建议修改 |
session.timeout.ms | Kafka 消费者和 coordinator 之间连接超时时间,默认 45s。超过该值,该消费者被移除,消费者组执行再平衡。 |
max.poll.interval.ms | 消费者处理消息的最大时长,默认是 5 分钟。超过该值的消费者被移除,消费者组执行再平衡 |
partition.assignment.strategy | 消费者分区分配策略,默认策略是Range + CooperativeSticky。Kafka 可以同时使用多个分区分配策略,可以选择的策 略包括:Range、RoundRobin、Sticky 、CooperativeSticky。 |
fetch.min.bytes | 默认 1 个字节。消费者获取服务器端一批消息最小的字节 |
fetch.max.wait.ms | 默认 500ms。如果没有从服务器端获取到一批数据的最小字节数。该时间到仍然会返回数据。 |
fetch.max.bytes | 默认 Default: 52428800(50m)。消费者获取服务器端一批消息最大的字节数。如果服务器端一批次的数据大于该值仍然可以拉取回来这批数据,因此,这不是一个绝对最大值。一批次的大小受 message.max.bytes (服务器可以接收的最大消息)或者max.message.bytes (单个topic可以接收的最大消息,无需重启)影响。 |
max.poll.records | 一次 poll 拉取数据返回消息的最大条数,默认是 500 条。 |
7.4 消费者分区分配以及再平衡
思考:具体由哪个消费者去消费哪个分区的数据,kafka怎么处理的?
kafka有四种主流的分区分配策略,可以通过partition.assignment.strategy
修改分区分配策略。
具体流程同消费者组初始化流程
7.4.1 Range以及再平衡
-
分区分配策略原理
-
Range 是对每个 topic 而言的。首先对同一个 topic 里面的分区按照序号进行排序,并对消费者按照字母顺序进行排序。
-
假如现在有 7 个分区,3 个消费者,排序后的分区将会是0,1,2,3,4,5,6;消费者排序完之后将会是C0,C1,C2。
-
通过 partitions数/consumer数来决定每个消费者应该消费几个分区。如果除不尽,那么前面几个消费者将会多消费 1 个分区。
-
例如,7/3 = 2 余 1 ,除不尽,那么 消费者 C0 便会多消费 1 个分区。 8/3=2余2,除不尽,那么C0和C1分别多消费一个。
-
注意:如果只是针对 1 个 topic 而言,C0消费者多消费1个分区影响不是很大。但是如果有 N 多个 topic,那么针对每个 topic,消费者 C0都将多消费 1 个分区,topic越多,C0消费的分区会比其他消费者明显多消费 N 个分区。容易产生数据倾斜。
-
-
分区分配再平衡案例
-
停止掉 一个消费者,快速重新发送消息观看结果(45s 以内,越快越好)
-
消费者挂掉后,消费者组需要按照超时时间 45s 来判断它是否退出,等到时间到了 45s 后,判断它真的退出就会把任务分配给其他 broker 执行。
-
-
45s后重新发送数据
-
消费者已经被踢出消费者组,所以重新按照 range 方式分配。
-
-
7.4.2 RoundRobin以及再平衡
-
分区分配策略原理
-
RoundRobin 针对集群中所有Topic而言。
-
具体策略是把所有分区和消费者按照hashCode排序,排序后轮训分配。
-
-
分区分配再平衡案例
-
API:
Properties.put(ConsumerConfig.PARTITION_ASSIGNMENT_STRATEGY_CONFIG, RoundRobinAssignor.class);
-
停止一个消费者,45s内快速重新发送消息
-
消费者的任务会按照 RoundRobin 的方式,把数据轮询分到别的分区,分别由 其他消费者消费。
-
-
45s后重新发送数据
-
消费者已经被踢出消费者组,所以重新按照 RoundRobin 方式分配。
-
-
7.4.3 Sticky以及再平衡
-
粘性分区:Kafka首先会尽量均衡的放置分区到消费者上面,在出现组内消费者出现问题的时候,尽量保持原有分配的分区不变化。
-
验证同其他类型
7.5 offset位移
从0.9版本开始,consumer默认将offset保存在Kafka一个内置的topic中,该topic为
__consumer_offsets
,之前是维护在zk中;__consumer_offsets 主题采用 KV 的形式存储数据,K是group.id+topic+分区号,V是当前offset的值,每隔一段时间,kafka内部会对这个topic进行压缩,只保留最新的KV。
7.5.1 消费offset案例
-
思想:__consumer_offsets是Kafka中的Topic,就可以被消费。
-
修改/opt/modules/kafka/config/consumer.properties配置文件,添加
exclude.internal.topics=false
,默认为true表示不能消费系统主题。 -
使用命令行创建一个生产者和消费者,消费者需要指定消费者组,更好观察数据存储位置(key 是 group.id+topic+分区号)。
-
查看offset信息
bin/kafka-console-consumer.sh --topic __consumer_offsets --bootstrap-server localhost:9092 --consumer.config config/consumer.properties --formatter "kafka.coordinator.group.GroupMetadataManager\$OffsetsMessageFormatter" --from-beginning
7.5.2 自动提交offset
为了让消费者能够更专注于自己业务,kafka提供了自动提交offset的功能,默认开启。
自动提交offset的相关数
enable.auto.commit:是否开启自动提交offset功能,默认是true auto.commit.interval.ms:自动提交offset的时间间隔,默认是5s
如果设置了 enable.auto.commit 的值为 true,默认情况下,消费者每5s钟自动向__consumer_offsets主题提交一次offset。
Properties.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, true); Properties.put(ConsumerConfig.AUTO_COMMIT_INTERVAL_MS_CONFIG, 5000);
7.5.3 手动提交offset
虽然自动提交offset很方便,但是由于自动提交是基于时间来的,很难把握具体的提交时机,就有了手动提交offset功能。
手动提交offset的方法有两种:分别是commitSync(同步提交)和commitAsync(异步提交),都会提交本次消费的一批数据中最高的偏移量。
-
commitSync(同步提交)
同步提交阻塞当前线程,一直到提交成功,并且会自动失败重试(不可控因素也会出现提交失败)。必须等待offset提交完毕,再消费下一批数据。
KafkaConsumer.commitSync();
-
commitAsync(异步提交)
异步提交则没有失败重试机制,故有可能提交失败。发送完提交offset请求后,就开始消费下一批数据了。
KafkaConsumer.commitAsync();
7.5.4 指定offset消费
auto.offset.reset = earliest | latest | none 默认是 latest。
当Kafka中没有初始偏移量(CG第一次消费)或者系统主题中不存在当前偏移量(数据被删除),怎么处理?
-
earliest:自动将offset重置为最早的,等价于--from-beginning
-
latest(默认值):自动将offset重置为最新的
-
none:如果未找到消费者组的先前偏移量,则向消费者抛出异常
public static void main(String[] args) {
// 0 配置信息
Properties properties = new Properties();
// 连接
properties.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "192.168.23.128:9092");
// key value 反序列化
properties.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());
properties.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());
properties.put(ConsumerConfig.GROUP_ID_CONFIG, "test2");
// 1 创建一个消费者
KafkaConsumer<String, String> kafkaConsumer = new KafkaConsumer<>(properties);
// 2 订阅一个主题
ArrayList<String> topics = new ArrayList<>();
topics.add("first");
kafkaConsumer.subscribe(topics);
Set<TopicPartition> assignment= new HashSet<>();
while (assignment.size() == 0) {
// 设置 1s 消费一批数据
kafkaConsumer.poll(Duration.ofSeconds(1));
// 获取消费者分区分配信息(有了分区分配信息才能开始消费)
assignment = kafkaConsumer.assignment();
}
// 遍历所有分区,并指定 offset 从 1700 的位置开始消费
for (TopicPartition tp: assignment) {
kafkaConsumer.seek(tp, 1700);
}
// 3 消费该主题数据
while (true) {
ConsumerRecords<String, String> consumerRecords = kafkaConsumer.poll(Duration.ofSeconds(1));
for (ConsumerRecord<String, String> consumerRecord : consumerRecords) {
System.out.println(consumerRecord);
}
}
}
7.5.5 指定时间消费
在生产环境中,有时会遇到最近消费的几个小时数据异常,想重新按照时间消费,例如按照时间从前一天的数据开始消费,怎么处理?
核心代码
// 订阅Topic后
Set<TopicPartition> assignment = new HashSet<>();
while (assignment.size() == 0) {
kafkaConsumer.poll(Duration.ofSeconds(1));
// 获取消费者分区分配信息(有了分区分配信息才能开始消费)
assignment = kafkaConsumer.assignment();
}
HashMap<TopicPartition, Long> timestampToSearch = new HashMap<>();
// 封装集合存储,每个分区对应一天前的数据
for (TopicPartition topicPartition : assignment) {
timestampToSearch.put(topicPartition, System.currentTimeMillis() - 1 * 24 * 3600 * 1000);
}
// 获取从前一天开始消费的每个分区的offset
Map<TopicPartition, OffsetAndTimestamp> offsets = kafkaConsumer.offsetsForTimes(timestampToSearch);
// 遍历每个分区,对每个分区设置消费时间
for(TopicPartition topicPartition : assignment) {
OffsetAndTimestamp offsetAndTimestamp = offsets.get(topicPartition);
// 根据时间指定开始消费的位置
if (offsetAndTimestamp != null){
kafkaConsumer.seek(topicPartition, offsetAndTimestamp.offset());
}
}
7.5.6 漏消费和重复消费
-
漏消费:设置offset为手动提交,当offset被提交时,数据还在内存中未落盘,刚好消费者线程被kill掉,offset已经提交数据未处理,导致这部分内存中的数据丢失。
-
重复消费:消费者每5s自动提交offset,如果提交后的2s消费者挂了,消费者重启后会从上次提交的offset处继续消费,就会出现重复消费的情况。
使用消费者事务机制便面这两种情况。
7.5.7 消费者事务
如果想完成Consumer端的精准一次性消费,那么需要Kafka消费端将消费过程和提交offset过程做原子绑定。
import org.apache.kafka.clients.consumer.ConsumerConfig;
import org.apache.kafka.clients.consumer.ConsumerRecord;
import org.apache.kafka.clients.consumer.ConsumerRecords;
import org.apache.kafka.clients.consumer.KafkaConsumer;
import org.apache.kafka.clients.producer.KafkaProducer;
import org.apache.kafka.clients.producer.ProducerConfig;
import org.apache.kafka.clients.producer.ProducerRecord;
import org.apache.kafka.clients.producer.RecordMetadata;
import org.apache.kafka.common.TopicPartition;
import org.apache.kafka.common.errors.ProducerFencedException;
import org.apache.kafka.common.errors.OutOfOrderSequenceException;
import org.apache.kafka.common.errors.AuthorizationException;
import org.apache.kafka.common.errors.KafkaException;
import org.apache.kafka.common.serialization.StringDeserializer;
import org.apache.kafka.common.serialization.StringSerializer;
import org.apache.kafka.clients.consumer.OffsetAndMetadata;
import java.time.Duration;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
public class KafkaTransactionalConsumerProducer {
public static void main(String[] args) {
// 配置消费者属性
Properties consumerProps = new Properties();
consumerProps.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092");
consumerProps.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());
consumerProps.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());
consumerProps.put(ConsumerConfig.GROUP_ID_CONFIG, "my-group");
consumerProps.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, "false");// 禁用自动提交偏移量
consumerProps.put(ConsumerConfig.ISOLATION_LEVEL_CONFIG, "read_committed");// 者只能读取到已提交的事务消息
// 配置生产者属性
Properties producerProps = new Properties();
producerProps.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092");
producerProps.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
producerProps.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
producerProps.put(ProducerConfig.TRANSACTIONAL_ID_CONFIG, "transactional-producer-id");
KafkaConsumer<String, String> consumer = new KafkaConsumer<>(consumerProps);
KafkaProducer<String, String> producer = new KafkaProducer<>(producerProps);
// 初始化事务
producer.initTransactions();
// 订阅主题
consumer.subscribe(Collections.singletonList("my-topic"));
try {
while (true) {
// 开始消费消息
ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(100));
if (!records.isEmpty()) {
// 开启事务
producer.beginTransaction();
// 处理消息
for (ConsumerRecord<String, String> record : records) {
System.out.println("Received message: " + record.value());
// 业务处理逻辑,比如将结果发送到另一个主题
ProducerRecord<String, String> producerRecord = new ProducerRecord<>("my-output-topic", record.key(), record.value());
producer.send(producerRecord);
}
// 提交偏移量到事务
Map<TopicPartition, OffsetAndMetadata> offsets = new HashMap<>();
for (ConsumerRecord<String, String> record : records) {
offsets.put(new TopicPartition(record.topic(), record.partition()), new OffsetAndMetadata(record.offset() + 1));
}
producer.sendOffsetsToTransaction(offsets, consumer.groupMetadata());
// 提交事务
producer.commitTransaction();
}
}
} catch (ProducerFencedException | OutOfOrderSequenceException | AuthorizationException e) {
e.printStackTrace();
producer.close();
} catch (KafkaException e) {
e.printStackTrace();
producer.abortTransaction();
} finally {
consumer.close();
producer.close();
}
}
}
7.5.8 数据积压
-
如果是Kafka消费能力不足,则可以考虑增加Topic的分区数,并且同时提升消费组的消费者数量,消费者数 = 分区数。
-
如果是下游的数据处理不及时,提高每批次拉取的数量。批次拉取数据过少(拉取数据/处理时间 < 生产速度),使处理的数据小于生产的数据,也会造成数据积压。
从一次最多拉取500条,调整为一次最多拉取1000条
。
8 生产调优策略
场景说明
100 万日活,每人每天 100 条日志,每天总共的日志条数是 100 万 * 100 条 = 1 亿条;
每秒钟1150 条采集日志,每条日志0.5k~2k,每秒钟大约1m数据;
高峰期是平常的20倍,每秒钟20m的数据。
8.1 硬件配置
8.1.1 服务器数量
-
计算公式:2 * (生产者峰值生产速率 * 副本数 / 100) + 1
-
本次场景计算结果: 2 * (20m/s * 2 / 100) + 1 = 3
8.1.2 磁盘选择
-
kafka 底层主要是顺序写,固态硬盘和机械硬盘的顺序写性能差不多,建议选择普通的机械鹰牌;
-
每天的总数据量1亿的话,也就是100G的硬盘空间;对于本次场景建议三台服务器硬盘总大小大于等于 1T;
-
计算公式:每日总数据量占用磁盘空间 * 副本数 * 保存时间 / 0.7
8.1.3 内存选择
-
Kafka内存组成:堆内存 和 页缓存
-
Kafka 堆内存建议每个节点:10g ~ 15g,至少分配6-8g的堆内存,默认1G
-
使用JDK8并启用G1垃圾回收器
-
页缓存:页缓存是 Linux 系统服务器的内存。我们只需要保证 1 个 segment(1g)中25%的数据在内存中就好
-
每个节点页缓存大小 =(分区数 * 1g * 25%)/ 节点数。例如 10 个分区,页缓存大小 =(10 * 1g * 25%)/ 3 ≈ 1g
-
建议服务器内存大于等于堆内存 + 页缓存之和
-
# 修改启动脚本
vim /opt/modules/kafka/bin/kafka-server-start.sh
if [ "x$KAFKA_HEAP_OPTS" = "x" ]; then
export KAFKA_HEAP_OPTS="-Xmx10G -Xms10G"
fi
# 根据 Kafka 进程号,查看 Kafka 的 GC 情况
jstat -gc 2321 1s 10
参数说明: S0C:第一个幸存区的大小; S1C:第二个幸存区的大小 S0U:第一个幸存区的使用大小; S1U:第二个幸存区的使用大小 EC:伊甸园区的大小; EU:伊甸园区的使用大小 OC:老年代大小; OU:老年代使用大小 MC:方法区大小; MU:方法区使用大小 CCSC:压缩类空间大小; CCSU:压缩类空间使用大小 YGC:年轻代垃圾回收次数;(重点关注) YGCT:年轻代垃圾回收消耗时间 FGC:老年代垃圾回收次数; FGCT:老年代垃圾回收消耗时间 GCT:垃圾回收消耗总时间; # 根据 Kafka 进程号,查看 Kafka 的堆内存 jmap -heap 2321 需要重点关注Heap Usage的G1 Heap和G1 Old Generation中的used情况
8.1.4 CPU选择
-
num.io.threads = 8 负责写磁盘的线程数,整个参数值要占总核数的 50%。
-
num.replica.fetchers = 1 副本拉取线程数,这个参数占总核数的 50%的 1/3。
-
num.network.threads = 3 数据传输线程数,这个参数占总核数的 50%的 2/3。
8.1.5 网络选择
网络带宽 = 峰值吞吐量 ≈ 20MB/s 选择千兆网卡即可
8.2 调优策略
8.2.1 提升生产者吞吐量
-
buffer.memory:发送消息的缓冲区大小,默认值是 32m,可以增加到 64m。
-
batch.size:默认是 16k。如果 batch 设置太小,会导致频繁网络请求,吞吐量下降;如果 batch 太大,会导致一条消息需要等待很久才能被发送出去,增加网络延时。
-
linger.ms,这个值默认是 0,意思就是消息必须立即被发送。一般设置一个 5-100毫秒。如果 linger.ms 设置的太小,会导致频繁网络请求,吞吐量下降;如果 linger.ms 太长,会导致一条消息需要等待很久才能被发送出去,增加网络延时。
-
compression.type:默认是 none,不压缩,但是也可以使用 lz4 压缩,效率还是不错的,压缩之后可以减小数据量,提升吞吐量,但是会加大 producer 端的 CPU 开销。
-
增加分区
8.2.2 提升消费者吞吐量
-
调整 fetch.max.bytes 大小,默认是 50m。
-
调整 max.poll.records 大小,默认是 500 条。
-
增加消费者处理能力。
8.2.3 合理设置分区数
创建一个只有 1 个分区的 topic,测试这个 topic 的 producer 吞吐量和 consumer 吞吐量;假设他们的值分别是 Tp 和 Tc,单位可以是 MB/s,假设他们的值分别是 Tp 和 Tc,单位可以是 MB/s。
例如:producer 吞吐量 = 20m/s;consumer 吞吐量 = 50m/s,期望吞吐量 100m/s,分区数 = 100 / 20 = 5 分区;
分区数一般设置为3-10个,需要根据集群压测结果灵活调整。
8.3 集群压测
前置操作:创建三个分区三个副本的topic
生产者压测脚本
bin/kafka-producer-perf-test.sh --topic test --record-size 1024 --num-records 1000000 --throughput 10000 --producer-props bootstrap.servers=192.168.23.128:9092,192.168.23.129:9092,192.168.23.130:9092 batch.size=32768 linger.ms=5 compression.type=snappy buffer.memory=67108864
record-size 是一条信息有多大,单位是字节,本次测试设置为 1k num-records 是总共发送多少条信息,本次测试设置为 100 万条。 throughput 是每秒多少条信息,设成-1,表示不限流,尽可能快的生产数据,可测出生产者最大吞吐量。本次实验设置为每秒钟 1 万条。 producer-props 后面可以配置生产者相关参数,batch.size 配置为 16k。 关注指标: records/sec (13.93 MB/sec)
消费者压测脚本
修改config/consumer.properties配置文件的max.poll.records和fetch.max.bytes参数值 bin/kafka-consumer-perf-test.sh --bootstrap-server 192.168.23.128:9092,192.168.23.129:9092,192.168.23.130:9092 --topic test --messages 1000000 --consumer.config config/consumer.properties --bootstrap-server 指定 Kafka 集群地址 --topic 指定 topic 的名称 --messages 总共要消费的消息个数。本次实验 100 万条 关注指标:MB.sec的值
9 KafkaStreams
KafkaStream与Hadoop的MapRedus的区别是什么?
Kafka Streams 和 Hadoop MapReduce 是两种不同的数据处理框架,它们在设计和使用上有很多区别。
-
处理模式:
-
Kafka Streams:Kafka Streams 是一个实时数据处理框架,它允许开发者通过编写简单的 Java/Scala 应用程序来处理实时流式数据。Kafka Streams 提供了高级别的 API,使得实时流处理变得简单和直观。
-
Hadoop MapReduce:Hadoop MapReduce 是一种批处理数据处理框架,它将数据分为小块(分片),然后并行地在多个节点上处理这些小块,最后将结果合并起来。MapReduce 适用于离线、批处理的数据处理任务。
-
-
实时性:
-
Kafka Streams:Kafka Streams 支持实时数据处理,可以在数据产生时立即进行处理,适用于需要低延迟和实时响应的场景。
-
Hadoop MapReduce:Hadoop MapReduce 是批处理框架,对于大规模的数据处理任务,需要等待所有数据都被处理完毕才能得到最终结果,不适合实时性要求高的场景。
-
-
数据模型:
-
Kafka Streams:Kafka Streams 处理的是流式数据,它直接从 Kafka 主题中读取数据,并对流进行实时处理。
-
Hadoop MapReduce:Hadoop MapReduce 处理的是批量数据,需要将数据存储在分布式文件系统(如 HDFS)中,然后通过 Map 和 Reduce 阶段对数据进行批量处理。
-
-
复杂性:
-
Kafka Streams:Kafka Streams 提供了高级别的 API,使得流处理变得简单和直观,开发者可以快速上手。适用于相对简单的流处理任务。
-
Hadoop MapReduce:Hadoop MapReduce 需要编写更复杂的 Map 和 Reduce 函数,并需要手动管理数据的分片和合并,对开发者的技能要求较高。适用于更复杂、大规模的数据处理任务。
-
总的来说,如果需要实时处理流式数据并对简单的业务逻辑进行处理,Kafka Streams 是一个很好的选择。而如果需要处理大规模的离线数据,并能接受较高的处理延迟,Hadoop MapReduce 则是更合适的选择。
Kafka Streams、Apache Flink和Apache Spark之间的区别是什么?
-
处理模式:
-
Kafka Streams:Kafka Streams 是一个轻量级的实时数据处理框架,专注于流式数据的处理。它直接从 Kafka 主题中读取数据,并通过高级别的 API来进行实时流处理。
-
Apache Flink:Flink 是一个流式处理和批处理的统一框架。它提供了流处理和批处理的 API,支持复杂事件处理、窗口操作和状态管理。
-
Apache Spark:Spark 也是一个统一的数据处理框架,提供了批处理和流处理的 API。Spark Streaming 是 Spark 的流处理模块,基于微批处理(micro-batch processing)实现。
-
-
实时性:
-
Kafka Streams:Kafka Streams 支持实时数据处理,可以在数据产生时立即进行处理,适用于需要低延迟和实时响应的场景。
-
Apache Flink:Flink 是一个真正的流式处理框架,支持事件时间处理,并且可以实现精确一次(exactly-once)的状态一致性保证。
-
Apache Spark:Spark Streaming 使用微批处理,处理延迟通常在几秒到几分钟之间,对于一些对实时性要求较高的场景可能不够满足。
-
-
处理模型:
-
Kafka Streams:Kafka Streams 是一个本地模型,处理的数据通常在同一个 Kafka 集群中。它不需要外部依赖,适用于简单的流处理任务。
-
Apache Flink:Flink 是一个分布式模型,可以运行在多个节点上,支持复杂的事件处理和状态管理。适用于更复杂的流处理任务。
-
Apache Spark:Spark Streaming 也是一个分布式模型,可以运行在多个节点上。但由于使用微批处理,对于一些低延迟的场景可能不够适用。
-
-
复杂性:
-
Kafka Streams:Kafka Streams 提供了高级别的 API,使得流处理变得简单和直观,开发者可以快速上手。适用于相对简单的流处理任务。
-
Apache Flink:Flink 提供了丰富的 API 和状态管理功能,可以处理更复杂、大规模的流处理任务,但相应地也带来了一定的学习曲线和复杂性。
-
Apache Spark:Spark Streaming 作为 Spark 的一部分,提供了相对简单的 API,但由于微批处理的模式,对于一些高实时性要求的场景可能需要进行额外的优化。
-
总的来说,Kafka Streams、Apache Flink和Apache Spark都是用于数据处理的分布式框架,适用于不同类型的任务。如果需要简单的实时流处理,Kafka Streams 是一个很好的选择。如果需要更复杂的流处理和批处理任务,Flink 和 Spark 提供了更多的功能和灵活性。
什么是简单的实时流处理,什么是复杂的流处理和批处理任务,请举例说明
简单的实时流处理通常指的是相对简单、实时性要求较低的数据处理任务。这类任务通常包括一些基本的数据转换、过滤、聚合等操作,涉及的数据量较小,处理逻辑相对简单。
举例说明:
-
简单的实时流处理: 在一个电商网站中,监控用户的浏览行为。当用户访问一个商品详情页面时,将其浏览记录写入 Kafka 主题。然后使用 Kafka Streams 进行实时流处理,计算用户在每个商品上的平均停留时间,并将结果写入另一个 Kafka 主题。这个流处理任务是相对简单的,涉及的数据量较小,计算逻辑不复杂。
复杂的流处理和批处理任务通常指的是涉及更复杂的数据处理逻辑和更大规模的数据量,需要考虑更多的优化和性能问题。
举例说明:
-
复杂的流处理: 在一个实时广告投放平台中,需要根据用户的兴趣和行为动态地投放广告。这个任务涉及复杂的实时数据分析和模型计算,需要处理大量的实时数据流。使用 Apache Flink 进行流处理,涉及的数据量大,需要考虑优化计算性能和处理延迟。
-
批处理任务: 在一个电商企业中,每天需要从各个线下门店收集销售数据,然后进行销售额统计和库存管理。这个任务属于批处理任务,因为数据是一天一天积累的,每天都需要进行一次批处理计算,对整个一天的销售数据进行聚合和统计。使用 Apache Spark 进行批处理,处理数据量较大,但处理时延不要求实时性。
综上所述,简单的实时流处理通常适用于相对简单、实时性要求不高的任务,而复杂的流处理和批处理任务则适用于需要更复杂处理逻辑和更大规模数据的场景。选择合适的框架和处理方式需要根据具体的业务需求和数据规模来决定。
对于温度、湿度、扬尘等传感器数据的实时计算场景,Kafka Streams 是一个更合适的选择。Kafka Streams 简单轻量,适用于实时性要求不高且相对简单的流处理任务。在实时计算传感器数据是否超出阈值并发送到不同主题时,Kafka Streams 提供了高效的实时流处理能力。
通常情况下,使用 Kafka Streams 比手动使用 Kafka 消费者订阅主题并计算结果可能更快,这是因为 Kafka Streams 提供了一系列优化和抽象,使得流处理应用程序能够更有效地处理数据。以下是一些原因:
-
并行处理:
-
Kafka Streams 可以自动将流处理任务分配到多个线程或实例中,并在多个任务之间进行负载均衡。这允许流处理应用程序在并行环境中高效处理数据。
-
-
优化操作:
-
Kafka Streams 提供了内置的高级操作,如窗口操作、聚合、连接等,这些操作在底层进行了优化,使得数据的处理更加高效。
-
-
状态管理:
-
Kafka Streams 内置了状态管理机制,可以有效地管理和访问状态数据。这对于处理有状态的计算和窗口操作非常有帮助,而不需要手动管理状态。
-
-
动态调优:
-
Kafka Streams 具有内置的动态调优机制,可以根据应用程序的实际负载和资源情况进行自动调整,从而实现更高的吞吐量和更低的延迟。
-
-
内置容错性:
-
Kafka Streams 内置了容错性和状态恢复机制,确保应用程序在故障发生时能够从之前的状态中恢复,避免数据丢失。
-
然而,是否更快还取决于具体的应用场景、实现细节以及数据量等因素。在一些情况下,手动使用 Kafka 消费者可能会更加适合,特别是在需要更精细的控制和特定处理逻辑的情况下
使用Nginx直接将采集数据写入Kafka和使用Nginx代理Netty采集服务,然后在采集服务中将数据写入Kafka都有各自的优劣势,取决于你的具体需求和架构设计。
直接使用Nginx将数据写入Kafka的优劣势:
优势:
-
简化架构: 直接使用Nginx可以减少架构中的组件和复杂性,因为你不需要额外的采集服务。
-
性能: Nginx是一个高性能的反向代理和负载均衡服务器,能够处理大量并发请求。这意味着它可以有效地处理高流量负载。
-
轻量级: Nginx的内存消耗相对较低,对系统资源的占用也比较少。
-
配置灵活: Nginx提供了丰富的配置选项,可以进行高度定制,以满足不同的需求。
劣势:
-
数据处理能力有限: Nginx主要设计为反向代理和负载均衡服务器,不适用于复杂的数据处理。因此,它的数据处理能力相对有限,可能无法处理一些复杂的数据转换和预处理任务。
-
不支持业务逻辑: Nginx不是一个通用的应用程序服务器,不能执行业务逻辑。如果需要进行数据转换或处理,你可能需要编写自定义的Nginx模块或使用Lua脚本来扩展功能。
-
有限的协议支持: Nginx主要支持HTTP和TCP协议,如果你需要处理其他协议的数据,可能需要额外的开发工作。
使用Nginx代理Netty采集服务再写入Kafka的优劣势:
优势:
-
灵活性: 使用Netty采集服务可以更灵活地处理和转换数据,因为你可以编写自定义的业务逻辑来满足特定需求。
-
适用于非HTTP协议: 如果你需要处理非HTTP协议的数据,Netty更容易适应不同的协议要求。
-
扩展性: 你可以轻松地扩展采集服务,以满足未来的需求,并且可以添加额外的功能,如数据验证、加密等。
劣势:
-
复杂性: 使用Netty和自定义采集服务会增加系统的复杂性和开发工作量。你需要编写和维护自己的采集服务,这可能需要更多的时间和资源。
-
性能影响: 自定义采集服务可能会增加系统的负载,特别是在处理大量数据时。如果不进行适当的优化,可能会对性能产生不利影响。
综上所述,选择将数据直接写入Kafka还是使用Nginx代理Netty采集服务再写入Kafka取决于你的具体需求和架构复杂性。如果你需要简化架构、处理HTTP数据,而且不需要复杂的数据处理逻辑,直接使用Nginx可能是一个不错的选择。如果你需要更多的灵活性、协议支持和自定义数据处理,那么使用Nginx代理Netty采集服务可能更合适。最终的决策应根据项目要求和可用资源来做出。