先说个反直觉的事。我入行大数据开发三年,前两年几乎把所有精力都花在追新框架上——谁出个新版本马上更新,谁出了个新概念必须聊两句。直到上个月,我们线上一个 Flink 实时任务因为 checkpoint 连续失败,导致 Kafka 消费位点积压了 8000 万条,这事直接让我背上一个 P0。复盘的时候才发现,问题根本不在 Flink 本身,而是我对它背后的状态后端和网络模型理解太浅。从那一刻起我意识到,与其天天纠结 Spark 好还是 Flink 好,不如先把几个关键维度吃透:延迟模型、状态管理、恢复粒度、吞吐成本、生态耦合度。
先说延迟模型,很多文章张口就是“Flink 是毫秒级,Spark 是秒级”,但实际你很难直接对比。Spark Structured Streaming 默认是微批执行,你可以把 trigger 调到 100ms,也能到 500ms,但吞吐一上来 CPU 直接打满,而且微批的 overhead 占总耗时的比例会飙升。我测过同一个业务逻辑,在同样 4 核 8G 的容器里,Spark 微批 1 秒跑 18000 条/秒,Flink 用 1 秒 checkpoint、事件时间处理能到 23000 条/秒,但延迟从 900ms 降到 200ms。问题在于 Flink 的延迟优势不是白来的,它需要你配置好网络缓冲:taskmanager.memory.network 默认是堆外内存的 10%,如果你 Kafka 分区多、单条消息大,这个值不调大会出现反压。而 Kafka Streams 的延迟比很多人预想得低,因为它直接跑在应用进程里,没有独立的集群调度开销,但代价是如果你的 topology 比较复杂,局部反压会直接卡住整个 Processor 的线程池。
再说状态管理,这是三个引擎最拉开差距的地方。Flink 的 state 默认存在 RocksDB,但很多人不知道 RocksDB 的读性能跟你的 key 分布强相关,我们压测时发现 value 大于 1KB 的场景下,RocksDB 的读写吞吐会掉到纯内存 HashMapStateBackend 的 40% 左右。而且 Flink 的 checkpoint 上传如果走 S3,要小心 AWS S3 的 ListObjects 请求限速,默认是每秒 3500 个 LIST 请求,当你的 state 文件数超过 2 万个时,checkpoint 时长会指数级上升。Spark 的状态更新其实本质是 partition 级别的 updateStateByKey,它做不到 key 级别的事务处理,所以在精确一次语义上,Spark 更依赖“输出幂等”而不是“计算幂等”。Kafka Streams 的状态存储默认是本地 RocksDB 加 Kafka changelog topic,这个设计很优雅,但你在生产环境必须监控 changelog topic 的写入放大——一次 update 会同时写 state store、写 changelog、写下游 topic,磁盘 I/O 开销是 Flink 的 1.4 倍左右(我们同硬件上测的)。
第三个维度是恢复粒度,这是大部分人没真正对比过的灾难现场。Flink 恢复时要从最近一个 checkpoint 或者 savepoint 恢复,如果你的 state 很大(比如 50GB),从 S3 拉到本地的过程可能就得 3-5 分钟,期间任务是不可用的。Spark Structured Streaming 因为每个微批结束就写一次 offset,恢复粒度天然是“批级”,丢失的数据最多是当前未提交的批,这个语义对很多金融业务反而更友好。Kafka Streams 如果你想做到秒级恢复,需要在实例重启时从本地 RocksDB 和 changelog 两端同时恢复,我们的经验是如果你的实例是突然被 kill 的,RocksDB 的 WAL 可能损坏,这时你要是不小心删了本地 state 目录让它从 changelog 重建,恢复时间会比你预期的多 6 倍,别问我怎么知道的。
最后我说点更独立的观点:大数据开发的核心竞争力,正在从“谁会调几个火焰图参数”转向“谁能把单位算力成本压下来”。我以前面试别人总爱问 Flink 的 two-phase commit 原理,后来我意识到大多数实时数仓根本用不到跨源事务一致性,他们更大的痛点是 Kafka 扩容后如何让 Flink 自动感知 partition 数量的变化,以及如何让离线任务和实时任务共用同一套 Iceberg 表而不是重复存两遍。所以我现在建议,不管是初学者还是资深开发者,多去关注任务积压、资源利用率和存储成本的关联关系,别只看算子耗时。比如我们的 ods 层,原来的 Flink 任务里 join 了 6 个 Kafka topic,每个 topic 都保留 3 天原始数据,后来改成先用 Kafka Streams 做局部聚合再把结果发到同一个 topic,Flink 只管最后的状态聚合,整个集群的 CPU 使用率从 63% 降到了 41%,节省了 6 个 taskmanager 的规格。这才是能写在简历上的优化,而不是天天吹自己用了哪个引擎的哪个新特性。