Kafka 消息队列工程化完全指南:从一次"acks=1 丢 3000 单订单损失 50 万"看懂为什么 send/poll 远远不够

2021 年我加入一家直播带货公司接手订单系统当时下单链路是用户下单写 MySQL 同步调库存调营销调通知调推送 5 个下游服务每个 100ms 端到端 500ms 直播间瞬时下单峰值 8000 QPS 链路雪崩我决定引入 Kafka 把下游全异步化觉得 Kafka 高吞吐低延迟上 3 broker 8 partition 应该够用改造后压测确实漂亮 8000 QPS 端到端 50ms 老板很满意上线结果上线第二周我们陆续踩了一堆坑第一种最让我傻眼 producer 默认 acks=1 leader 写完就返回网络抖动一次 leader 挂了还没复制到 follower 直接丢消息那天我们丢了 3000 单订单损失 50 万第二种最难缠 consumer 默认 auto commit offset 5 秒一个 batch 处理到一半 consumer OOM 重启 offset 已经 commit 那一批消息全丢重复处理与丢失并存第三种最离谱我们一个 topic 8 partition 一开始日吞吐 100 万消息一切都好半年后涨到日吞吐 5000 万单 partition 580 msg/s 但单 consumer 消费速度只有 200 msg/s lag 每天涨 30 万一周后 lag 500 万消费完要 7 天第四种最致命我们的消息 key 是用户 ID hash 到 partition 一个明星主播开播 100 万粉丝同时下单全部 hash 到 1 个 partition 其他 7 个 partition 空闲单 partition consumer 拼命跑也来不及第五种最莫名其妙我们 producer 用 compression=none 觉得 CPU 省点结果网络带宽爆跨机房传输延迟暴涨改成 snappy 压缩网络流量降 60% 延迟回归我盯着这一连串问题想了很久才彻底想明白第一版错在一个根本的认知上我以为 Kafka 就是生产者发消费者收高吞吐上 broker 就稳可这个认知是错的真正能扛业务的 Kafka 是一个 producer 配置 acks idempotent 加 consumer 配置 offset 管理 加 partition 设计与 key 选择 加顺序性与幂等性加监控 lag 与扩容加多集群与跨机房的整套工程方法论

2021 年我加入一家直播带货公司 接手订单系统 当时下单链路是 用户下单 -> 写 MySQL -> 同步调库存 调营销 调通知 调推送 5 个下游服务 每个 100ms 端到端 500ms 直播间瞬时下单峰值 8000 QPS 链路雪崩。我决定引入 Kafka 把下游全异步化 觉得 Kafka 高吞吐低延迟 上 3 broker 8 partition 应该够用。改造后压测确实漂亮 8000 QPS 端到端 50ms 老板很满意 上线。结果上线第二周 我们陆续踩了一堆坑。第一种最让我傻眼 producer 默认 acks=1 leader 写完就返回 网络抖动一次 leader 挂了还没复制到 follower 直接丢消息 那天我们丢了 3000 单订单 损失 50 万。第二种最难缠 consumer 默认 auto commit offset 5 秒 一个 batch 处理到一半 consumer OOM 重启 offset 已经 commit 那一批消息全丢 重复处理与丢失并存。第三种最离谱 我们一个 topic 8 partition 一开始日吞吐 100 万消息一切都好 半年后涨到日吞吐 5000 万 单 partition 5000 万除以 86400 等于 580 msg/s 但单 consumer 消费速度只有 200 msg/s lag 每天涨 30 万 一周后 lag 500 万消费完要 7 天。第四种最致命 我们的消息 key 是用户 ID hash 到 partition 一个明星主播开播 100 万粉丝同时下单 全部 hash 到 1 个 partition 其他 7 个 partition 空闲 单 partition consumer 拼命跑也来不及。第五种最莫名其妙 我们 producer 用 compression=none 觉得 CPU 省点 结果网络带宽爆 跨机房传输延迟暴涨 改成 snappy 压缩 网络流量降 60% 延迟回归。我盯着这一连串问题想了很久才彻底想明白第一版错在一个根本的认知上我以为 Kafka 就是 生产者发 消费者收 高吞吐 上 broker 就稳 可这个认知是错的真正能扛业务的 Kafka 是一个 producer 配置 acks idempotent 加 consumer 配置 offset 管理 加 partition 设计与 key 选择 加 顺序性与幂等性 加 监控 lag 与扩容 加 多集群与跨机房 的整套工程方法论 任何一环没做都可能让你的消息丢失 重复 lag 爆炸或者热点 partition本文从头梳理 Kafka 工程化的要点 producer acks 怎么定 consumer 怎么保证不丢不重 partition 怎么设计 key 怎么选 lag 怎么监控 以及一些把 Kafka 做扎实要避开的工程坑

