做一个实时数仓项目,最深的体会是:网上Flink的教程铺天盖地,WordCount、Kafka到ES的Hello World一搜一大把,但一旦把它丢进真实的业务环境,接上公司自己的Kafka集群、自己的运维平台、自己的JDBC连接池,各种问题会从四面八方涌出来。我今年完整负责了一套实时数仓从零到上线的全过程,这篇文章不打算复述官方文档,而是把项目落地过程中真正让人头疼的、以及最终跑通之后值得沉淀的经验,挑重点拆开来讲。涉及的内容包括实时链路选型、Flink集群搭建、Flink SQL消费Kafka写入Elasticsearch、JDBC连接器异常排查、SASL认证异常排查,以及并行度与资源成本控制的思路。
整套系统现在的状态是:每天稳定处理上亿条业务消息,端到端延迟控制在秒级,大屏指标、实时报表、异常监控都跑在它上面。中间踩过的坑不少,但每一个坑背后都有清晰的根因。这篇文章就是把这些根因和排查路径完整记录下来。
1. 为什么最终选了Flink这套链路
1.1 离线数仓的痛点和实时化的真实诉求
项目背景是一家电商零售公司,业务方每天上午十点开晨会,晨会第一件事就是看昨天的销售数据。离线数仓凌晨跑批,早上九点出结果,勉强能赶上。但2023年下半年开始,运营团队提出了新需求:大促期间要实时看GMV、实时看各区域订单量、实时监控库存水位和支付成功率。这些需求一出来,离线数仓就彻底顶不住了。
这里需要先厘清一个概念:实时数仓并不是要把离线数仓推翻重来,而是把离线链路中延迟最高的那一层,从T+1改成秒级或分钟级。离线数仓处理的是海量历史数据,实时数仓处理的是正在发生的数据,两者的技术栈可以完全不同。
我们最初考虑过几条技术路线:
- 单纯用Kafka Streams做实时计算。优点是轻量、不需要额外引入计算引擎;缺点是状态管理能力弱,复杂事件处理、窗口计算、流表关联写起来非常痛苦。
- 用Spark Structured Streaming。优点是生态成熟、与离线批处理共用一套代码;缺点是微批模式带来的延迟比较高,而且对事件时间语义的支持不如Flink成熟。
- 用Flink。计算模型天然就是流式的,状态后端、精确一次语义、Checkpoint机制都是为流处理设计的,加上Flink SQL的成熟度这几年提升非常快,周报、报表、大屏这类场景写SQL就能搞定。
选型时还有一个关键考虑:团队后续要做实时特征工程、实时风控,这些场景对延迟和状态管理的要求极高,Flink在这方面的优势是不可替代的。
1.2 链路选型:从数据源到OLAP引擎的完整拼图
实时数仓不等于Flink一个组件,Flink只是中间的实时计算引擎。完整链路需要考虑数据采集、消息队列、计算引擎、存储引擎、查询引擎五个层面。我们最终定下来的链路是这样的:
| 环节 | 选型 | 理由 |
|---|---|---|
| 数据采集 | Canal + Debezium | Canal成熟稳定,Debezium适合跨数据库版本兼容 |
| 消息队列 | Kafka | 吞吐量高、生态兼容最好,Flink与Kafka的集成度最高 |
| 实时计算 | Flink SQL为主、DataStream API为辅 | SQL开发效率高,复杂场景用DataStream兜底 |
| 实时存储 | Elasticsearch + Doris | ES支撑明细查询和大屏,Doris支撑OLAP分析 |
| 调度运维 | DataSophon + 自研告警 | 可视化管控Flink集群,便于权限与资源管理 |
这套链路里最容易忽视的是入口:实时数仓的入口数据往往不止业务库的binlog,还有客户端埋点日志(通过Nginx打到Kafka)、第三方回调数据。所以Kafka topic的规范设计非常重要,我们把topic按业务域.数据层级.事件名称的规则来命名,比如order.dwd.payment_success,这样Flink SQL建表时一目了然,也方便后续做权限管控。
为什么实时存储选了ES和Doris两个?因为它们解决的是不同问题。ES擅长全文检索和明细倒排查询,适合大屏展示、日志检索、订单明细查询;Doris擅长大规模聚合分析,适合给BI报表提供秒级响应的OLAP查询。Flink计算结果双写两份,虽然会多一些资源开销,但换来了查询灵活性和稳定性的双保险。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. Flink集群装配与资源基线的确定
2.1 集群搭建时最容易踩的内存坑
Flink集群搭建本身不复杂,难的是把内存参数调对。我见过太多人把taskmanager.memory.process.size设置得很大,结果集群一启动,TaskManager进程占满物理内存,直接把操作系统拖垮。还有人是把JobManager和TaskManager部署在同一批机器上,内存分配又没隔离开,导致OOM、GC频繁,作业状态一直不健康。
我先说我们最终采用的Standalone模式集群配置,三台物理机,每台16核64G内存,部署方式是:两台跑JobManager做HA,三台同时跑TaskManager。flink-conf.yaml里关键参数如下:
yaml复制jobmanager.memory.process.size: 4096m
taskmanager.memory.process.size: 16384m
taskmanager.memory.managed.fraction: 0.4
taskmanager.numberOfTaskSlots: 4
parallelism.default: 8
这里有两个细节容易踩坑:
第一,taskmanager.numberOfTaskSlots到底是设多少。每核一个slot是最朴素的经验,但实际要看任务类型。如果任务里有很多大状态的窗口计算,CPU其实不是瓶颈,堆内存才是,slot设4个比设8个更稳妥,因为每个TaskManager的堆内存是固定拆分的,slot越多每个slot分到的内存越少,大状态作业容易OOM。如果任务是纯ETL、无状态或轻状态,slot可以适当增加,比如8个。
第二,taskmanager.memory.managed.fraction是Flink用于RocksDB状态后端的内存比例。这个值我见过有人设0.6、甚至0.7,结果作业一跑,RocksDB疯狂占用堆外内存,最后进程被系统OOM Killer杀掉。0.4是一个相对稳妥的起点,如果你用RocksDB做状态后端还开了增量Checkpoint,这个值可以根据实际监控微调。
2.2 通过DataSophon做可视化管控
Flink原生的Web UI只能看作业状态和反压情况,如果想做集群维度、多租户维度的管控,原生的东西就不够用了。我们团队用DataSophon来管理Flink Standalone集群,这里分享一些实际体验。
DataSophon对Flink组件的支持方式,简单说就是把一大堆配置文件集中管理起来,通过界面修改配置然后分发到各节点。实际使用中我觉得最有用的是两个功能:
- 组件健康检查。JobManager和TaskManager存活状态一目了然,节点宕机了能第一时间在告警里看见,不用自己写脚本去轮询API。
- 配置版本管理。Flink配置改动之前先备份,改出问题可以一键回滚,这个对于生产环境很重要。
通过DataSophon部署Flink时有一个容易疏忽的点:DataSophon分发的Flink客户端,其conf/flink-conf.yaml和lib/目录下的依赖jar包,需要和集群实际保持一致。否则你在客户端用flink run提交作业时,可能因为jar包版本不一致,出现NoSuchMethodError或者类型转换异常。当时我们的Flink是1.16版本,客户端要用配套的对应版本连接器,这个一定要核对。
2.3 资源基线与slot规划
集群搭好之后,第一个现实问题是:这么多作业,怎么分配slot才算合理?我在初期吃过一次亏,所有作业都走默认并行度8,结果某个大状态作业把TaskManager的堆内存挤爆了,整个TaskManager上的其他作业全部跟着一起挂。那次事故之后,我开始给作业做分层管理:
核心数据管道作业(比如订单实时汇总、支付实时监控)和大查询作业分开集群。我们虽然只有一个Flink Standalone集群,但通过限制不同作业的并行度来隔离影响范围,比如核心作业的并行度固定为4,避免它抢占太多slot导致其他作业饿死。
这里还要注意一个概念:Flink作业的slot是独占的,不是共享的。同一个TaskManager的slot被不同作业占用后,各作业之间的内存隔离是操作系统级别做不到的,JVM堆还是共享的。所以如果一个TaskManager上有多个作业,单个作业的堆内存失控会拖垮整个TaskManager。尽量把大状态、高内存消耗的作业调度到独立的TaskManager上,或者让这些作业使用不同的TaskManager组来部署。
3. 实时ETL作业:消费Kafka写ES的全过程
3.1 数据源与目标表的Flink SQL DDL
我们最核心的一条实时管道,是把Kafka里的订单宽表数据落到Elasticsearch,供大屏实时查询。用Flink SQL实现,整个作业不过几十行SQL,但每一行都值得深究。
Kafka源表DDL:
sql复制CREATE TABLE dwd_order_detail (
order_id STRING,
user_id STRING,
product_id STRING,
pay_amount DECIMAL(10, 2),
order_status INT,
province_id INT,
event_time TIMESTAMP(3),
watermark for event_time as event_time - INTERVAL '5' SECOND
) WITH (
'connector' = 'kafka',
'topic' = 'order.dwd.detail',
'properties.bootstrap.servers' = 'kafka1:9092,kafka2:9092,kafka3:9092',
'properties.group.id' = 'flink-realtime-etl-group',
'properties.sasl.jaas.config' = 'org.apache.kafka.common.security.plain.PlainLoginModule required username="flink_user" password="******";',
'properties.security.protocol' = 'SASL_PLAINTEXT',
'properties.sasl.mechanism' = 'PLAIN',
'scan.startup.mode' = 'latest-offset',
'format' = 'json',
'json.ignore-parse-errors' = 'true'
);
ES目标表DDL:
sql复制CREATE TABLE es_order_sink (
order_id STRING,
user_id STRING,
product_id STRING,
pay_amount DECIMAL(10, 2),
order_status INT,
province_id INT,
event_time TIMESTAMP(3),
PRIMARY KEY (order_id) NOT ENFORCED
) WITH (
'connector' = 'elasticsearch-7',
'hosts' = 'http://es1:9200,http://es2:9200',
'index' = 'realtime_order_detail',
'sink.bulk-flush.max-actions' = '1000',
'sink.bulk-flush.max-size' = '5mb',
'sink.bulk-flush.interval' = '2s',
'format' = 'json'
);
然后一条INSERT INTO就搞定:
sql复制INSERT INTO es_order_sink
SELECT order_id, user_id, product_id, pay_amount, order_status, province_id, event_time
FROM dwd_order_detail;
这里有几个细节值得单独说明。
3.2 Flink SQL消费Kafka时容易忽略的参数
scan.startup.mode是我每次都会被问到的问题。latest-offset表示作业启动时从最新的offset开始消费,适合实时性要求高、不需要回放历史数据的场景;earliest-offset表示从最早可用的offset开始消费,适合需要从某个时间点补数据的场景。但要注意:如果你的作业重启了,Checkpoint里已经记录了offset,scan.startup.mode这个参数是不生效的,Flink会优先从Checkpoint恢复,从状态里记录的offset继续消费。只有当作业没有Checkpoint(比如第一次启动)时,这个参数才会作为启动策略生效。
json.ignore-parse-errors这个参数也要说下。生产环境的数据质量永远没有你想象的那么好,上游一个字段类型变化、一个空值、一个格式错误,都可能让整个作业因为解析异常而失败。设置为true可以跳过解析失败的记录,但代价是丢数据。我的建议是:启动阶段设置为false,让日志把脏数据打出来,确认数据形态稳定后再改成true,并且把json.fail-on-missing-field设置为false,避免字段缺失导致整个作业失败。
还有一个很多人不知道的参数:properties.allow.auto.create.topics。如果Flink作业消费的topic在Kafka集群里不存在(比如命名敲错了),默认情况下客户端的metadata获取会一直重试,作业会卡在RUNNING状态但没有任何数据进来。表面上看起来像是作业"活着"但实际没干活,排查起来非常迷惑。我在生产环境把这个参数显式设置为false,让不存在的topic快速抛错,让问题在启动阶段暴露出来。
3.3 Checkpoint和状态后端的生产级配置
实时的关键是"不丢不重"或者至少"不丢"。Flink的Exactly-Once就是靠Checkpoint机制实现的。我们在生产环境的Checkpoint配置如下:
yaml复制execution.checkpointing.interval: 60s
execution.checkpointing.timeout: 3min
execution.checkpointing.min-pause: 30s
execution.checkpointing.max-concurrent-checkpoints: 1
state.backend: rocksdb
state.checkpoints.dir: hdfs://nameservice/flink-checkpoints
interval设为60秒,太短的Checkpoint会频繁做快照、耗性能;太长的一次故障恢复要重放很多数据,恢复时间也会变长。min-pause保证两个Checkpoint之间至少有30秒的间隔,避免快照风暴。max-concurrent-checkpoints必须设置为1,多个并发Checkpoint同时进行会有快照数据不一致的风险。
状态后端我们选择了RocksDB,而不是默认的HashMap。核心原因是RocksDB可以支撑比可用内存大得多的状态,而且支持增量Checkpoint,在大状态场景下性能稳定得多。代价是需要额外管理RocksDB占用的堆外内存,配合前面说的managed.fraction参数来控制。
这里分享一个实战经验:如果作业状态不大(几百MB以内)而且状态访问很频繁,用HashMap状态后端反而更快,因为RocksDB有序列化和反序列化的开销。不是所有作业都要无脑上RocksDB。
3.4 维度关联:实时数仓的又一个隐藏坑
实时ETL只做简单的字段透传是远远不够的,现实业务里最常见的是维度关联:订单表需要关联商品信息、用户信息、店铺信息,而这些维度数据往往在MySQL里。
我们的方案有两种,配合使用:
一种是Flink SQL的FOR SYSTEM_TIME AS OF时态表关联,把MySQL里的维度表通过JDBC连接器注册成Flink的维度表,然后流表关联时用事件时间进行Join。这个方式的代码很简单:
sql复制CREATE TABLE dim_product (
product_id STRING,
product_name STRING,
category_id INT,
PRIMARY KEY (product_id) NOT ENFORCED
) WITH (
'connector' = 'jdbc',
'url' = 'jdbc:mysql://mysql-host:3306/dim_db',
'table-name' = 'product_dim',
'username' = 'flink_user',
'password' = '******'
);
SELECT
o.order_id,
o.user_id,
o.product_id,
dp.product_name,
dp.category_id,
o.pay_amount,
o.event_time
FROM dwd_order_detail AS o
LEFT JOIN dim_product FOR SYSTEM_TIME AS OF o.event_time AS dp
ON o.product_id = dp.product_id;
这个方案适合维度数据量小、更新不频繁的场景。JDBC连接器默认的缓存是关闭的,也就是说每条流数据到达时都会实时查一次MySQL,这在高吞吐场景下会把MySQL打爆。生产环境一定要开缓存:
sql复制'lookup.cache.max-rows' = '50000',
'lookup.cache.ttl' = '5min',
'lookup.max-retries' = '3'
缓存时间不要设置太长,否则维度数据更新了,实时数据还在用老维度。
另一种是维度数据也走Kafka,用Flink SQL把维表binlog同步到Kafka之后,通过双流Join实现维度关联。这个方式实时性更好、对MySQL的压力更小,但开发复杂度高一些,适合频繁变化的维度。推荐的做法是:核心大促场景用双流Join,日常报表场景用JDBC异步查询加缓存就够用了。
4. 两个最常见的生产环境异常:JDBC连接和Kafka鉴权
4.1 JDBC连接器异常,先别急着怀疑网络
Flink SQL用到JDBC连接器的场景非常多,从MySQL读维表、写MySQL结果表都需要它。生产环境里flink的jdbc连接器异常这个关键词被搜索的频率很高,我基于自己的排查经验,把最可能的几个原因列出来。
第一个是驱动冲突。Flink官方提供的JDBC连接器jar包一般自带对应数据库的驱动,自带的版本可能与你们公司内部数据库版本不匹配。比如连接MySQL 8.x,但Flink自带的驱动是5.x的,连接时报错通常是java.sql.SQLException: No suitable driver found。解决方法是把正确的驱动jar包放到Flink的lib/目录,并移除冲突的旧驱动。
第二个是shaded jar包导致的类冲突。Flink 1.16之后,官方把很多连接器拆成了独立的jar包,如果你从旧版本升级上来,之前可能把多个连接器的jar包都堆在lib/目录里,这时候容易出现ClassNotFoundException或者NoClassDefFoundError。我第一次碰到的时候特别懵,因为作业前一天还是好的,第二天升级了一个连接器版本就报这个错。排查下来发现是新的ES连接器jar包和老的ES连接器jar包在classpath里打架。解决方法是清理掉lib/目录下不要的旧jar包,只保留一个版本。
第三个是时区问题。JDBC连接MySQL时,连接URL里如果没有显式指定serverTimezone,在Flink SQL里读取TIMESTAMP类型字段时可能会出现相差8个小时的问题。这个问题的排查思路是一步步缩小范围:
text复制作业报错或数据时间不对
→ 先看源数据和目标数据的时间
→ 发现目标比源多8个小时
→ 检查Flink任务时区设置
→ 发现JDBC连接URL没有serverTimezone
→ 在URL参数中补上serverTimezone=Asia/Shanghai
→ 重启作业验证数据时间正确
第四个是连接池泄漏。高并发场景下Flink JDBC连接器频繁创建连接、关闭连接,如果连接器版本有bug,或者目标数据库的连接数上限设置太小,会出现Too many connections错误。我们当时把MySQL的max_connections从默认的151调到了500,并且用lookup.cache降低了维表关联时的连接频率,问题才真正解决。
4.2 Flink SQL抛SASL认证错误,问题往往在客户端配置
Kafka开启鉴权之后,Flink SQL消费Kafka的认证配置是一个高频出错点。报错信息通常长这样:
text复制org.apache.kafka.common.errors.SaslAuthenticationException:
Authentication failed due to invalid credentials with SASL mechanism PLAIN
排查这个问题的关键,是理解Flink SQL连接Kafka时,认证参数是通过properties.前缀传给Kafka客户端的。很多人在Flink SQL参数里直接写'security.protocol' = 'SASL_PLAINTEXT',这是错的,应该写'properties.security.protocol' = 'SASL_PLAINTEXT'。少了properties.前缀,这个参数会被Flink忽略,Kafka客户端拿到的还是默认的无认证配置,自然连接不上。
还有一种情况是认证信息本身没问题,但KafkaClient侧的SASL机制和broker端不一致。比如broker用的是SASL_SSL,你配置的是SASL_PLAINTEXT,两边协商不上,也会报类似错误。这里需要先确认Kafka服务端到底开启了哪种安全协议,是SASL_PLAINTEXT还是SASL_SSL,SSL的还需要额外配置properties.ssl.truststore.location和properties.ssl.truststore.password。
另外一点,properties.sasl.jaas.config这段配置的格式必须精确,一个字符都不能错。正确的PLAIN写法是:
sql复制'properties.sasl.jaas.config' = 'org.apache.kafka.common.security.plain.PlainLoginModule required username="flink_user" password="your_password";'
注意分号不能少,用户名和密码必须用双引号括起来。如果Kafka用了SCRAM-SHA-256或SCRAM-SHA-512机制,模式要改成ScramLoginModule。
这类认证问题的排查思路是:先确认服务端开启的协议和机制,再确认客户端配置是否完整,再确认用户名密码是否有效。任何一环不匹配,报错信息都会指向认证失败,但实际上根本不是密码的问题。
4.3 从作业日志反推链路配置的通用方法
很多Flink用户一遇到问题就上社区提问,把整页的异常栈贴上去。实际上,Flink的报错日志是层层嵌套的,最底层的Caused by才是根因,最上层的异常往往只是表象。我的排查习惯是:
- 先在日志里找到
Caused by关键字,把根因异常单独摘出来看。 - 看异常信息里的类名,判断是客户端问题、服务端问题还是网络问题。
- 如果是客户端问题,优先检查作业提交时的连接器配置和classpath。
- 如果是网络问题,用
nc -vz host port测试连通性,不猜。
JDBC和SASL这两类异常,只要按这个路径走,一般都能在十分钟内定位到根因。
5. 并行度、资源消耗与"智能扩展"的实践
5.1 手动定并行度为什么那么费劲
parallelism.default是Flink作业最基础也最容易被忽视的配置。很多人设置并行度的时候非常随意:默认8、默认16,或者照着网上教程抄一个,结果就是作业运行一段时间后出现严重的数据倾斜:
有一个Subtask处理了几百万条数据,另一个只有几千条;有的Subtask反压到99%,有的却闲得发慌。资源消耗巨大,但吞吐量并不理想。
手动调并行度的问题在于:它是静态的。业务流量是波动的,白天流量大,凌晨流量小,用一个固定并行度去应对所有时段,要么白天资源不够,要么晚上资源严重浪费。这就是"抛弃并行度设置、转向智能扩展"思路的出发点。
5.2 根据反压监控动态调整并行度
Flink 1.15之后,反压监控和自动扩缩容机制越来越成熟。我们的实践中,核心思路分两步:
第一步,用反压指标来判断当前并行度是否合理。Flink Web UI里可以直接看每个作业的反压状态,OK表示正常,HIGH表示当前算子处理能力不足,LOW表示偶尔有尖峰。如果一个作业的反压长期是HIGH,而且上游Kafka的消费lag还在持续增加,说明并行度肯定不够。
第二步,根据流量趋势预划分时段,通过Cron定时任务在流量高峰期前把作业并行度调高,低峰期前再调低。这个方法在早期很好用,但有两个麻烦:一是每次调整并行度都要重启作业,重启过程会中断实时数据流;二是预划分时段毕竟不够智能,遇到突发流量依然手足无措。
后来我们升级到Flink 1.16版本,开始尝试Flink的Reactive Mode和自适应调度。Reactive Mode可以让TaskManager的数量动态变化,并行度会根据可用slot自动调整,作业不需要重启。对于弹性扩缩容场景,这是一个很有价值的方向。不过要诚实地说,这套机制在真正复杂的生产环境里还需要充分验证,目前我们做得最多的是把反压指标接入告警系统,在指标异常时自动通知运维介入,而不是完全依赖自动扩展。
5.3 资源消耗最小化的几个实用配置
"资源消耗最小化"不是指把并行度调小、把资源调到刚好够跑,而是在保证作业稳定性的前提下,减少无效的资源占用。我分享几个亲测有效的方法。
第一,合理设置空闲State的清理策略。Flink的Keyed State如果一直不清理,会随着时间无限增长,最终撑爆内存。生产环境一定要配置TTL:
java复制StateTtlConfig ttlConfig = StateTtlConfig
.newBuilder(org.apache.flink.api.common.time.Time.days(7))
.setUpdateType(StateTtlConfig.UpdateType.OnCreateAndWrite)
.setStateVisibility(StateTtlConfig.StateVisibility.NeverReturnExpired)
.build();
在Flink SQL里,如果你用的是SQL作业,可以给表设置'table.exec.state.ttl' = '7d',过期状态会自动被清理。这个配置我见过太多人忽略了,代价就是作业越跑越慢,资源消耗越来越大。
第二,合理设置窗口的闲置超时。在事件时间窗口计算里,如果某个key长时间没有新数据,它对应的窗口会一直挂在状态里不释放。配置table.exec.source.idle-timeout为5分钟,可以让空闲的key对应的窗口及时超时释放。
第三,开启动态Slot管理。如果你有很多小作业,每个作业显式设置并行度会导致slot碎片化,就像磁盘碎片一样,明明总量够用但就是分配不出来。通过动态Slot管理,让TaskManager按需创建slot,可以显著提升资源利用率。
第四,再提醒一次taskmanager.memory.managed.fraction的调优。这个值设置的太大,RocksDB能用的堆外内存多了,但JVM堆就少了,TaskManager容易OOM;设置太小,RocksDB频繁刷盘,性能下降。我测试过不同任务类型,从0.3到0.5之间是一个比较平衡的范围,具体可以结合监控指标做调整。
5.4 智能扩展:从手动调优到自适应调度
最后聊一下"抛弃并行度设置:Flink智能扩展"这个比较新的趋势。早期版本的Flink调度是静态的,作业提交时并行度就固定死了。Flink 1.16之后,自适应调度成为一个重要特性。
自适应调度的基本原理是:作业提交时不指定固定并行度,而是指定一个需要的总slot数量范围,Flink根据当前集群可用的slot数量自动决定每个算子的并行度。当TaskManager数量变化时,作业的并行度也随之调整。
举例来说,提交作业时可以只设置parallelism.default为-1,然后通过jobmanager.scheduler配置启用自适应调度。实际使用中,这个模式对无状态作业效果最好,比如简单的Kafka到ES的ETL管道;对有大状态、有窗口计算的作业,并行度调整会导致状态重新分布,成本很高,要谨慎使用。
资源消耗这块,我的体会是:与其追求极致的资源利用率,不如追求合理的资源配置加完善的监控告警。一个集群最怕的不是资源浪费,而是作业突然OOM、任务堆积、数据延迟,那种故障带来的业务损失远大于省下的那几台机器的成本。所以我们目前的策略是核心链路粗放多配一点,非核心链路精细调优省资源,整体平衡。
从我自己的实践看,实时数仓项目里最花时间的永远是排错。Flink本身提供的指标和日志足以定位绝大多数问题,前提是你要知道从哪看起。这轮项目做完,我最大的心得是:无论链路多复杂、引擎多强大,真正决定成败的还是配置管理、监控告警、异常恢复这些扎实的工程细节。先把这些基本功做好了,再来谈业务创新才有底气。
