未记录事实核验 · 状态:维护中

记录仅说明核验时间与维护状态,不保证适用于所有环境;使用前请留意正文中的版本、适用条件与风险提示。

warning 适用边界与维护说明

本文适用于具备 Kafka、Flink 基础的数据开发工程师和架构师,开展假设性电商实时平台的分层设计、磁盘容量估算、积压排查及灾备演练方案讨论,不是已验证的企业案例或可直接部署的完整工程;本次仅作全文结构审阅,未作外部事实核验或运行验证。Filebeat 片段未生成规范要求的全部字段,${timestamp} 仅为环境变量替换;Flink 示例仍需补齐状态初始化、TTL、Checkpoint、配套类型及下游实现。普通消费组查询、Lag 告警和灾备等待循环均已使用第 6 列 LAG,灾备循环也已处理查询失败、无有效记录和不可用位点,不应再引用旧说明中的相关错误。灾备 Step 1 仍在备端查询未经确认的硬编码复制组,不能据此确认同步完成;切换前须核对实际源端复制组、目标 Topic、数据复制结果和消费位点映射,Step 6 的日期输出也不等于实际 RTO。全链路图中的 GMV 数据来源尚未与订单支付链路衔接,宜作为待完善的设计草图。容量模型支持存储估算,不能独立推导 Broker 数量;分区、吞吐、告警阈值及容量余量须结合具体负载确定,ZooKeeper 建议仅适用于相应部署模式。RPO/RTO 为待实测或演练的目标,已取消跳转的两篇延伸材料不阻断现有练习。 已取消无法确定目标的可点击引用「03-Flink与Kafka集成」,保留原提及;Missing link target: 03-Flink与Kafka集成 已取消无法确定目标的可点击引用「01-ClickHouse概述」,保留原提及;Missing link target: 01-ClickHouse概述

企业级项目实战 - Kafka综合应用案例

abstract 本节概览

  • 学习目标:通过企业级项目掌握 Kafka 综合应用,建立从架构设计到生产运维的完整视角
  • 前置知识:Kafka 最佳实践
  • 预计用时:60 分钟
  • 后续学习:Flink 流处理与 Kafka 深度联动

项目背景:电商实时数据平台

业务需求分析

本项目采用假设性电商平台场景:日活跃用户 5000 万,峰值每秒产生 120 万条事件,涉及用户行为、订单支付、库存变更等十几个业务系统;这些数值是设计输入,不代表已验证的企业实际数据。核心诉求:实时感知业务状态,分钟级驱动运营决策。

%%{init: {"theme":"base","themeVariables":{"fontFamily":"PingFang SC, -apple-system, Source Han Serif SC, serif","fontSize":"16px","background":"#FDFBF5","primaryColor":"#FDFBF5","primaryTextColor":"#2D2A26","primaryBorderColor":"#6E6A60","secondaryColor":"#E8EFE8","secondaryBorderColor":"#8FA890","secondaryTextColor":"#2D2A26","tertiaryColor":"#FDFBF5","tertiaryBorderColor":"#9A9488","tertiaryTextColor":"#6E6A60","lineColor":"#6E6A60","textColor":"#2D2A26","mainBkg":"#FDFBF5","nodeBorder":"#2D5A3D","clusterBkg":"#F3EEDF","clusterBorder":"#9A9488","edgeLabelBackground":"#FDFBF5"},"flowchart":{"htmlLabels":true,"curve":"basis","padding":20,"nodeSpacing":60,"rankSpacing":70,"useMaxWidth":false},"themeCSS":".node rect, .node polygon, .node circle { stroke-width: 1.5px; } .edgePath .path { stroke-width: 1.5px; } .cluster-label .nodeLabel, .cluster-label foreignObject { color: #2D2A26 !important; } .edgeLabel, .edgeLabel foreignObject { color: #2D2A26 !important; background-color: #FDFBF5; } .edgeLabel rect { fill: #FDFBF5; }"}}%%
flowchart TD
    classDef default fill:#FDFBF5,stroke:#6E6A60,color:#2D2A26,stroke-width:1.5px
    classDef startend fill:#2D2A26,stroke:#2D2A26,color:#FDFBF5,font-weight:bold
    classDef decision fill:#F6EAD8,stroke:#C08A1A,color:#2D2A26,font-weight:bold
    classDef hub fill:#3B5A80,stroke:#3B5A80,color:#FDFBF5,font-weight:bold
    classDef io fill:#E5EAF0,stroke:#3B5A80,color:#2D2A26
    classDef accent fill:#A6432C,stroke:#A6432C,color:#FDFBF5,font-weight:bold
    classDef storage fill:#F3EEDF,stroke:#9A9488,color:#2D2A26
    SRC["数据源层\n用户埋点 / 订单系统 / 支付系统\nMySQL Binlog / IoT 设备 / 第三方 API"]
    KAFKA["Kafka 消息层\nODS Topic(原始) / DWD Topic(明细)\nADS Topic(聚合指标)"]
    PROC["Flink 实时计算层\n数据清洗 / 维度关联\n窗口聚合 / 实时指标"]
    STORE["存储应用层\nHBase(用户画像)/ Redis(实时大屏)\nClickHouse(历史分析)/ MySQL(业务回写)"]
    SRC --> KAFKA --> PROC --> STORE
    class SRC,KAFKA,PROC,STORE default

实时数仓全链路架构

数据分层与 Topic 命名规范