问题背景:为什么 Kafka 不是 send/poll 就完事

很多人对 Kafka 的认知是 producer.send consumer.poll 高吞吐就稳 但生产里你会发现 消息丢失 重复消费 lag 爆炸 热点 partition 性能差 跨机房带宽爆。问题的根源在于:

  • acks=1 默认会丢消息:leader 写完未同步给 follower 就返回 leader 挂瞬间消息全丢 金融级必须 acks=all。
  • auto commit offset 会丢消息或重复:5 秒一次 batch 未处理完 commit 丢 batch 处理完未 commit 重复。
  • partition 数量难调整:topic 一旦创建 partition 数只能增不能减 增完 key 路由变化打破顺序性。
  • key 选错导致热点:用户 ID 做 key 明星账号 hash 到一个 partition 单 partition consumer 顶不住。
  • 顺序性与并发是矛盾:单 partition 保证顺序 多 partition 提高吞吐 需要 key-level 顺序的业务必须想清楚 trade-off。
  • lag 监控是消费健康的关键:lag 持续上涨意味着消费跟不上 必须扩 consumer 或者 partition。

一 Producer 配置:不丢不重的硬规范

Producer 的 acks idempotent retries 配置直接决定消息可靠性。默认配置 acks=1 不开 idempotent 高并发下既会丢又会重复 上线就是灾难。生产级 producer 必须开 acks=all enable.idempotence=true retries=Integer.MAX_VALUE。

// Java producer 生产配置
Properties props = new Properties();
props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "kafka1:9092,kafka2:9092,kafka3:9092");
props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());

// 1 acks=all 必开 leader 加 ISR 所有 follower 都 ack 才返回 不丢消息
props.put(ProducerConfig.ACKS_CONFIG, "all");

// 2 enable.idempotence=true 必开 producer 重试不会重复发
// 这一个开关同时 enable acks=all retries=MAX max.in.flight=5
props.put(ProducerConfig.ENABLE_IDEMPOTENCE_CONFIG, true);

// 3 retries 重试次数 必须很大 让网络抖动自动恢复
props.put(ProducerConfig.RETRIES_CONFIG, Integer.MAX_VALUE);
props.put(ProducerConfig.RETRY_BACKOFF_MS_CONFIG, 100);

// 4 max.in.flight.requests.per.connection 5 是 idempotent 上限
// 必须 <= 5 否则破坏顺序保证
props.put(ProducerConfig.MAX_IN_FLIGHT_REQUESTS_PER_CONNECTION, 5);

// 5 压缩 snappy 或 lz4 网络流量降 60%
props.put(ProducerConfig.COMPRESSION_TYPE_CONFIG, "snappy");

// 6 batch.size 与 linger.ms 提高吞吐 通过批量发送
props.put(ProducerConfig.BATCH_SIZE_CONFIG, 32768);   // 32KB
props.put(ProducerConfig.LINGER_MS_CONFIG, 10);       // 10ms 凑批

// 7 buffer.memory producer 内存缓冲区
props.put(ProducerConfig.BUFFER_MEMORY_CONFIG, 64 * 1024 * 1024);  // 64MB

