技术探索与实践的一些经验分享

宋洋◇
2025-06-25 20:29
阅读 213

开篇:为什么要写这篇文章?

开篇:为什么要写这篇文章?

作为在互联网公司工作的研发工程师,我们每天都在面对新的技术挑战和业务需求。从刚入职时的新人,到后来参与核心项目、承担关键任务,再到今天能够独立主导一个系统模块的设计与优化,我走过了一条不算平坦但非常值得的路。

在这几年的工作中,我逐渐意识到,光有技术知识是不够的。真正能在实战中发挥作用的,是那些从实际问题出发、不断尝试、迭代改进的实践经验。这篇文章,就是想结合我在某个实际项目中的经历,来聊聊技术探索与实践过程中的一些心得和教训。

也许你在做性能优化,也许你在处理复杂逻辑,也可能你正在面临一次重大的技术选型决策,那么希望我的经验能给你带来一些启发。

项目背景:为什么这个问题非解决不可?

项目背景:为什么这个问题非解决不可?

故事得从2023年的一个项目说起。当时我们团队负责的是公司内部一个重要的数据处理平台。这个平台主要用于支持多个业务线的数据分析和实时报表生成。随着公司业务量的增长,特别是用户行为日志的暴涨,原本还凑合用的系统开始暴露出严重的问题。

具体来说,我们的数据处理流程大致是这样的:

  1. 用户行为日志通过Kafka流入;
  2. 经过Flink进行流式处理,提取部分特征字段;
  3. 最终写入ClickHouse,供上层BI报表使用。

这套架构其实在初期运行得很好。但是到了2023年初,我们发现:

  • Flink作业延迟越来越高,经常堆积几个小时的数据;
  • CPU和内存占用飙升,资源成本急剧上升;
  • 上线新版本变得困难,一个微小的改动也可能引发不可预测的错误;
  • 系统稳定性变差,经常出现反压(Backpressure),甚至作业崩溃的情况。

于是,我们决定对整个流处理流程进行全面的优化与重构。

这不仅仅是简单的“加机器”就能解决问题的事情,它背后涉及到多个维度的技术考量,包括但不限于:

  • Flink作业本身的结构设计
  • 数据分区策略
  • 状态管理与检查点机制
  • 写入ClickHouse的性能瓶颈
  • 资源调度与监控体系

接下来,我想重点分享我们在其中两个关键环节遇到的挑战以及最终的解决方案:一个是Flink流处理链路的优化,另一个是向ClickHouse写入的大批量性能提升。

遇到的第一个大问题:Flink作业频繁反压,处理延迟高

问题现象

我们一开始以为是单纯的流量高峰导致的问题,所以试图通过增加TaskManager数量和调整并发度的方式来缓解。然而,效果并不理想,反而带来了更多的资源浪费——有些节点忙死,有些节点空转。

我们尝试查看Flink的Web UI,发现:

  • 某些算子的吞吐量明显低于预期
  • 输入缓冲区积压严重,特别是在Source和Map算子之间;
  • 反压主要集中在某几个下游算子(比如KeyBy + Aggregation);
  • Checkpoint时间越来越长,有时超过设定的超时时间,导致任务重启。

这时候我们意识到,单纯扩容并不能从根本上解决问题,而是需要深入挖掘链路上的瓶颈点。

解决思路与方案设计

1. 分析热点算子和数据倾斜问题

我们首先在Flink Web UI中找到响应慢的算子,使用内置的累加器功能获取每个subtask处理的数据量,发现某些subtask处理的数据远多于其他subtask。说明出现了数据倾斜(Data Skew)

进一步查看数据分布,发现很多记录的key被集中分配到了少数几个分区(因为我们用了KeyBy)。由于我们聚合逻辑依赖Key的状态保存,这些热点分区处理压力很大,直接导致了整体的延迟。

🔥 这里有个小插曲:刚开始的时候,我们误以为是状态太大导致Checkpoint缓慢,于是试着把StateBackend从RocksDB换成了FsStateBackend,结果发现Checkpoint确实快了,但整体处理延迟并没有改善,说明根本问题还是出在算子本身。

