在Nodejs中使用kafka(三)offset偏移量控制策略,数据保存策略

offset偏移量控制策略

Kafka 中的 offset(偏移量) 是指消费者在 Kafka 分区内读取消息的位置,表示消费者已经消费到哪个消息。在 Kafka 中,offset 控制策略 对于确保消息的正确消费、处理顺序、容错能力和高可用性至关重要。Kafka 提供了多种方式来管理和控制消费者的 offset,以下是 Kafka 中常见的 offset 控制策略

1. 自动提交(Auto Commit)

自动提交 是 Kafka 消费者最简单的 offset 管理方式。在这种模式下,消费者会定期将当前消费的 offset 自动提交到 Kafka 集群。在nodejs环境使用kafkajs时,自动提交无需做任何配置。

优点:

  • 简单易用,适用于一些不要求严格精确一次消费的场景。

缺点

  • 可能丢失消息:如果消费者在自动提交偏移量之前崩溃,可能会重复消费一些消息。
  • 无法精确控制:偏移量提交的时机和粒度不容易控制,可能不适合一些需要精确消费顺序的场景。

2. 手动提交(Manual Commit)

手动提交偏移量允许消费者精确控制何时提交偏移量。消费者可以在消息成功消费后显式地提交偏移量。在nodejs环境使用kafkajs实现方式。

javascript 复制代码
await consumer.run({
  // 不进行自动提交
  autoCommit: false, 
  partitionsConsumedConcurrently: 0,
  eachMessage: async ({ topic, partition, message }) => {
    const [id, name, age, sex, desc] = message.value?.toString()?.split?.('@_@') || [];

    console.log(partition, { id, name, age, sex, desc });

    // 手动提交控制偏移量
    await consumer.commitOffsets([{ topic, partition, offset: String(count++) }]);
  },
});

优点

  • 精确控制:消费者可以在确认消息已正确处理后再提交偏移量,确保消息不被丢失或重复消费。
  • 避免不必要的重复消费:只有当消费者完成整个消费处理(例如事务处理或确认某些条件成立)后,才会提交偏移量。

缺点

  • 需要额外的开发工作:消费者需要显式地处理提交偏移量的逻辑,为了防读取的数据偏移量offset丢失也可将每个topic中读取到的partition分区上的数据偏移量offset保存在数据库中,手动提交控制offset增加了开发复杂性。

3.Offset 重置(Seek)

Offset 重置允许手动调整消费者在分区中的消费位置,常用于数据回溯或修复异常。直接指定目标 Offset,从该位置开始消费。

javascript 复制代码
// 跳转到分区 0 的 Offset 100
await consumer.seek({ 
  topic: 'topic1', 
  partition: 0, 
  offset: '100'
});

示例

如果要增加消费者组中partition分区数量需要使用命令扩展,执行后分区变为4个,0,1,2,3。

javascript 复制代码
kafka-topics.sh --alter --topic topic1 --partitions 4 --bootstrap-server localhost:9092

生产者向topic1发送消息,存储在partition分区0,发给消费者组group1和group2。(topic1创建好后默认只有一个分区)。

producer.ts

javascript 复制代码
import { CompressionTypes, Kafka } from 'kafkajs';


async function run() {
  const kafka = new Kafka({
    clientId: 'test1',
    brokers: ['localhost:9092'],
    connectionTimeout: 1000, // 1 秒连接超时
  });

  const producer = kafka.producer();

  await producer.connect();

  for (let i = 60; i <= 70; ++i) {
    let name = Math.random().toString().slice(2, -1);
    let age = Math.ceil(Math.random() * 40);
    let sex = Math.random() * 100 > 50 ? 1 : 0;
    let person = {
      id: i,
      name,
      age,
      sex,
      desc: `${i},我是${name},年龄${age},性别${sex}`,
    };

    await producer.send({
      topic: 'topic1',
      messages: [
        {
          // value: Buffer.from(`${person.id}@_@${person.name}@_@${person.age}@_@${person.sex}@_@${person.desc}`) 
          // 默认会转成buffer
          value: `${person.id}@_@${person.name}@_@${person.age}@_@${person.sex}@_@${person.desc}`,
          // key:'key1',
          // partition: 0,
        },
      ],
      // None = 0, 不处理
      // GZIP = 1, 压缩率较高,但可能会带来一些性能开销
      // Snappy = 2, 压缩和解压速度很快,压缩率不如gzip
      // LZ4 = 3, 压缩和解压速度很快,压缩率不如gzip
      // ZSTD = 4, 压缩比和压缩速度之间提供了较好的平衡
      compression: CompressionTypes.GZIP,
    });

    console.log(`${person.id}@_@${person.name}@_@${person.age}@_@${person.sex}@_@${person.desc}`);
  }

  await producer.disconnect();

  producer.on('producer.connect', (evt) => {

  });

  producer.on('producer.disconnect', (evt) => {

  });

  producer.on('producer.network.request', (evt) => {

  });

  producer.on('producer.network.request_queue_size', () => {

  });

  producer.on('producer.network.request_timeout', (evt) => {

  });
}