实时数仓复用离线数仓的分层理念,每一层对应独立的 Kafka Topic,层间由 Flink 作业驱动流转。

层次 职责 Topic 命名规范 分区数 保留时长
ODS(原始层) 存储原始事件,不做任何加工 ods.{业务域}.{表名} 与上游并发对齐 7 天
DWD(明细层) 清洗、脱敏、维度关联后的明细事件 dwd.{业务域}.{主题} 与 Flink 并发对齐 3 天
DWS(汇总层) 按业务维度的轻度聚合结果 dws.{业务域}.{粒度} 较少,通常 4-8 1 天
ADS(应用层) 面向大屏、接口的指标推送 ads.{应用}.{指标} 1-4(低并发消费) 1 小时
# ODS 层 Topic 创建示例(原始用户事件)
kafka-topics.sh --bootstrap-server kafka-01:9092,kafka-02:9092,kafka-03:9092 \
    --create --topic ods.user.behavior_event \
    --partitions 24 --replication-factor 3 \
    --config retention.ms=604800000 \
    --config compression.type=lz4 \
    --config min.insync.replicas=2

# DWD 层 Topic(清洗后的用户行为明细)
kafka-topics.sh --bootstrap-server kafka-01:9092,kafka-02:9092,kafka-03:9092 \
    --create --topic dwd.user.behavior_detail \
    --partitions 24 --replication-factor 3 \
    --config retention.ms=259200000 \
    --config compression.type=lz4 \
    --config min.insync.replicas=2

# ADS 层 Topic(实时 GMV 指标)
kafka-topics.sh --bootstrap-server kafka-01:9092,kafka-02:9092,kafka-03:9092 \
    --create --topic ads.dashboard.realtime_gmv \
    --partitions 4 --replication-factor 3 \
    --config retention.ms=3600000 \
    --config compression.type=lz4

数据源到 ODS 的接入方式

%%{init: {"theme":"base","themeVariables":{"fontFamily":"PingFang SC, -apple-system, Source Han Serif SC, serif","fontSize":"16px","background":"#FDFBF5","primaryColor":"#FDFBF5","primaryTextColor":"#2D2A26","primaryBorderColor":"#6E6A60","secondaryColor":"#E8EFE8","secondaryBorderColor":"#8FA890","secondaryTextColor":"#2D2A26","tertiaryColor":"#FDFBF5","tertiaryBorderColor":"#9A9488","tertiaryTextColor":"#6E6A60","lineColor":"#6E6A60","textColor":"#2D2A26","mainBkg":"#FDFBF5","nodeBorder":"#2D5A3D","clusterBkg":"#F3EEDF","clusterBorder":"#9A9488","edgeLabelBackground":"#FDFBF5"},"flowchart":{"htmlLabels":true,"curve":"basis","padding":20,"nodeSpacing":60,"rankSpacing":70,"useMaxWidth":false},"themeCSS":".node rect, .node polygon, .node circle { stroke-width: 1.5px; } .edgePath .path { stroke-width: 1.5px; } .cluster-label .nodeLabel, .cluster-label foreignObject { color: #2D2A26 !important; } .edgeLabel, .edgeLabel foreignObject { color: #2D2A26 !important; background-color: #FDFBF5; } .edgeLabel rect { fill: #FDFBF5; }"}}%%
flowchart LR
    classDef default fill:#FDFBF5,stroke:#6E6A60,color:#2D2A26,stroke-width:1.5px
    classDef startend fill:#2D2A26,stroke:#2D2A26,color:#FDFBF5,font-weight:bold
    classDef decision fill:#F6EAD8,stroke:#C08A1A,color:#2D2A26,font-weight:bold
    classDef hub fill:#3B5A80,stroke:#3B5A80,color:#FDFBF5,font-weight:bold
    classDef io fill:#E5EAF0,stroke:#3B5A80,color:#2D2A26
    classDef accent fill:#A6432C,stroke:#A6432C,color:#FDFBF5,font-weight:bold
    classDef storage fill:#F3EEDF,stroke:#9A9488,color:#2D2A26
    subgraph 日志类
        FB[Filebeat\n收集 Nginx/App 日志]
        FL[Flume\n多业务线汇聚]
    end
    subgraph 数据库变更
        CANAL[Canal\n监听 MySQL Binlog]
        DT[Debezium\n通用 CDC 方案]
    end
    subgraph 端上埋点
        SDK[JS/App SDK\n行为埋点]
        GW[数据网关\n接入层限流]
    end
    FB --> |ods.infra.nginx_access| K[(Kafka ODS)]
    FL --> |ods.infra.app_log| K
    CANAL --> |ods.order.order_info| K
    DT --> |ods.pay.pay_record| K
    SDK --> GW --> |ods.user.behavior_event| K
    class K storage
    class FB,FL,CANAL,DT,SDK,GW default

日志收集系统:多业务线隔离

Filebeat 到 Kafka 的接入配置片段

日志收集场景的核心挑战:多业务线 Topic 隔离(避免互相干扰)+ 日志格式标准化(方便下游统一处理)。