2. 引入两阶段KeyBy分组策略(Local & Global)

为了解决数据倾斜的问题,我们借鉴了社区中的一些经验,采用了“两阶段KeyBy”的方法:

  1. 第一阶段使用KeyBy(key % N)对key做局部哈希,让每个子分区单独计算中间状态;
  2. 第二阶段再将各个中间结果发送到同一个全局KeyBy分区进行最终合并。

这样做的好处是,既避免了单一热点分区的压力,又保证了最终结果的准确性。

举个例子:假设我们要统计不同用户的点击次数。原始做法是 KeyBy(userId),所有相同userId的事件都会进同一个subtask。如果某些用户特别活跃,就会拖慢整体进度。

而改造成两阶段之后:

stream
    .map(...) // 提取基础特征
    .keyBy(keySelectorA) // 局部KeyBy,N个分区
    .process(new LocalAggregateProcessFunction())
    .shuffle()
    .keyBy(keySelectorB) // 全局KeyBy
    .process(new FinalAggregateProcessFunction())

这样一来,热点Key被拆分到不同的局部分区中,负载更加均衡。最终只需要做一个轻量级的合并操作。

3. 增加Buffer缓存和Watermark策略优化

为了应对流量波动带来的瞬时冲击,我们还做了以下几个细节上的优化:

  • 调整Kafka Source的fetch.max.bytesmax.poll.records参数,控制单次拉取的数据量;
  • 在Sink端增加了异步批写入缓冲池,减少IO压力;
  • 使用EventTime模式,并设置合理的Watermark延迟策略,防止迟到数据影响窗口聚合。

4. 检查点与状态后端调优

最后,在状态和检查点方面也进行了如下优化:

  • 将状态后端统一改为RocksDB(因为状态较大),开启增量检查点;
  • 调整state.checkpoints.dir路径到高性能存储上;
  • 减少CheckPoint间隔,同时适当放宽超时时间,避免因偶尔卡顿导致失败重启。

优化后的效果

经过这一轮优化之后,作业的整体表现发生了显著变化:

指标 优化前 优化后
平均延迟 2-3小时 <5分钟
Checkpoint耗时 >10分钟 <1分钟
CPU利用率 90%+ 稳定在70%左右
资源成本 下降约20%

不仅提高了系统的稳定性和可用性,也为后续新增业务逻辑预留了更多空间。


第二个挑战:ClickHouse大批量写入性能不足

解决了Flink侧的处理效率问题后,接下来我们遇到了一个新的难题:数据写入ClickHouse的速度跟不上输出速度,造成Sink成为新的瓶颈。

问题描述

我们的Flink Sink使用的是自定义的ClickHouse JDBC连接器,采用每条记录逐条插入的方式写入数据库。这种做法在早期数据量不大的时候还能凑合,但随着日志量上涨,很快暴露了几个问题:

  • 插入速度慢,大量请求等待提交;
  • ClickHouse的写入QPS达到瓶颈,频繁报错“Too many parts”;
  • 数据无法及时落地,BI报表时常显示滞后信息。

解决思路与实践过程

1. 改进写入方式:由单条写入改为批量插入

首先想到的是把单条插入改成批量写入。我们修改Sink的实现,维护一个临时队列,当队列达到一定大小或时间间隔触发时才执行一次Insert操作。

例如,将每次处理1条记录改为累积2000条后再插入,大大降低了网络往返开销和ClickHouse的连接压力。

2. 使用ClickHouse自带的Table引擎:ReplacingMergeTree + Buffer

除了代码层面的改动,我们也重新设计了ClickHouse表结构:

  • 使用ReplacingMergeTree替代MergeTree,自动去重旧数据;
  • 在前端接入层引入Buffer引擎表,先写入Buffer,后台异步刷入主表。

这样一来,既提升了写入效率,又有效减少了Merge过程的CPU负担。

3. 利用Flink-Chunjun等生态组件进行对接

