技术探索与实践:我的一次真实踩坑经历

林智
2025-06-26 10:45
阅读 240

背景介绍:为何要讲这个故事?

背景介绍:为何要讲这个故事?

事情得从两年前说起。那会儿我在一家中型互联网公司担任技术团队负责人,主要负责后端架构和团队管理。当时我们正在做一个新的项目——一款面向企业用户的 SaaS 产品,核心功能是数据清洗与可视化分析。

项目初期,技术选型上我们选择了一个比较前沿的方案:用 Kafka 作为消息队列,结合 Flink 做实时流处理。前端是 React 主导的单页应用,后端使用 Spring Boot + PostgreSQL 架构。整体来看,这是一套在业内较为常见的搭配。但问题就在于,我们的业务需求对数据一致性要求极高,同时还需要支持高并发下的复杂查询。

说实话,当时我内心其实有点犹豫:Flink 虽然强大,但对于我们团队来说是个新东西;而 Kafka 的性能虽好,但它在某些场景下的一致性保障是否足够?这些疑问并没有阻止我们推进项目,反而成了后续一系列“踩坑”之旅的开端。

问题描述:现实总比理想骨感

问题描述:现实总比理想骨感

项目进行到中期时,各种挑战开始浮现。最直接的表现就是系统的数据延迟变得越来越严重,甚至有时候出现数据丢失的情况。最初我们以为是代码写得不够健壮,于是花了很多时间优化消费逻辑、增加日志监控、设置重试机制……但效果并不明显。

更糟的是,某天我们在测试环境部署完一个版本之后,发现系统会出现周期性的“数据漂移”现象。比如:同一份原始数据,在不同时间点被处理出来的结果居然不一样。我们做了大量的排查,最后发现问题根源出在 Kafka 消费策略的配置不合理,以及 Flink 的检查点(Checkpoint)机制使用不当。

当时我们采用的是默认的 offset 提交方式,加上 Checkpoint 没有正确开启 Exactly-Once 语义,导致部分消息被重复消费或漏消费,最终影响了数据准确性。这个问题让我们整个开发团队都陷入了被动,产品上线日期也被迫延后。

解决方案:调整架构思路,重审技术选型

解决方案:调整架构思路,重审技术选型

面对这些问题,我们决定暂停迭代开发一周,专门做一次彻底的技术复盘。目标很明确:要么调整当前架构让其稳定下来,要么果断换掉不合适的技术栈。

第一步:重新审视 Kafka 的使用方式

我们首先回顾了 Kafka 的使用情况,重点分析消费者的提交偏移量(offset commit)行为:

  • 手动提交 vs 自动提交:我们之前为了简化流程采用了自动提交,但实际上这种方式存在一定的不稳定性,尤其是在 Checkpoint 间隔较短的情况下容易出错。
  • Exactly-Once 语义:Flink 本身支持 Kafka Source 的 Exactly-Once 语义,但我们之前的配置没有启用相关参数,导致无法保证端到端一致性。

修改后的消费逻辑如下:

Properties props = new Properties();
props.setProperty("bootstrap.servers", "localhost:9092");
props.setProperty("group.id", "flink-group");
props.setProperty("enable.auto.commit", "false"); // 禁用自动提交

KafkaSource<String> source = KafkaSource.<String>builder()
    .setBootstrapServers("localhost:9092")
    .setTopic("input-topic")
    .setStartingOffsets(OffsetsInitializer.latest())
    .setValueDeserializer(new SimpleStringDeserializer())
    .setProperty("partition.discovery.interval.ms", "10000") // 动态发现分区
    .build();

StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.enableCheckpointing(5000); // 启用 Checkpoint,每5秒触发一次
env.setRestartStrategy(RestartStrategies.fixedDelayRestart(3, Time.of(10, TimeUnit.SECONDS)));

env.fromSource(source, WatermarkStrategy.noWatermarks(), "Kafka Source")
   .process(new ProcessFunction<String, String>() {
       @Override
       public void processElement(String value, Context ctx, Collector<String> out) {
           // 处理逻辑
           out.collect(value.toUpperCase());
       }
   })
   .addSink(new MyCustomSink()); // 自定义 Sink,需实现 TwoPhaseCommitSinkFunction 来支持 Exactly-Once

env.execute("Flink Streaming Job");

这里的关键在于我们启用了 Checkpoint,并通过自定义 Sink 实现两阶段提交来确保 Exactly-Once 语义。同时,将 offset 的提交方式由自动改为手动控制,并在 Checkpoint 完成后再提交 offset,以避免重复消费。

第二步:引入状态管理机制优化数据一致性

