1. 为什么把实时数仓项目落在腾讯云:架构选型与云资源规划
1.1 实时数仓到底解决什么问题
先说项目背景。当时业务方提了一个很现实的需求:数据大屏上的指标不能再看T+1的离线报表,老板打开大屏就想看到当前这一分钟的成交金额、订单量、活跃用户数,运营同学要实时看各渠道的转化漏斗。如果继续用离线数仓跑Hive SQL,数据延迟至少半小时以上,根本没法用。于是实时数仓这个项目就提上了日程。
实时数仓和离线数仓最大的区别在于数据处理延迟。离线数仓按天、按小时调度,适合批量加工历史数据;实时数仓要处理的是流式数据,数据从产生到最终可查询,延迟要求通常控制在秒级到分钟级。这个项目最终的目标是:业务库数据变更后,最迟5分钟内同步到分析库,核心指标延迟控制在30秒以内。
当时考虑过自建机房、混部其它云厂商,最终选了腾讯云,原因比较实际:团队对腾讯云的VPC、CVM、CDB这些产品比较熟,后续要打通企业微信告警也方便;CVM按量计费和竞价实例在测试阶段能省不少钱;另外腾讯云容器镜像服务(CCR)配合TKE,后续扩缩容会轻松很多。对于中小团队来说,云上部署实时数仓的性价比明显高于自建机房。
1.2 云上组件选型:自建还是托管
这是项目初期争论最多的地方。实时数仓的核心链路无非四块:数据接入、消息缓冲、实时计算、OLAP分析。
- 数据接入:业务库是MySQL,需要在腾讯云数据库CDB上开启binlog,用Canal订阅增量变更。
- 消息缓冲:选了Kafka,版本2.8。自建在CVM上,没用CKafka托管,原因有两个:一是托管版按分区数和流量计费,测试阶段数据量不大时候价格不划算;二是团队需要自己管理Kafka的Topic分区策略,为后面的实时链路调优留出空间。
- 实时计算:Flink 1.13,自建集群。当时也考虑过腾讯云流计算Oceanus,但团队对Flink SQL已经有一定积累,直接在CVM上部署更灵活,改代码、看日志、调参数都不受平台限制。
- OLAP分析:选型时对比了ClickHouse、Doris、StarRocks。最终选了ClickHouse,因为查询性能好、部署简单,而且团队里有人之前用过,踩坑成本低。Doris在实时更新模型上更好,但当时版本升级快,社区资料少,怕踩坑没人能问。
整个架构串起来就是:MySQL binlog -> Canal -> Kafka -> Flink SQL -> ClickHouse,Redis作为维表缓存和结果缓存。这套链路在业界已经很成熟,每个组件都有大量实践案例,踩坑时比较容易找到解决方案。
1.3 服务器与网络规划
资源规划上,测试环境用了3台CVM,配置4核8G,系统盘50G SSD,数据盘200G高性能云硬盘。生产环境起步是5台8核16G,后面按负载再扩容。这里有一个教训:Kafka和ClickHouse对磁盘IO要求很高,千万别图便宜用普通云硬盘,最好上SSD。刚开始测试环境用普通云硬盘,Kafka在高写入下磁盘IO直接打满,延迟飙到几十秒,后来换了SSD才稳定。
网络规划上,所有组件部署在同一个VPC内,通过内网IP互通,不暴露公网端口。安全组只开放必要的端口给应用服务器,比如Kafka的9092只允许Flink节点访问,ClickHouse的8123和9000只允许报表服务和数据开发机访问。测试环境为了方便,一度把端口全放开,结果被扫描端口撞库,Redis被种了挖矿木马,这是血的教训。后面全部收紧了安全组策略,Redis也关了公网访问。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 实时数仓的分层设计与数据流向:从业务库到分析报表
2.1 四层模型怎么落到实时链路里
如果没有离线数仓经验,可能不太理解为什么实时数仓也要分层。直接用Kafka原始数据算指标,看起来很直接,但业务复杂了根本维护不了。比如今天要算成交金额,明天要加一个退款率指标,如果每次都从原始binlog开始算,逻辑会越堆越乱,出了问题也不好排查。
所以项目里参考了离线数仓的分层思想,把实时链路分成四层:
- ODS层:Kafka中原始binlog和日志数据,不加工,只存储,对应Kafka Topic。
- DWD层:清洗、过滤、维度补充后的明细数据,如订单明细、用户行为明细,也存Kafka。
- DWS层:按业务维度聚合后的结果数据,如每分钟订单金额、各渠道活跃数,存Kafka或直接落ClickHouse。
- ADS层:面向业务应用的数据,按指标需求做轻度加工,ClickHouse里的物化视图和结果表。
这样的好处是:每一层职责清晰,DWD层负责把“脏数据”挡掉,DWS层负责口径统一,ADS层只做最后的结果输出。业务方如果发现指标不对,可以按链路一层一层排查,而不是在一大坨SQL里找逻辑。
2.2 数同步链路:Canal + Kafka
Canal是阿里开源的一个组件,模拟MySQL的从库协议,把自己伪装成binlog的订阅者,从而拿到增量变更数据。部署上,Canal需要单独分配一台机器,或者和Kafka共用,因为它要维持和MySQL的binlog连接,还要把数据推送到Kafka,资源占用不能忽视。
Canal推送的数据格式是JSON,里面包含库名、表名、事件类型(INSERT、UPDATE、DELETE),以及变更前后的数据。这里有个细节:数据量大的表,binlog日志一天能涨到好几G,要确认腾讯云CDB的binlog保留时间,项目里设置的是7天,防止Canal挂了恢复时binlog已经被清理,导致数据丢失。
Kafka的Topic命名我习惯用三层结构,比如ods_order_db_order代表ODS层、order_db库、order表,dwd_order_detail代表清洗后的明细数据。这样在Flink SQL里一眼就能看出数据属于哪一层,排查问题时省很多事。
2.3 Flink SQL与状态管理
DWD到DWS的加工,大部分逻辑用Flink SQL实现。Flink SQL最大的优势是可以像写关系型SQL一样处理流数据,不用手动写Java代码去处理Watermark、窗口、Join这些复杂逻辑。比如实时统计每分钟各渠道的订单金额,一段SQL就搞定了:
sql复制CREATE TABLE dwd_order_detail (
order_id BIGINT,
channel STRING,
amount DECIMAL(10,2),
ts TIMESTAMP(3),
WATERMARK FOR ts AS ts - INTERVAL '5' SECOND
) WITH (
'connector' = 'kafka',
'topic' = 'dwd_order_detail',
'properties.bootstrap.servers' = 'localhost:9092',
'format' = 'json',
'scan.startup.mode' = 'latest-offset'
);
CREATE TABLE ads_channel_gmv (
channel STRING,
window_time TIMESTAMP(3),
gmv DECIMAL(14,2),
PRIMARY KEY (channel, window_time) NOT ENFORCED
) WITH (
'connector' = 'clickhouse',
'url' = 'clickhouse://localhost:8123',
'table-name' = 'ads_channel_gmv'
);
INSERT INTO ads_channel_gmv
SELECT
channel,
TUMBLE_START(ts, INTERVAL '1' MINUTE) AS window_time,
SUM(amount) AS gmv
FROM dwd_order_detail
GROUP BY channel, TUMBLE(ts, INTERVAL '1' MINUTE);
这里要注意Watermark的设置。实际生产里数据到达Flink的时间可能有延迟,比如手机断网恢复后批量上报,或者业务服务器时间不准,如果按照Processing Time处理,统计结果会偏。项目里用事件时间(Event Time)加Watermark,允许5秒延迟,超过延迟的数据就不进本次窗口了。这套逻辑第一次跑的时候看起来简单,但第二天发现某个渠道的GMV明显偏低,排查下来是埋点上报延迟超过5秒的比较多,后来调整到了10秒才基本覆盖。
状态管理是Flink容易出问题的地方。实时去重、多流Join都要用到状态,默认状态后端是内存,数据量大直接OOM。我们换成了RocksDB状态后端,开启增量检查点,同时设置了状态的TTL(过期时间),比如去重状态保留2小时,避免状态无限增长。配置如下:
yaml复制state.backend: rocksdb
state.backend.incremental: true
state.backend.rocksdb.localdir: /data/flink/rocksdb
state.ttl: 2 h
execution.checkpointing.interval: 60s
execution.checkpointing.min-pause: 30s
3. 核心组件部署实录:Kafka、Flink、Redis、ClickHouse的落地细节
3.1 Kafka集群部署与参数调整
Kafka部署本身不难,下载安装包解压改配置就能启动。Cluster配置就三个节点,每个节点的server.properties里broker.id必须唯一,advertised.listeners要配内网IP而不是localhost,否则Flink从其它机器访问不到。
真正考验人的是参数调优。生产环境有个现象:Kafka Topic的分区写不均匀,导致某个broker磁盘被打满。排查后发现是生产者端没有指定key,消息按round-robin策略分发,但某个业务方又手动指定了分区,把大量流量都发到了同一个分区。后面统一规范了生产者配置,要求必须带key,同时把Topic分区数从3扩到了9,让消息能更均匀分布。
Kafka的内存参数也要注意。KAFKA_HEAP_OPTS设置的是JVM堆内存,但我们遇到过一个诡异情况:堆内存明明设置成4G,GC却很频繁,后来发现是操作系统PageCache占用太高,Kafka大量使用PageCache来缓存数据,这是正常现象,不应该为了避免GC去限制PageCache。真正要调整的是num.network.threads和num.io.threads,默认值对高并发场景不够,分别调到8和16,消费延迟明显下降。
注意:Kafka的副本数不要贪多。3节点集群设置副本为3看起来安全,但每个broker上既有主副本又有从副本,网络和磁盘开销都翻倍。测试下来副本数设2足够,配合Kafka的ISR机制,一样能容忍单节点故障。
3.2 Flink任务提交与资源分配
Flink的任务提交,项目里用的是YARN模式,把Flink任务提交到集群上,这样每个任务都有独立的资源配额,互不影响。如果没有YARN,也可以直接Standalone模式,但任务多了以后资源没法隔离,一个任务把内存吃满,其它任务全部卡死。
资源分配上,一个常见的坑:Flink任务里设置parallelism.default=4,但提交命令里没有指定TaskManager的数量和内存,导致任务起来后资源不够,一直处于RESTARTING状态。后来形成了标准模板:
bash复制./bin/flink run \
-m yarn-cluster \
-d \
-p 4 \
-ys 2 \
-yjm 1024 \
-ytm 2048 \
-c com.example.RealTimeGmvJob \
realtime-job.jar
-p是并行度,-ys是每个TaskManager的slot数,-yjm和-ytm分别指定JobManager和TaskManager的内存。实际运行中发现,-ytm 2048在数据量大时经常出现背压,后来调整到4096才正常。如果任务里要做维表Join,还要额外开启异步IO,否则每条数据都要等Redis响应,吞吐根本上不去。
3.3 Redis维表缓存与密码修改后重启失败的排查坑
Redis在这个项目里的定位是维表缓存,存放商品信息、渠道名称等变化频率低的维度数据,让Flink在做Join时不用每次都查MySQL。把维度数据加载到内存后,Join性能能提升几十倍。
这里说一下热搜里经常有人问的“修改Redis密码之后重启一直失败”的问题。我在项目里也踩过同样的坑。排查步骤很重要:
- 先用
redis-cli ping看服务是不是真的没起来,有时候服务已经起来了,但因为配置了requirepass,没有密码的ping会被拒绝,看起来像启动失败。 - 再看日志。Redis默认日志路径可能没配置,去
redis.conf里找logfile,如果值为空,日志会打到stdout,用systemd启动时日志会被journald捕获,执行journalctl -u redis -n 50查看。 - 检查
redis.conf有没有非法字符。我遇到的情况是,从网页上复制密码时带了不可见的空格,粘贴到配置文件里,Redis启动时解析密码失败,直接报错退出。
还有一个非常隐蔽的坑:Redis按守护进程方式启动时会生成pid文件,如果pidfile配置的路径没有写权限,启动到一半就退出。明明改了密码,看起来却像是密码问题,实际上跟密码一点关系都没有。用/usr/local/bin/redis-server /etc/redis/redis.conf前台方式启动,会直接看到具体报错原因。
提示:修改Redis配置前,先备份原配置文件。改完密码后一定用
redis-cli -a 新密码 ping测试,不要只依赖systemd的启动状态判断。
3.4 ClickHouse表引擎选择与物化视图
ClickHouse的表引擎选择直接决定读写性能。项目里最常用的是MergeTree家族:
ReplacingMergeTree:有重复数据时按ORDER BY字段去重,适合存放订单最新状态。SummingMergeTree:按聚合字段预汇总,适合流水数据,如订单金额、商品点击数。AggregatingMergeTree:配合AggregatingFunction使用,适合更复杂的聚合逻辑。
以实时GMV为例,数据是每分钟聚合的,存在ClickHouse时用SummingMergeTree,可以省去查询时的大量SUM操作。
sql复制CREATE TABLE ads_channel_gmv (
channel String,
window_time DateTime,
gmv Decimal(18,2)
) ENGINE = SummingMergeTree()
ORDER BY (channel, window_time);
注意ClickHouse的SummingMergeTree不是插入时就立即合并,而是在后台异步合并,合并完成后重复的ORDER BY字段才会被聚合成一条。所以查询时不要假设表里的数据已经聚合完毕,还是要显示用SUM(gmv)来取数,否则会查出多行重复结果。
物化视图也是一个高效方案。Kafka数据可以直接写入ClickHouse的物化视图,由ClickHouse自己在后台完成聚合,省掉Flink一层计算。但实际项目里我们还是选择用Flink做聚合再写入ClickHouse,原因是物化视图的聚合逻辑一旦出错,排查难度比Flink SQL大得多,而且Flink可以做更复杂的多流Join和状态管理。物化视图适合场景固定、逻辑简单的指标。
4. 实时链路稳定性的关键:监控、告警与数据一致性
4.1 延迟监控的落地方式
实时数仓最怕的是数据延迟,但业务方不会告诉你“链路有积压”,只会说“大屏数字不动了”。如果等业务方发现,事情就大了。所以监控必须从第一天就做。
最直接的监控点是Kafka消费延迟。用Kafka自带的命令行工具可以查看消费组当前消费到的位置:
bash复制kafka-consumer-groups.sh \
--bootstrap-server localhost:9092 \
--describe \
--group flink-gmv-group
LAG列就是积压的消息数。如果这个值持续增长,说明Flink任务处理不过来或者链路有问题。但这个命令需要人工执行,不能作为告警手段。项目里写了一个定时脚本,每分钟执行一次,把LAG值上报到Prometheus,配上AlertManager告警规则,LAG超过5000就发企业微信通知。
Flink自身的延迟也要监控。Flink Web UI里的Watermark能够看出事件时间是否还在往前推进。如果Watermark长时间不动,说明源头数据断了或者窗口没有触发,这时候Kafka LAG可能是正常的,但业务已经产生了延迟。
Canal侧的监控容易忽略。Canal在推送数据到Kafka时,如果Kafka写入变慢,Canal会积累内存队列,时间久了Canal进程会挂掉。所以Canal机器需要监控JVM内存和GC情况,这是整个链路里最薄弱的环节。
4.2 数据积压的排查方法论
说一个实际案例。某天下午,GMV大屏数据停了20分钟没有更新。第一反应是看Kafka LAG,结果发现dwd_order_detail这个Topic的LAG高达10万条,Flink任务还在跑但就是消费不动。
排查过程如下:
- 看Flink Web UI的Backpressure(背压)指标,发现Source节点没有背压,但Keyed Aggregation节点背压严重。
- 看TaskManager日志,发现大量的Full GC日志。
- 用
jstat查看JVM内存,发现旧生代占用达到95%以上,GC之后内存也不下降,说明存在内存泄漏或者状态快速增长。
问题定位到了:某个渠道的埋点错误,把所有用户ID都发成了同一个值,导致Flink的Keyed State按照用户ID分组时,所有数据都堆积在同一个Key上,状态数据全部集中到一台TaskManager,内存直接被打爆。也就是所谓的“热点Key”问题。
解决办法分成临时和长期两种。临时的办法是重启任务,把状态清空,快速恢复链路。长期的方案是给Key加盐(Salted Key),比如地理位置字段,让热点Key分散到多个子Key上,聚合后再去掉盐值合并。加盐的做法会增加一次聚合操作,但能避免单点热点。
数据一致性也是个大问题。Flink的EXACTLY_ONCE语义依赖Checkpoint机制,但只有满足两个条件才能真正做到端到端精确一次:Kafka Source需要记录Offset到状态中,Kafka Sink需要支持事务性写入。Kafka本身支持事务,配置好isolation.level=read_committed后,下游读取端就能只读到已经提交的数据。如果配置不对,会读到重复数据,GMV指标会偏大。
4.3 常见的重复消费场景
深入排查时发现,重复消费在初期非常常见,因为Kafka的Offset提交方式和Flink的Checkpoint机制之间有个理解偏差。
Flink的Kafka Consumer是在Checkpoint完成时提交Offset,而不是消费完一条就提交。如果任务在两次Checkpoint之间崩溃,重启后Kafka的Offset会回退到上一次Checkpoint的位置,导致一部分数据被重复消费。所以ClickHouse的GMV表用了SummingMergeTree加ORDER BY去重,就是为了对抗重复消费带来的重复数据。
但如果业务要求精确到订单级别的去重,比如实时订单数量,就不能依赖聚合表的合并。项目里的做法是:在DWD层对订单ID做去重,用Flink SQL的ROW_NUMBER()加PARTITION BY order_id ORDER BY ts DESC去重,保留最新一条。配合RocksDB状态后端,这个去重逻辑在全天千万级数据量下也没出现明显性能问题。
5. 性能和成本调优的实操记录
5.1 Flink SQL的执行计划优化
Flink SQL写出来能跑,和跑得高效,之间还有很大距离。项目里印象最深的是维表Join的优化。第一版代码直接用了FOR SYSTEM_TIME AS OF这种标准写法,运行时每条数据都同步查询一次Redis,吞吐上不去,CPU忙不过来。
改成异步IO之后查询吞吐提升了6到8倍。具体实现是在Flink TableConfig里设置:
yaml复制table.exec.async-lookup.buffer-capacity: 1000
table.exec.async-lookup.timeout: 30 s
buffer-capacity控制异步请求的并发数,太大会打爆Redis,太小没有并发效果。30秒的超时是针对单个Redis查询的超时,实际场景一个查询不到1毫秒,设30秒只是兜底。
还有一个小技巧:把状态里的维表缓存打开。即使有了异步IO,每一条数据都查询Redis也是浪费,毕竟商品信息不会每秒都变。Flink在1.13版本中,维表Join支持lookup.cache配置:
sql复制WITH (
'connector' = 'jdbc',
'lookup.cache.max-rows' = '10000',
'lookup.cache.ttl' = '10 min'
)
这个配置可以在一定时间内缓存维表查询结果,TTL设置10分钟,既能保证数据不会太旧,又能显著降低Redis查询压力。
5.2 ClickHouse查询性能的持续优化
ClickHouse快是真的,但用不好一样会慢。最开始建表时为了图方便,所有字段都用String类型,查询时大量字段参与WHERE过滤,结果查询响应时间不稳定,从几十毫秒到几秒都有。
后来做了一次认真优化:
- 数值字段改为
Int64、Float64,时间字段改为DateTime,避免不必要的类型转换。 ORDER BY字段的选择和查询维度对齐。GMV明细表的ORDER BY是(channel, window_time),查询也按这个维度过滤,就能利用稀疏索引加速。- 大表做分区。按天分区,查询时用
WHERE window_time >= today(),ClickHouse会直接跳过无关分区。
优化后,单表千亿数据量下的多维度聚合查询,响应时间稳定在200毫秒以内。但要说明的是,ClickHouse更适合宽表明细查询,如果业务需要频繁的精确更新单行数据,需要另找方案。
5.3 成本控制的几个小细节
云上成本是老板最关心的,项目里做过几件事控制成本:
- 测试环境用竞价实例(Spot Instance),白天跑业务,晚上下班后自动关机,光这块就省了40%的机器成本。
- Kafka数据有TTL清理策略,
log.retention.hours设置为24小时,ODS层原始数据没必要存太久,需要长期保存的离线数据走数据湖。 - ClickHouse的
TTL字段可以自动过期老数据,比如明细表设置30天TTL,过期数据按时删除,不用手动写定时任务。
还有一个容易忽略的成本点:Flink任务的Checkpoint会持久化到HDFS或S3。如果Checkpoint太频繁,会产生大量小文件,占用存储空间。需要定期清理过期的Checkpoint。Flink有自动清理机制,但如果用的是腾讯云COS作为Checkpoint存储,要注意生命周期规则里配上过期清理策略。
6. 项目复盘与再进一步的方向
6.1 实时数仓面试与复盘中的高频问题
这个项目做完之后再回头梳理,发现实时数仓的知识点高度集中,面试也好、内部答辩也好,核心问题逃不过这四类:
第一类是架构设计:实时数仓为什么需要分层?离线数仓和实时数仓的区别是什么?这类问题考的是对体系的理解。回答的关键是把“为什么不用一个Kafka Topic直接算所有指标”的思考过程讲清楚。
第二类是精确一次语义:Flink是怎么做到端到端Exactly Once的?这类问题要能画出Checkpoint和两阶段提交的流程,说清楚Source端状态记录、算子端状态快照、Sink端事务提交这三个环节的关系。
第三类是数据处理:Watermark、窗口、状态TTL、数据倾斜怎么解决?这类问题一定要结合项目里的真实场景,比如我们遇到的埋点延迟和热点Key问题,光背概念没用。
第四类是选型对比:ClickHouse为什么比Doris好?Kafka为什么不用腾讯云托管版?这类问题没有标准答案,关键是能说清楚当时场景下的取舍理由。只要逻辑自洽、有实测数据支撑,面试官通常都会认可。
6.2 从单机Demo到集群化部署的演进思路
这个项目最开始的版本其实是在一台腾讯云轻量应用服务器上搭的,所有组件都跑在一台2核4G的机器上,Canal、Kafka、Flink、ClickHouse全堆在一起,动不动就内存爆掉。但那段时间反而最有价值,因为能快速理解整个链路是怎么转起来的。
从单机到集群,我认为有几个里程碑:
- 组件拆分。把Kafka、ClickHouse、Flink分别部署到独立机器,防止资源互相抢占。
- 引入资源管理。Flink从Standalone模式切换为YARN模式,让任务可以独占资源,也方便不同任务之间的资源隔离。
- 配套监控体系。机器上安装了Node Exporter,配合Prometheus和Grafana,把CPU、内存、磁盘IO、Kafka LAG、Flink背压这些指标统一监控起来。
- 数据备份与高可用。ClickHouse做了副本表,Kafka把副本数从1调到2,Canal部署了两个实例,一组挂了另一组能顶上去。
演进过程中最大的体会是:不要在一开始就追求完美架构。先把链路跑通,让业务看到实时数据的效果,再逐步迭代优化。如果一开始就上复杂的K8s集群、多机房容灾,项目大概率会在环境搭建阶段就流产。
6.3 实时数仓的下一步演进
项目上线后,团队讨论过几个后续方向。一个是把Flink的批流一体能力用起来,把离线任务和实时任务统一到同一套Flink SQL里,减少维护两套代码的成本。另一个是引入数据湖(Iceberg或Hudi),实现湖仓一体,让实时数仓的数据也能回溯到历史状态,而不仅仅保留最近几天。
但这些方向目前还在评估阶段。以我们团队现在的数据量和业务复杂度,当前这套“Kafka + Flink + ClickHouse”的架构至少还能顶两年。技术的价值在于解决当下问题,不在于追新。
我个人在这轮项目中最大的收获,不是把某项技术用得多精通,而是建立了一种“链路思维”:不再只盯着单个组件怎么用,而是从数据产生到最终消费,全链路地理解延迟、一致性问题出在哪个环节,如何用监控和数据验证来判断问题。这种思维方式,换任何技术栈都适用。