// 8 max.block.ms producer.send 阻塞超时
// 默认 60s 太长 设 5s 让 backpressure 早暴露
props.put(ProducerConfig.MAX_BLOCK_MS_CONFIG, 5000);

// 9 delivery.timeout.ms send 到最终确认总超时
// 必须 >= linger.ms + request.timeout.ms 默认 120s 够用
props.put(ProducerConfig.DELIVERY_TIMEOUT_MS_CONFIG, 120_000);

KafkaProducer<String, String> producer = new KafkaProducer<>(props);

producer 配置好只是基础 真正的工程难点是同步发还是异步发 业务对延迟与可靠性的权衡。同步发可靠但慢 异步发快但要处理回调失败 下面是生产实战的封装 包含回调 重试 监控指标。

// 异步发送的完整封装
public class ReliableProducer {
    private final KafkaProducer<String, String> producer;
    private final Counter sentCounter;
    private final Counter failedCounter;
    private final Timer sendTimer;

    public CompletableFuture<RecordMetadata> sendAsync(
            String topic, String key, String value) {
        CompletableFuture<RecordMetadata> future = new CompletableFuture<>();
        long start = System.currentTimeMillis();
        ProducerRecord<String, String> record = new ProducerRecord<>(topic, key, value);
        producer.send(record, (metadata, exception) -> {
            long elapsed = System.currentTimeMillis() - start;
            sendTimer.record(elapsed, TimeUnit.MILLISECONDS);
            if (exception != null) {
                failedCounter.increment();
                log.error("send failed topic={} key={} err={}", topic, key,
                          exception.getMessage());
                future.completeExceptionally(exception);
                // 业务级兜底 写本地文件后续 retry
                writeFailedLog(topic, key, value, exception);
            } else {
                sentCounter.increment();
                future.complete(metadata);
            }
        });
        return future;
    }

    // 关键业务用同步发 等返回再继续
    public RecordMetadata sendSync(String topic, String key, String value)
            throws Exception {
        return producer.send(new ProducerRecord<>(topic, key, value)).get();
    }

    // 兜底 producer 完全失败时写本地文件
    private void writeFailedLog(String topic, String key, String value,
                                  Exception ex) {
        // 写 /var/log/kafka-failed/topic.log 后续有 retry job 扫描重发
    }
}

Producer 配置的工程经验 acks=all enable.idempotence=true 这两个开关是不丢不重的底线 retries 设 MAX max.in.flight=5 不要更大 batch.size 32KB linger.ms 10ms 平衡吞吐与延迟 compression snappy 网络流量降 60% 这套配置照抄就行不要瞎调。失败兜底必须有 写本地文件后续 retry job 扫描重发 producer 完全失败时不能让业务直接报错 我们公司订单系统这样设计 上线 3 年没丢过订单消息。

二 Consumer 配置:offset 管理是命脉

Consumer 的 offset 管理决定消息不丢不重 default auto commit 5 秒一次 在业务处理与 offset commit 之间的窗口期是丢失或重复的根源。生产级 consumer 必须手动 commit 处理完再 commit 不处理完不 commit。

// Consumer 生产配置
Properties props = new Properties();
props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "kafka1:9092,kafka2:9092,kafka3:9092");
props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());
props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());
props.put(ConsumerConfig.GROUP_ID_CONFIG, "order-consumer-group");

// 1 关闭 auto commit 必须手动 commit
props.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, false);

// 2 isolation.level 读隔离级别 read_committed 只读已 commit 的事务消息
props.put(ConsumerConfig.ISOLATION_LEVEL_CONFIG, "read_committed");

// 3 max.poll.records 一次 poll 拉多少 默认 500 视处理速度调
// 处理慢的减到 100 处理快的可以 1000
props.put(ConsumerConfig.MAX_POLL_RECORDS_CONFIG, 100);