由于我们的数据需要多次聚合处理,因此引入了状态(State)机制。例如,使用 MapState 存储中间计算结果,配合 RocksDB 后端持久化,保证状态在失败恢复时不会丢失。

我们还优化了窗口函数的使用方式,把原来的滚动窗口改成了带有触发器的滑动窗口,以便更好地控制窗口的处理时机。这部分的改动提升了系统的准确性和资源利用率。

第三步:引入容错设计,提升系统健壮性

我们还在关键组件上增加了熔断机制,比如使用 Hystrix 或 Resilience4j 包裹外部服务调用,防止因为某个服务异常拖垮整个系统。同时,日志埋点更加细致,特别是在消息入队、出队、消费各阶段都记录上下文信息,便于快速定位问题。

踩坑经验:那些年我们一起踩过的坑

踩坑经验:那些年我们一起踩过的坑

坑1:盲目相信“最佳实践”

项目初期我们参考了一些社区推荐的最佳实践文档,比如默认使用 Kafka 的自动提交、Flink 的默认 Checkpoint 间隔等。但这些设置并不一定适用于我们的业务场景。后来才意识到,所谓“最佳实践”,只是通用建议,不能照搬。

小插曲:记得有一次我问一位刚加入团队的同学:“你有没有看过 Kafka Consumer 的 offset 提交机制?”他一脸自信地说:“看过了,自动提交就好。”后来那次生产事故就出在他写的模块……

所以,技术选型和配置务必要贴合自己的实际业务需求,不要迷信权威文档。

坑2:低估了状态管理的复杂性

我们原本以为使用 Flink 的 State 管理很简单,但在实际使用过程中发现,当 State 数据变大之后,RocksDB 的压缩和读取效率下降特别明显。后来我们不得不引入更细粒度的状态分区,以及异步快照(Asynchronous Snapshot)来缓解压力。

坑3:忽略上下游系统的兼容性

另一个问题是 Kafka 和下游数据库之间的同步问题。我们最初采用 RabbitMQ + MySQL 的混合架构,结果 Kafka 消息中的字段结构频繁变更,导致下游消费者经常报错。为了解决这个问题,我们后来引入了 Schema Registry(Apache Avro),统一管理消息格式,并强制上下游服务必须兼容旧版本。

效果总结:从“踩坑”到“稳住”

经过将近两周的修复和优化,系统逐步趋于稳定。以下是改造后的一些关键指标变化:

指标 改造前 改造后
数据延迟 最高峰超过10分钟 稳定在2秒以内
数据一致性 存在漂移 100%一致
系统吞吐量 ~2k msg/sec ~5k msg/sec
异常日志数量 日均几百条 下降到个位数

最重要的是,用户反馈明显好转。产品按时上线,并且在接下来的几次灰度发布中都表现良好。

经验分享:写给同行们的一些建议

系统架构设计-1

技术选型要理性,不要盲目追新

Flink 很强大,但如果你团队对它不熟悉,贸然大规模使用可能会带来很多未知风险。尤其是像 Exactly-Once 这类高级特性,一定要理解清楚再使用。

我们的教训是:不要只看到工具的强大功能,更要评估它的运维成本和学习曲线。

架构设计要有“可逆性”

在初期架构设计时,我们就犯了一个错误:所有模块高度耦合,更换组件非常困难。后来我们花了很大代价来做解耦,比如引入适配层、抽象接口、依赖注入等手段。现在我带团队都会强调一点:任何模块的设计都要考虑未来能否轻松替换

日志和监控是你的救命稻草

别等到线上出了事才想起加日志。我们早期的日志体系非常薄弱,很多问题只能靠猜。后来我们搭建了一整套 Prometheus + Grafana 的监控平台,并接入 ELK 日志分析系统。这套系统不仅帮助我们发现问题,还能用来做容量预估和性能调优。

保持技术敏感性,但也要脚踏实地

技术趋势当然重要,比如现在大家都在聊 AI、云原生、Serverless,但这不代表你要马上全部用上。对于大多数中型项目来说,稳定性 > 性能 > 创新。该用成熟框架的时候就别折腾新轮子,该用老数据库的时候就别强求分布式。


结语:写给自己,也写给你

技术探索的过程,本质上是一个不断试错和修正的过程。很多时候我们以为找到了最优解,结果一上线才发现这只是冰山一角。但正是这些“踩坑”的经历,才让我真正理解什么是工程思维。

如果你现在也在做类似的探索,不妨多思考几个问题:

  • 你的业务真的需要这么高的实时性吗?
  • 当前的技术栈是否能满足未来三年的增长?
  • 团队是否有足够的能力支撑这套架构?

愿你在技术的路上少走弯路,多些沉淀。

Stay Curious, Keep Learning.

评论 0

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