技术探索与实践:我的一次真实踩坑经历
背景介绍:为何要讲这个故事?

事情得从两年前说起。那会儿我在一家中型互联网公司担任技术团队负责人,主要负责后端架构和团队管理。当时我们正在做一个新的项目——一款面向企业用户的 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 |
| 异常日志数量 | 日均几百条 | 下降到个位数 |
最重要的是,用户反馈明显好转。产品按时上线,并且在接下来的几次灰度发布中都表现良好。
经验分享:写给同行们的一些建议

技术选型要理性,不要盲目追新
Flink 很强大,但如果你团队对它不熟悉,贸然大规模使用可能会带来很多未知风险。尤其是像 Exactly-Once 这类高级特性,一定要理解清楚再使用。
我们的教训是:不要只看到工具的强大功能,更要评估它的运维成本和学习曲线。
架构设计要有“可逆性”
在初期架构设计时,我们就犯了一个错误:所有模块高度耦合,更换组件非常困难。后来我们花了很大代价来做解耦,比如引入适配层、抽象接口、依赖注入等手段。现在我带团队都会强调一点:任何模块的设计都要考虑未来能否轻松替换。
日志和监控是你的救命稻草
别等到线上出了事才想起加日志。我们早期的日志体系非常薄弱,很多问题只能靠猜。后来我们搭建了一整套 Prometheus + Grafana 的监控平台,并接入 ELK 日志分析系统。这套系统不仅帮助我们发现问题,还能用来做容量预估和性能调优。
保持技术敏感性,但也要脚踏实地
技术趋势当然重要,比如现在大家都在聊 AI、云原生、Serverless,但这不代表你要马上全部用上。对于大多数中型项目来说,稳定性 > 性能 > 创新。该用成熟框架的时候就别折腾新轮子,该用老数据库的时候就别强求分布式。
结语:写给自己,也写给你
技术探索的过程,本质上是一个不断试错和修正的过程。很多时候我们以为找到了最优解,结果一上线才发现这只是冰山一角。但正是这些“踩坑”的经历,才让我真正理解什么是工程思维。
如果你现在也在做类似的探索,不妨多思考几个问题:
- 你的业务真的需要这么高的实时性吗?
- 当前的技术栈是否能满足未来三年的增长?
- 团队是否有足够的能力支撑这套架构?
愿你在技术的路上少走弯路,多些沉淀。
Stay Curious, Keep Learning.

评论 0