引言
工作流的成本问题有一个特点:它不会自己暴露。任务跑得慢会被发现,任务失败会告警,但「每天多花 3000 元的算力」不会触发任何告警——账单月底才到,而且是一笔合并的数字,看不出是谁花的。
因此成本优化的第一步不是优化,而是度量与归因。你需要能回答「订单日报这条流程每天花多少钱」「哪一步最贵」「这个月比上个月贵在哪」。没有这三个答案,所有的优化都是凭感觉。
有了度量之后,优化的空间通常集中在五个地方。任务粒度:任务切得太细,调度与启动开销超过计算本身;切得太粗,失败重试的代价与资源浪费变高。缓存与增量计算:重复计算是最大的浪费来源,一个每天全量重算的指标可能只需要算增量。实例类型:抢占式实例的价格通常是按需的 30%~40%,但被回收时的重试成本必须计入。闲置资源:Worker 池的余量、常驻的 GPU、未清理的中间结果都在持续计费。重试成本:一个失败率 20% 的任务,重试开销可能超过正常执行的开销。
本文按「度量 → 粒度与缓存 → 实例与重试 → 回收 → 预算与治理」的顺序展开,每个环节都给出可量化的判断标准。引擎的成本模型与容量规划参见 工作流引擎全景与选型 ,LLM 类步骤的成本控制参见 工作流与 AI Agent 编排 。
目录
- 工作流成本的构成
- 成本归因:把账单拆到流程
- 任务粒度与调度开销
- 调度开销的量化方法
- 缓存与增量计算
- 增量物化与分区裁剪
- 计算下推与列裁剪
- 抢占式实例的收益与代价
- 重试成本的核算
- 幂等与重试预算的成本视角
- 闲置资源的识别与回收
- 存储成本:中间结果与历史
- 弹性伸缩的成本账
- 单位成本指标
- 预算控制与限额
- 成本与可靠性的取舍
- 优化项的优先级排序
- 组织与流程:谁负责成本
- 落地路线图
- 权衡取舍
- 常见坑清单
- 小结
1. 工作流成本的构成
工作流的成本分五块,比例因场景差异极大,但先分类才能定位:
| 成本项 | 来源 | 典型占比 | 主要优化手段 |
|---|---|---|---|
| 计算 | 任务执行消耗的 CPU / 内存 | 50%~70% | 增量计算、下推、实例类型 |
| 存储 | 中间结果、实例状态、日志 | 10%~25% | 生命周期、压缩、引用传递 |
| 调度 | 引擎自身的运行开销 | 5%~15% | 减少事件数、批量提交 |
| 网络 | 跨区传输、外网流量 | 5%~20% | 就近计算、压缩传输 |
| 人力 | 运维、排障、救火 | 隐性但常最高 | 自动化与可观测性 |
人力成本常被完全忽略,但往往是最大的一项。一个需要人工介入才能恢复的流程,每次故障消耗 1 小时工程师时间,成本可能超过它一个月的算力费用,因此「降低人工介入次数」也应该被当作成本优化项目来评估。
2. 成本归因:把账单拆到流程
云厂商的账单是资源维度的(多少 CPU 小时、多少 GB 存储),而你要的是业务维度的(哪条流程花了多少)。中间的桥梁是标签(tag / label)。
# 每个任务容器都打上归属标签,云账单按标签聚合
metadata:
labels:
workflow: orders_daily_pipeline
flow-step: transform
tenant: team-order
env: prod
cost-center: cc-1024
标签体系的三个设计要点。一是标签的维度要稳定:workflow / step / tenant / env 这四个维度足够覆盖绝大多数归因需求,不要随意新增维度(维度越多,遗漏标签的资源越多)。二是必须有强制兜底:准入控制强制要求所有 Pod 带 cost-center 标签,没有标签的 Pod 不允许创建。三是标签要能被账单系统识别:云厂商通常需要开启「成本分配标签」并等待 24 小时才生效,这一步要提前做。
-- 按流程聚合近 30 天的成本(假设资源使用数据已按标签落表)
SELECT labels->>'workflow' AS workflow,
SUM(cpu_millis) / 3600000.0 AS cpu_hours,
SUM(cpu_millis) / 3600000.0 * 0.05 AS cpu_cost,
SUM(mem_byte_millis) / 3600000000000.0 * 0.6 AS mem_cost,
COUNT(DISTINCT run_id) AS runs
FROM resource_sample WHERE sample_time > NOW() - INTERVAL '30 days'
GROUP BY 1 ORDER BY cpu_cost + mem_cost DESC;
归因的产出应该是**「单位成本」**而不是「总成本」:总成本会随业务量增长而增长,看不出效率。每千次订单处理的成本、每 GB 处理数据的成本、每个活跃用户的成本 这类指标才能反映效率的变化。可观测体系的建设参见 工作流可观测与调试
。
3. 任务粒度与调度开销
任务粒度是一个有最优解的权衡,不是「越细越好」也不是「越粗越好」。
切得太细:调度开销占比高、事件数爆炸 -> 例:1000 个任务各跑 2 秒,开销占比 60%
切得太粗:重试粒度大、资源不均衡、并行度受限 -> 例:跑 4 小时的任务失败一次全部重来
判断标准是「调度与启动开销 / 任务实际执行时间」。这个比值超过 0.2 就说明切得太细,应该合并;反过来,如果一个任务里包含了多个「可以独立失败」的逻辑单元,且它的执行时间超过 10 分钟,就应该考虑拆分。
粒度决策表(单任务平均执行时间)
< 10 秒 合并到同批处理,或改用常驻 Worker 池
10 秒~30 分 合适的粒度(注意失败重试的代价)
> 30 分 考虑按逻辑边界拆分,或加检查点
一个具体的反例:某团队把「读文件 → 解析 → 转换 → 写库」拆成 4 个任务,每个耗时不到 1 秒,但每个都要经历一次调度(约 200 ms)、状态持久化与日志写入,总耗时 4 秒中 3 秒是开销。合并后总耗时降到 1.2 秒,成本降低 70%。
4. 调度开销的量化方法
调度开销必须被量化才能优化。三个可测量的量是单次调度延迟(任务入队到开始执行)、单任务事件数(调度、开始、完成、心跳产生的事件条数)与元数据体积(状态记录 + 日志 + 索引)。测量方法是在任务里埋点,上报「实际计算时间」与「从入队到开始的时间」:
@task
def process(batch):
schedule_overhead = time.time() - get_queued_at() # 从入队到开始
t0 = time.time()
result = do_work(batch) # 真正的计算
metrics.timing("task.compute", time.time() - t0)
metrics.gauge("task.overhead_ratio",
schedule_overhead / max(time.time() - t0, 0.001))
return result
overhead_ratio 是最有用的单一指标。按任务类型分组看它的 P95:超过 0.5 的任务就是合并或改用常驻池的候选。注意这个比值对短任务天然偏高,应与「任务绝对耗时」一起看,避免误判。
5. 缓存与增量计算
缓存与增量计算是成本优化里收益最高的两项,因为它们直接消除重复工作,而不是让同样的工作跑得更快。
缓存的三种粒度是结果缓存(同一输入直接返回上次的输出,是幂等任务的天然优化)、中间缓存(缓存解析后的中间产物,跳过上游重算)、数据缓存(把远端数据缓存到本地,减少网络与源系统压力)。
缓存的关键是缓存键的设计。缓存键必须包含「所有影响输出的输入」,否则会返回错误结果。一个常见的错误是缓存键只包含业务参数,忽略了「代码版本」——代码更新后缓存仍然命中,返回旧逻辑的结果。
def cache_key(task_name, params, code_version):
payload = json.dumps({ # 所有影响输出的输入都必须在键里
"task": task_name, "params": params,
"code_version": code_version, "schema_version": SCHEMA_V,
}, sort_keys=True)
return f"cache:{task_name}:{sha256(payload.encode()).hexdigest()}"
增量计算的收益更大,但前提是能识别「哪些数据变了」:时间增量(只处理自上次以来的新数据,按 updated_at 或 CDC 位点)、分区增量(只重算受影响的分区)、依赖增量(只重算依赖发生变化的步骤)。
时间增量最简单也最常用,但要注意**「迟到数据」**:一条 3 天前的数据今天才到达,按时间增量会被漏掉。对策是保留一个「回看窗口」(比如每次多算最近 3 天),或者用 CDC 位点而不是时间戳。数据编排侧的增量与分区机制参见 Dagster 与 Prefect 数据编排 。
6. 增量物化与分区裁剪
在数仓场景里,增量物化的实现方式直接决定成本:
-- 全量重算:每天扫描全表,成本随历史数据量线性增长
CREATE TABLE dws.orders_daily AS SELECT ... FROM dwd.orders WHERE dt <= CURRENT_DATE;
-- 增量物化:只算当天分区,再合并
INSERT OVERWRITE TABLE dws.orders_daily PARTITION (dt = :biz_date)
SELECT ... FROM dwd.orders WHERE dt = :biz_date;
全量重算的成本会随时间无限增长(历史数据越来越多),而增量物化的成本是常数。从全量改增量的收益通常是一次性的巨大跃迁(比如从每天扫描 3 年数据变成只扫 1 天,成本降到 1/1000),因此这类改造应该排在优化清单的最前面。
分区裁剪是增量计算的必要配套:查询必须带上分区条件,且条件不能包在函数里,否则优化器无法裁剪。
-- 正确:分区条件在 WHERE 里,可被裁剪
SELECT * FROM dwd.orders WHERE dt = :biz_date;
-- 错误:分区条件包在函数里,裁剪失效
SELECT * FROM dwd.orders WHERE date_format(created_at, 'yyyy-MM-dd') = :biz_date;
第二条查询对 created_at 做函数运算,导致分区裁剪失效。这是「明明写了分区条件却仍然全表扫描」的最常见原因,也是排查「为什么这个任务突然变贵」时的第一个检查点。
7. 计算下推与列裁剪
「把计算推到数据所在的地方」是降低传输与计算成本的基本原则。三个层次:
层次一:列裁剪 只读需要的列,而不是 SELECT *
层次二:条件下推 把过滤条件下推到数据源,而不是拉全量再过滤
层次三:计算下推 把聚合、连接下推到数仓/数据库,而不是拉到应用层算
层次三的收益最大,也最常被违反。典型反模式是「把 100 万行数据拉到 Python 里做 group by」——同样的聚合在数据库里可能只需 1 秒、扫描 10 MB,在 Python 里要传 100 MB、占 2 GB 内存、跑 30 秒。
# 反模式:全量拉取后在应用层聚合(传输 + 内存 + 计算都在应用侧)
df = pd.read_sql("SELECT * FROM orders WHERE dt = %s", conn, params=[biz_date])
summary = df.groupby("channel")["amount"].sum()
# 正例:聚合下推到数据库,只传结果
summary = pd.read_sql(
"SELECT channel, SUM(amount) AS amount FROM orders "
"WHERE dt = %s GROUP BY channel", conn, params=[biz_date])
8. 抢占式实例的收益与代价
抢占式(Spot)实例的价格通常只有按需的 30%~40%,但它的成本账必须算全:
Spot 的真实成本 = 实例价格 × 运行时长 + 被回收后的重试成本
重试成本 = 回收概率 × 平均已完成进度 × 任务单次成本
举例:一个任务跑 30 分钟,按需价格 1 元/小时,Spot 价格 0.35 元/小时,被回收概率 10%(每次运行),回收时平均已完成 50%。
按需:0.5 小时 × 1.0 元 = 0.50 元
Spot:0.5 小时 × 0.35 元 + 0.1 × 0.5 × 0.5 元 × ...
= 0.175 元 + 重试的期望成本
如果重试从零开始,期望成本是 0.1 × (0.175 + 0.1 × ...),等比级数求和约为 0.194 元,仍然远低于按需的 0.5 元。结论是 Spot 在「可重试」的前提下几乎总是更便宜,除非任务的单次成本极高(比如跑了 3 小时、回收概率 40%)。
三个前提必须满足:任务幂等(重试不产生重复副作用)、任务可重试(失败后自动重试而不是等人处理)、任务能感知中断(收到信号后优雅退出,最好能保存检查点)。检查点的价值在于把「重试从零开始」变成「从断点继续」,能把 Spot 的成本再降一半以上。
# 用节点亲和性与容忍度把批处理任务引到 Spot 节点
affinity:
nodeAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
preference: { matchExpressions: [{ key: node-lifecycle, operator: In, values: ["spot"] }] }
tolerations: [{ key: spot, operator: Exists, effect: NoSchedule }]
9. 重试成本的核算
重试不是「免费的重来」,它有明确的成本,且经常超过正常执行成本。一个失败率 20% 的任务:
正常执行成本 = 1 单位
重试成本 = 0.2 × 1 + 0.04 × 1 + 0.008 × 1 + ... ≈ 0.25 单位
总成本 = 1.25 单位(比无失败时高 25%)
如果失败率是 50%,总成本变成 2 单位(翻倍)。因此「降低失败率」本身就是成本优化,而不是单纯的可靠性改进。
更隐蔽的是失败发生的位置:一个任务跑到 90% 才失败,重试成本是「从零跑到 90%」的时间,但产出的价值为零。对策是把易失败的步骤前置(先校验再计算,快速失败)与加检查点(失败后从断点继续)。
# 把校验前置,避免「跑了 40 分钟才发现参数不对」
@task
def transform(params):
validate(params) # 快速失败,成本毫秒级
return load_and_compute(params) # 昂贵计算
**重试预算(retry budget)**是控制重试成本的机制:给每个任务一个「重试消耗的总时间上限」,超过就不再重试而是转人工。这与 重试幂等与补偿设计 里讲的退避与熔断是同一套机制,只是从成本视角看它的收益。
10. 幂等与重试预算的成本视角
幂等与成本的关系有两层,都值得展开。
第一层:不幂等导致的重试成本是「双份」。一个不幂等的任务重试后可能产生重复数据,需要额外的人工清理或补偿流程——这部分成本(数据修复 + 人工介入)往往超过算力成本本身。因此幂等是成本优化的前提,不是可选项。
第二层:幂等让「激进的重试策略」变得可行。一个幂等的任务可以放心跑在 Spot 实例上、可以设较短超时(超时就重试而不是长时间等待)、可以并发重试多个副本,这些策略都能降低「等待与闲置」的成本。
**对冲执行(hedged execution)**是幂等带来的一个具体收益:对延迟敏感的任务同时发起两个副本,取先返回的结果。它的成本是「正常情况下多花一倍算力」,收益是「消除长尾延迟」。适用条件是任务幂等、副本成本低、长尾延迟的代价高。对大多数批处理任务不划算,对「报表必须在 8 点前产出」这类有硬时限的任务可能划算。
11. 闲置资源的识别与回收
闲置资源是成本优化里最容易拿到的收益,因为它不需要改代码。四类常见闲置:
常驻容量过大:Worker 池按峰值配置,平时利用率 20%
空闲 GPU:GPU 节点被占用但没有任务使用 GPU
未清理的中间结果:任务结束后临时文件与中间产物仍在计费
未释放的配额:僵尸任务占用配额,导致新任务排队而资源空闲
识别方法是用「已分配 vs 实际使用」的比值:
-- 找出「分配了但几乎没用」的资源(按任务类型聚合)
SELECT task_name, AVG(allocated_cpu_milli) AS alloc_cpu, AVG(actual_cpu_milli) AS used_cpu,
ROUND(AVG(actual_cpu_milli)::numeric / NULLIF(AVG(allocated_cpu_milli), 0), 3) AS util_ratio
FROM task_resource_sample WHERE sample_time > NOW() - INTERVAL '7 days'
GROUP BY task_name
HAVING AVG(actual_cpu_milli)::numeric / NULLIF(AVG(allocated_cpu_milli), 0) < 0.3
ORDER BY alloc_cpu DESC;
util_ratio < 0.3 的任务就是「requests 设得过高」的候选,把它们下调能直接释放容量。注意不要把 requests 调到实际峰值以下,否则会引发调度失败与运行时争抢——安全的做法是「requests 设为 P95 实际使用量,limits 保持较高」。
未清理的中间结果是另一大块,对象存储的生命周期规则必须显式配置(比如 workflow-scratch/ 前缀 7 天过期),且要有监控确认存储总量按预期下降。
12. 存储成本:中间结果与历史
存储成本有三块,各自的优化手段不同:
| 存储 | 增长原因 | 优化手段 |
|---|---|---|
| 中间结果 | 任务产出不清理 | 生命周期规则、压缩、引用传递 |
| 实例状态与历史 | 事件数多、保留期长 | 历史截断、缩短保留期、归档冷存储 |
| 日志 | 全量落盘、无采样 | 分级采样、压缩、短期热存 |
压缩是最容易被忽略的低成本高收益项。Parquet 加 Snappy 压缩通常能把体积降到原始 CSV 的 1/5 到 1/10,而 CPU 开销可以忽略。对于已经在用的数据集,检查是否用了列式格式与压缩是排查存储成本的第一步。
历史保留期要与实际需求对齐。「保留 90 天」这个默认值很少被质疑,但实际需求可能是「最近 7 天用于排障 + 更早的归档到冷存储」。分层保留能把存储成本降一个数量级:
# 分层保留:热存 7 天、温存 30 天、冷归档 1 年
lifecycle:
- transition: { days: 7, storageClass: STANDARD_IA }
- transition: { days: 30, storageClass: GLACIER_IR }
- expiration: { days: 365 }
日志采样是另一个高收益项。工作流日志的 90% 是「正常路径的常规输出」,只有异常路径的日志有排查价值,对正常路径做 1% 采样、对异常路径全量保留,能把日志成本降 90% 而不损失排查能力。
13. 弹性伸缩的成本账
弹性伸缩不是「自动省钱」,它有明确的成本与收益,必须算账:
收益 = 减少的闲置资源成本
成本 = 伸缩延迟期间的排队成本 + 频繁伸缩的抖动成本 + 运维复杂度
伸缩延迟是关键的隐藏成本。扩容要 3~10 分钟,这段时间任务在排队——排队本身不花钱,但满足 SLA 的压力会推动团队配置更大的常驻容量,从而抵消弹性的收益。弹性伸缩划算的三个条件:负载有明显的波峰波谷(波峰/波谷比大于 3)、波峰持续时间足够长(超过 30 分钟,即超过扩容延迟)、任务足够长(大于 2 分钟,冷启动开销可忽略)。三条都不满足时,「固定容量 + 高利用率」比弹性更经济——这也是很多团队尝试弹性后又回退的原因。
混合策略在实践中效果最好:基线容量用固定实例(覆盖 60% 的常态负载),峰值部分用弹性(覆盖 40% 的波动)。这样既避免了「平时浪费」,又避免了「峰值来不及扩」。基线部分还可以用「预留实例 / 节省计划」进一步降价(通常能省 30%~50%)。
14. 单位成本指标
成本优化的目标不是「降低总成本」,而是「降低单位成本」。总成本会随业务量增长,只有单位成本反映效率。
四类常用的单位成本指标:
| 指标 | 定义 | 适用场景 |
|---|---|---|
| 每次运行成本 | 总成本 / 运行次数 | 事件驱动型流程 |
| 每 GB 处理成本 | 总成本 / 处理数据量 | 数据管道 |
| 每千次业务操作成本 | 总成本 / 业务操作数 | 交易类流程 |
| 每活跃用户成本 | 总成本 / 活跃用户数 | 面向用户的流程 |
指标必须与业务量一起看,否则会误判。比如「总成本下降 20%」看起来是好事,但如果同期业务量下降了 40%,单位成本其实上升了 33%。
unit_cost = total_cost / max(business_volume, 1) # 单位成本
cost_delta = (total_cost - prev_total_cost) / prev_total_cost
volume_delta = (business_volume - prev_volume) / prev_volume
# 单位成本上升 = cost_delta > volume_delta
单位成本指标还应该按流程、按团队分别看:全站的单位成本可能稳定,但某个流程可能翻倍了,只有分组才能发现。
15. 预算控制与限额
度量与优化之后,还需要硬性约束防止失控。三层控制:
第一层:配额(quota) 限制同时占用的资源量,防止单个团队占满集群
第二层:预算(budget) 限制周期内的总花费,超过告警或拒绝
第三层:熔断(circuit breaker) 异常时自动停止(比如某流程成本突然涨 10 倍)
预算告警要分级:70% 预警(通知负责人)、90% 严重(通知管理者)、100% 超限(自动限制新任务提交或需要审批)。只做「超限告警」而不做「接近预警」,会导致每次都是事后补救。
# 预算策略示例
budget:
scope: { tenant: team-order, period: monthly }
amount: 50000 # 单位:元
thresholds:
- { pct: 70, action: notify, channel: "#team-order" }
- { pct: 90, action: notify, channel: "#team-order-leads" }
- { pct: 100, action: throttle, max_concurrent_tasks: 10 }
熔断适合「异常成本」场景:某个流程因为数据量突增或死循环导致成本是平时的 10 倍,熔断能自动止损,实现方式是「与历史基线比较」:
-- 检测成本异常:与过去 7 天的同流程成本基线比较
SELECT flow_name, today_cost, baseline_cost, today_cost / NULLIF(baseline_cost, 0) AS ratio
FROM flow_cost_daily
WHERE biz_date = CURRENT_DATE
AND today_cost > baseline_cost * 5; -- 超过基线 5 倍即告警/熔断
16. 成本与可靠性的取舍
成本优化最容易犯的错误是「为了省钱牺牲可靠性」,而后者的代价往往是前者的数倍。
三类典型的风险交易:
用 Spot 实例换成本 风险:被回收导致延迟,需要幂等与重试兜底
缩短保留期换存储成本 风险:排障时历史数据不够,故障定位变慢
降低冗余换计算成本 风险:单点故障导致整体不可用
判断标准是「省下的钱 vs 故障的期望成本」。一次 P1 故障的成本(业务损失 + 人工 + 声誉)通常是几千到几十万元,而一项优化可能每月省几百元。因此在关键路径上的优化要格外保守。
一个实用的原则是**「区分关键路径与非关键路径」**:关键路径(影响线上业务、有硬 SLA 的流程)用按需实例、保留完整历史;非关键路径(离线报表、实验性任务、可重跑批处理)可以激进优化,用 Spot、短保留期、低冗余。
17. 优化项的优先级排序
优化项很多,但收益与成本差异巨大。排序依据是「收益 / 实施成本」:
| 优化项 | 典型收益 | 实施成本 | 优先级 |
|---|---|---|---|
| 全量改增量计算 | 50%~99% | 中 | 最高 |
| 计算下推 | 30%~80% | 低 | 最高 |
| 中间结果生命周期清理 | 10%~30% | 极低 | 最高 |
| requests 按实际校准 | 20%~40% | 低 | 高 |
| 合并过细任务 | 30%~70% | 中 | 高 |
| 批处理任务迁 Spot | 50%~65% | 中 | 高 |
| 结果缓存 | 20%~60% | 中 | 中 |
| 日志采样 | 5%~15% | 低 | 中 |
| 弹性伸缩与换引擎 | 10%~30% | 极高 | 低 |
前四项应该先做,因为它们的实施成本低而收益高,且不需要改业务逻辑。特别是「中间结果清理」——它可能只需要配置一条生命周期规则,就能省下可观的存储费用。
18. 组织与流程:谁负责成本
技术手段之外,成本优化还需要组织机制。三个实践:
一是成本可见性下放到团队。每个团队应该能看到自己流程的成本看板,而不是只有平台团队能看到总账单。可见性是行为改变的前提——团队看到「这个流程每天花 2000 元」才会有优化动机。
二是成本纳入发布评审。对于新增的流程或任务,评审时应该问「它的预估成本是多少」「数据量增长后成本如何变化」。这与性能评审是同一类实践。
三是设立成本目标。比如「单位成本每季度下降 10%」。有目标才有持续投入,否则成本优化永远是「有空再做」的事。
需要注意的是成本目标不能压过可靠性目标,两个目标冲突时可靠性优先。这个原则必须显式声明,否则会出现「为了达标而砍掉必要冗余」的情况。
19. 落地路线图
- 第 1 周:给所有任务打上归属标签(workflow / step / tenant / env / cost-center),开启云厂商的成本分配标签。
- 第 2 周:建立成本归因看板,能回答「哪条流程最贵」「哪一步最贵」。
- 第 3 周:配置中间结果的生命周期规则与日志采样,这是最快见效的两项。
- 第 4 周:接入资源使用采集,输出
util_ratio < 0.3的任务清单,逐个校准 requests。 - 第 5 周:找出全量重算的任务,评估改增量计算的可行性,从数据量最大的开始。
- 第 6 周:把可重试的批处理任务迁到 Spot 实例,观察回收率与重试成本。
- 第 7 周:建立单位成本指标与预算告警(70% / 90% / 100% 三级)。
顺序上「打标签」必须最先做,因为没有归因就无法验证任何优化的效果。前四周的优化项都是低实施成本高收益的,应该先拿到这些收益再考虑复杂的改造。
20. 权衡取舍
| 选择 | 收益 | 代价 |
|---|---|---|
| 任务切细 | 失败重试粒度小、并行度高 | 调度开销占比高 |
| 任务合并 | 调度开销低 | 重试代价大、资源不均衡 |
| 结果缓存 | 消除重复计算 | 缓存失效逻辑复杂、有正确性风险 |
| 增量计算 | 成本与数据量解耦 | 迟到数据处理复杂 |
| 全量重算 | 逻辑简单、无状态 | 成本随历史增长 |
| Spot 实例 | 成本降低 50%~65% | 被回收导致延迟,需幂等与重试 |
| 按需实例 | 稳定、无中断 | 成本高 |
| 弹性伸缩 | 降低闲置成本 | 冷启动延迟、抖动、复杂度 |
| 固定容量 | 稳定、可预测 | 峰值需要预留余量 |
| 缩短保留期 | 存储成本低 | 排障与审计能力下降 |
| 保留完整历史 | 排障容易 | 存储成本持续增长 |
| 成本硬限额 | 防止失控 | 可能阻塞正常业务 |
21. 常见坑清单
- 没有成本归因标签,账单是一笔合并数字,无法定位是谁花的钱。
- 缓存键不包含代码版本,代码更新后仍返回旧逻辑的缓存结果。
- 分区条件写在函数里(如
date_format(created_at)),分区裁剪失效导致全表扫描。 - 把百万行数据拉到应用层做聚合,传输、内存、计算成本全部放大。
- 中间结果没有生命周期规则,对象存储占用逐月增长直到成为账单大头。
- 只做超限告警不做接近预警,每次都是事后补救。
- requests 按峰值配置,节点看起来满了但实际利用率只有 20%。
- 任务切得过细,单任务耗时 1 秒而调度开销 3 秒,开销占比 75%。
- 重试不设预算,一个失败率 50% 的任务成本翻倍且无人察觉。
- 把不可重试的任务(有外部副作用)迁到 Spot 实例,回收后产生不一致状态。
- 弹性伸缩用在「波峰只有 10 分钟」的负载上,扩容还没完成峰值就过了。
- 只看总成本不看单位成本,业务量下降时误以为优化成功。
- 日志全量落盘不做采样,正常路径的常规输出占了 90% 的日志成本。
- 为了降本砍掉关键路径的冗余,一次故障的损失超过全年省下的钱。
22. 小结
成本优化的起点是度量,终点是单位成本指标的持续下降。中间的手段虽然多,但收益分布极不均衡:归因、生命周期清理、requests 校准、全量改增量这四项的实施成本低、收益高,应该先做完再考虑复杂改造。
判断一个优化是否值得做,标准是「省下的钱 vs 故障的期望成本」。关键路径上要保守(用按需实例、保留完整历史),非关键路径上可以激进(Spot、短保留期、低冗余)。这个区分必须显式声明,否则优化会无差别地侵蚀可靠性。
最后,成本是「组织问题」而不只是「技术问题」。没有可见性、没有目标、没有评审机制,技术手段的效果会随时间衰减——优化一次,然后慢慢反弹。把成本指标放进团队的日常看板,比任何单次优化都更持久。成本相关的容量规划参见 工作流引擎全景与选型 ,执行器侧的资源配置参见 Kubernetes 与云原生专题 。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。