# filebeat.yml - 接入配置示例,非完整生产配置;使用前需确认 Filebeat 版本及 log input 兼容性
filebeat.inputs:
  # 业务线 A:电商订单服务
  - type: log
    paths:
      - /var/log/order-service/*.log
    fields:
      biz_line: order
      env: production
    fields_under_root: true
    multiline:
      pattern: '^\d{4}-\d{2}-\d{2}'
      negate: true
      match: after

  # 业务线 B:用户行为服务
  - type: log
    paths:
      - /var/log/user-service/*.log
    fields:
      biz_line: user
      env: production
    fields_under_root: true

processors:
  # 标准化日志格式:统一添加采集元数据
  - add_fields:
      target: ''
      fields:
        collector_host: "${HOSTNAME}"
        # ${timestamp} 是环境变量替换,不会逐条生成采集时间;动态采集时间需另行处理
        collect_ts: "${timestamp}"
  # 丢弃空行
  - drop_event:
      when:
        equals:
          message: ""

output.kafka:
  hosts: ["kafka-01:9092", "kafka-02:9092", "kafka-03:9092"]
  # 按业务线路由到不同 Topic
  topic: 'ods.%{[biz_line]}.application_log'
  partition.round_robin:
    reachable_only: true
  required_acks: 1
  compression: lz4
  max_message_bytes: 1048576
  # 批量发送提升吞吐
  bulk_max_size: 2048
  flush_timeout: 1s

日志格式标准化规范

所有接入 ODS 的日志事件必须携带以下标准字段,下游 Flink 作业依赖这些字段进行路由和时间对齐:

{
  "event_id": "uuid-v4",
  "biz_line": "order",
  "env": "production",
  "host": "app-server-01",
  "log_level": "INFO",
  "timestamp": 1700000000000,
  "message": "原始日志内容",
  "collector_host": "filebeat-node-03",
  "collect_ts": 1700000000123
}

warning 时间字段陷阱

timestamp 应使用业务时间(事件发生时间),而非采集时间。Flink 的事件时间窗口依赖此字段,若用采集时间会导致窗口计算结果偏差。

用户行为分析:埋点事件实时链路

端到端架构

%%{init: {"theme":"base","themeVariables":{"fontFamily":"PingFang SC, -apple-system, Source Han Serif SC, serif","fontSize":"16px","background":"#FDFBF5","primaryColor":"#FDFBF5","primaryTextColor":"#2D2A26","primaryBorderColor":"#6E6A60","secondaryColor":"#E8EFE8","secondaryBorderColor":"#8FA890","secondaryTextColor":"#2D2A26","tertiaryColor":"#FDFBF5","tertiaryBorderColor":"#9A9488","tertiaryTextColor":"#6E6A60","lineColor":"#6E6A60","textColor":"#2D2A26","mainBkg":"#FDFBF5","nodeBorder":"#2D5A3D","clusterBkg":"#F3EEDF","clusterBorder":"#9A9488","edgeLabelBackground":"#FDFBF5"},"flowchart":{"htmlLabels":true,"curve":"basis","padding":20,"nodeSpacing":60,"rankSpacing":70,"useMaxWidth":false},"themeCSS":".node rect, .node polygon, .node circle { stroke-width: 1.5px; } .edgePath .path { stroke-width: 1.5px; } .cluster-label .nodeLabel, .cluster-label foreignObject { color: #2D2A26 !important; } .edgeLabel, .edgeLabel foreignObject { color: #2D2A26 !important; background-color: #FDFBF5; } .edgeLabel rect { fill: #FDFBF5; }"}}%%
flowchart TD
    classDef default fill:#FDFBF5,stroke:#6E6A60,color:#2D2A26,stroke-width:1.5px
    classDef startend fill:#2D2A26,stroke:#2D2A26,color:#FDFBF5,font-weight:bold
    classDef decision fill:#F6EAD8,stroke:#C08A1A,color:#2D2A26,font-weight:bold
    classDef hub fill:#3B5A80,stroke:#3B5A80,color:#FDFBF5,font-weight:bold
    classDef io fill:#E5EAF0,stroke:#3B5A80,color:#2D2A26
    classDef accent fill:#A6432C,stroke:#A6432C,color:#FDFBF5,font-weight:bold
    classDef storage fill:#F3EEDF,stroke:#9A9488,color:#2D2A26
    CLIENT["客户端 SDK\n点击 / 曝光 / 搜索 / 加购"]
    GW["数据接入网关\n限流 / 鉴权 / 格式校验"]
    ODS["ods.user.behavior_event\n原始埋点事件"]
    FLINK_DWD["Flink DWD 作业\n会话拼接 / 设备识别 / 脱敏"]
    DWD["dwd.user.behavior_detail\n标准化明细事件"]
    FLINK_ADS["Flink ADS 作业\n实时 UV/PV / 漏斗计算"]
    HBASE["HBase\n用户实时画像"]
    REDIS["Redis\n实时大屏指标"]
    CLIENT --> GW --> ODS --> FLINK_DWD --> DWD
    DWD --> FLINK_ADS --> REDIS
    DWD --> HBASE
    class CLIENT,GW,ODS,FLINK_DWD,DWD,FLINK_ADS,HBASE,REDIS default
// 用户行为实时分析 Flink 作业(核心片段)
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setParallelism(24);

// 从 Kafka DWD 层读取
KafkaSource<UserBehaviorEvent> source = KafkaSource.<UserBehaviorEvent>builder()
    .setBootstrapServers("kafka-01:9092,kafka-02:9092,kafka-03:9092")
    .setTopics("dwd.user.behavior_detail")
    .setGroupId("flink-ads-user-behavior")
    .setStartingOffsets(OffsetsInitializer.committedOffsets(OffsetResetStrategy.LATEST))
    .setValueOnlyDeserializer(new UserBehaviorEventDeserializer())
    .build();

DataStream<UserBehaviorEvent> events = env
    .fromSource(source, WatermarkStrategy
        .<UserBehaviorEvent>forBoundedOutOfOrderness(Duration.ofSeconds(10))
        .withTimestampAssigner((e, ts) -> e.getEventTimestamp()), "behavior-source");

// 1 分钟滚动窗口实时 PV/UV
DataStream<PageMetric> pageMetrics = events
    .filter(e -> "PAGE_VIEW".equals(e.getEventType()))
    .keyBy(UserBehaviorEvent::getPageId)
    .window(TumblingEventTimeWindows.of(Time.minutes(1)))
    .aggregate(new PageMetricAggregator(), new PageMetricWindowFunction());

// 写入 Redis 供大屏消费
pageMetrics.addSink(new RedisSink<>(jedisPoolConfig, new PageMetricMapper()));

// 同时写回 ADS Kafka Topic(供其他系统订阅)
pageMetrics.sinkTo(KafkaSink.<PageMetric>builder()
    .setBootstrapServers("kafka-01:9092,kafka-02:9092,kafka-03:9092")
    .setRecordSerializer(KafkaRecordSerializationSchema.builder()
        .setTopic("ads.dashboard.page_metrics")
        .setValueSerializationSchema(new PageMetricSerializer())
        .build())
    .setDeliveryGuarantee(DeliveryGuarantee.AT_LEAST_ONCE)
    .build());

订单状态机:Canal Binlog 实时同步

订单系统的状态变更通过 MySQL Binlog 捕获,经 Canal 转成结构化事件写入 Kafka,再由 Flink 驱动状态机处理。

%%{init: {"theme":"base","themeVariables":{"fontFamily":"PingFang SC, -apple-system, Source Han Serif SC, serif","fontSize":"16px","background":"#FDFBF5","primaryColor":"#FDFBF5","primaryTextColor":"#2D2A26","primaryBorderColor":"#6E6A60","secondaryColor":"#E8EFE8","secondaryBorderColor":"#8FA890","secondaryTextColor":"#2D2A26","tertiaryColor":"#FDFBF5","tertiaryBorderColor":"#9A9488","tertiaryTextColor":"#6E6A60","lineColor":"#6E6A60","textColor":"#2D2A26","mainBkg":"#FDFBF5","nodeBorder":"#2D5A3D","clusterBkg":"#F3EEDF","clusterBorder":"#9A9488","edgeLabelBackground":"#FDFBF5"},"flowchart":{"htmlLabels":true,"curve":"basis","padding":20,"nodeSpacing":60,"rankSpacing":70,"useMaxWidth":false},"themeCSS":".node rect, .node polygon, .node circle { stroke-width: 1.5px; } .edgePath .path { stroke-width: 1.5px; } .cluster-label .nodeLabel, .cluster-label foreignObject { color: #2D2A26 !important; } .edgeLabel, .edgeLabel foreignObject { color: #2D2A26 !important; background-color: #FDFBF5; } .edgeLabel rect { fill: #FDFBF5; }"}}%%
sequenceDiagram
    participant MySQL as MySQL 订单库
    participant Canal as Canal Server
    participant Kafka as Kafka(ods.order.order_info)
    participant Flink as Flink CDC 作业
    participant State as Flink 状态存储(RocksDB)
    participant Sink as 下游系统

    MySQL->>Canal: Binlog 变更事件
    Canal->>Kafka: 序列化为 JSON/Avro 格式
    Kafka->>Flink: 消费事件(at-least-once)
    Flink->>State: 读取订单当前状态
    Flink->>State: 幂等性校验(event_id 去重)
    State->>Flink: 允许/拒绝状态转移
    Flink->>Sink: 写出状态变更结果

Canal 配置要点

# canal.properties - 核心配置项
canal.instance.mysql.slaveId=1234
canal.instance.master.address=mysql-primary:3306
canal.instance.dbUsername=canal
canal.instance.dbPassword=canal_password

# 输出到 Kafka
canal.mq.servers=kafka-01:9092,kafka-02:9092,kafka-03:9092
canal.mq.topic=ods.order.order_info
# 按 order_id 路由,保证同一订单事件有序
canal.mq.partitionsNum=12
canal.mq.partitionHash=.*\\.order_info:order_id
// 订单状态机处理(关键:幂等性校验)
public class OrderStateMachineFunction extends KeyedProcessFunction<String, OrderBinlogEvent, OrderStateChange> {

    // 存储订单当前状态
    private ValueState<OrderState> currentState;
    // 已处理事件 ID;本片段未实现 TTL,24 小时去重窗口需在状态描述符或清理逻辑中另行配置
    private MapState<String, Long> processedEventIds;

    @Override
    public void processElement(OrderBinlogEvent event, Context ctx, Collector<OrderStateChange> out) throws Exception {
        // 1. 幂等性校验
        if (processedEventIds.contains(event.getEventId())) {
            return; // 重复事件,直接跳过
        }

        OrderState state = currentState.value();
        String fromStatus = state != null ? state.getStatus() : "INIT";
        String toStatus = event.getNewStatus();

        // 2. 状态机合法性校验
        if (!isValidTransition(fromStatus, toStatus)) {
            // 记录异常,不抛出(避免消费阻塞)
            log.warn("非法状态转移: orderId={}, from={}, to={}", event.getOrderId(), fromStatus, toStatus);
            return;
        }

        // 3. 更新状态
        currentState.update(new OrderState(toStatus, event.getTimestamp()));
        processedEventIds.put(event.getEventId(), event.getTimestamp());

        // 4. 输出状态变更事件
        out.collect(new OrderStateChange(event.getOrderId(), fromStatus, toStatus, event.getTimestamp()));
    }

    private boolean isValidTransition(String from, String to) {
        // 合法的订单状态流转矩阵
        Map<String, Set<String>> transitions = Map.of(
            "INIT",      Set.of("CREATED"),
            "CREATED",   Set.of("PAID", "CANCELLED"),
            "PAID",      Set.of("SHIPPED", "REFUNDING"),
            "SHIPPED",   Set.of("DELIVERED"),
            "DELIVERED", Set.of("COMPLETED", "REFUNDING"),
            "REFUNDING", Set.of("REFUNDED")
        );
        return transitions.getOrDefault(from, Set.of()).contains(to);
    }
}

大规模集群运维

Topic 分区规划原则

分区数是 Kafka 集群最重要的规划决策之一,确定后调整成本极高。

规划维度 建议 说明
分区数规划起点 结合目标消费并行度与实测吞吐确定 同一消费组内的分区消费并行度受分区数限制,生产者数量不构成分区数的硬性下限
分区数上限 Broker 数量 × 100(经验值) 过多分区增加 Controller 和 ZooKeeper 负担
消息量参考 单分区 10-50 MB/s 超过此吞吐需增加分区
分区键选择 高基数、分布均匀的业务 ID 避免数据倾斜,user_id 好于 category_id
扩容时机 消费 Lag 持续增长且无法通过参数调优解决 分区扩容可能改变按 key 的分区路由;需设计迁移与消费衔接,不能默认保证同一业务键跨扩容的顺序
# 查询集群所有 Topic 的分区分布情况
kafka-topics.sh --bootstrap-server kafka-01:9092 --describe | \
  awk '/Topic:/{topic=$2} /PartitionCount:/{print topic, $0}' | \
  sort -k3 -n -r | head -20

# 检查分区 Leader 分布是否均匀
kafka-topics.sh --bootstrap-server kafka-01:9092 --describe | \
  awk '/Leader:/{print $6}' | sort | uniq -c | sort -rn

Consumer Group 管理规范

# 查看所有 Consumer Group 的消费 Lag
kafka-consumer-groups.sh --bootstrap-server kafka-01:9092 \
    --describe --all-groups 2>/dev/null | \
    awk 'NR>1 && $6 ~ /^[0-9]+$/ && $6 > 0 {print $1, $2, $3, "LAG="$6}' | \
    sort -t= -k2,2nr | head -20

# 重置指定 Consumer Group 的 Offset(谨慎操作)
# 场景:Flink 作业重启后需要从指定时间点重新消费
kafka-consumer-groups.sh --bootstrap-server kafka-01:9092 \
    --group flink-ads-user-behavior \
    --topic dwd.user.behavior_detail \
    --reset-offsets --to-datetime 2024-01-15T00:00:00.000 \
    --execute

Lag 告警体系

生产环境必须对 Consumer Lag 建立分级告警,防止消息积压影响业务实时性。

#!/bin/bash
# lag_monitor.sh - Consumer Lag 监控脚本,配合 Prometheus AlertManager 使用

BOOTSTRAP_SERVERS="kafka-01:9092,kafka-02:9092,kafka-03:9092"
LAG_WARN_THRESHOLD=10000      # 告警阈值:积压超过 1 万条
LAG_CRITICAL_THRESHOLD=100000 # 严重阈值:积压超过 10 万条

kafka-consumer-groups.sh --bootstrap-server $BOOTSTRAP_SERVERS \
    --describe --all-groups 2>/dev/null | \
    awk -v warn=$LAG_WARN_THRESHOLD -v critical=$LAG_CRITICAL_THRESHOLD \
    'NR>1 && $6 ~ /^[0-9]+$/ {
        lag = $6 + 0
        if (lag >= critical) {
            printf "CRITICAL: group=%s topic=%s partition=%s lag=%s\n", $1, $2, $3, $6
        } else if (lag >= warn) {
            printf "WARNING: group=%s topic=%s partition=%s lag=%s\n", $1, $2, $3, $6
        }
    }'

灾备方案:MirrorMaker 2 跨机房同步

整体灾备架构

%%{init: {"theme":"base","themeVariables":{"fontFamily":"PingFang SC, -apple-system, Source Han Serif SC, serif","fontSize":"16px","background":"#FDFBF5","primaryColor":"#FDFBF5","primaryTextColor":"#2D2A26","primaryBorderColor":"#6E6A60","secondaryColor":"#E8EFE8","secondaryBorderColor":"#8FA890","secondaryTextColor":"#2D2A26","tertiaryColor":"#FDFBF5","tertiaryBorderColor":"#9A9488","tertiaryTextColor":"#6E6A60","lineColor":"#6E6A60","textColor":"#2D2A26","mainBkg":"#FDFBF5","nodeBorder":"#2D5A3D","clusterBkg":"#F3EEDF","clusterBorder":"#9A9488","edgeLabelBackground":"#FDFBF5"},"flowchart":{"htmlLabels":true,"curve":"basis","padding":20,"nodeSpacing":60,"rankSpacing":70,"useMaxWidth":false},"themeCSS":".node rect, .node polygon, .node circle { stroke-width: 1.5px; } .edgePath .path { stroke-width: 1.5px; } .cluster-label .nodeLabel, .cluster-label foreignObject { color: #2D2A26 !important; } .edgeLabel, .edgeLabel foreignObject { color: #2D2A26 !important; background-color: #FDFBF5; } .edgeLabel rect { fill: #FDFBF5; }"}}%%
flowchart TB
    classDef default fill:#FDFBF5,stroke:#6E6A60,color:#2D2A26,stroke-width:1.5px
    classDef startend fill:#2D2A26,stroke:#2D2A26,color:#FDFBF5,font-weight:bold
    classDef decision fill:#F6EAD8,stroke:#C08A1A,color:#2D2A26,font-weight:bold
    classDef hub fill:#3B5A80,stroke:#3B5A80,color:#FDFBF5,font-weight:bold
    classDef io fill:#E5EAF0,stroke:#3B5A80,color:#2D2A26
    classDef accent fill:#A6432C,stroke:#A6432C,color:#FDFBF5,font-weight:bold
    classDef storage fill:#F3EEDF,stroke:#9A9488,color:#2D2A26
    subgraph DC1["主机房(上海)"]
        KP1[Kafka Primary Cluster\n3 Broker / 3 ZK]
        MM2P[MirrorMaker 2\n同步进程]
    end
    subgraph DC2["备机房(北京)"]
        KP2[Kafka Standby Cluster\n3 Broker / 3 ZK]
        MM2S[MirrorMaker 2\n反向同步进程]
    end
    APP["业务应用\n正常写主机房"]
    OPS["运维控制台\n灾备切换操作"]

    APP --> KP1
    KP1 --> MM2P --> KP2
    KP2 --> MM2S --> KP1
    OPS --> APP
    class DC1,KP1,MM2P,DC2,KP2,MM2S,APP,OPS default

MirrorMaker 2 配置

# mm2.properties - 双向同步配置
clusters = primary, standby
primary.bootstrap.servers = kafka-sh-01:9092,kafka-sh-02:9092,kafka-sh-03:9092
standby.bootstrap.servers = kafka-bj-01:9092,kafka-bj-02:9092,kafka-bj-03:9092

# 正向同步:主 -> 备
primary->standby.enabled = true
primary->standby.topics = ods\..*, dwd\..*, dws\..*
# 排除内部 Topic 和已同步的 Topic(避免循环同步)
primary->standby.topics.blacklist = .*\.internal, .*\.replica

# 反向同步:备 -> 主(用于备机房写入场景)
standby->primary.enabled = true
standby->primary.topics = ads\..*

# Offset 同步:用于恢复消费位点;是否可连续恢复还取决于位点转换、目标消费组状态和实际同步结果
primary->standby.sync.group.offsets.enabled = true
primary->standby.sync.group.offsets.interval.seconds = 60

# 性能配置
replication.factor = 3
tasks.max = 8

RPO / RTO 目标与切换演练 Checklist

指标 目标值 实现机制
RPO(恢复点目标) < 1 分钟(设计目标,需实测) 以实际数据复制延迟及故障时未复制的数据量评估;60s 消费位点同步间隔不等于 RPO
RTO(恢复时间目标) < 5 分钟(设计目标,需演练验证) 完成接入切换、目标 Topic 确认和有效消费位点恢复;auto.offset.reset=latest 在无有效位点时可能跳过已有消息,不能作为连续恢复保障

灾备切换演练 Checklist(每季度执行一次):

# Step 1:确认备机房数据同步状态(Lag 应为 0 或极小值)
kafka-consumer-groups.sh --bootstrap-server kafka-bj-01:9092 \
    --group mirrormaker2-primary-to-standby --describe

# Step 2:停止主机房生产者写入(模拟主机房故障)
# [手动操作:通知各业务方暂停写入或修改配置指向备机房]

# Step 3:在源端核对实际复制消费组的进度;此检查不替代目标数据和消费位点验证
# 必须根据实际部署设置源端地址和复制消费组,不能假定示例组名存在
: "${MM2_SOURCE_BOOTSTRAP:?请设置实际源端 bootstrap.servers}"
: "${MM2_REPLICATION_GROUP:?请设置已确认的实际复制消费组}"
while true; do
    if ! output=$(kafka-consumer-groups.sh \
        --bootstrap-server "$MM2_SOURCE_BOOTSTRAP" \
        --group "$MM2_REPLICATION_GROUP" --describe); then
        echo "复制消费组查询失败,停止追平判断" >&2
        exit 1
    fi
    if ! lag=$(printf '%s\n' "$output" | awk '
        $4 ~ /^[0-9]+$/ && $5 ~ /^[0-9]+$/ && $6 ~ /^[0-9]+$/ {
            sum += $6; records++; next
        }
        $3 ~ /^[0-9]+$/ {invalid = 1}
        END {
            if (records == 0 || invalid) exit 1
            printf "%.0f\n", sum
        }'); then
        echo "无有效分区记录或位点不可用,不能判断追平" >&2
        exit 1
    fi
    echo "源端复制消费组 Lag: $lag"
    [ "$lag" -eq 0 ] && break
    sleep 10
done

# Step 4:修改业务应用的 bootstrap.servers 指向备机房
# [手动操作:更新配置中心或重启各业务应用]

# Step 5:验证备机房消费正常
kafka-consumer-groups.sh --bootstrap-server kafka-bj-01:9092 \
    --describe --all-groups | grep -v "^$" | head -30

# Step 6:记录切换完成时间,计算实际 RTO
echo "灾备切换完成,RTO = $(date)"

容量规划

消息量估算模型

容量规划的输入来自业务估算,输出是 Broker 数量、磁盘大小和网络带宽需求。

输入参数(示例:某电商平台):
  - 每日消息总量:D = 100 亿条
  - 平均消息体大小:S = 500 字节
  - 峰值/均值流量比:P = 3 倍
  - 数据保留天数:R = 3 天
  - 副本数:RF = 3

计算过程:
  日均总数据量 = D × S = 100亿 × 500B = 5 TB/天
  日均写入速率 = 5TB / 86400s ≈ 60 MB/s
  峰值写入速率 = 60 MB/s × 3 = 180 MB/s(生产者侧)

存储容量:
  原始存储 = D × S × R = 5TB × 3 = 15 TB
  含副本存储 = 15TB × RF = 45 TB
  预留 20% 余量 = 45TB × 1.2 = 54 TB

扩容时间点判断

监控指标 扩容阈值 处理方式
Broker 磁盘使用率 > 70% 增加磁盘或迁移分区
网络带宽使用率 > 60%(峰值) 增加 Broker 节点
Consumer Lag 持续增长 Lag 增长速率 > 0 且无法追平 增加分区数或消费者实例
Broker CPU > 70%(持续 15 分钟) 减少 Broker 上分区数或升配
ZooKeeper 请求延迟 > 100ms(p99) 优化 ZK 集群或迁移至 KRaft 模式
# 存储容量实时监控
kafka-log-dirs.sh --bootstrap-server kafka-01:9092 \
    --topic-list ods.user.behavior_event,dwd.user.behavior_detail \
    --describe 2>/dev/null | \
    python3 -c "
import sys, json
data = json.load(sys.stdin)
for broker in data['brokers']:
    total = sum(log['size'] for log_dir in broker['logDirs'] for log in log_dir['partitions'])
    print(f'Broker {broker[\"broker\"]}: {total/1024/1024/1024:.2f} GB')
"

完整全链路架构图

%%{init: {"theme":"base","themeVariables":{"fontFamily":"PingFang SC, -apple-system, Source Han Serif SC, serif","fontSize":"16px","background":"#FDFBF5","primaryColor":"#FDFBF5","primaryTextColor":"#2D2A26","primaryBorderColor":"#6E6A60","secondaryColor":"#E8EFE8","secondaryBorderColor":"#8FA890","secondaryTextColor":"#2D2A26","tertiaryColor":"#FDFBF5","tertiaryBorderColor":"#9A9488","tertiaryTextColor":"#6E6A60","lineColor":"#6E6A60","textColor":"#2D2A26","mainBkg":"#FDFBF5","nodeBorder":"#2D5A3D","clusterBkg":"#F3EEDF","clusterBorder":"#9A9488","edgeLabelBackground":"#FDFBF5"},"flowchart":{"htmlLabels":true,"curve":"basis","padding":20,"nodeSpacing":60,"rankSpacing":70,"useMaxWidth":false},"themeCSS":".node rect, .node polygon, .node circle { stroke-width: 1.5px; } .edgePath .path { stroke-width: 1.5px; } .cluster-label .nodeLabel, .cluster-label foreignObject { color: #2D2A26 !important; } .edgeLabel, .edgeLabel foreignObject { color: #2D2A26 !important; background-color: #FDFBF5; } .edgeLabel rect { fill: #FDFBF5; }"}}%%
flowchart TD
    classDef default fill:#FDFBF5,stroke:#6E6A60,color:#2D2A26,stroke-width:1.5px
    classDef startend fill:#2D2A26,stroke:#2D2A26,color:#FDFBF5,font-weight:bold
    classDef decision fill:#F6EAD8,stroke:#C08A1A,color:#2D2A26,font-weight:bold
    classDef hub fill:#3B5A80,stroke:#3B5A80,color:#FDFBF5,font-weight:bold
    classDef io fill:#E5EAF0,stroke:#3B5A80,color:#2D2A26
    classDef accent fill:#A6432C,stroke:#A6432C,color:#FDFBF5,font-weight:bold
    classDef storage fill:#F3EEDF,stroke:#9A9488,color:#2D2A26
    subgraph 数据源
        APP_SDK["APP/Web SDK\n用户行为埋点"]
        MYSQL_ORDER["MySQL 订单库\nBinlog CDC"]
        MYSQL_USER["MySQL 用户库\nBinlog CDC"]
        NGINX_LOG["Nginx 访问日志\nFilebeat 采集"]
    end

    subgraph Kafka_ODS["Kafka ODS 层"]
        ODS_BEHAVIOR["ods.user.behavior_event\n24 分区 / 7 天保留"]
        ODS_ORDER["ods.order.order_info\n12 分区 / 7 天保留"]
        ODS_USER["ods.user.user_info\n8 分区 / 7 天保留"]
        ODS_LOG["ods.infra.nginx_access\n16 分区 / 3 天保留"]
    end

    subgraph Flink_DWD["Flink DWD 作业"]
        F1["行为清洗作业\n脱敏 / 格式校验"]
        F2["订单 CDC 作业\n状态机 / 幂等去重"]
        F3["用户维度关联作业\n维表 Join"]
    end

    subgraph Kafka_DWD["Kafka DWD 层"]
        DWD_BEHAVIOR["dwd.user.behavior_detail\n24 分区 / 3 天保留"]
        DWD_ORDER["dwd.order.order_detail\n12 分区 / 3 天保留"]
    end

    subgraph Flink_ADS["Flink ADS 作业"]
        F4["实时 GMV 计算\n1 分钟窗口"]
        F5["用户漏斗分析\n会话窗口"]
        F6["订单状态聚合\n滚动窗口"]
    end

    subgraph 存储应用
        REDIS["Redis\n实时大屏"]
        HBASE["HBase\n用户画像"]
        CK["ClickHouse\n历史分析"]
    end

    APP_SDK --> ODS_BEHAVIOR
    MYSQL_ORDER --> |Canal| ODS_ORDER
    MYSQL_USER --> |Canal| ODS_USER
    NGINX_LOG --> ODS_LOG

    ODS_BEHAVIOR --> F1 --> DWD_BEHAVIOR
    ODS_ORDER --> F2 --> DWD_ORDER
    ODS_USER --> F3 --> DWD_BEHAVIOR

    DWD_BEHAVIOR --> F4 & F5
    DWD_ORDER --> F6

    F4 --> REDIS
    F5 --> HBASE
    F6 --> REDIS
    DWD_BEHAVIOR --> CK
    DWD_ORDER --> CK
    class APP_SDK,MYSQL_ORDER,MYSQL_USER,NGINX_LOG,Kafka_ODS,ODS_BEHAVIOR,ODS_ORDER,ODS_USER,ODS_LOG,Flink_DWD,F1,F2,F3,Kafka_DWD,DWD_BEHAVIOR,DWD_ORDER,Flink_ADS,F4,F5,F6,REDIS,HBASE,CK default

系统架构设计

数据采集实现(核心生产者配置)

// 高性能、高可靠生产者配置
private KafkaProducer<String, GenericRecord> createOptimizedProducer() {
    Properties props = new Properties();
    props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG,
            "kafka-01:9092,kafka-02:9092,kafka-03:9092");
    props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
    props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, KafkaAvroSerializer.class);
    props.put(AbstractKafkaSchemaSerDeConfig.SCHEMA_REGISTRY_URL_CONFIG,
            "http://schema-registry:8081");

    // 可靠性配置:ODS 层要求不丢消息
    props.put(ProducerConfig.ACKS_CONFIG, "all");
    props.put(ProducerConfig.ENABLE_IDEMPOTENCE_CONFIG, true);
    props.put(ProducerConfig.RETRIES_CONFIG, Integer.MAX_VALUE);
    props.put(ProducerConfig.MAX_IN_FLIGHT_REQUESTS_PER_CONNECTION, 5);

    // 性能配置:批量发送降低请求次数
    props.put(ProducerConfig.BATCH_SIZE_CONFIG, 65536);        // 64KB
    props.put(ProducerConfig.LINGER_MS_CONFIG, 5);             // 最多等待 5ms 凑批
    props.put(ProducerConfig.COMPRESSION_TYPE_CONFIG, "lz4");
    props.put(ProducerConfig.BUFFER_MEMORY_CONFIG, 67108864);  // 64MB 发送缓冲区

    return new KafkaProducer<>(props);
}

掌握检查

完成以下检查,确保已掌握企业级 Kafka 应用:

  • [ ] 能描述实时数仓 ODS/DWD/ADS 三层的职责,并说明 Topic 命名规范
  • [ ] 理解 Canal Binlog 同步到 Kafka 的配置要点和幂等性保障方式
  • [ ] 能设计 Consumer Lag 告警体系,说明分级阈值的设置依据
  • [ ] 掌握 MirrorMaker 2 的 RPO/RTO 目标及灾备切换步骤
  • [ ] 能独立完成容量规划:从业务量估算出 Broker 数量和磁盘需求

练习题

  1. 某业务的 Kafka Topic 每天消息量 50 亿条,平均消息大小 1KB,保留 7 天,副本数 3,计算所需总存储容量。
  2. Flink 作业消费 Kafka 时发现 Consumer Lag 持续增长,列举至少 5 种可能的原因和对应的排查方法。
  3. 如果订单状态机收到了乱序的 Binlog 事件(如先收到 DELIVERED 再收到 SHIPPED),如何处理?
  4. 设计一个支持多机房容灾的 Kafka 架构,要求 RPO < 30 秒,RTO < 3 分钟,画出架构图并说明关键配置。

相关文档

前置知识:Kafka 最佳实践 - 生产环境经验总结

完整知识体系:Kafka 学习路线图 - 回到起点,温故知新

生态联动:Flink 与 Kafka 集成 - 流处理作业开发实战

存储层:ClickHouse - 实时分析数仓存储选型


tip 项目成功要素

企业级 Kafka 项目成功的关键:

  • 需求理解:深入理解业务 SLA(实时性要求、数据可靠性要求)
  • 架构设计:分层清晰(ODS/DWD/ADS),Topic 命名规范统一
  • 容量规划:提前估算,留足 30% 余量,避免突发流量压垮集群
  • 监控运维:Consumer Lag 告警 + 灾备演练 + 容量预警是三大核心

好的实时数仓架构,既要能撑住今天的业务量,也要让明天的扩容足够简单。