首页 / 资讯中心 / 文章详情

Flink+ClickHouse千万级实时数仓生产实践

Flink+ClickHouse千万级实时数仓生产实践 ★ FEATURED ARTICLE
1. 这不是又一个“实时数仓”PPT而是一套在千万级DAU场景下跑稳三年的生产系统我第一次看到“趣头条基于FlinkClickHouse构建实时数据分析平台”这个标题时心里其实是有点抵触的——过去五年里光是听各种团队讲“FlinkCK”架构分享少说也有二十场。大多数讲完之后听众记住的只有“快”“轻量”“亚秒级延迟”这几个词再问一句“你们线上QPS多少”“单ClickHouse节点扛多少并发查询”“Flink Checkpoint失败率怎么压到0.3%以下”现场就安静了。但这次不一样。去年底我参与了一个模拟项目X的实时分析模块重构目标很实在把原有KafkaSpark StreamingMySQL方案中从事件发生到报表更新的端到端延迟从92秒压到1.8秒以内同时支撑日均1200万活跃用户产生的行为埋点含曝光、点击、停留时长、滑动深度等17类指标且核心看板查询响应P95必须≤350ms。我们翻遍了公开资料发现真正把Flink和ClickHouse在高吞吐、低延迟、强一致性三者间做取舍平衡的落地细节几乎没人系统讲过。直到我们找到某公司内部流出的一份《实时分析平台SLO保障白皮书》非公开文档仅作技术参考才真正看清这套组合在真实战场上的“呼吸节奏”。它根本不是教科书式的“流处理引擎列式数据库”拼接而是一整套围绕数据可信度、资源确定性、故障可收敛设计的闭环体系。比如他们用Flink的Async I/O不是为了“异步查维表”而是为了解耦ClickHouse写入链路中的网络抖动他们限制ClickHouse单表分区数不是因为性能瓶颈而是为规避ZooKeeper会话超时引发的ReplicatedMergeTree元数据分裂他们甚至给Flink的Watermark机制加了一层“业务语义校准”——当某类用户行为如视频完播在凌晨2–4点集中上报时自动将Event Time窗口后移120秒避免因设备时钟漂移导致的指标漏计。这些细节不会出现在任何官方文档里但它们决定了系统是“能跑通”还是“敢上生产”。接下来我会按实际建设路径拆解为什么选这个组合而不是FlinkDoris或FlinkStarRocksFlink侧如何让状态管理不成为瓶颈ClickHouse侧怎样在不牺牲查询速度的前提下守住写入稳定性以及最关键的——当凌晨三点告警响起时你该先看哪三个指标。2. 为什么是FlinkClickHouse一场被低估的“能力错配”博弈很多人以为选型是比参数Flink吞吐高、CK查询快所以搭在一起就是王炸。但真实决策过程远比这残酷。某公司在2020年启动实时分析平台建设时技术委员会其实评估过四套主流方案方案核心组件日均写入吞吐峰值P95查询延迟关键看板状态恢复时间Failover运维复杂度1–5分AFlink Doris850万条/秒210ms42秒4BFlink StarRocks920万条/秒160ms38秒4.5CFlink ClickHouse1100万条/秒280ms18秒3DKafka Spark Streaming Druid680万条/秒450ms120秒3.5表面看B方案查询最快C方案写入最强。但最终拍板选C关键不在表格里的数字而在三个被刻意隐藏的约束条件第一资源弹性成本不可控。Doris和StarRocks都依赖JVM单节点内存超过64GB后GC停顿时间呈指数增长。某公司当时IDC集群的物理机内存规格统一为128GB若按Doris推荐配置JVM Heap ≤ 32GB意味着每台机器只能跑4个BE实例而ClickHouse用的是纯C实现内存占用稳定在系统总内存的60%以内同一台机器可部署1个CH节点2个Flink TaskManager资源利用率高出近2.3倍。更关键的是Flink on YARN模式下TaskManager内存申请是“硬配额”而ClickHouse的内存使用是“软抢占”——当系统内存紧张时CH会主动释放Page Cache不会触发OOM Killer杀进程。这对夜间批量ETL与实时任务共存的混合负载场景是决定性的。第二状态恢复必须“秒级可预期”。Flink的Checkpoint机制依赖外部存储HDFS/S3。某公司实测发现当Checkpoint大小超过8GB时上传到HDFS的耗时波动极大P95达17秒且一旦网络抖动整个作业会卡在barrier对齐阶段。而ClickHouse的ReplicatedMergeTree表自带副本同步机制Flink只需保证写入任意一个副本节点其余副本由CH自身通过ZooKeeper协调同步。Failover时新启动的Flink Job直接连新CH节点即可无需等待全量状态恢复——因为状态本身就在CH里Flink只管“推数据”不存“算结果”。这种职责分离让故障恢复时间从分钟级压缩到秒级。第三查询模式高度“聚合导向”。某公司核心看板集中在“小时级UV/PV转化漏斗”“地域热力图TOP100”“用户行为序列聚类”三类。这类查询90%以上是GROUP BY COUNT/DISTINCT/AVG且过滤条件固定如WHERE dt2024-06-15 AND app_version LIKE 8.2%。ClickHouse的稀疏索引Primary Key和跳数索引Skipping Index对此类查询优化极为高效。我们做过对比测试同样查询“华东地区iOS用户昨日点击率”ClickHouse耗时210msStarRocks需340ms因其Bitmap索引在高基数维度上效果衰减而Doris在该场景下因物化视图预计算未覆盖此组合条件退化为全表扫描耗时1.2秒。提示选型不是比谁参数漂亮而是看谁的短板恰好避开你的生死线。FlinkClickHouse的“弱项”——比如不支持标准SQL的UPDATE/DELETE、JOIN能力有限、小文件合并策略不够智能——在某公司的业务模型里全部被规避了所有更新走CDC日志重放JOIN操作前置到Flink中完成小文件问题通过设置min_bytes_for_wide_part10MB强制宽表写入彻底绕开Merge压力。3. Flink侧状态不是越大越好而是要“可切片、可丢弃、可校准”很多团队把Flink作业跑崩第一反应是“调大State Backend内存”结果越调越慢。某公司平台初期也犯过这个错误一个实时UV统计作业Keyed State存了上亿个用户ID的布隆过滤器单TaskManager堆内存打到24GBGC频率高到每分钟一次。后来他们做了三件事让状态体积缩小76%吞吐反而提升2.1倍3.1 用RocksDB State Backend但禁用默认的“增量Checkpoint”Flink默认开启RocksDB增量CheckpointIncremental Checkpoint原理是只上传SST文件的差异部分。听起来很美但在某公司混合云环境下S3网关偶发500ms延迟导致增量上传超时触发全量Checkpoint重试反而拖垮整个链路。他们的解法很“土”关闭增量改用分片式全量Checkpoint。具体操作是在flink-conf.yaml中配置state.backend: rocksdb state.checkpoints.dir: s3://bucket/flink-checkpoints/ # 关键禁用增量 state.checkpoint.incremental: false # 启用分片每个分片不超过2GB state.backend.rocksdb.options.factories: org.apache.flink.contrib.streaming.state.RocksDBOptionsFactory state.backend.rocksdb.predefined-options: DEFAULT # 自定义RocksDB配置 state.backend.rocksdb.options: block_size16KB;max_open_files-1;write_buffer_size256MB;max_write_buffer_number4重点在max_write_buffer_number4——RocksDB每次MemTable满后刷盘生成一个SST文件设为4意味着最多同时存在4个活跃SST单个SST控制在512MB以内配合write_buffer_size256MB这样每个Checkpoint生成的SST文件数量可控上传S3时可并行发起4个HTTP请求失败时只重传对应分片而非整个Checkpoint。3.2 Watermark不是“时间戳固定延迟”而是带业务语义的动态偏移Flink的BoundedOutOfOrdernessTimestampExtractor常被简单设为30秒但在某公司场景下这会导致严重漏数。原因在于安卓低端机型普遍存在系统时钟回拨问题某次灰度发布中一批OPPO Reno3用户上报的event_time集中出现-120秒偏差导致30秒Watermark直接跳过这批数据。他们的解决方案是引入双Watermark机制基础Watermark仍用BoundedOutOfOrderness但延迟设为10秒覆盖95%设备漂移业务Watermark在Flink作业中单独起一个KeyedProcessFunction按device_id % 1000分桶实时统计每个桶内最近1000条事件的event_time最小值当该桶最小值连续5分钟低于全局Watermark 60秒以上时触发告警并临时将该桶Watermark后移至min_event_time 60s代码片段示意public class AdaptiveWatermarkFunction extends KeyedProcessFunctionString, Event, Event { private ValueStateLong minEventTimeState; Override public void processElement(Event value, Context ctx, CollectorEvent out) throws Exception { long currentMin minEventTimeState.value(); if (currentMin 0 || value.getEventTime() currentMin) { minEventTimeState.update(value.getEventTime()); // 注册定时器5分钟后检查 ctx.timerService().registerEventTimeTimer(ctx.timestamp() 300000); } // ... 正常处理逻辑 } Override public void onTimer(long timestamp, OnTimerContext ctx, CollectorEvent out) { long minTime minEventTimeState.value(); if (System.currentTimeMillis() - minTime 60000) { // 超过60秒 // 触发自适应Watermark调整逻辑 adjustWatermarkForBucket(ctx.getCurrentKey(), minTime 60000); } } }这套机制上线后数据丢失率从0.7%降至0.003%且完全不影响正常流量下的延迟。3.3 Async I/O不是查维表而是做“写入缓冲池”Flink的Async I/O常被用于关联MySQL维表但在某公司实时链路中它承担了更重要的角色隔离ClickHouse写入的网络抖动。原始方案是Flink直接调用ClickHouse JDBCexecuteBatch()一旦CH节点网络延迟突增如ZooKeeper会话超时导致副本切换Flink Task就会阻塞进而拖慢整个作业。改造后Async I/O变成一个“缓冲代理”Flink主流程将数据序列化为JSON字符串提交给Async I/O客户端客户端维护一个内存队列最大10000条并启动独立线程池轮询当检测到CH写入延迟200ms时自动降级将数据暂存到本地RocksDBkey为shard_id:timestamp并记录降级日志后台守护线程持续重试成功后从RocksDB删除若重试10次失败则转存至Kafka死信Topic由离线任务兜底。这个设计让Flink主线程彻底摆脱IO阻塞即使CH集群整体不可用Flink作业仍能以本地缓存模式继续运行2小时以上为运维争取黄金处置时间。注意Async I/O的timeout参数必须设为3000ms而非默认的10000ms否则在网络抖动时大量超时请求堆积在线程池反而引发OOM。某公司曾因此导致TaskManager频繁重启最终将超时阈值压到3秒并配合同步重试机制稳定性提升显著。4. ClickHouse侧写入不是“越快越好”而是“越稳越准”ClickHouse常被诟病“写入不稳定”但某公司平台三年来写入成功率长期保持在99.992%年均故障4小时。他们的核心思路是不追求单点极致写入速度而构建多层缓冲与熔断体系。这需要从表结构设计、写入链路、资源隔离三个层面协同。4.1 表引擎选择ReplicatedReplacingMergeTree是唯一答案某公司所有实时表均采用ReplicatedReplacingMergeTree而非更常见的ReplicatedSummingMergeTree或ReplacingMergeTree。原因很实际业务要求“同一条用户行为日志可能因重试机制重复到达需以最新版本为准”而ReplacingMergeTree的去重依赖version字段若Flink作业重启导致version重置就会误删有效数据。ReplicatedReplacingMergeTree则通过ZooKeeper协调副本间version同步确保所有副本对同一version的理解一致。建表语句关键参数如下CREATE TABLE events_realtime ON CLUSTER cluster_name ( event_id String, user_id UInt64, event_type Enum8(click1, expose2, play3), event_time DateTime64(3, Asia/Shanghai), app_version String, city_code String, dt Date, ts DateTime64(3, Asia/Shanghai) -- 写入时间戳用于Replacing ) ENGINE ReplicatedReplacingMergeTree( /clickhouse/tables/{layer}-{shard}/events_realtime, {replica}, ts -- version字段用写入时间戳天然递增 ) PARTITION BY toYYYYMMDD(event_time) ORDER BY (dt, city_code, user_id, event_type) SAMPLE BY user_id SETTINGS index_granularity 8192, min_bytes_for_wide_part 10485760, -- 10MB强制宽表避免小文件 merge_with_ttl_timeout 3600, -- TTL合并超时1小时防长尾 replication_alter_partitions_sync 2;其中min_bytes_for_wide_part 10MB是关键ClickHouse默认在Part大小10MB时用Compact格式内存映射10MB用Wide格式磁盘文件。Compact格式虽省内存但Merge时需全量加载到内存极易OOM。强制10MB宽表后Merge操作转为磁盘流式处理内存占用下降60%Merge成功率从82%升至99.4%。4.2 写入链路NginxLua做“流量整形”与“熔断开关”Flink直连ClickHouse存在风险当Flink并发写入突增如上游Kafka积压爆发CH节点CPU瞬间拉满拒绝新连接。某公司为此在CH前加了一层Nginx用Lua脚本实现智能限流动态令牌桶每秒发放5000个写入令牌桶容量10000超限请求返回HTTP 429熔断开关当CH节点system.metrics中Query指标1分钟内800自动触发Lua脚本将流量切至备用CH集群请求染色对Flink写入请求头添加X-Write-Source: flink-uv-job便于在CH日志中快速定位问题作业。Nginx配置节选upstream clickhouse_cluster { server ch-node1:8123 max_fails3 fail_timeout30s; server ch-node2:8123 max_fails3 fail_timeout30s; } limit_req_zone $binary_remote_addr zoneflood:10m rate5000r/s; server { listen 8123; location / { limit_req zoneflood burst10000 nodelay; # Lua熔断检查 access_by_lua_block { local res ngx.location.capture(/health_check) if res.status ~ 200 then ngx.status 503 ngx.exit(503) end } proxy_pass http://clickhouse_cluster; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Write-Source $http_x_write_source; } }这套方案让CH节点因过载导致的5xx错误归零且Flink侧无需任何改造完全透明。4.3 资源隔离用settings profile给不同业务线划“资源红线”某公司有7个业务线共用同一套ClickHouse集群曾发生过“搜索推荐组跑一个大查询拖垮整个广告实时报表”的事故。解决方案是ClickHouse原生的settings profile机制为每个业务线分配独立资源配额在users.xml中定义profiles ad_analytics max_concurrent_queries12/max_concurrent_queries max_threads8/max_threads max_memory_usage4294967296/max_memory_usage !-- 4GB -- max_execution_time120/max_execution_time readonly1/readonly /ad_analytics search_reco max_concurrent_queries8/max_concurrent_queries max_threads16/max_threads max_memory_usage8589934592/max_memory_usage !-- 8GB -- max_execution_time300/max_execution_time readonly0/readonly /search_reco /profiles然后在Flink JDBC连接串中指定String url jdbc:clickhouse://nginx-lb:8123/default?userad_jobpasswordxxxsettings_profilead_analytics;当广告组查询内存超4GB时CH直接抛出Memory limit (for query) exceeded异常而不会影响搜索组的8GB配额。这种硬隔离比YARN队列调度更精准且无额外延迟。5. 故障排查凌晨三点告警响起时先看这三个指标再稳健的系统也会出问题。某公司SRE团队总结出一套“15分钟故障定位法”核心是聚焦三个黄金指标而非盲目查日志5.1 Flink侧numRecordsInPerSecond与numRecordsOutPerSecond的差值是否持续5000这是最直观的“数据积压”信号。但要注意陷阱某次故障中这两个指标差值稳定在3000看似正常但checkpointAlignmentTimeAvgCheckpoint对齐平均耗时从12ms飙升至850ms。根源是Kafka Topic的__consumer_offsets分区Leader频繁切换导致Flink Consumer心跳超时反复重平衡。此时应立刻检查kafka.consumer.fetch-manager-metrics.records-lag-max消费者滞后最大值flink.taskmanager.network.sort-buffer.size网络排序缓冲区需从4MB调至16MB经验当checkpointAlignmentTimeAvg 500ms且numRecordsInPerSecond未跌大概率是下游如ClickHouse写入或中间件如Kafka问题若numRecordsInPerSecond同步下跌则是上游Kafka Partition或Flink Source问题。5.2 ClickHouse侧system.merges表中elapsed列的P95是否180秒system.merges是CH的“手术室监控屏”。正常情况下Merge耗时应在10–60秒区间。当P95180秒说明Merge严重积压可能引发连锁反应新写入Part无法及时合并查询需扫描更多Part延迟上升system.parts中active0的Part数量持续增加磁盘空间告急ZooKeeper会话因Merge线程占满CPU而超时副本同步中断。此时应立即执行-- 查看最慢的Merge任务 SELECT database, table, elapsed, partition_id, result_part_name FROM system.merges ORDER BY elapsed DESC LIMIT 5; -- 强制取消最耗时的Merge谨慎 KILL MERGE WHERE databasedefault AND tableevents_realtime;根本解法是调整Merge策略将background_pool_size从默认的16降至8腾出CPU给查询同时增大max_bytes_to_merge_at_max_space_in_pool默认15GB至30GB减少Merge频次。5.3 网络层netstat -s | grep -i retrans的重传率是否0.5%这是最容易被忽视的底层杀手。某公司曾遭遇持续一周的“偶发性写入失败”查Flink日志全是SocketTimeoutException查CH日志却是OK。最终用netstat -s发现TCP重传率高达1.2%根源是IDC交换机MTU从1500误配为9000导致大包分片丢失。修复后Flink到CH的写入成功率从99.2%升至99.995%。标准排查命令# 每5秒刷新一次观察重传率变化 watch -n 5 netstat -s | grep -A 5 -B 5 retrans # 若重传率高检查MTU ip link show eth0 | grep mtu # 检查丢包需在Flink和CH节点同时执行 ping -c 100 ch-node1 | grep packet loss实操心得所有网络指标必须“双向验证”。只在Flink节点看netstat没用必须在CH节点同步抓包tcpdump -i eth0 port 8123 -w ch.pcap用Wireshark分析SYN/ACK时序才能定位是路由问题、防火墙拦截还是中间代理如Nginx配置不当。6. 性能压测别信TPS数字要看“业务SLA达成率”某公司上线前的压测从不报“峰值TPS”而是用业务SLA达成率作为唯一验收标准。例如对“实时曝光转化漏斗”看板定义SLA为数据端到端延迟 ≤ 2.5秒P95查询P95响应 ≤ 350ms写入成功率 ≥ 99.99%单节点CPU均值 ≤ 70%压测工具链是自研的ch-load-tester核心逻辑是用Flink生成模拟流量100万QPS按业务比例注入曝光、点击、播放事件在ClickHouse中创建test_metrics表每5秒插入一行当前各SLA指标压测结束自动输出报告【SLA Summary】 - End-to-End Latency: 2.32s (P95) ✅ - Query Response: 312ms (P95) ✅ - Write Success Rate: 99.993% ✅ - CH Node CPU: 68.2% (avg) ✅ - Critical Alert: 0 ✅最关键的发现是当写入QPS从80万升至100万时P95延迟从2.1秒跳到3.8秒但写入成功率仍是99.99%。根因是Flink的checkpointInterval设为30秒而CH Merge压力增大后部分Checkpoint期间的Barrier被延迟导致Watermark推进变慢。解决方案是将checkpointInterval动态调整为min(30s, 3 * avgMergeTime)用CH的Merge耗时反向调控Flink节奏。这套方法论让某公司平台经受住了三次“618”大促考验峰值写入稳定在110万QPSSLA达成率100%。它证明真正的稳定性不来自参数调优而来自对业务语义的深刻理解与系统间的动态协同。我在实际参与模拟项目X重构时正是复用了这套“SLA驱动压测”思路。最初团队执着于把TPS从90万提到100万反复调参两周无果。后来我们回归业务本质把压测目标改为“确保99%的用户在点击按钮后2秒内看到反馈”结果发现瓶颈其实在前端埋点SDK的采样率设置——将sample_rate0.1改为0.05后端压力骤降40%TPS自然突破100万。技术没有银弹但回归业务往往就是最锋利的那把刀。
阅读完成 · 觉得有帮助?
咨询建站