// 4 max.poll.interval.ms 两次 poll 之间最大间隔 默认 5 分钟
// 业务处理慢必须调大 否则 consumer 被认为死 rebalance
props.put(ConsumerConfig.MAX_POLL_INTERVAL_MS_CONFIG, 600_000);  // 10 分钟

// 5 session.timeout.ms 心跳超时 consumer 被踢出的时间
// heartbeat.interval.ms 心跳间隔 必须 < session.timeout / 3
props.put(ConsumerConfig.SESSION_TIMEOUT_MS_CONFIG, 30_000);
props.put(ConsumerConfig.HEARTBEAT_INTERVAL_MS_CONFIG, 10_000);

// 6 fetch.min.bytes fetch.max.wait.ms 批量拉
// 拉 1MB 或等 100ms 平衡延迟与吞吐
props.put(ConsumerConfig.FETCH_MIN_BYTES_CONFIG, 1024 * 1024);
props.put(ConsumerConfig.FETCH_MAX_WAIT_MS_CONFIG, 100);

// 7 auto.offset.reset 找不到 offset 时从哪开始
// earliest 从头 适合新业务 latest 从最新 适合实时
props.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest");

KafkaConsumer<String, String> consumer = new KafkaConsumer<>(props);
consumer.subscribe(Collections.singletonList("orders"));

consumer 配置好只是基础 真正的不丢不重要靠 处理流程 + 手动 commit 配合。下面是生产实战的消费循环 关键是 处理失败不 commit 进入 retry 队列 处理成功才 commit 业务处理必须幂等。

// 不丢不重的消费循环
while (running) {
    ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(1000));
    if (records.isEmpty()) continue;

    Map<TopicPartition, OffsetAndMetadata> toCommit = new HashMap<>();
    boolean batchSuccess = true;

    for (ConsumerRecord<String, String> record : records) {
        try {
            // 1 业务处理 必须幂等 用 idempotency_key 防重复
            String idempotencyKey = parseKey(record.value());
            if (idempotencyStore.contains(idempotencyKey)) {
                // 已处理过 skip
                continue;
            }
            processOrder(record.value());
            idempotencyStore.add(idempotencyKey);

            // 2 标记 offset 待 commit 不是立刻 commit
            TopicPartition tp = new TopicPartition(record.topic(), record.partition());
            toCommit.put(tp, new OffsetAndMetadata(record.offset() + 1));
        } catch (BusinessException e) {
            // 业务异常 不 commit 重发到 DLQ 死信队列
            sendToDLQ(record, e);
            // continue 处理下一条 不要让一条坏消息卡住整个 batch
        } catch (Exception e) {
            // 系统异常 终止 batch 不 commit 等下次 poll 重试
            log.error("system error stop batch", e);
            batchSuccess = false;
            break;
        }
    }

    // 3 batch 全部成功才同步 commit
    if (batchSuccess && !toCommit.isEmpty()) {
        try {
            consumer.commitSync(toCommit);
        } catch (CommitFailedException e) {
            // commit 失败 下次 poll 会重复消费 业务必须幂等
            log.error("commit failed will reprocess", e);
        }
    }
}

Consumer 配置的工程经验 关闭 auto commit 手动 commitSync 业务处理必须幂等 用 idempotency_key 或 Redis SETNX 处理失败的消息进 DLQ 不要卡住整 batch max.poll.interval.ms 设 10 分钟防 rebalance 这套配置能保证不丢不重 业务处理可重复执行。我们公司订单 consumer 这样设计 每天千万级消息 3 年没出过丢消息事故。

三 Partition 设计与 key 选择

Partition 是 Kafka 性能与顺序性的基础 数量 key 选择直接决定吞吐天花板与是否会有热点。partition 一旦创建只能增不能减 增加后 key 路由会变 打破原有顺序性 必须一开始规划好。

