1. 先别被“实时”两个字带偏,判定步骤才决定要不要 Flink
最近被问得最多的问题差不多都是同一个方向:实时场景的技术方案选型,是不是直接上 Flink 就行。每次听到这么问,我都会让提问的人先停一下,把“实时”拆成可量化的指标再谈。原因很简单,很多项目嘴上说的是实时,实际拉到会议室里一聊,五分钟更新一次也能接受;也有一些项目嘴上说五秒延迟,实际业务场景里数据晚到一分钟就会造成资损。这两种需求背后的架构选型完全不一样,硬套一套方案,后面所有环节都会别扭。
我通常会把“实时”按响应时间切成四档:毫秒级、秒级、分钟级、小时级。毫秒级实时多见于工业控制、极低延迟交易链路,这套东西和 Flink 关系不大,更多要靠消息中间件、内存计算和定制网关去拼;秒级实时才是 Flink、Spark Structured Streaming、Kafka Streams 这些计算引擎的主战场;分钟级和小时级的需求,有相当一部分用定时调度、增量批处理就能吃下来,不需要为了实时而实时。技术方案选型的第一步,是先让业务方在表格里写清楚“最晚多久必须看到结果”,而不是让他们描述“我们想要实时大屏”。大屏上那个三秒刷新,很多时候只是把三分钟前算好的结果喂给了图表,和流计算引擎没有必然关系。
把时效指标定下来之后,再看真正决定选型下限的三个东西:状态、窗口、故障恢复。如果需求只是做一个过滤转发,比如从 Kafka 读日志、解析字段、写到下游,那确实可以用很轻的方案;但如果要按用户维度做累计、做 session 切分、做双流 join,那就离不开状态存储和窗口管理。状态意味着引擎需要保存中间结果,窗口意味着引擎要处理乱序数据和时间语义,故障恢复意味着作业重启后状态不能丢、数据不能重算错。这三个特性凑到一起,才是 Flink 的看家本领,也是它和一批轻量实时框架拉开差距的地方。
Flink 的容错依赖分布式快照机制,用白话讲就是定期把每个算子的状态连同当前处理位置一起打一个全局一致性的“标记”。保存这一份标记需要额外的协调通信,也要求 source 端支持数据回放。这也是选型时不能只看延迟指标的原因:一个引擎号称毫秒级延迟,但如果状态不能可靠持久化,生产环境一重启就全乱,那这种延迟没有意义。所以我在早期评审阶段就会让团队把下面的检查单过一遍:
- 作业是否允许重复消费,还是必须精确一次语义。
- 需要维护的状态规模大概多大,单机内存能不能兜住。
- 允许的故障恢复时间是多少,能不能接受从最近 checkpoint 恢复。
- 处理的数据是否有乱序,业务上允许的乱序容忍窗口是多少。
这些问题看起来基础,但很多选型分歧恰恰是到这里才暴露的。比如有一个场景里,业务方希望做跨多个数据源的实时 join,但团队给的数据源本身不是 Kafka 这种可回放的消息队列,而是某个业务库的变更流。那这里的核心其实不是选 Flink 还是选别的引擎,而是先解决“变更数据能不能低成本、可靠地进消息链路”的问题。引擎选型一旦脱离数据接入链路去谈,后面一定会有连接器层面的返工。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 比拼引擎时不要只看 Star 数,我用业务特性做横向过滤
技术方案选型走到第二步,一般会在几类引擎之间打转:Flink、Spark Structured Streaming、Kafka Streams,偶尔还会有人提 Storm。先说 Storm,这个框架在早期实时计算里确实地位很高,但它对状态管理和增量恢复的支持太弱,一个稍复杂的窗口需求就要自己写很多外围逻辑,除非团队有极强的底层能力去维护二次开发框架,否则今天再做新项目我不会把 Storm 放进候选清单。
Spark Structured Streaming 的优势在“复用 Spark 生态”。如果团队已经在用 Spark SQL 做离线数仓,实时作业只是顺带需要分钟级延迟,那很自然会把 Structured Streaming 作为备选。它底层的微批模型天然和 Spark 批处理共用一套 SQL 层、数据源和调度体系,运维侧学习成本低。代价也是明显的:微批模型在事件时间、乱序处理和端到端延迟上的表现,比真正意义上的流处理引擎要粗糙一些。新版本 Spark 引入了持续处理模式,但成熟度、连接器覆盖和实际生产案例仍然不如它的微批主线。对延迟要求能放到十秒以上、团队 Spark 经验强的项目,我会尊重这个选型;但别指望用它做秒级以下的大状态计算。
Kafka Streams 是非常容易被小看的一个方案。它不需要独立计算集群,直接嵌在业务应用里,和 Kafka 配合时能做到天然的 partition 对齐。如果你的实时场景本质就是“Kafka 进、Kafka 出”,处理逻辑以单条转换、简单聚合为主,状态规模可控,那 Kafka Streams 的性价比会非常高,至少省掉了 Flink 集群的部署和运维成本。可是当数据源不止 Kafka,或者需要做跨多数据源 join、复杂窗口、比较多分支写入,Kafka Streams 的集成成本就会涨得很快,因为它更像个库,而不是完整平台。
这时候再看 Flink,它的定位更像“通用流处理平台”。我认可它并不是因为它什么延迟都能做,而是因为它在几个容易被实际业务卡住的地方综合得分最高:状态访问能力、事件时间处理、分布式快照、连接器生态以及 SQL 层的成熟度。很多新团队被 Flink 吸引,是从 Flink SQL 开始的;一条 create table 对应一个外部系统,一段 insert into 就能把实时链路串起来,这对生产力是实打实的提升。下面这张对比表是我在评审时常用来给团队兜底的参考:
| 维度 | Flink | Spark Structured Streaming | Kafka Streams |
|---|---|---|---|
| 典型延迟 | 秒级以下 | 秒到分钟级 | 秒级以下 |
| 事件时间/乱序 | 支持完善 | 支持但偏批式 | 需依赖 Kafka 时间戳 |
| 状态能力 | 强,原生状态后端 | 较强,依赖 checkpoint | 中,本地状态+changelog |
| 多数据源支持 | 丰富 | 丰富 | 偏 Kafka 生态 |
| SQL 成熟度 | 高 | 高 | 中低 |
| 独立集群 | 需要 | 需要 | 不需要 |
| 运维复杂度 | 偏高 | 中 | 低 |
这张表只是起点,真正做决定时还要叠加团队资源情况。如果数据平台团队只有三五个人,既要负责离线数仓又要维护 Flink、Kafka、ClickHouse 这一整套实时链路,那我会建议用 Spark Structured Streaming 顶住第一波需求,等确实出现大状态、秒级窗口、复杂事件时间处理的新场景,再引入 Flink 也不迟。选型的本质不是拿一个引擎去适配所有场景,而是评估现有团队在环境搭建、故障排查、任务调优上的成本承受力。一个运维能力只能支撑三个 Flink 集群的团队,硬上二十个实时作业,再好的引擎也会变成事故源头。
还有一个经常被忽略的维度是 sink 和下游系统。实时计算的价值最后要靠数据落点来体现。Flink 对 Kafka、JDBC、Iceberg、Hudi、Doris、ClickHouse 这些常用 sink 的连接器成熟度直接影响交付周期。我在选型时会专门检查“从 Flink SQL 直接写目标库”这条链路在两小时内能不能跑通,跑不通就说明版本兼容性或连接器配置有问题,需要重新评估。这种验证看起来不够“高深”,却能挡住大半后期集成风险。
3. Linux 上部署 Flink,先定运行模式而不是先下安装包
很多团队验证选型时会先在 Linux 服务器上把 Flink 下载下来,解压,执行一条 start-cluster.sh,看到 Web UI 出现就认为环境通了。这种验证方式只适合第一次摸 Flink 的人找感觉,真到了生产方案阶段,不管是资源隔离还是故障恢复,都不能靠这个默认 standalone 模式扛。我在做实时场景技术方案时,会把“部署形态”放在安装动作之前,先回答三个问题:作业跑在 YARN 上还是 Kubernetes 上,状态后端和 checkpoint 放哪里,谁负责在作业异常时拉起新实例。
如果公司已经有大数据集群,Flink on YARN 仍然是很多团队的第一选择。原因是资源可以跟 Hive、Spark 作业放到同一个调度池里,复用现有队列,权限和监控也顺手。安装 Flink 相对简单,下载二进制包后配置环境变量即可,但要注意版本依赖。这里举一个常见的最小安装过程,假设服务器是 Linux x86_64、JDK 已装好:
bash复制tar -xzf flink-1.18.0-bin-scala_2.12.tgz
mv flink-1.18.0 /opt/flink
export JAVA_HOME=/usr/local/jdk-11
export PATH=$JAVA_HOME/bin:/opt/flink/bin:$PATH
flink --version
这里有个经常让人栽跟头的地方:不要只配一个 java 路径就完事,还要检查 Flink 的 lib 目录里有没有可能冲突的旧版本依赖。部分团队习惯把自研连接器 jar 直接丢到 Flink 的 lib 目录下,结果不同作业之间互相污染,最后排查半天发现是 A 作业带进来的驱动把 B 作业的类覆盖了。我的习惯是 lib 目录只放引擎和平台级插件,业务作业依赖用 fat jar 的方式在提交时随作业携带,隔离性更强。
部署形态确定后,并行度和资源预估才是大头。很多人一上来就问“Flink 默认并行度设多少”,其实这个问题要反过来从上游分区数和单条数据量推。假设上游 Kafka topic 有 24 个分区,数据源读进来的并行度要想充分利用分区,建议 source 的并行度先按分区数的整数倍去对齐。注意 Flink 里如果某个环节用了 keyBy,后续算子只要并行度设置不当,就可能出现某个 key 全部压到一个 subtask 上的情况,这会让资源分配看起来没问题、实际热点很高。
做资源粗估时,可以用一个最简单模型:任务并行度乘以每个并行子任务需要的堆内存和托管内存,再加上网络缓冲。假设一个实时清洗作业预估并行度 12,每路处理 8 万条每秒,每条消息解析后约 1KB,那实测中每个 task slot 给 2GB 到 4GB 堆内存比较常见。如果配置 TaskManager 的槽数为 4,那就需要 3 个 TaskManager。这里算出的是一个起步值,还要额外留出 30% 的余量给反压和状态膨胀。
接下来必须在 flink-conf.yaml 里改掉几个安装后的默认配置。首先是进程总内存,很多新手只调堆内存,忽略了 jobmanager.memory.process.size 和 taskmanager.memory.process.size 这组参数,导致容器被 YARN 或 Kubernetes 判定 OOM,但 Flink 日志里根本没有 Java heap 溢出,因为内存其实被堆外结构吃掉了。其次是 checkpoint 目录,务必配成 HDFS 或 S3 这样的分布式文件系统,不要留给本地临时目录。分布式文件系统上,checkpoint 才会在 TaskManager 崩溃后仍然可访问。这是流式作业能不能恢复的命根子。
yaml复制jobmanager.memory.process.size: 2048m
taskmanager.memory.process.size: 4096m
taskmanager.numberOfTaskSlots: 4
parallelism.default: 8
state.backend: rocksdb
state.checkpoints.dir: hdfs://namenode/flink-checkpoints
execution.checkpointing.interval: 60s
execution.checkpointing.min-pause-between-checkpoints: 30s
这几行配置不是拍脑袋写的。RocksDB 做状态后端适合大状态,但快照时要把本地文件上传到远端,所以 checkpoint 间隔不能太短,否则资源都耗在快照传输上;min-pause-between-checkpoints 是为了避免上一次 checkpoint 还没结束、下一次又启动,形成连锁压力。实际调优经验是,如果一次 checkpoint 的时长经常超过 30 秒,优先检查是不是状态太大或下游存储稳定,而不是单纯调大间隔掩盖问题。
4. 连接器层的异常往往是选型后最大的返工区,JDBC 和 Kafka 都要防着点
技术选型时大家喜欢讨论引擎的架构优势,但交付时真正让团队熬夜的往往是连接器。实时场景里最常见的两个接入对象就是关系型数据库和 Kafka。Flink 官方连接器的成熟度已经比早期好了很多,但版本兼容、认证配置、连接池释放、事务语义这些问题,仍然有大量细节需要处理。
先聊 JDBC 连接器。许多人第一次用 Flink SQL 写 MySQL,看到报错第一反应是 SQL 写错了,实际排查下来大部分是连接层面的问题。常见的原因有三个:驱动 jar 没放全、并发连接数打满数据库、批量写入时触发了死锁或主键冲突。Flink 的 JDBC sink 本质上是一个带缓冲的写入器,不是分布式事务写入器,它不能保证端到端的精确一次。很多团队在链路上游选了 Kafka 的精确一次语义,到下游写 MySQL 时以为也能精确一次,结果重复消息一来,主键一样的记录就把任务搞挂。这个坑在设计方案时就要想好解决出口:要么 MySQL 目标表设计成幂等更新,要么接受 at least once 加上应用层去重。
我在排查一个 JDBC 连接器异常时,看到错误是 Communications link failure,第一反应不是去看数据库是不是重启,而是去看 TaskManager 的连接池是否被耗尽了。Flink SQL 每个并行子任务会创建自己的数据库连接,当并行度高而数据库 max_connections 很小时,真正的根因是连接数超限,页面上的 SQL 看不出来。处理方式需要两头配合:数据库侧调大连接上限,Flink 侧减少无效连接占用,同时给 sink 增加合理的缓冲策略:
sql复制CREATE TABLE mysql_sink (
id BIGINT primary key,
val STRING
) WITH (
'connector' = 'jdbc',
'url' = 'jdbc:mysql://...',
'table-name' = 'sink_table',
'username' = '...',
'password' = '...',
'sink.buffer-flush.max-rows' = '1000',
'sink.buffer-flush.interval' = '1s',
'sink.max-retries' = '3'
);
把 sink.buffer-flush.max-rows 调得太小不好,太小等于每几条就发起一次写库,吞吐上不去;调得太大也不好,太大任务失败时缓冲在内存里的批次记录全部丢失,重放范围变大。我一般以每秒写一批为基准,先根据目标库的写入能力倒推 max-rows。比如单条写库耗时 2 毫秒,一批 500 条串行执行就需要 1 秒,那设置到 500 到 1000 是相对稳妥的初始值。
再聊 Kafka 相关的连接问题。热搜词里有一个 Flink SQL Kafka SASL 认证配置,这是几乎所有线上环境都绕不开的点。Kafka 集群开启安全认证后,在 Flink SQL 里建表时不能只写 broker 地址,还要把安全协议和认证机制配置好。SASL_PLAINTEXT 表示认证信息不加密、但传输走普通 TCP;如果集群要求全链路加密,则应该用 SASL_SSL,实际选哪种取决于 Kafka 服务端配置。下面是一个表格定义示例:
sql复制CREATE TABLE kafka_source (
user_id BIGINT,
behavior STRING,
ts TIMESTAMP(3),
WATERMARK FOR ts AS ts - INTERVAL '5' SECOND
) WITH (
'connector' = 'kafka',
'topic' = 'app-user-log',
'properties.bootstrap.servers' = 'kafka1:9092,kafka2:9092',
'properties.security.protocol' = 'SASL_PLAINTEXT',
'properties.sasl.mechanism' = 'SCRAM-SHA-256',
'properties.sasl.jaas.config' = 'org.apache.kafka.common.security.scram.ScramLoginModule required username="flink" password="换成实际密码";',
'properties.group.id' = 'flink-job-01',
'scan.startup.mode' = 'latest-offset',
'format' = 'json'
);
这段配置里最容易错的就是 JAAS 字符串。不同 Kafka 版本支持的认证模块类名不一样,PLAIN 和 SCRAM 的类名不能混用。如果程序提交时报 SaslAuthenticationException,第一件事不是检查密码,而是确认服务端到底开了哪种 mechanism,再反向修改客户端配置。另一点是不要把用户名密码写在无法管理的业务代码里,如果作业统一从一个配置文件读取认证信息,至少要保证该文件的权限最小化。多人共用 Flink SQL Client 调试时,一个不留神把密码通过 shell 历史记录泄露出去的事,我见过不止一次。
连接器异常里还有一类是和 CDC 工具同时使用时出现的 jar 冲突。Flink 项目里既有 flink-connector-jdbc 又有 flink-cdc-connector 时,二者可能携带不同版本的 MySQL 驱动,最终表现出来是 ClassNotFoundException 或者 No suitable driver found。遇到这种情况不用急着去下载所谓“新驱动”,先把作业的 fat jar 做一次依赖树输出,看最终打入的驱动版本是什么,有必要的话用 shade 插件把重复的类 relocate 掉。运维层面我还会给每个实时作业建一个依赖清单,至少记录连接器名称和版本,排查问题的时候能省下一半时间。
5. 资源消耗最小化与并行度解绑,自适应扩展也要防着预期跑偏
资源消耗最小化是实时作业选型之后立刻会遇到的现实问题。固定并行度的方案最直观:在 flink-conf.yaml 里把 parallelism.default 设为 16,所有作业跑起来就是 16 个并行子任务,不管凌晨三点流量只剩高峰的十分之一,还是某个链路已经积压了严重的反压,资源都按 16 一直占着。对私有化集群来说,这意味着需要为每个作业预留峰值资源;对云上容器环境来说,这意味着多数时间在为空闲 slot 付费。
我处理过的一个典型案例是实时用户行为特征作业。它白天高峰期的流量能到每小时千万级,凌晨低谷只有几万,固定并行度设置成 32 后,白天没有问题,晚上整个集群产生大量空转 TaskManager。一开始想靠人肉定时改并行度来解决,但 Kafka 的分区数、作业的 key 分布、下游写入压力都会随时间变化,改一版合适了,下一个周期流量一变又失效。后来我们转向“外部弹性伸缩 + Flink 自适应调度”的组合,才把集群资源曲线压下来。
Flink 从 1.15 开始加强了对自适应调度的支持。在配置上可以打开自适应调度器,让作业根据当前可获得的 slot 数量来调整并行度,而不是把并行度焊死在代码里。本质上,它是把“并行度多少由资源供给决定”这件事还给了调度层。配套的参数文件里常见的设置思路类似这样:
yaml复制jobmanager.scheduler: adaptive
execution.scheduler.adaptive.parallelism.max: 64
execution.scheduler.adaptive.parallelism.min: 4
注意,包括自适应调度器在内的能力会随 Flink 版本迭代产生差异,我在这里只讲思路和使用的版本定位,具体配置项名称一定要以官方文档为准。开通前需要有一个潜意识纠偏:自适应调度不能替代上下游存储的容量规划。它解决的是“作业恢复和资源变动时,task 能更快地调度到可用 slot 上,而不是立刻整体失败重启”的问题,不解决下游数据库一次性涌入巨量写入的问题。说得再直白点,资源消耗最小化的核心在于让资源更接近实时水位,而不是让系统帮你扛住所有峰值冲击。
真正落地资源最小化,我会按下面几步走。先给作业接入反压监控和 CPU 监控,观察哪些算子的负载长期低于 20%,哪些算子在高峰期已经反压到 source。反压如果传导到 source,说明算子并行度或者单并行能力不足,此时调大并行度确实有收益。CPU 长期不到 30% 的作业,先不要急着调整并行度,检查是不是每条消息的 keyBy 分布不均匀导致热点,热点没解决前调大并行度通常只会把热点复制成两个热点。
接下来在一个非核心作业上做压测:把作业的并行度从 8 调整到 6,保持原有输入速率,观察端到端延迟和 checkpoint 耗时是不是还处于可接受范围内。如果延迟变化不大,说明资源存在盈余,可以继续下调。每压一档,记录一次延迟、CPU、内存和状态增长速度。经过几轮之后得到的并行度往往比拍脑袋设的值少三成左右,这就是长期平稳运行要去争取的空间。别在核心交易链路上一上来就压,先拿计算密集但容忍度高的作业积累数据,再逐步推广。
还有一类资源浪费来自无意义的恢复和重复计算。很多作业直接使用默认的每 60 秒做一次 checkpoint,但有些简单过滤作业根本不需要那么高的恢复频率。checkpoint 太频繁会不停把状态上传到远端存储,占用带宽并拉高磁盘 IO;checkpoint 太稀疏又会导致崩溃后恢复时间过长。资源最小化不是只盯着并行度和 CPU,恢复策略也要一起审视。生产上我会给不同的作业打标签:核心在线链路作业用高频 checkpoint,离线分析辅助作业可以放宽到 5 分钟一次。
有一种容易被忽视的节省资源方式是减少重复集群。很多团队的实时场景一开始只需要几十个作业,却按 Flink 集群级别的资源隔离思路,每个团队各搭一套 standalone 环境,最后光 JobManager 进程就占了一批机器。更合理的方式是把作业都放到一个统一 Flink on Kubernetes 或 YARN 的托管环境里,用队列或者命名空间来隔离。JobManager 可以复用,TaskManager 按需启动,作业空闲时能自动降级到更少资源。对中小团队来说,这种“平台化”带来的节省比在单个作业上调半天并行度有效得多。
智能扩展在实践中还要注意 state 的约束。流作业最常见的问题就是状态无法无限水平扩展:如果一个 keyBy 后的算子并行度从 8 调到 16,key 的分布会重新打散,原本在一个 task 上的状态需要重新分发,这个过程成本不低。Flink 的 keyed state 依赖 key 的 hash 分布,想靠任意调整并行度来节省资源,很多时候不仅没节省,反而触发了大范围 state 迁移。所以凡是涉及 keyed state 的核心作业,并行度调整前要模拟流量重新分布后的状态访问情况;简单无状态作业才适合频繁弹性伸缩。
整体上我自己的心态是:不要在选型阶段追求一个完美引擎,也不要指望配置一改资源就自动降到最低。Flink 能给你的是明确的计算语义和可控的故障恢复机制,但每一条业务链路的流量曲线、状态大小、下游容量都不一样,这些特征只能靠持续监控和记录来沉淀。下面这部分算是我留给团队的一张内部自检表,每次新接入一个实时场景都过一遍,后面基本能避开大多数选型失误。
6. 技术方案选型的最后一公里,是把场景参数变成团队的执行约束
很多团队到最后失败不是因为 Flink 能力不够,而是因为选型讨论一直停留在“Flink 好”这种层面,没有把场景转化成具体的执行参数。我在项目复盘时经常看到,两个实时作业表面上都是读 Kafka 写 MySQL,一个要求精确一次、有大量状态、需要分钟级恢复;另一个只是做简单转发、允许少量重复。如果这两类作业最后用完全相同的连接器参数和 checkpoint 配置,那只能说选型没有走到最后一公里。
选型评审时我会让团队用一张简单的表格记录每个场景的关键约束。这张表不是要写多复杂,而是要把之前口头说过的话落在纸面上,用来制约后面写配置的人:
| 场景约束 | 记录内容 | 典型影响 |
|---|---|---|
| 时效目标 | 多少秒内可见 | 决定引擎和窗口参数 |
| 数据来源 | Kafka、CDC、文件 | 决定 source 连接器 |
| 数据去向 | MySQL、ClickHouse、消息队列 | 决定 sink 语义 |
| 状态规模 | 每条 key 状态多大、key 数量多少 | 决定内存与 RocksDB |
| 恢复目标 | 可用多少分钟内恢复 | 决定 checkpoint 间隔 |
| 精确性要求 | 重复消息是否可容忍 | 决定下游幂等设计 |
| 流量曲线 | 高峰/低峰倍率 | 决定是否做弹性伸缩 |
这张表的另一个价值是挡住需求蔓延。有一次评审实时用户分群服务,业务方一开始说延迟要求三秒,后来聊到某个上游系统每天凌晨要执行一次全量数据修正,问能不能一并处理。如果把这个小时级修正需求也塞进秒级链路,整个方案的复杂度和资源成本立刻翻倍,而实际用户并不会因为凌晨修正慢了五分钟就流失。最后我们把它拆成单独的批处理任务,Flink 只处理增量实时部分。实时场景的技术方案选型,很多时候不是做加法而是做减法,明确哪些场景不要用 Flink 也是选型的构成部分。
我还有一个习惯,每次实时作业上线后会让负责同事在文档里留下两个数字:实际高峰吞吐备份和实际资源用量。下个月再来看,如果线上流量翻了两倍但作业并行度没动、资源没加,那就说明当时的预估偏保守或监控数据有问题;如果流量没怎么变但作业经常因为 checkpoint 超时告警,那就说明状态膨胀或者下游存储有问题。这些数字会慢慢变成团队自己的“经验系数”,比任何一篇外部案例都能更好地指导下一次选型。
真正能稳定支撑实时业务的技术底座,不是把一个引擎用到极致,而是形成一套“场景参数到引擎能力”的映射方法。Flink 在这个映射表里覆盖的场景很广,但它不是唯一答案。理解了判定标准、横向比较路径、部署方式、连接器细节和资源调优手段之后,选型这件事才算真正闭环。以后再有人问你实时场景怎么选型,你可以让他先把时效目标、状态规模和流量曲线写清楚,再决定要不要坐到 Flink 这张桌子前。