run();

consumer.ts

javascript 复制代码
import { Kafka } from 'kafkajs'


const kafka = new Kafka({
  clientId: 'test1',
  brokers: ['localhost:9092'],
  connectionTimeout: 100, // 0.1 秒连接超时
  requestTimeout: 1000,
});

const consumer = kafka.consumer({
  groupId: 'group1',
  rackId: 'test1.group1.consumer1',
  maxBytes: 3 * 1024 * 1024,  // 单次 poll 请求从每个分区拉取的最大数据量(默认1MB,单位字节Bytes)
  sessionTimeout: 60000,      // 消费者心跳超时时间(默认 30s)
  rebalanceTimeout: 60000,   // rebalance 最大等待时间(默认 60s)
  heartbeatInterval: 6000,    // 心跳间隔(默认 3s)
  maxWaitTimeInMs: 500,       // 每次拉取消息等待的时间(默认0.5s)
});

await consumer.connect();

await consumer.subscribe({
  topic: 'topic1',
  fromBeginning: true, // 从头开始消费
});

let count = BigInt(0);
await consumer.run({
  autoCommit: false,  // 禁用自动提交
  partitionsConsumedConcurrently: 10, // 控制每次最多消费的分区数
  eachMessage: async ({ topic, partition, message }) => {
    const [id, name, age, sex, desc] = message.value?.toString()?.split?.('@_@') || [];
    console.log(partition, { id, name, age, sex, desc });
    await consumer.commitOffsets([{ topic, partition, offset: String(count++) }]);
  },
});

consumer.on('consumer.network.request', (evt) => {

});

consumer.on('consumer.connect', (evt) => {

});

consumer.on('consumer.disconnect', (evt) => {

});

// consumer.seek({ topic: 'topic1', partition: 0, offset: '0' });

consumer2.ts

javascript 复制代码
import { Kafka } from 'kafkajs'


const kafka = new Kafka({
  clientId: 'test2',
  brokers: ['localhost:9092'],
  connectionTimeout: 100, // 0.1 秒连接超时
  requestTimeout: 1000,
});

const consumer = kafka.consumer({
  groupId: 'group2',
  rackId: 'test2.group2.consumer2',
  maxBytes: 3 * 1024 * 1024,  // 单次 poll 请求从每个分区拉取的最大数据量(默认1MB,单位字节Bytes)
  sessionTimeout: 60000,      // 消费者心跳超时时间(默认 30s)
  rebalanceTimeout: 60000,    // rebalance 最大等待时间(默认 60s)
  heartbeatInterval: 6000,    // 心跳间隔(默认 3s)
  maxWaitTimeInMs: 500,       // 每次拉取消息等待的时间(默认0.5s)
});

await consumer.connect();

await consumer.subscribe({
  topic: 'topic1',
  fromBeginning: true, // 从头开始消费
});

let count = BigInt(0);
await consumer.run({
  autoCommit: false,  // 禁用自动提交
  partitionsConsumedConcurrently: 10, // 控制每次最多消费的分区数
  eachMessage: async ({ topic, partition, message }) => {
    const [id, name, age, sex, desc] = message.value?.toString()?.split?.('@_@') || [];
    console.log(partition, { id, name, age, sex, desc });
    await consumer.commitOffsets([{ topic, partition, offset: String(count++) }]);
  },
});

consumer.on('consumer.network.request', (evt) => {

});