我们调研了社区中比较成熟的Flink连接器,最终选择了基于Apache DolphinScheduler衍生出来的chunjun,它提供了一个完整的Flink to ClickHouse的同步组件。

这个组件支持:

  • 批量写入
  • 并发配置
  • 错误重试机制
  • 自动类型映射转换

使用它之后,我们不再自己维护JDBC连接池和异常处理逻辑,系统更健壮、开发效率更高。

成果对比

指标 旧方案(单条JDBC) 新方案(批量 + chunjun)
写入QPS ~8k/s ~60k/s
错误率 较高(常报错) 接近0
BI展示延迟 数十分钟 实时/分钟级更新
后续扩展性 更容易横向扩展

这项改进使得我们的报表系统得以跟上实时业务节奏,极大提升了用户体验。


总结:技术探索不是选择最“酷炫”的方案,而是选择最“合适”的方案

回顾整个项目过程,我深刻体会到:

  • 不要迷信“标准答案”。很多时候所谓最佳实践,其实是在特定场景下有效的。我们之所以能取得好的效果,是因为我们敢于质疑现有架构,敢于根据真实业务做定制优化。

  • 数据驱动是王道。每一次调优之前都要有明确的指标支撑,不能拍脑袋。Flink的UI、Prometheus监控、ClickHouse的日志分析工具都帮了我们很大的忙。

  • 不要忽视“基础设施”的重要性。良好的监控体系、稳定的CI/CD流程、完善的日志追踪机制,是持续交付高质量服务的基础。

  • 技术方案要具备演进性。我们没有一次性追求极致性能,而是阶段性推进,确保每一步都能验证可行性、快速反馈调整。这种方式比一开始就堆砌各种高级特性要稳健得多。


给读者的一些建议

如果你正在或者即将面对类似的性能问题,以下几点建议或许对你有所帮助:

1. 从简单开始,逐步深入

复杂的系统往往是从小的模块开始搭建起来的。不要一上来就想着要用Spark、Flink、Presto等全链路架构,先把最小可运行模型跑通,然后再考虑横向拓展。

2. 合理利用日志和监控工具

一定要建立清晰的监控体系。我见过太多团队在排查问题时完全靠“猜”,这是低效且危险的做法。Flink原生的Metrics收集、Prometheus、Grafana等工具都很强大,善用它们可以让你事半功倍。

3. 重视测试与压测环境

线上环境永远是最真实的,但我们不能随便在线上试验。一个模拟真实数据流的小型测试环境,对我们理解系统瓶颈至关重要。可以用Docker快速部署一套本地集群,方便验证。

4. 保持开放心态,善用社区资源

别总想着重复造轮子。像Flink的Table API、ClickHouse的JDBC connector、甚至现成的ETL工具如Airbyte、Chunjun,都是非常成熟的产品。站在巨人肩膀上,才能看得更远。

5. 关注性能之外的质量保障

性能优化很重要,但千万别忽略了系统的稳定性、可观测性和容错能力。有时候一个小错误可能会导致整个作业瘫痪。所以:

  • 设计好断点续传机制;
  • 做好异常兜底处理;
  • 设置合理的告警规则;
  • 加强自动化运维能力;

这些都是构建一个成熟系统的必要前提。


结语:技术探索是一场马拉松,而不是短跑冲刺

说实话,在这场优化的过程中我也曾焦虑、也曾怀疑自己的判断是否正确。尤其是在一次次尝试失败后还要坚持下去,那段时间真的挺难熬。

但回头再看,正是这些挑战让我成长了许多。技术和工程从来不是非黑即白的选择题,而是一个不断试错、迭代的过程。只要你愿意沉下心来去思考问题本质,相信大多数情况下都能找到一个相对平衡的解法。

希望我的分享能给你带来一些共鸣,也欢迎你在评论区跟我交流你的实战经验和踩过的坑。毕竟,技术的成长从来都不是一个人的事,而是一群人互相照亮的结果。

一起加油吧!💪

评论 0

最热最新
暂无评论
匿名用户Lv.1
0
影响力
0
文章
0
粉丝