4、ODS层实战:基于Kafka的实时数据接入、JSON/AVRO格式处理

ODS层,说白了就是实时数仓的“第一道门”。

所有业务系统的数据,不管你是MySQL的Binlog、APP的埋点日志,还是IoT设备的传感器数据,都得先经过ODS层。我习惯把它比作“数据码头”——船(数据)先靠岸,再决定往哪个仓库(后续分层)送。

这一章,我们重点解决两个问题:怎么把数据实时接进来,以及怎么处理JSON和AVRO这两种主流格式。

4.1 为什么选Kafka作为ODS层核心载体?

你可能会问:为什么非要用Kafka?用消息队列不行吗?

嗯,这里要注意。Kafka在实时数仓里的角色,不只是消息队列那么简单。它更像一个“分布式日志存储系统”。我曾在项目中见过有人用RabbitMQ做ODS层,结果数据量一上来,消费端跟不上,消息积压直接把内存撑爆了。

Kafka的优势,我总结了三句话:

  • 高吞吐:单机每秒几十万条消息,轻松应对业务高峰。
  • 持久化:数据落盘,可以回溯重放。你想想看,如果下游挂了,数据还能找回来,这多重要。
  • 多订阅者:一份数据,多个消费者各取所需。比如实时ETL用一份,离线批处理用另一份。

核心原则:ODS层的数据,必须保持“原汁原味”。不要做任何聚合、清洗、过滤。哪怕是一条脏数据,也得原样存下来。为什么?因为下游的DWD层可能需要它做回溯分析。

4.2 实时数据接入:从源头到Kafka

数据接入的方式,取决于你的数据源。我挑三个最常见的场景来讲。

4.2.1 MySQL Binlog 接入(Canal + Kafka)

这是实时数仓最经典的接入方式。业务库的增删改,通过Canal解析Binlog,再推送到Kafka。

我曾经踩过一个坑:Canal默认只推送增量数据,但如果你要初始化全量数据,就得手动做一次“全量+增量”的衔接。后来我写了个脚本,先拉全量到Kafka的一个独立Topic,再切到增量Topic,下游通过时间戳做去重。

配置示例(Canal的instance.properties):

# 指定要监听的数据库和表
canal.instance.filter.regex=test_db\\..*
# 投递到Kafka的Topic
canal.mq.topic=ods_binlog_test_db
# 分区数,建议和Kafka分区数一致
canal.mq.partitions=6

4.2.2 日志文件接入(Filebeat + Kafka)

APP埋点、服务器日志,通常走Filebeat或Flume。我个人更倾向Filebeat,因为它轻量,资源占用低。

配置示例(Filebeat的filebeat.yml):