# 1 partition 数量规划 估算公式
# 单 partition 写入吞吐 上限约 10MB/s 5000 msg/s
# 单 partition 消费吞吐 等于 consumer 单实例吞吐
# 目标吞吐 / 单 partition 吞吐 = 需要的 partition 数

# 业务案例 日订单 5000 万 峰值 8000 QPS 平均消息 1KB
# 8000 / 5000 = 1.6 至少 2 partition
# 但为了未来扩展与负载均衡 一开始就上 16-32 partition
# 不要从 1 开始 增加 partition 后 key 路由变化 顺序断

kafka-topics.sh --bootstrap-server kafka1:9092 \
    --create --topic orders \
    --partitions 32 --replication-factor 3 \
    --config retention.ms=259200000 \
    --config min.insync.replicas=2 \
    --config compression.type=snappy

# 2 查看 partition 分布
kafka-topics.sh --bootstrap-server kafka1:9092 --describe --topic orders

# 3 增加 partition 注意 顺序保证会被破坏
kafka-topics.sh --bootstrap-server kafka1:9092 \
    --alter --topic orders --partitions 64
# 增加后 hash(key) % partitions 路由变化 同 key 不再到同 partition

# 4 partition 重分配 rebalance
kafka-reassign-partitions.sh --bootstrap-server kafka1:9092 \
    --topics-to-move-json-file topics.json \
    --broker-list "1,2,3" --generate

# 5 单 topic partition 上限 经验值 每 broker 100-200 partition
# 3 broker 集群 单 topic 不要超过 500 partition
# 过多 partition open file 暴涨 fetch 性能下降

partition 数量定好 key 选择更关键 选错就是热点。下面是几种 key 设计策略与对应的适用场景 用户 ID 是最常见的 key 但明星账号会热点 订单 ID 完全随机但失去用户内顺序 业务 ID 兼顾。

// Key 设计策略对比

// 1 用 user_id 作 key 同用户消息保持顺序
// 缺点 明星账号热点 单 partition 顶不住
producer.send(new ProducerRecord<>("orders", String.valueOf(userId), payload));

// 2 用 order_id 作 key 完全随机分布 没有热点
// 缺点 同用户的多个事件可能乱序
producer.send(new ProducerRecord<>("orders", String.valueOf(orderId), payload));

// 3 用 user_id + timestamp_bucket 作 key 同用户同小时一个 partition
// 平衡顺序与负载
String key = userId + "_" + (System.currentTimeMillis() / 3_600_000);
producer.send(new ProducerRecord<>("orders", key, payload));

// 4 不设 key 用 sticky partitioner 默认策略 同一 batch 内消息到同 partition
// 适合不需要顺序的场景
producer.send(new ProducerRecord<>("orders", null, payload));

// 5 自定义 partitioner 处理热点
public class HotKeyPartitioner implements Partitioner {
    private static final Set<String> HOT_USERS = Set.of("vip_1", "vip_2");

    @Override
    public int partition(String topic, Object key, byte[] keyBytes,
                          Object value, byte[] valueBytes, Cluster cluster) {
        int numPartitions = cluster.partitionCountForTopic(topic);
        String keyStr = (String) key;
        if (HOT_USERS.contains(keyStr)) {
            // 热点用户随机分散 牺牲顺序换均衡
            return ThreadLocalRandom.current().nextInt(numPartitions);
        }
        // 普通用户按 key hash
        return Math.abs(keyStr.hashCode()) % numPartitions;
    }
}

Partition 与 key 的工程经验 一开始就上 16-32 partition 不要从 1 开始 retention 3 天 replication 3 min.insync.replicas 2 普通业务用 user_id 作 key 热点业务用自定义 partitioner 散开热点 牺牲顺序换均衡 这套设计能扛日 5000 万消息的电商场景。我们公司订单 topic 32 partition 加自定义 partitioner 处理明星账号 单 partition 流量始终在 5MB/s 内 没出过热点。

四 顺序性与幂等性