consumer.on('consumer.connect', (evt) => {

});

consumer.on('consumer.disconnect', (evt) => {

});

// consumer.seek({ topic: 'topic1', partition: 0, offset: '2' });

数据保存策略

队列数据(即生产者发送的消息)直接存储在日志文件中。Kafka 使用日志结构来持久化消息,并且这些日志文件是 Kafka 存储机制的核心部分。当数据超过存储时间或者指定大小时会删除数据,如果存储空间不足Kafka 会加速对旧段文件的清理过程,确保有足够的空间用于新消息的存储。

1. 基于时间的保留策略

Kafka 支持基于时间的消息保留策略,默认情况下,Kafka 会根据配置的时间阈值来删除旧消息。

**log.retention.ms:**指定消息在日志中保留的最大时间(毫秒)。例如,设置为 7 * 24 * 60 * 60 * 1000 表示保留 7 天的消息。(默认 log.retention.hours=168,即保留 7 天的消息。)

**log.retention.minutes 和 log.retention.hours:**与 log.retention.ms 类似,但单位分别为分钟和小时。优先级高于 log.retention.ms

2. 基于大小的保留策略

除了基于时间的保留策略外,Kafka 还支持基于日志文件大小的保留策略。

log.retention.bytes: 指定每个分区可以占用的最大字节数。当分区的日志文件达到这个大小时,Kafka 会开始删除最旧的消息,直到总大小不超过该限制。(默认:-1,不限制日志文件大小)

**注意:**这个配置是针对每个分区的,而不是整个主题。因此,如果你有一个主题有多个分区,总的存储空间会乘以分区数。

3. 基于段文件的保留策略

Kafka 将日志划分为多个段文件(segments),每个段文件包含一定数量的消息。Kafka 可以根据段文件的大小或时间来决定是否删除旧的段文件。

**log.segment.bytes:**指定每个日志段文件的最大大小(字节)。当一个段文件达到这个大小时,Kafka 会创建一个新的段文件,并将旧的段文件标记为可删除(如果符合保留策略)。(默认1G)

**log.roll.ms 或 log.roll.hours:**指定段文件的最大存活时间(毫秒或小时)。即使段文件未达到最大大小,也会在达到指定时间后滚动到新的段文件。 (默认:604800000或168,7天)

4. 清理策略

Kafka 还提供了两种主要的日志清理策略:

**delete:**这是默认的清理策略,按照上述保留策略删除旧消息。 (默认)

**compact:**用于保留最新的键值对,适用于需要去重的场景。在这种模式下,Kafka 会定期压缩日志,删除重复的键值对,只保留每个键的最新值。

示例

数据保存策略在server.properties配置

bash 复制代码
# 基于时间的保留策略
log.retention.hours=168  # 保留 7 天

# 基于大小的保留策略
log.retention.bytes=1073741824  # 1GB

# 日志段文件配置
log.segment.bytes=1073741824  # 1GB


# 每5分钟检查一次
log.retention.check.interval.ms=300000


log.roll.hours=24  # 每 24 小时滚动一次
相关推荐
xiao-xiang8 分钟前
kafka-集群缩容
分布式·kafka
比花花解语11 分钟前
Kafka在Windows系统使用delete命令删除Topic时出现的问题
windows·分布式·kafka
解决方案工程师13 分钟前
【Kafka】Kafka高性能解读
分布式·kafka
yellowatumn16 分钟前
RocketMq\Kafka如何保障消息不丢失?
分布式·kafka·rocketmq
python资深爱好者36 分钟前
什么容错性以及Spark Streaming如何保证容错性
大数据·分布式·spark
坚定信念,勇往无前1 小时前
springboot单机支持1w并发,需要做哪些优化
java·spring boot·后端
老友@1 小时前
OnlyOffice:前端编辑器与后端API实现高效办公
前端·后端·websocket·编辑器·onlyoffice
HeartRaindj2 小时前
【中间件开发】kafka使用场景与设计原理
分布式·中间件·kafka
祈澈菇凉2 小时前
如何优化 Webpack 的构建速度?
前端·webpack·node.js
风月歌2 小时前
基于springboot校园健康系统的设计与实现(源码+文档)
java·spring boot·后端·mysql·毕业设计·mybatis·源码