这是我参与8月更文挑战的第5天,活动详情查看:8月更文挑战
Kafka 高级特性
生产者
数据生产流程解析
- Producer创建时,会创建一个Sender线程并设置为守护线程
- 生产消息,内部其实是异步流程,生产的消息先经过拦截器, 序列化器,分区器,然后将消息存放在RecordAccumulator缓冲区
- 批次发送条件是: 缓冲区数据大小达到batch.size或者linger.ms达到上限
- 批次发送后,发到指定分区, 落盘到broker;如果生产者配置了retries参数大于0并且失败后允许重试,客户端内部就会对消息进行重试。
- 落盘到broker成功, 返回生产元数据给生产者。
- 元数据返回两种方式: 一种阻塞直接返回,一种回调返回。
broker配置
| 属性 | 说明 |
|---|---|
| bootstrap.servers | 生产者与broker集群建立连接需要的broker地址列表,不需要写全部,但也不要写一个 |
| key.serializer | 实现了接口org.apache.kafka.common.serialization.Serializer的key序列化类 |
| value.serializer | 实现了接口org.apache.kafka.common.serialization.Serializer的value序列化类 |
| acks | 控制已发送消息的持久性 , 默认1 acks=0:生产者不等待broker的任何消息确认,只要放到了socket缓冲区就认为已发送。retries设置不起作用,客户端不关心消息是否发送失败,收到的偏移量永远是-1 acks=1: leader将记录写到本地日志,就响应客户端信息, acks=all/-1: leader等待所有同步的副本确认消息,只要有一个同步副本存在就不会丢失消息 |
| compression.type | 生产者生成数据的压缩格式,默认none, 允许值: none, gzip, snappy和lz4 |
| retries | 设置大于1的值,将在消息发送失败的时候重新发送消息,允许重试但是不设置max.in.flight.requests.per.connection为1,存在消息乱序的可能, 可选值[0, 2147483647] |
| retry.backoff.ms | 在向一个指定的主题分区重发消息的时候,重试之间的等待时间。比如3次重试,每次重试之后等待该时间长度,再接着重试。在一些失败的场景,避免了密集循环的重新发送请求。long型值,默认100。可选值:[0,...] |
| request.timeout.ms | 客户端等待请求响应的最大时长。如果服务端响应超时,则会重发请求,除非达到重试次数。该设置应该比 replica.lag.time.max.ms (副本的 LEO 是否不小于 leader 副本的 HW, follow落后leader10s)要大,以免在服务器延迟时间内重发消息。int类型值,默认:30000,可选值:[0,...] |
| interceptor.classes | 拦截器类必须实现org.apache.kafka.clients.producer.ProducerInterceptor 接口 |
| batch.size | 以字节为单位控制默认批的大小。发送给broker的请求将包含多个批次,每个分区一个,并包含可发送的数据。 |
| client.id | 生产者发送请求的时候传递给broker的id字符串。 |
| send.buffer.bytes | TCP发送数据的时候使用的缓冲区(SO_SNDBUF)大小。如果设置为0,则使用操作系统默认的。 |
| buffer.memory | 生产者可以用来缓存等待发送到服务器的记录的总内存字节。如果记录的发送速度超过了将记录发送到服务器的速度,则生产者将阻塞 max.block.ms的时间,此后它将引发异常。此设置应大致对应于生产者将使用的总内存,但并非生产者使用的所有内存都用于缓冲。一些额外的内存将用于压缩(如果启用了压缩)以及维护运行中的请求。long型数据。默认值:33554432,可选值:[0,...] |
| max.block.ms | 控制 KafkaProducer.send() 和 KafkaProducer.partitionsFor() 阻塞的时长。当缓存满了或元数据不可用的时候,这些方法阻塞。在用户提供的序列化器和分区器的阻塞时间不计入。long型值,默认:60000,可选值:[0,...] |
| connections.max.idle.ms | 当连接空闲时间达到这个值,就关闭连接。long型数据,默认:540000 |
| linger.ms | 默认值是0(没有延迟)。如果设置linger.ms=5 ,则在一个请求发送之前先等待5ms。long型值,默认:0,可选值:[0,...] |
| max.request.size | 单个请求的最大字节数。该设置会限制单个请求中消息批的消息个数,以免单个请求发送太多的数据。服务器有自己的限制批大小的设置,与该配置可能不一样。int类型值,默认1048576,可选值:[0,...] |
| partitioner.class | 默认org.apache.kafka.clients.producer.internals.DefaultPartitioner |
| receive.buffer.bytes | TCP接收缓存(SO_RCVBUF),如果设置为-1,则使用操作系统默认的值。int类型值,默认32768,可选值:[-1,...] |
| security.protocol | 跟broker通信的协议:PLAINTEXT, SSL, SASL_PLAINTEXT, SASL_SSL string类型值,默认:PLAINTEXT |
| max.in.flight.requests.per | 单个连接上未确认请求的最大数量。达到这个数量,客户端阻塞。如果该值大于1,且存在失败的请求,在重试的时候消息顺序不能保证。int类型值,默认5。可选值:[1,...] |
| reconnect.backoff.max.ms | 对于每个连续的连接失败,每台主机的退避将成倍增加,直至达到此最大值。在计算退避增量之后,添加20%的随机抖动以避免连接风暴。long型值,默认1000,可选值:[0,...] |
| reconnect.backoff.ms | 尝试重连指定主机的基础等待时间。避免了到该主机的密集重连。该退避时间应用于该客户端到broker的所有连接。long型值,默认50。可选值:[0,...] |
序列化配置
kafka使用org.apache.kafka.common.serialization.Serializer将数据序列化成字节数组, 系统提供了ByteArraySerializer, ByteBufferSerializer, BytesSerializer, DoubleSerializer, FloatSerializer, IntegerSerializer,StringSerializer,LongSerializer, ShortSerializer。 数据的序列化一般生产中使用avro, 自定义序列化器实现serialize方法。
public class UserSerializer implements Serializer<User> {
@Override
public void configure(Map<String, ?> configs, boolean isKey) {
// do nothing
// 用于接收对序列化器的配置参数,并对当前序列化器进行配置和初始化的
}
@Override
public byte[] serialize(String topic, User data) {
try {
if (data == null) {
return null;
} else {
final Integer userId = data.getUserId();
final String username = data.getUsername();
if (userId != null) {
if (username != null) {
final byte[] bytes = username.getBytes("UTF-8");
int length = bytes.length;
// 第一个4个字节用于存储userId的值
// 第二个4个字节用于存储username字节数组的长度int值
// 第三个长度,用于存放username序列化之后的字节数组
ByteBuffer buffer = ByteBuffer.allocate(4 + 4 + length);
// 设置userId
buffer.putInt(userId);
// 设置username字节数组长度
buffer.putInt(length);
// 设置username字节数组
buffer.put(bytes);
// 以字节数组形式返回user对象的值
return buffer.array();
}
}
}
} catch (Exception e) {
throw new SerializationException("数据序列化失败");
}
return null;
}
@Override
public void close() {
// do nothing
// 用于关闭资源等操作。需要幂等,即多次调用,效果是一样的。
}
}
分区器
源码在KafkaProceducer和DefaultPatitioner中, 默认分区计算:
- 如果提供了分区号,使用提供的分区号
- 如果没有提供分区号, 使用key序列化后的值的hash值对分区数量取模
- 如果record没有提供分区号,也没有提供key, 使用轮询的方式分配分区号
- 首先在可用分区中分配分区号
- 如果没有可用分区,就在所有分区中分配分区号
自定义分区器:
- 开发partitioner接口实现类
- 设置config.put("partitioner.class", "xx.class");
public class MyPartitioner implements Partitioner {
@Override
public int partition(String topic, Object key, byte[] keyBytes, Object value, byte[] valueBytes, Cluster cluster) {
// 此处可以计算分区的数字。
return 2;
}
@Override
public void close() {
}
@Override
public void configure(Map<String, ?> configs) {
}
}
拦截器
Producer和Consumer的Interceptor用于Client端的定制化控制逻辑, Interceptor可能运行在多个线程中, 在具体实现时需要自己确保线程安全。拦截器链捕获异常记录到日志文件而不向上传递。
public interface ProducerInterceptor<K, V> extends Configurable {
//封装进KafkaProducer.send方法中, 运行在用户主线程中
public ProducerRecord<K, V> onSend(ProducerRecord<K, V> record);
// 在消息被应答或消息发送失败后调用,通常在Proceducer回调逻辑触发前,运行在Producer的IO线程,不能放入很重逻辑,影响发送效率
public void onAcknowledgement(RecordMetadata metadata, Exception exception);
// 关闭Interceptor, 执行资源清理
public void close();
}
自定义拦截器:
- 实现ProceducerInterceptor接口
- 设置config.put(ProducerConfig.INTERCEPROR_CLASSES_CONFIG, "xx.class, xx.class");
public class InterceptorOne implements ProducerInterceptor<Integer, String> {
private static final Logger LOGGER = LoggerFactory.getLogger(InterceptorOne.class);
@Override
public ProducerRecord<Integer, String> onSend(ProducerRecord<Integer, String> record) {
System.out.println("拦截器1 -- go");
// 消息发送的时候,经过拦截器,调用该方法
// 要发送的消息内容
final String topic = record.topic();
final Integer partition = record.partition();
final Integer key = record.key();
final String value = record.value();
final Long timestamp = record.timestamp();
final Headers headers = record.headers();
// 拦截器拦下来之后根据原来消息创建的新的消息
// 此处对原消息没有做任何改动
ProducerRecord<Integer, String> newRecord = new ProducerRecord<Integer, String>(
topic,
partition,
timestamp,
key,
value,
headers
);
// 传递新的消息
return newRecord;
}
@Override
public void onAcknowledgement(RecordMetadata metadata, Exception exception) {
System.out.println("拦截器1 -- back");
// 消息确认或异常的时候,调用该方法,该方法中不应实现较重的任务
// 会影响kafka生产者的性能。
}
@Override
public void close() {
}
@Override
public void configure(Map<String, ?> configs) {
final Object classContent = configs.get("classContent");
System.out.println(classContent);
}
}
原理
KafkaProducer有两个基本线程
- 主线程: 负责消息创建, 拦截器,序列化器,分区器等操作,并将消息追加到消息收集器RecordAccumulator。
- RecordAccumulator为每个分区都维护了一个Deque类型的双端队列
- ProducerBatch批量发送有利于提升吞吐量,降低网络影响
- 由于生产者客户端使用 java.io.ByteBuffer 在发送消息之前进行消息保存,并维护了一个 BufferPool 实现 ByteBuffer 的复用;该缓存池只针对特定大小( batch.size指定)的 ByteBuffer进行管理,对于消息过大的缓存,不能做到重复利用。
- 每次追加一条ProducerRecord消息,会寻找/新建对应的双端队列,从其尾部获取一个ProducerBatch,判断当前消息的大小是否可以写入该批次中。若可以写入则写入;若不可以写入,则新建一个ProducerBatch,判断该消息大小是否超过客户端参数配置 batch.size 的值,不超过,则以 batch.size建立新的ProducerBatch,这样方便进行缓存重复利用;若超过,则以计算的消息大小建立对应的 ProducerBatch ,缺点就是该内存不能被复用了。
- Sender线程:
- 从消息收集器获取缓存的消息,将其处理为 <Node, List 的形式, Node 表示集群的broker节点。
- 进一步将<Node, List转化为<Node, Request>形式,此时才可以向服务端发送数据
- 在发送之前,Sender线程将消息以 Map<NodeId, Deque> 的形式保存到InFlightRequests 中进行缓存,可以通过其获取 leastLoadedNode ,即当前Node中负载压力最小的一个,以实现消息的尽快发出。
消费者
消费者组
多个从同一个主题消费的消费者加入到一个消费者组中, 消费者组中的消费者共享group.id
kafka心跳是Consumer和Broker之间的健康检查, 只有当Broker Coordinator正常时,Consumer才会发送消息
| 参数 | 字段 |
|---|---|
| session.timeout.ms | MemberMetadata.sessionTimeoutMs |
| max.poll.interval.ms | MemberMetadata.rebalanceTimeoutMs |
broker sessionTimeoutMs参数。broker逻辑在GroupCoordinator中, 如果心跳超期, broker coordinator会把消费者从group中移除并触发rebalance
consumer sessionTimeoutMs rebalanceTimeoutMs参数。如果客户端发现心跳超期, 客户端会标记coordinator不可用,阻塞心跳线程,如果poll消息的间隔超过了rebalanceTimeoutMs, consumer告知broker主动离开消费组, 触发rebalance。
消息接收
| 参数 | 说明 |
|---|---|
| bootstrap.servers | 向Kafka集群建立初始连接用到的host/port列表 |
| key.deserializer | org.apache.kafka.common.serialization.Deserializer实现类 |
| value.deserializer | org.apache.kafka.common.serialization.Deserializer实现类 |
| client.id | 从服务器消费消息的时候向服务器发送的id字符串 |
| group.id | 用于唯一标志当前消费者所属的消费组的字符串。如果消费者使用组管理功能如subscribe(topic)或使用基于Kafka的偏移量管理策略,该项必须设置。 |
| auto.offset.reset | earliest:自动重置偏移量到最早的偏移量 latest:自动重置偏移量为最新的偏移量 none:如果消费组原来的(previous)偏移量不存在,则向消费者抛异 常 anything:向消费者抛异常 |
| enable.auto.commit | 如果设置为true,消费者会自动周期性地向服务器提交偏移量。 |
| auto.commit.interval.ms | 如果设置了 enable.auto.commit 的值为true,则该值定义了消费者偏移量向Kafka提交的频率。 |
| fetch.min.bytes | 服务器对每个拉取消息的请求返回的数据量最小值。 |
| fetch.max.wait.ms | 如果服务器端的数据量达不到 fetch.min.bytes 的话,服务器端不能立即响应请求。该时间用于配置服务器端阻塞请求的最大时长。 |
| fetch.max.bytes | 服务器给单个拉取请求返回的最大数据量。消费者批量拉取消息,如果第一个非空消息批次的值比该值大,消息批也会返回,以让消费者可以接着进行。即该配置并不是绝对的最大值。broker可以接收的消息批最大值通过message.max.bytes (broker配置)或 max.message.bytes (主题配置)来指定。 |
| connections.max.idle.ms | 在这个时间之后关闭空闲的连接。 |
| check.crcs | 自动计算被消费的消息的CRC32校验值。可以确保在传输过程中或磁盘存储过程中消息没有被破坏.它会增加额外的负载,在追求极致性能的场合禁用。 |
| exclude.internal.topics | 是否内部主题应该暴露给消费者。如果该条目设置为true,则只能先订阅再拉取。 |
| isolation.level | 控制如何读取事务消息; read_committed和 read_uncommitted (默认值) |
| heartbeat.interval.ms | 当使用消费组的时候,该条目指定消费者向消费者协调器发送心跳的时间间隔。 |
| session.timeout.ms | 当使用Kafka的消费组的时候,消费者周期性地向broker发送心跳数表明自己的存在。如果经过该超时时间还没有收到消费者的心跳,则broker将消费者从消费组移除,并启动再平衡。该值必须在broker配置 group.min.session.timeout.ms 和group.max.session.timeout.ms 之间。 |
| max.poll.records | 一次调用poll()方法返回的记录最大数量。 |
| max.poll.interval.ms | 使用消费组的时候调用poll()方法的时间间隔。该条目指定了消费者调用poll()方法的最大时间间隔。如果在此时间内消费者没有调用poll()方法,则broker认为消费者失败,触发再平衡,将分区分配给消费组中其他消费者。 |
| max.partition.fetch.bytes | 对每个分区,服务器返回的最大数量.消费者按批次拉取数据。如果非空分区的第一个记录大于这个值,批处理依然可以返回,以保证消费者可以进行下去。broker接收批的大小由 message.max.bytes (broker参数)或max.message.bytes (主题参数)指定。 |
| send.buffer.bytes | 用于TCP发送数据时使用的缓冲大小(SO_SNDBUF),-1表示使用OS默认的缓冲区大小 |
| retry.backoff.ms | 在发生失败的时候如果需要重试,则该配置表示客户端<等待多长时间再发起重试。该时间的存在避免了密集循环。 |
| request.timeout.ms | 客户端等待服务端响应的最大时间。如果该时间超时,则客户端要么重新发起请求,要么如果重试耗尽,请求失败。 |
| reconnect.backoff.ms | 重新连接主机的等待时间。避免了重连的密集循环。该等待时间应用于该客户端到broker的所有连接。 |
| reconnect.backoff.max.ms | 重新连接到反复连接失败的broker时要等待的最长时间(以毫秒为单位)。如果提供此选项,则对于每个连续的连接失败,每台主机的退避将成倍增加,直至达到此最大值。在计算退避增量之后,添加20%的随机抖动以避免连接风暴。 |
| receive.buffer.bytes | TCP连接接收数据的缓存(SO_RCVBUF)。-1表示使用操作系统的默认值。 |
| partition.assignment.strategy | 当使用消费组的时候,分区分配策略的类名。 |
| metrics.sample.window.ms | 计算指标样本的时间窗口。 |
| metrics.recording.level | 指标的最高记录级别。 |
| metrics.num.samples | 用于计算指标而维护的样本数量 |
| interceptor.classes | 拦截器类的列表。默认没有拦截器拦截器是消费者的拦截器,该拦截器需要实现org.apache.kafka.clients.consumer.ConsumerInterceptor 接口。拦截器可用于对消费者接收到的消息进行拦截处理。 |
-
反序列化
实现 org.apache.kafka.common.serialization.Deserializer, 系统提供了ByteArrayDeserializer, ByteBufferDeserializer, ByteDeserializer, DoubleDeserializer, FloatDeserializer, IntegerDeserializer, LongDeserializer, ShortDeserializer, StringDeserializer等
自定义反序列化
public class UserDeserializer implements Deserializer<User> { @Override public void configure(Map<String, ?> configs, boolean isKey) { } @Override public User deserialize(String topic, byte[] data) { ByteBuffer buffer = ByteBuffer.allocate(data.length); buffer.put(data); buffer.flip(); final int userId = buffer.getInt(); final int usernameLength = buffer.getInt(); String username = new String(data, 8, usernameLength); return new User(userId, username); } @Override public void close() { } } -
位移提交
-
自动提交
- enable.auto.commit=true
- auto.commit.interval.ms 默认5s
- kafka会保证在调用poll时,提交上次返回的所有消息,不会出现消息丢失,但会重复消费(比如每5s提交offset, 在3s的时候发生rebalance)
-
同步提交
- KafkaConsumer#commitSync
- 影响TPS
- 可以拉长提交间隔,导致Consumer提交频率下降, 重启后有更多消息被消费
-
异步提交
- KafkaConsumer#commitAsync
- 出问题不会重试
try { while(true) { ConsumerRecords<String, String> records = consumer.poll(Duration.ofSeconds(1)); process(records); // 处理消息 commitAysnc(); // 使用异步提交规避阻塞 } } catch(Exception e) { handle(e); // 处理异常 } finally { try { consumer.commitSync(); // 最后一次提交使用同步阻塞式提交 } finally { consumer.close(); } }
-
-
位移管理API
API 说明 public void assign(Collection partitions) 给当前消费者手动分配一系列主题分区。
手动分配分区不支持增量分配,如果先前有分配分区,则该操作会覆盖之前的分配。如果给出的主题分区是空的,则等价于调用unsubscribe方法。
手动分配主题分区的方法不使用消费组管理功能。当消费组成员变了,或者集群或主题的元数据改变了,不会触发分区分配的再平衡。手动分区分配assign(Collection)不能和自动分区分配subscribe(Collection,ConsumerRebalanceListener)一起使用。如果启用了自动提交偏移量,则在新的分区分配替换旧的分区分配之前,会对旧的分区分配中的消费偏移量进行异步提交。public Set assignment() 获取给当前消费者分配的分区集合。如果订阅是通过调用assign方法直接分配主题分区,则返回相同的集合。如果使用了主题订阅,该方法返回当前分配给该消费者的主题分区集合。如果分区订阅还没开始进行分区分配,或者正在重新分配分区,则会返回none。 public Map<String, List> listTopics() 获取对用户授权的所有主题分区元数据。该方法会对服务器发起远程调用。 public List partitionsFor(String topic) 获取指定主题的分区元数据。如果当前消费者没有关于该主题的元数据,就会对服务器发起远程调用 public Map<TopicPartition, Long> beginningOffsets(Collectionpartitions) 对于给定的主题分区,列出它们第一个消息的偏移量。注意,如果指定的分区不存在,该方法可能会永远阻塞。该方法不改变分区的当前消费者偏移量。 public void seekToEnd(Collection partitions) 将偏移量移动到每个给定分区的最后一个。该方法延迟执行,只有当调用过poll方法或position方法之后才可以使用。如果没有指定分区,则将当前消费者分配的所有分区的消费者偏移量移动到最后。如果设置了隔离级别为isolation.level=read_committed,则会将分区的消费偏移量移动到最后一个稳定的偏移量,即下一个要消费的消息现在还是未提交状态的事务消息 public void seek(TopicPartition partition, long offset) 将给定主题分区的消费偏移量移动到指定的偏移量,即当前消费者下一条要消费的消息偏移量。若该方法多次调用,则最后一次的覆盖前面的。如果在消费中间随意使用,可能会丢失数据。 public long position(TopicPartition partition) 检查指定主题分区的消费偏移量 public void seekToBeginning(Collection partitions) 将给定每个分区的消费者偏移量移动到它们的起始偏移量。该方法懒执行,只有当调用过poll方法或position方法之后才会执行。如果没有提供分区,则将所有分配给当前消费者的分区消费偏移量移动到起始偏移量。 #查看位移_consumer_offsets kafka-console-consumer.sh --topic __consumer_offsets --bootstrap-server node1:9092 --formatter "kafka.coordinator.group.GroupMetadataManager\$OffsetsMessageFormatter" -- consumer.config /opt/kafka_2.12-1.0.2/config/consumer.properties --from- beginning | headconsumer_offsets主题配置了compact策略,总是保存最新的位移信息,即控制了总体的日志容量,也实现了保存offset的目的。
-
重平衡
触发条件:
- 消费者组内成员发生变更
- 主题分区数发生改变
- 订阅主题发生改变
session.timeout.ms参数是超时时间, heartbeat.interval.ms控制发送心跳的频率, max.poll.interval.ms表示两次poll之间的时间间隔默认5分钟
相对合理的配置:
- session.timeout.ms: 6s
- heartbeat.interval.ms: 2s
- max.poll.interval.ms 消费者处理消息最长耗时加1分钟
Group Coordinator来执行对于消费组的管理。当消费组的第一个消费者启动的时候,它会去和Kafka Broker确定谁是它们组的组协调器。之后该消费组内所有消费者和该组协调器协调通信。
确定Coordinator:
- 确定消费组位移写入到_consumers_offsets哪个分区. 计算方法是Math.abs(groupId.hashCode()%offsets.topic.num.partitions)
- 此分区的leader所在的broker就是组协调器
Rebalance Generation表示Rebalance之后主题分区到消费组中消费者映射关系的一个版本,主要是用于保护消费组,隔离无效偏移量提交的.
五种处理与消费组协调相关的协议
- Heartbeat请求:consumer需要定期给组协调器发送心跳来表明自己还活着
- LeaveGroup请求:主动告诉组协调器我要离开消费组
- SyncGroup请求:消费组Leader把分配方案告诉组内所有成员
- JoinGroup请求:成员请求加入组
- DescribeGroup请求:显示组的所有信息,包括成员信息,协议名称,分配方案,订阅信息等。通常该请求是给管理员使用
过程:
- Join: 加入组。所有成员都向消费组协调器发送JoinGroup请求,请求加入消费组。一旦所有成员都发送了JoinGroup请求,协调i器从中选择一个消费者担任Leader的角色,并把组成员信息以及订阅信息发给Leader。
- Sync,Leader开始分配消费方案,即哪个消费者负责消费哪些主题的哪些分区。一旦完成分配,Leader会将这个方案封装进SyncGroup请求中发给消费组协调器,非Leader也会发SyncGroup请求,只是内容为空。消费组协调器接收到分配方案之后会把方案塞进SyncGroup的response中发给各个消费者。
在协调器收集到所有成员请求前,它会把已收到请求放入一个叫purgatory(炼狱)的地方
组内分区分配策略:
-
RangeAssignor
对每个topic进行独立分区分配,首先对分区按照分区ID进行数值排序, 然后订阅这个topic的消费者进行字典排序,尽量均匀分配
原理是按照消费者总数和分区总数进行整除运算来获得一个跨度,然后将分区按照跨度进行平均分配,以保证分区尽可能均匀地分配给所有的消费者。对于每一个Topic,RangeAssignor策略会将消费组内所有订阅这个Topic的消费者按照名称的字典序排序,然后为每个消费者划分固定的分区范围,如果不够平均分配,那么字典序靠前的消费者会被多分配一个分区。
这种分配方式明显的一个问题是随着消费者订阅的Topic的数量的增加,不均衡的问题会越来越严重
-
RoundRobinAssignor
将消费组内订阅的所有Topic的分区及所有消费者进行排序后尽量均衡的分配(RangeAssignor是针对单个Topic的分区进行排序分配的.对于消费组内消费者订阅Topic不一致的情况会不均衡
-
StrickyAssignor
无论是RangeAssignor,还是RoundRobinAssignor,当前的分区分配算法都没有考虑上一次的分配结果。显然,在执行一次新的分配之前,如果能考虑到上一次分配的结果,尽量少的调整分区分配的变动,显然是能节省很多开销的。 分区的分配尽量的均衡.每一次重分配的结果尽量与上一次分配结果保持一致
消费者组状态
- Dead:组内已经没有任何成员的最终状态,组的元数据也已经被组协调器移除了。这种状态响应各种请求都是一个response: UNKNOWN_MEMBER_ID
- Empty:组内无成员,但是位移信息还没有过期。这种状态只能响应JoinGroup请求
- PreparingRebalance:组准备开启新的rebalance,等待成员加入
- AwaitingSync:正在等待leader consumer将分配方案传给各个成员
- Stable:再均衡完成,可以开始消费。
-
消费者拦截器
实现org.apache.kafka.clients.consumer.ConsumerInterceptor<K, V>接口
ConsumerInterceptor回调发生在KafkaConsumer#poll(long)方法同一个线程
props.setProperty(ConsumerConfig.INTERCEPTOR_CLASSES_CONFIG,"xxx" );
public interface ConsumerInterceptor<K, V> extends Configurable { //poll方法返回之前调用 public ConsumerRecords<K, V> onConsume(ConsumerRecords<K, V> records); //提交偏移量调用 public void onCommit(Map<TopicPartition, OffsetAndMetadata> offsets); public void close(); }public class OneInterceptor implements ConsumerInterceptor<String, String> { @Override public ConsumerRecords<String, String> onConsume(ConsumerRecords<String, String> records) { // poll方法返回结果之前最后要调用的方法 System.out.println("One -- 开始"); // 消息不做处理,直接返回 return records; } @Override public void onCommit(Map<TopicPartition, OffsetAndMetadata> offsets) { // 消费者提交偏移量的时候,经过该方法 System.out.println("One -- 结束"); } @Override public void close() { // 用于关闭该拦截器用到的资源,如打开的文件,连接的数据库等 } @Override public void configure(Map<String, ?> configs) { // 用于获取消费者的设置参数 configs.forEach((k, v) -> { System.out.println(k + "\t" + v); }); } }
主题
- kafka-topics.sh脚本
| 选项 | 说明 |
|---|---|
| --config String:name=value | 为创建的或修改的主题指定配置信息。支持下述 配置条目: cleanup.policy compression.type delete.retention.ms file.delete.delay.ms flush.messages flush.ms follower.replication.throttled.replicas index.interval.bytes leader.replication.throttled.replicas max.message.bytes message.format.version message.timestamp.difference.max.ms message.timestamp.type min.cleanable.dirty.ratio min.compaction.lag.ms min.insync.replicas preallocate retention.bytes retention.ms segment.bytes segment.index.bytes segment.jitter.ms segment.ms unclean.leader.election.enable |
| --create | 创建一个新主题 |
| --delete | 删除一个主题 |
| --delete-config <String: name> | 删除现有主题的一个主题配置条目。这些条目就是在--config中给出的配置条目。 |
| --alter | 更改主题的分区数量,副本分配和/或配置条目。 |
| --describe | 列出给定主题的细节。 |
| --disable-rack-aware | 禁用副本分配的机架感知。 |
| --force | 抑制控制台提示信息 |
| --help | 打印帮助信息 |
| --if-exists | 如果指定了该选项,则在修改或删除主题的时候,只有主题存在才可以执行。 |
| --if-not-exists | 在创建主题的时候,如果指定了该选项,则只有主题不存在的时候才可以执行命令 |
| --list | 列出所有可用的主题。 |
| --partitions <Integer: # of partitions> | 要创建或修改主题的分区数。 |
| --replica-assignment <String:broker_id_for_part1_replica1 :broker_id_for_part1_replica2 ,broker_id_for_part2_replica1 :broker_id_for_part2_replica2 , ...> | 当创建或修改主题的时候手动指定partition-to- broker的分配关系。 |
| --replication-factor <Integer:replication factor> | 要创建的主题分区副本数。1表示只有一个副本, 也就是Leader副本。 |
| --topic <String: topic> | 要创建、修改或描述的主题名称。除了创建,修改和描述在这里还可以使用正则表达式。 |
| --topics-with-overrides | if set when describing topics, only show topics that have overridden configs |
| --unavailable-partitions | if set when describing topics, only show partitions whose leader is not available |
| --under-replicated-partitions | if set when describing topics, only show under replicated partitions |
| --zookeeper <String: urls> | 必需的参数:连接zookeeper的字符串,逗号分 隔的多个host:port列表。多个URL可以故障转移。 |
主题中的参数
| 属性 | 默认值 | 服务器默认属性 | 说明 |
|---|---|---|---|
| cleanup.policy | delete | log.cleanup.policy | 要么是”delete“要么是”compact“; 这个字符串指明了 针对旧日志部分的利用方式;默 认方式("delete")将会丢弃旧 的部分当他们的回收时间或者尺 寸限制到达时。”compact“将会 进行日志压缩 |
| compression.type | none | producer用于压缩数据的压缩类 型。默认是无压缩。正确的选项 值是none、gzip、snappy。压 缩最好用于批量处理,批量处理 消息越多,压缩性能越好。 | |
| delete.retention.ms | 86400000 (24hours) | log.cleaner.delete.retention.ms | 对于压缩日志保留的最长时间 |
| flush.ms | None | log.flush.interval.ms | 此项配置用来置顶强制进行fsync 日志到磁盘的时间间隔;例如, 如果设置为1000,那么每 1000ms就需要进行一次fsync。 一般不建议使用这个选项 |
| flush.messages | None | log.flush.interval.messages | 此项配置指定时间间隔:强制进 行fsync日志。 |
| index.interval.bytes | 4096 | log.index.interval.bytes | 默认设置保证了我们每4096个字节就对消息添加一个索引, |
| max.message.bytes | 1000000 | max.message.bytes | kafka追加消息的最大尺寸。 |
| min.cleanable.dirty.ratio | 0.5 | min.cleanable.dirty.ratio | 此项配置控制log压缩器试图进行 清除日志的频率。默认情况下,将避免清除压缩率超过50%的日志。这个比率避免了最大的空间 浪费 |
| min.insync.replicas | 1 | min.insync.replicas | 当producer设置request.required.acks为-1时, min.insync.replicas指定replicas的最小数目 |
| retention.bytes | None | log.retention.bytes | 如果使用“delete”的retention 策 略,这项配置就是指在删除日志之前,日志所能达到的最大尺 寸。默认情况下,没有尺寸限制而只有时间限制 |
| retention.ms | 7 days | log.retention.minutes | 如果使用“delete”的retention策略,这项配置就是指删除日志前日志保存的时间。 |
| segment.bytes | 1GB | log.segment.bytes | kafka中log日志是分成一块块存储的,此配置是指log日志划分成块的大小 |
| segment.index.bytes | 10MB | log.index.size.max.bytes | 此配置是有关offsets和文件位置之间映射的索引文件的大小;一般不需要修改这个配置 |
| segment.jitter.ms | 0 | log.roll.jitter.{ms,hours} | The maximum jitter to subtract from logRollTimeMillis. |
| segment.ms | 7 days | log.roll.hours | 即使log的分块文件没有达到需要删除、压缩的大小,一旦log 的 时间达到这个上限,就会强制新建一个log分块文件 |
| unclean.leader.election.enable | true | 指明了是否能够使不在ISR中replicas设置用来作为leader |
-
创建主题
kafka-topics.sh --zookeeper localhost:2181/myKafka --create --topic topic_x - -partitions 1 --replication-factor 1 kafka-topics.sh --zookeeper localhost:2181/myKafka --create --topic topic_test_02 --partitions 3 --replication-factor 1 --config max.message.bytes=1048576 --config segment.bytes=10485760 -
查看主题
kafka-topics.sh --zookeeper localhost:2181/myKafka --list kafka-topics.sh --zookeeper localhost:2181/myKafka --describe --topic topic_x kafka-topics.sh --zookeeper localhost:2181/myKafka --topics-with-overrides -- describe -
修改主题
kafka-topics.sh --zookeeper localhost:2181/myKafka --create --topic topic_test_01 --partitions 2 --replication-factor 1 kafka-topics.sh --zookeeper localhost:2181/myKafka --alter --topic topic_test_01 --config max.message.bytes=1048576 kafka-topics.sh --zookeeper localhost:2181/myKafka --describe --topic topic_test_01 kafka-topics.sh --zookeeper localhost:2181/myKafka --alter --topic topic_test_01 --config segment.bytes=10485760 kafka-topics.sh --zookeeper localhost:2181/myKafka --alter --delete-config max.message.bytes --topic topic_test_01 -
删除主题
kafka-topics.sh --zookeeper localhost:2181/myKafka --delete --topic topic_x -
增加分区
kafka-topics.sh --zookeeper localhost/myKafka --alter --topic myTop1 -- partitions 2 -
分区副本分配
在不考虑机架信息的情况下:
- 第一个副本分区通过轮询的方式挑选一个broker,进行分配。该轮询从broker列表的随机位置
进行轮询。
- 其余副本通过增加偏移进行分配。
-
偏移量管理
bin/kafka-consumer-groups.sh
| 参数 | 说明 |
|---|---|
| --all-topics | 将所有关联到指定消费组的主题都划归到 reset-offsets 操作范围 |
| --bootstrap-server <String: server toconnect to> | 必须:(基于消费组的新的消费者): 要连接的服务器地址。 |
| --by-duration String:duration | 距离当前时间戳的一个时间段。格式:'PnDTnHnMnS' |
| --command-config <String:command config propertyfile> | 指定配置文件,该文件内容传递给Admin Client和消费者 |
| --delete | 传值消费组名称,删除整个消费组与所有主题的各个分区偏移量和所有者关系。 --group g1 --group g2 --topic t1 。 |
| --describe | 描述给定消费组的偏移量差距(有多少消息还没有消费)。 |
| --execute | 执行操作。支持的操作: reset-offsets |
| --export | 导出操作的结果到CSV文件。支持的操作: reset-offsets |
| --from-file <String: path to CSV file> | 重置偏移量到CSV文件中定义的值。 |
| --group <String: consumergroup> | 目标消费组。 |
| --list | 列出所有消费组 |
| --new-consumer | 使用新的消费者实现。这是默认值。随后的发行版中会删除这一操作。 |
| --reset-offsets | 重置消费组的偏移量。当前一次操作只支持一个消费组,并且该消费组应该是不活跃的 1. (默认)plan:要重置哪个偏移量。 2. execute:执行 reset-offsets 操作 3. process:配合 --export 将操作结果导出到CSV格式。 可以使用如下选项 --to-datetime --by-period --to-earliest --to-latest --shift-by --from-file --to-current 要定义操作的范围,使用:--all-topics --topic |
| --shift-by Long:number-of-offsets | 重置偏移量n个。n可以是正值,也可以是负值。 |
| --timeout <Long: timeout(ms)> | 对某些操作设置超时时间 默认时间: 5000 。 |
| --to-current | 重置到当前的偏移量。 |
| --to-datetime String:datetime | 重置偏移量到指定的时间戳。格式:'YYYY-MM-DDTHH:mm:SS.sss' |
| --to-earliest | 重置为最早的偏移量 |
| --to-latest | 重置为最新的偏移量 |
| --to-offset Long:offset | 重置到指定的偏移量。 |
| --topic <String: topic> | 指定哪个主题的消费组需要删除,或者指定哪个主题的消费组需要包含到 reset-offsets 操作中。对于 reset-offsets 操作,还可以指定分区: topic1:0,1,2 。其中0,1,2表示要包含到操作中的分区号。重置偏移量的操作支持多个主题一起操作。 |
| --zookeeper <String: urls> | --zookeeper node1:2181/myKafka |
查看有那些 group ID 正在进行消费:
kafka-consumer-groups.sh --bootstrap-server node1:9092 --list
查看偏移量
kafka-consumer-groups.sh --bootstrap-server node1:9092 --describe --group group
偏移量设置最早
kafka-consumer-groups.sh --bootstrap-server node1:9092 --reset-offsets --group group --to-earliest --topic topic1
偏移量设置最新
kafka-consumer-groups.sh --bootstrap-server node1:9092 --reset-offsets --group group --to-latest --topic topic1
kafka-consumer-groups.sh --bootstrap-server node1:9092 --reset-offsets --group group --topic topic1 --shift-by -10
分区
副本
同步节点的定义:
- 节点必须能够维持与ZK的会话
- 不能落后太多,落后太多意思是该Follow复制的消息落后于Leader的条数超过预定值(参数: replica.lag.max.messages 默认值:4000)或者Follow长时间没有向Leader发送fetch请求(参数: replica.lag.time.max.ms 默认值:10000)。
分区重新分配
kafka-reassign-partitions.sh
- generate模式,自动生成reassign plan (并不执行)
- execute 根据指定的reassignPlan 重新分配
- verify 验证重新分配是否成功
生成reassign plan
-
定义文件
cat topic-to-move.json
{ "topics":[ { "topics": "te_re_01" } ], "version": 1 }kafka-reassign-partitions.sh --zookeeper node1:2181/myKafka --topics-to-move-json-file topics-to-move.json --broker-list "0,1" --generate ## 将上面结果放到json中 kafka-reassign-partitions.sh --zookeeper node1:2181/myKafka --reassignment-json-file topics-to-execute.json --execute kafka-reassign-partitions.sh --zookeeper node1:2181/myKafka --reassignment-json-file topics-to-execute.json --verify
leader重新分配
kafka-topics.sh --zookeeper node1:2181/myKafka --create --topic tp_demo_03 --replica-assignment "0:1,1:0,0:1"
重新修改回原来副本
{
"partitions": [
{
"topic":"tp_demo_03",
"partition":0
},
{
"topic":"tp_demo_03",
"partition":1
},
{
"topic":"tp_demo_03",
"partition":2
}
]
}
kafka-preferred-replica-election.sh --zookeeper
node1:2181/myKafka --path-to-json-file preferred-replicas.json
修改副本因子
{
"version":1,
"partitions":[
{"topic":"tp_re_02","partition":0,"replicas":[0,1]},
{"topic":"tp_re_02","partition":1,"replicas":[0,1]},
{"topic":"tp_re_02","partition":2,"replicas":[1,0]}
]
}
kafka-reassign-partitions.sh --zookeeper
node1:2181/myKafka --reassignment-json-file increase-replication-
factor.json --execute
物理存储
| 配置 | 默认值 | 说明 |
|---|---|---|
| log.index.interval.bytes | 4096(4K) | 增加索引项字节间隔密度,会影响索引文件中的区间密度和查询效率 |
| log.segment.bytes | 1073741824(1G) | 日志文件最大值 |
| log.roll.ms | 当前日志分段中消息的最大时间戳与当前系的时间戳的差值允许的最大范围,单位毫秒 | |
| log.roll.hours | 168(7天) | 当前日志分段中消息的最大时间戳与当前系的时间戳的差值允许的最大范围,单位小时 |
| log.index.size.max.bytes | 10485760(10MB) | 触发偏移量索引文件或时间戳索引文件分段字节限额 |
kafka-run-class.sh kafka.tools.DumpLogSegments --files 00000000000000000000.log --print-data-log | head
kafka-run-class.sh kafka.tools.DumpLogSegments --files 00000000000000000000.index --print-data-log | head
在偏移量索引文件中,索引数据都是顺序记录 offset ,但时间戳索引文件中每个追加的索引时间戳必须大于之前追加的索引项,否则不予追加。在 Kafka 0.11.0.0 以后,消息信息中存在若干的时间戳信息。如果 broker 端参数log.message.timestamp.type 设置为 LogAppendTIme ,那么时间戳必定能保持单调增长。反之如果是 CreateTime 则无法保证顺序。
Kafka 提供两种日志清理策略:
-
日志删除:按照一定的删除策略,将不满足条件的数据进行数据删除
-
基于时间
日志删除任务会根据 log.retention.hours/log.retention.minutes/log.retention.ms 设定日志保留的时间节点。如果超过该设定值,就需要进行删除。默认是 7 天, log.retention.ms 优先级最高。
Kafka 依据日志分段中最大的时间戳进行定位。
首先要查询该日志分段所对应的时间戳索引文件,查找时间戳索引文件中最后一条索引项,若最后一条索引项的时间戳字段值大于 0,则取该值,否则取最近修改时间。
删除过程
- 从日志对象中所维护日志分段的跳跃表中移除待删除的日志分段,保证没有线程对这些日志分段进行读取操作。
- 这些日志分段所有文件添加 上 .delete 后缀。
- 交由一个以 "delete-file" 命名的延迟任务来删除这些 .delete 为后缀的文件。延迟执行时间可以通过 file.delete.delay.ms 进行设置
- Kafka 会先切分出一个新的日志分段作为活跃日志分段,该日志分段不删除,删除原来的日志分段
-
基于日志大小
日志删除任务会检查当前日志的大小是否超过设定值。设定项为 log.retention.bytes ,单个日志分段的大小由 log.segment.bytes 进行设定
删除过程
- 计算需要被删除的日志总大小 (当前日志文件大小(所有分段)减去retention值)
- 从日志文件第一个 LogSegment 开始查找可删除的日志分段的文件集合。
- 执行删除。
-
基于偏移量
根据日志分段的下一个日志分段的起始偏移量是否大于等于日志文件的起始偏移量,若是,则可以 删除此日志分段。
删除过程
- 从头开始遍历每个日志分段,日志分段1的下一个日志分段的起始偏移量为21,小于logStartOffset,将日志分段1加入到删除队列中
- 日志分段4的下一个日志分段的其实偏移量为71,大于logStartOffset,则不进行删除。
-
-
日志压缩:针对每个消息的 Key 进行整合,对于有相同 Key 的不同 Value 值,只保留最后一个版本。
Kafka 提供 log.cleanup.policy 参数进行相应配置,默认值: delete ,还可以选择compact 。 主题级别的配置项是 cleanup.policy 。
日志压缩与key有关,确保每个消息的key不为null。压缩是在Kafka后台通过定时重新打开Segment来完成的
Kafka的后台线程会定时将Topic遍历两次:
- 记录每个key的hash值最后一次出现的偏移量
- 第二次检查每个offset对应的Key是否在后面的日志中出现过,如果出现了就删除对应的日志。
log.cleanup.policy 设置为 compact ,Broker的配置,影响集群中所有的Topic。
log.cleaner.min.compaction.lag.ms ,用于防止对更新超过最小消息进行压缩,如果没有设置,除最后一个Segment之外,所有Segment都有资格进行压缩
log.cleaner.max.compaction.lag.ms ,用于防止低生产速率的日志在无限制的时间内不压缩。
磁盘储存
kafka的两个过程:
1、网络数据持久化到磁盘 (Producer 到 Broker)
数据落盘通常都是非实时的,Kafka的数据并不是实时的写入硬盘,它充分利用了现代操作系统分页存储来利用内存提高I/O效率。
2、磁盘文件通过网络发送(Broker 到 Consumer)
零拷贝
磁盘数据通过DMA(Direct Memory Access,直接存储器访问)拷贝到内核态 Buffer.直接通过 DMA 拷贝到 NIC Buffer(socket buffer),无需 CPU 拷贝。除了减少数据拷贝外,整个读文件 ==> 网络发送由一个 sendfile 调用完成,整个过程只有两次上下文切换,因此大大提高了性能。fileChannel.transferTo( position, count, socketChannel);
页缓存
把磁盘中的数据缓存到内存中,把对磁盘的访问变为对内存的访问.Kafka接收来自socket buffer的网络数据,应用进程不需要中间处理、直接进行持久化时。可以使用mmap内存文件映射。将磁盘文件映射到内存, 用户通过修改内存就能修改磁盘文件。
mmap也有一个很明显的缺陷:不可靠,写到mmap中的数据并没有被真正的写到硬盘,操作系统会在程序主动调用flush的时候才把数据真正的写到硬盘。Kafka提供了一个参数 producer.type 来控制是不是主动flush;如果Kafka写入到mmap之后就立即flush然后再返回Producer叫同步(sync);写入mmap之后立即返回Producer不调用flush叫异步(async)
Java NIO,提供了一个MappedByteBuffer 类可以用来实现内存映射.MappedByteBuffer只能通过调用FileChannel的map()取得,再没有其他方式。mmap的文件映射,在full gc时才会进行释放。当close时,需要手动清除内存映射文件,可以反射调用sun.misc.Cleaner方法。
顺序写入
操作系统可以针对线性读写做深层次的优化,比如预读和后写
稳定性
事务
创建消费者代码,需要:
- 将配置中的自动提交属性(auto.commit)进行关闭
- 而且在代码里面也不能使用手动提交commitSync( )或者commitAsync( )
- 设置isolation.level
创建生成者,代码如下,需要:
- 配置transactional.id属性
- 配置enable.idempotence属性
broker config
| 配置项 | 说明 |
|---|---|
| transactional.id.timeout.ms | 事务协调器在生产者TransactionalId提前过期之前等待的最长时间,并且没有从该生产者TransactionalId接收到任何事务状态更新。默认是604800000(7天)。这允许每周一次的生产者作业维护它们的id |
| max.transaction.timeout.ms | 事务允许的最大超时。默认值为900000(15分钟)。 |
| transaction.state.log.replication.factor | 事务状态topic的副本数量。默认值:3 |
| transaction.state.log.num.partitions | 事务状态主题的分区数。默认值:50 |
| transaction.state.log.min.isr | 事务状态主题的每个分区ISR最小数量。默认值:2 |
| transaction.state.log.segment.bytes | 事务状态主题的segment大小。默认值:104857600字节 |
producer configs
| 配置项 | 说明 |
|---|---|
| enable.idempotence | 开启幂等 |
| transaction.timeout.ms | 事务超时时间.事务协调器在主动中止正在进行的事务之前等待生产者更新事务状态的最长时间。如果该值大于max.transaction.timeout。在broke中设置ms时,请求将失败,并出现InvalidTransactionTimeout错误。默认是60000。 |
| transactional.id | 用于事务性交付的TransactionalId。这支持跨多个生产者会话的可靠性语义,因为它允许客户端确保使用相同TransactionalId的事务在启动任何新事务之前已经完成。 |
consumer configs
| 配置项 | 说明 |
|---|---|
| isolation.level | - read_uncommitted:以偏移顺序使用已提交和未提交的消息。- read_committed:仅以偏移量顺序使用非事务性消息或已提交事务性消息。为了维护偏移排序,这个设置意味着我们必须在使用者中缓冲消息,直到看到给定事务中的所有消息。 |
Kafka为了实现幂等性,它在底层设计架构中引入了ProducerID和SequenceNumber。
- ProducerID:在每个新的Producer初始化时,会被分配一个唯一的ProducerID,这个ProducerID对客户端使用者是不可见的
- SequenceNumber:对于每个ProducerID,Producer发送数据的每个Topic和Partition都对应一个从0开始单调递增的SequenceNumber值。
控制器
Kafka通过Zookeeper的分布式锁特性选举集群控制器, 并在节点加入集群或退出集群时通知控制器。控制器负责在节点加入或离开集群时进行分区Leader选举。 控制器使用epoch 来避免“脑裂”。“脑裂”是指两个节点同时认为自己是当前的控制器。
一致性保证
LEO 和HW
LEO:即日志末端位移(log end offset),记录了该副本日志中下一条消息的位移值。
HW:即上面提到的水位值。对于同一个副本对象而言,其HW值不会大于LEO值。小于等于HW值的所有消息都被认为是“已备份”的(replicated)。Leader副本和Follower副本的HW更新不同。
-
Follower副本何时更新LEO
Kafka有两套Follower副本LEO:
一套LEO保存在Follower副本所在Broker的副本管理机中;
另一套LEO保存在Leader副本所在Broker的副本管理机中。Leader副本机器上保存了所有的follower副本的LEO
Kafka使用前者帮助Follower副本更新其HW值;利用后者帮助Leader副本更新其HW。
Follower副本的本地LEO何时更新? Follower副本的LEO值就是日志的LEO值,每当新写入一条消息,LEO值就会被更新。当Follower发送FETCH请求后,Leader将数据返回给Follower,此时Follower开始Log写数据,从而自动更新LEO值。
Leader端Follower的LEO何时更新? Leader端的Follower的LEO更新发生在Leader在处理Follower FETCH请求时。一旦Leader接收到Follower发送的FETCH请求,它先从Log中读取相应的数据,给Follower返回数据前,先更新Follower的LEO。
-
Follower副本何时更新HW?
Follower更新HW发生在其更新LEO之后,一旦Follower向Log写完数据,尝试更新自己的HW值.比较当前LEO值与FETCH响应中Leader的HW值,取两者的小者作为新的HW值。
-
Leader副本何时更新LEO?
Leader写Log时自动更新自己的LEO值。
-
Leader副本何时更新HW值
-
Follower副本成为Leader副本时:Kafka会尝试去更新分区HW。
-
Broker崩溃导致副本被踢出ISR时:检查下分区HW值是否需要更新是有必要的
-
生产者向Leader副本写消息时:因为写入消息会更新Leader的LEO,有必要检查HW值是否需要更新
-
Leader处理Follower FETCH请求时:首先从Log读取数据,之后尝试更新分区HW值
-
消息重复
-
生产者阶段
没有收到正确的broker响应, 进行了重试
解决:
- 启用幂等性 ack=all retries>1
-
broker阶段
- 禁用unclearn选举
- min.insync.replicas>1
-
消费者阶段
- 取消自动提交
- 下游做幂等, 记录offset
_consumer_offsets
kafka-run-class.sh kafka.tools.GetOffsetShell --broker-list node1:9092 --topic tp_test_01 --time -1
kafka-console-consumer.sh --bootstrap-server node1:9092 -- topic tp_test_01 --from-beginning
kafka-consumer-groups.sh --bootstrap-server node1:9092 --list
# 查询_consumer_offsets的内容, 先要在consumer.properties中设置exclude.internal.topics=false
kafka-console-consumer.sh --topic __consumer_offsets -- bootstrap-server node1:9092 --formatter "kafka.coordinator.group.GroupMetadataManager\$OffsetsMessageFormatter" -- consumer.config config/consumer.properties --from-beginning
kafka-simple-consumer-shell.sh --topic __consumer_offsets -- partition 19 --broker-list node1:9092 --formatter "kafka.coordinator.group.GroupMetadataManager\$OffsetsMessageFormatter"
延时队列
TimingWheel是kafka时间轮的实现,内部包含了一个TimerTaskList数组,每个数组包含了一些链表组成的TimerTaskEntry事件,每个TimerTaskList表示时间轮的某一格,这一格的时间跨度为tickMs,同一个TimerTaskList中的事件都是相差在一个tickMs跨度内的,整个时间轮的时间跨度为interval = tickMs * wheelSize,该时间轮能处理的时间范围在cuurentTime到currentTime + interval之间的事件。
当添加一个时间他的超时时间大于整个时间轮的跨度时, expiration >= currentTime + interval,则会将该事件向上级传递,上级的tickMs是下级的interval,传递直到某一个时间轮满足expiration <currentTime + interval,然后计算对应位于哪一格,然后将事件放进去,重新设置超时时间,然后放进jdk延迟队列
SystemTimer会取出queue中的TimerTaskList,根据expiration将currentTime往前推进,然后把里面所有的事件重新放进时间轮中,因为ct推进了,所以有些事件会在第0格,表示到期了,直接返回。然后将任务提交到java线程池中处理。
服务端在处理客户端的请求,针对不同的请求,可能不会立即返回响应结果给客户端。在处理这类请求时,服务端会为这类请求创建延迟操作对象放入延迟缓存队列中。延迟缓存的数据结构类似MAP,延迟操作对象从延迟缓存队列中完成并移除有两种方式:1,延迟操作对应的外部事件发生时,外部事件会尝试完成延迟缓存中的延迟操作 。2,如果外部事件仍然没有完成延迟操作,超时时间达到后,会强制完成延迟的操作 。