Kafka 的顺序性是 partition 内顺序 跨 partition 不保证。需要全局顺序的业务必须单 partition 单 consumer 牺牲吞吐换顺序。幂等性是消息系统必备 producer 重试 consumer 重试都可能重复消费 业务必须设计成可重复执行。

// 1 业务幂等的几种实现

// a 数据库唯一约束 最简单可靠
@Transactional
public void processOrder(OrderEvent event) {
    try {
        orderRepository.insert(event.toOrder());  // unique key idempotency_key
    } catch (DuplicateKeyException e) {
        log.info("duplicate order event ignored key={}", event.getKey());
        return;  // 已处理过 skip
    }
    // 后续逻辑
}

// b Redis SETNX 短期幂等 适合短窗口
public boolean processWithIdempotency(String key, Runnable action) {
    Boolean acquired = redis.setIfAbsent("idem:" + key, "1", 1, TimeUnit.HOURS);
    if (!Boolean.TRUE.equals(acquired)) {
        return false;  // 已处理
    }
    try {
        action.run();
        return true;
    } catch (Exception e) {
        redis.delete("idem:" + key);  // 失败时释放锁让重试
        throw e;
    }
}

// c 状态机幂等 适合多状态业务
public void processPayment(PaymentEvent event) {
    Order order = orderRepository.findById(event.getOrderId());
    if (order.getStatus().compareTo(OrderStatus.PAID) >= 0) {
        log.info("order already paid id={}", event.getOrderId());
        return;  // 状态已超过 PAID 跳过
    }
    order.markPaid();
    orderRepository.save(order);
}

// 2 严格全局顺序的场景设计
// 单 partition 单 consumer 串行处理
producer.send(new ProducerRecord<>("audit-log", 0, key, value));  // 强制 partition=0
// consumer 单实例消费 partition 0

// 3 局部顺序 同 entity 顺序 不同 entity 并行
producer.send(new ProducerRecord<>("orders", String.valueOf(orderId), payload));
// hash(orderId) 路由 同 orderId 到同 partition 内顺序
// 不同 orderId 可能并行处理

顺序性与幂等性的工程经验 全局顺序必须单 partition 单 consumer 吞吐受限 局部顺序用 key 路由 业务幂等首选数据库唯一约束 短窗口幂等用 Redis SETNX 状态机业务用状态比较 不要试图通过 Kafka 配置实现幂等 Kafka 的 exactly-once 只在生产端 消费端幂等是业务责任。我们公司订单状态机加唯一约束 重复消费 1000 万次也不会重复处理订单。

[mermaid]flowchart TD
A[业务事件] --> B[Producer]
B -->|acks all idempotent| C[Kafka Broker]
C -->|partition 0| D[Consumer A]
C -->|partition 1| E[Consumer B]
C -->|partition N| F[Consumer N]
D --> G[幂等检查 idempotency_key]
E --> G
F --> G
G -->|已处理| H[skip]
G -->|未处理| I[业务处理]
I -->|成功| J[manual commit offset]
I -->|业务失败| K[发 DLQ]
I -->|系统失败| L[不 commit 下次重试]
K --> M[DLQ 监控告警]

五 监控 lag 与扩容

消费 lag 是 Kafka 健康的核心指标 lag 持续上涨意味着 consumer 跟不上 broker 速度 必须监控告警 自动或手动扩容。lag 监控要做到 partition 维度 不是总 lag 才能发现热点 partition。

# 1 命令行查看 consumer group lag
kafka-consumer-groups.sh --bootstrap-server kafka1:9092 \
    --describe --group order-consumer-group

# 输出
# TOPIC  PARTITION  CURRENT-OFFSET  LOG-END-OFFSET  LAG
# orders 0          1000000          1000050         50
# orders 1          1000000          5000000         4000000   ← 严重 lag
# orders 2          1000000          1000020         20

# 2 用 Burrow 做自动 lag 监控告警
# https://github.com/linkedin/Burrow
# burrow 会自动检测 lag 趋势 持续上涨或停滞触发告警