filebeat.inputs:
- type: log
  enabled: true
  paths:
    - /var/log/app/*.log
  fields:
    log_type: app_log

output.kafka:
  hosts: ["kafka1:9092", "kafka2:9092"]
  topic: "ods_app_log"
  partition.round_robin:
    reachable_only: true

这里有个小技巧:在fields里加一个log_type标签,下游可以根据这个标签做路由,不用再解析日志内容来判断来源。

4.2.3 自定义API接入(HTTP + Kafka Producer)

有些第三方系统,既没有Binlog,也不产生日志文件,只提供HTTP接口。这时候,你需要自己写一个数据采集服务。

我建议用Spring Boot + Kafka Producer封装一个通用接入层。核心代码就几行:

@RestController
public class DataCollectController {
    @Autowired
    private KafkaTemplate<String, String> kafkaTemplate;

    @PostMapping("/collect")
    public String collect(@RequestBody String data) {
        // 直接投递到Kafka,不做任何处理
        kafkaTemplate.send("ods_raw_data", data);
        return "success";
    }
}

注意:这里一定要用异步发送,否则接口响应时间会拖垮上游。

4.3 JSON格式处理:灵活但“吃”性能

JSON是ODS层最常见的格式。为什么?因为业务系统改字段太频繁了。JSON的schema-less特性,让上下游解耦。

但JSON有个致命问题:解析性能差。我做过压测,同样的数据量,JSON解析比AVRO慢3-5倍。而且JSON是文本格式,占用存储空间大。

我的建议:ODS层可以保留JSON格式,但一定要做“字段裁剪”。比如一条日志有50个字段,你只用到20个,那就只保留这20个。别把垃圾数据全存进去,Kafka的磁盘空间也是钱。

JSON处理示例(Flink SQL):

CREATE TABLE ods_json (
    user_id STRING,
    event_type STRING,
    event_time BIGINT,
    properties MAP<STRING, STRING>
) WITH (
    'connector' = 'kafka',
    'topic' = 'ods_app_log',
    'properties.bootstrap.servers' = 'localhost:9092',
    'format' = 'json',
    'json.fail-on-missing-field' = 'false',
    'json.ignore-parse-errors' = 'true'
);

注意json.ignore-parse-errors这个参数。我建议设为true,因为生产环境里,偶尔会有一条格式异常的JSON。如果直接报错,整个任务就挂了。设为true后,异常数据会被丢弃,保证任务持续运行。

4.4 AVRO格式处理:高效但需要Schema管理

AVRO是二进制格式,解析快、体积小。如果你对性能有极致要求,或者数据量特别大(比如每天几百TB),我强烈推荐用AVRO。

但AVRO有个门槛:必须定义Schema。而且Schema一旦定义,改起来很麻烦。我曾在项目中因为改了AVRO Schema,忘记通知下游,结果下游反序列化直接报错,排查了半天。

4.4.1 AVRO Schema定义

一个典型的AVRO Schema长这样:

{
  "type": "record",
  "name": "UserEvent",
  "namespace": "com.example.ods",
  "fields": [
    {"name": "user_id", "type": "string"},
    {"name": "event_type", "type": "string"},
    {"name": "event_time", "type": "long"},
    {"name": "properties", "type": {"type": "map", "values": "string"}}
  ]
}

4.4.2 Schema Registry:解决版本兼容问题

为了解决Schema变更的问题,业界普遍使用Schema Registry。它就像一个Schema的“Git仓库”,每个版本都有记录。

我建议用Confluent Schema Registry,它支持三种兼容性策略:

策略 说明 适用场景
BACKWARD 新Schema可以读旧数据 新增可选字段
FORWARD 旧Schema可以读新数据 删除字段
FULL 双向兼容 严格约束的场景

避坑指南:我曾经把兼容性策略设为NONE(不兼容),结果上游改了Schema后,下游全部反序列化失败。那次事故让我明白:生产环境至少要用BACKWARD策略,给下游留出升级时间。

4.5 JSON vs AVRO:怎么选?

我直接给结论:

  • 选JSON:业务变化快、字段经常增删、团队对AVRO不熟。
  • 选AVRO:数据量大(日均百亿条以上)、对性能敏感、有完善的Schema管理流程。

如果你实在拿不准,我建议ODS层用JSON,DWD层转AVRO。这样既保留了ODS的灵活性,又享受了AVRO的高性能。

4.6 本章核心知识体系

下面这张图,是我对ODS层数据接入和格式处理的总结。你可以把它当作一个“操作手册”。

ODS层数据接入与格式处理架构图 ODS层数据接入与格式处理架构 数据源 MySQL Binlog APP日志 HTTP API 接入工具 Canal Filebeat Kafka Producer Kafka ODS层 ODS Topic 集群 ods_binlog / ods_log / ods_api 格式处理 JSON 解析 AVRO 序列化 Schema Registry 下游消费 Flink 实时ETL Spark 批处理 核心原则:ODS层保持数据原样,不做聚合清洗;JSON灵活但慢,AVRO高效但需Schema管理

4.7 避坑总结

最后,我把ODS层实战中常见的坑列出来,你遇到了可以直接翻到这一页:

  • 数据丢失:Kafka的acks参数一定要设为all,否则Leader挂了会丢数据。
  • 重复数据:Canal或Filebeat在网络抖动时会重发,下游必须做幂等处理。
  • Schema不兼容:AVRO格式一定要用Schema Registry,并设置BACKWARD策略。
  • 性能瓶颈:JSON解析太慢?试试用AVRO,或者用SIMD加速的JSON解析库(比如simdjson)。

我的习惯:每次上线ODS层任务前,我都会先压测一下。用Kafka自带的kafka-producer-perf-test.sh和kafka-consumer-perf-test.sh,看看吞吐量能不能扛住业务高峰。别等到上线了才发现性能不够,那时候就晚了。