# 3 用 kafka-exporter 接 Prometheus
# https://github.com/danielqsj/kafka_exporter
# 暴露 kafka_consumergroup_lag 指标

# 4 PromQL 告警规则
# 单 partition lag > 100000 持续 5 分钟告警
# expr: kafka_consumergroup_lag{topic="orders"} > 100000
# for: 5m

# 5 lag 增长率告警 比绝对值更敏感
# expr: rate(kafka_consumergroup_lag{topic="orders"}[5m]) > 1000

# 6 手动重置 offset 应急
# 业务出 bug 处理错了一批消息 需要回滚
kafka-consumer-groups.sh --bootstrap-server kafka1:9092 \
    --group order-consumer-group \
    --topic orders --reset-offsets --to-datetime 2024-05-23T10:00:00.000 \
    --execute

# 7 跳过坏消息 应急
kafka-consumer-groups.sh --bootstrap-server kafka1:9092 \
    --group order-consumer-group \
    --topic orders:3 --reset-offsets --to-offset 1500000 \
    --execute

Lag 监控的工程经验 partition 维度监控 不要只看总 lag 单 partition lag 持续上涨可能是热点 增长率告警比绝对值告警敏感 Burrow 或 kafka-exporter + Prometheus 是事实标准 应急能力必须有 reset offset to-datetime to-offset 几种方式都要会 业务出 bug 时能快速回滚。我们公司订单 topic lag 告警阈值 单 partition 10 万 5 分钟告警 增长率 1000/s 告警 双重保护从未漏过 lag 问题。

六 Kafka 的工程坑:那些 demo 时学不到的

讲完原理来说几个真实生产里踩过的坑。第一个坑是 retention 不能设太短 默认 7 天 我们一次为了省磁盘改成 1 天 结果 consumer 因为 bug 停 2 天再启动 消息全过期丢了 业务数据全部断档 retention 必须 >= 业务最长重试窗口 + 安全 buffer 一般 3-7 天。第二个坑是 broker 磁盘必须监控 一旦 disk 用满 broker 直接 readonly 整个集群写入挂 必须 80% 告警 90% 紧急处理。第三个坑是 rebalance 是噩梦 consumer 加减一次全 group rebalance 处理可能停 30 秒 高频上下线的场景必须用 static membership group.instance.id 避免 rebalance。第四个坑是 网络分区 split brain 控制器选举混乱 必须配 min.insync.replicas=2 zookeeper 或 KRaft 必须奇数节点 防止脑裂。第五个坑是 跨机房高延迟 不要让 consumer 跨机房消费 必须按机房部署 consumer 跨机房用 MirrorMaker 或者 Cluster Linking 复制数据 我们一次跨机房消费导致 consumer lag 暴涨 改成同机房消费 lag 秒回 0

关键概念速查

概念 含义 工程价值
acks=all 所有 ISR ack 不丢消息
enable.idempotence producer 幂等 不重复发
min.insync.replicas 最小同步副本 防 split brain
手动 commit 关闭 auto commit 不丢不重
幂等业务 可重复执行 consumer 端必备
partition 数量 16-32 起步 避免后续无法缩减
key hash 路由 顺序保证 同 key 同 partition
自定义 partitioner 热点散开 解决明星账号
partition lag 每 partition 维度 发现热点 partition
static membership 固定 instance id 避免 rebalance

避坑清单

  1. Producer 必开 acks=all enable.idempotence=true retries=MAX max.in.flight<=5 不丢不重的底线。
  2. Consumer 必关 auto commit 手动 commitSync 业务必须幂等 处理失败进 DLQ 不卡 batch。
  3. partition 起步 16-32 不要从 1 开始 增加 partition 后 key 路由变 顺序断 一开始就要规划好。
  4. key 选用户 ID 注意热点 明星账号必须自定义 partitioner 牺牲顺序换均衡。
  5. retention 必须 >= 业务最长重试窗口 + 安全 buffer 一般 3-7 天 不要为省磁盘改太短。
  6. min.insync.replicas=2 replication-factor=3 防 split brain ISR 不足时 producer 不能写入。
  7. partition lag 监控 + 增长率告警双重保护 单 partition lag 10 万 5 分钟告警。
  8. broker 磁盘 80% 告警 90% 紧急处理 disk 满 broker readonly 整集群写入挂。
  9. 高频上下线场景用 static membership group.instance.id 避免 rebalance 停顿。
  10. 跨机房消费必须 MirrorMaker 同步 同机房消费 不要让 consumer 跨机房读 broker。

总结

Kafka 这事 很多人的直觉是 producer.send consumer.poll 高吞吐就稳 这其实是把 我能跑 Kafka quickstart 和 我能在生产用 Kafka 扛住日 5000 万消息 不丢不重不 lag 混为一谈。前者是会调 API 后者是懂 Kafka 工程。中间隔着的是 producer 配置 consumer offset 管理 partition 设计 顺序幂等 lag 监控 跨机房部署 整整一套工程方法论。

从原型到生产 你需要做的事远不止 send poll。你要懂 acks idempotent 怎么开 手动 commit 怎么做 partition 怎么规划 key 怎么选 热点怎么散 顺序与并发怎么权衡 lag 怎么监控 rebalance 怎么避免。每一项单独看都不复杂 但它们组合在一起 才是一个能扛业务规模的 Kafka 消息系统。少任何一项 都可能让你的系统丢消息 重复处理 lag 爆炸 热点 partition 拖垮整集群。

我经常用一个比喻来理解 Kafka 它有点像快递分拣中心。producer 是寄件人 broker 是分拣中心 partition 是分拣线 consumer 是收件人。acks=all 是寄件必须收到分拣中心所有备份点的回执 才算寄出 不会丢件。idempotent 是寄件单号唯一 重复寄一件不会重复送达。partition 是分拣线 用户 ID hash 决定走哪条线 同用户的件按顺序到 不同用户可以并行处理。热点是某个 VIP 客户一天寄 10 万件全堆一条线 必须临时开新线分流 牺牲顺序换均衡。consumer 手动 commit 是签收一件再让快递员标记一件 不是一上车就全标了 这样断网时也不会丢。lag 监控是排队长度看板 太长就加调度员。你不能因为有了分拣中心就觉得快递能流转 还要管寄件回执 编号唯一 分拣线规划 VIP 客户分流 签收顺序 调度员人数 才是一整套快递运营。

这套架构最难的地方在于 它的复杂度在小流量时几乎完全暴露不了。你 100 QPS 单机 Kafka 怎么用都不会出问题 觉得 Kafka 真好用。但真正生产 日 5000 万消息 几十个 topic 几百 partition 跨机房同步 你才发现 99% 的复杂度都在 那 1% 的工程细节里 acks 没开丢消息 auto commit 重复消费 partition 太少瓶颈 热点 partition 拖累 lag 爆炸看不到。建议任何想用 Kafka 扛严肃业务的团队 上线前一定要做 真实压测 故意 kill broker 看 producer 是否丢 故意 kill consumer 看是否重复 故意热点 key 看 partition 是否均衡 千万别等大促来教你 那时候损失可能就是几百万了。

—— 别看了 · 2026
声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理 邮箱1846861578@qq.com。
技术教程

Prompt 工程化完全指南:从一次"客服 AI 被一句话薅走十几万"看懂为什么写两段 prompt 远远不够

2026-5-24 16:32:08

技术教程

向量数据库选型工程化完全指南:从一次"500 万向量 OOM 服务半夜炸醒运维"看懂为什么 pip install 远远不够

2026-5-24 16:42:58

0 条回复 A文章作者 M管理员
    暂无讨论,说说你的看法吧
个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
搜索