调度页面一切正常。绿点、成功、耗时 4 分 12 秒,什么都没红。但每天凌晨两三点那一批任务里,总有两条第二天早上翻日志是被 OOM killer 直接干掉的——进程没了,run 状态卡在 STARTED,webserver 上还显示"运行中",一直到有人手动去终止它。这种"页面全绿、实际已经死了"的状态,是 Dagster 部署里最常见、也最容易被误判的一类故障。
误判的方向通常是错的。第一反应都是"内存不够,加内存",加完确实好了一阵,两周后同样的 OOM 又回来,只是换了另一个任务。真正的原因不在内存总量,而在于 Dagster 的资源消耗根本不是一坨,而是分层的,各层吃的东西完全不一样:
这篇不按"概念—配置—价格"那套顺序走。顺着一次真实 OOM 的排查路径往下拆:先把进程拆开看谁在吃,再算并发槽该给几个,然后追到 IO 管理器这个内存开关,接着算 Postgres 那三套表的增长,最后落到两种部署拓扑的服务器规格怎么配。
Dagster 对外是一个"调度器",装起来是一个 pip 包,看起来是一套东西。实际跑起来是四类进程,加上一个你很容易忘掉的数据库。它们对服务器的要求几乎正交——这正是"加内存不管用"的物理原因。
webserver(老版本的 dagit)只做一件事:给 UI 和 GraphQL 接口提供查询。它本身不执行任何用户代码,也不调度任何东西。它的资源画像非常单一——常驻内存几百 M 到 1G 多,CPU 平时几乎为零,一旦有人打开一个 event 特别多的 run 详情页,它会瞬间发起一批 event_logs 查询,CPU 和 Postgres 连接同时上去。
这里有个很实际的问题:run 详情页的加载时间,跟你这次 run 产生了多少 event 行强相关。一个 op 数上百、日志级别开在 DEBUG 的作业,详情页能把几十万行 event 拉出来排序渲染。所以 webserver 的"慢",八成不是 webserver 的锅,是 event_logs 表太大、索引失效或者 Postgres 那台机器 IO 打满了。
对服务器规格的意义是:webserver 这一层不需要大内存,需要的是和 Postgres 之间低延迟的链路,以及一个别被别人抢走的 CPU。
daemon 是一个长期运行的后台进程,内部跑着几类循环:schedule 的 tick(检查有没有到点的调度)、sensor 的 evaluation(每个 sensor 按自己的 interval 轮询)、run 监控(检查卡住的 run 要不要标记失败)、以及 backfill 的推进。这些循环有个共同特征:它们是时间敏感的,但不吃内存。
daemon 的资源诉求是"稳定的、不被抢占的一小片 CPU,加一条稳定的 Postgres 连接"。单个 daemon 的常驻内存在几百 M 量级,但如果你的 sensor 写得不讲究——比如每次 evaluation 都去扫一遍上游系统的全量列表、把结果塞进 cursor——内存会被 Python 对象慢慢堆上去,表现为 daemon 跑几天后从 300M 涨到 2G,然后开始被 OOM 盯上。
还要记住一条硬约束:daemon 在一个 instance 里只能有一个活跃实例。它靠心跳与数据库锁来协调,多起几个不会带来高可用,只会带来互相等待和日志里的锁告警。想给 daemon 做高可用,走的是"另一台待命、靠外部机制拉起"的路子,不是"两台一起跑"。
run worker 才是真正执行你代码的地方。它的启动方式由 run launcher 决定:默认在本机起一个进程,换成 DockerRunLauncher 就是起一个容器,换成 K8sRunLauncher 就是起一个 pod。无论哪种形态,你写的 op 在哪跑,内存就在哪烧。
很多人混淆了两件事:run launcher 决定"这个 run 在哪跑",executor 决定"这个 run 内部的 op 怎么并发"。这两层各有一套并发参数,都要设,只设一个等于白设。run launcher 之上还有 run coordinator 管队列——开源版默认的 DefaultRunCoordinator 根本不排队,多少个 run 同时到点就同时往下放。
run worker 的内存曲线还有一个特征:它是 fork 出来的子进程,父子进程在 fork 那一刻共享页表,但 Python 的引用计数会触发 copy-on-write,很快就会各自持有自己的副本。也就是说,你以为"父进程加载一次依赖,子进程免费复用",实际上跑起来之后每个子进程都会实打实占一份。加上 pandas 这类库在 fork 之后的行为并不总是干净,实践中多进程 executor 比预想的更费内存。
这一层最容易被忽略。每个 code location 在 Dagster 里是一个独立的 gRPC 服务进程,webserver 和 daemon 都通过它来读取你的定义(job 长什么样、asset 有哪些、sensor 的 interval 是多少)。它常驻——不是 run 来了才起。
它的内存占用等于"你在这个 code location 里 import 了什么"。一个只 import dagster 加几个标准库的定义文件,进程可能一两百 M;一个 import pandas、pyarrow、sqlalchemy、boto3 的定义文件,进程轻松到 500M 到 1G;如果定义文件里还顺手做了重活(读配置文件拉远程 schema、建立连接池、预加载模型),那就更没边了。
更要紧的是数量。你开几个 code location,就常驻几个 gRPC 进程。把大项目按团队切成五六个 code location 是好实践,代价是机器上永远蹲着五六个几百 M 的进程。这部分内存跟今晚跑不跑任务一点关系都没有,它属于"常驻开销",在后面算并发槽公式时要单独列一项。
四种进程挤在一台机器上,故障不是"整体变慢"这么温和,而是两个方向上的互相伤害。
一个方向是 CPU 抢占导致的调度延迟。run worker 满负荷时把 CPU 打满,daemon 的 tick 拿不到时间片,schedule 到点了但没能及时提交,于是全天的调度集体顺延几分钟到几十分钟。这种延迟在 UI 上表现为"这次 run 的开始时间比计划时间晚",很容易被当成正常排队忽略掉,直到下游业务方来问"数据怎么还没到"。
另一个方向更狠:内存耗尽时,OOM killer 不挑人。它按 oom_score 选目标,而 oom_score 大体跟进程占用和运行时长相关。run worker 通常是最大的那个,被杀掉是应该的,但同一台机器上的 code location gRPC server 和 daemon 也在候选名单里。一旦 daemon 被杀,整个调度停摆,而 webserver 还在页面上绿着——因为页面读的是数据库里的旧状态。这就回到了开篇那个现象:页面全绿,实际已经死了。
还有一层是磁盘。run worker 的临时目录、fs_io_manager 的中间产物、compute log 都在同一块盘上,回填跑一夜把盘写满,第二天 webserver 和 Postgres 一起报错。盘满在 Linux 上从来不是温和降级。
同样四种进程,本地部署(宿主机上 pip 装好环境,code location 直接在宿主机起)与容器化部署(每个 code location 一个镜像,run 也在容器里跑)对服务器的要求差别不在内存。
本地部署的省与痛都很明确:省掉了镜像拉取和容器启动的几秒到几十秒冷启动,短任务特别划算;痛在依赖冲突——两个 project 要两个 pandas 版本,就得靠 venv 或者干脆两台机器,升级一个依赖要动宿主机环境,回滚靠人肉。
容器化把依赖隔离干净了,代价是两块经常被低估的开销:磁盘,一个装了 pandas、pyarrow、数据库驱动的数据平台镜像轻松 1G 到 2G,多版本并存加上构建缓存,几十 G 就没了,系统盘给 100G 会很紧张;启动开销,每次 run 拉起容器都要走一遍启动流程,op 粒度切得太碎的话,光启动时间就能超过计算时间。所以容器化部署的机器,规格上要多给磁盘、少操心 CPU,而本地部署的机器要给一块干净的系统盘外加严格的虚拟环境规范。
| 进程/角色 | 主要资源消耗 | 单机可合并性 | 扩容信号 | 踩过的典型坑 |
|---|---|---|---|---|
| webserver (Dagit/UI + GraphQL) |
常驻内存数百 M 至 1G 余;CPU 平时接近零,打开大 run 详情页时突增;消耗的是 Postgres 的读连接与网络 | 可与 daemon 同机,属于编排层 | 详情页打开要十几秒;并发访问人数上来后 GraphQL 排队 | 把页面慢归咎于 webserver,实际是 event_logs 表膨胀或 Postgres 那台 IO 打满 |
| daemon (schedule tick / sensor / run 监控) |
常驻内存数百 M;要的是稳定不被抢的 CPU 时间片与一条稳定的 Postgres 连接;sensor 数量决定 tick 频率 | 可与 webserver 同机,不建议与 run worker 同机 | run 实际开始时间持续晚于计划时间;sensor 的 evaluation 日志间隔被拉长 | 被 worker 抢 CPU 导致全天调度顺延,UI 上看不出来;多起一个 daemon 实例以为能高可用,结果互相等锁 |
| run worker (执行用户代码,run launcher 拉起) |
全部内存与 CPU 压力集中在这里;峰值 = 并发 run 数 × 单 run 并发 op 数 × 单 op 峰值内存;另吃临时磁盘 | 可横向加机器,是唯一值得加机器的角色 | OOM 频发;run 排队时长上升;临时目录写满系统盘 | fork 出来的子进程没复用依赖,每个子进程都实打实占一份内存;只加内存不加并发上限,两周后 OOM 复现 |
| code location (gRPC server,每 location 一个) |
常驻内存等于定义文件里 import 了什么,pandas/pyarrow 一类依赖可到数百 M 至 1G;不随 run 结束释放 | 调度层与执行层各需一份,注意别重复起 | 机器空闲时内存仍居高不下;新增一个 location 内存就上一台阶 | 按团队切十几个 code location,常驻开销吃掉整台机器的一半内存;被 OOM 误杀后 UI 报"加载失败" |
| Postgres (event log / run storage / schedule storage) |
磁盘容量与随机写 IOPS 双敏感;shared_buffers 一般给 2G 至 4G 起;大量 DELETE 后表与索引膨胀 | 小团队可与编排层同机,量大了必须独立 | event_logs 表月增幅失控;详情页查询从秒级掉到十几秒;autovacuum 长期在跑 | 半年不管直接撑爆数据盘;删完数据不处理膨胀,表文件一点没小;和业务库同一实例被慢查询拖死 |
| 本地临时目录与中产物 (DAGSTER_HOME 下 storage / compute log / tmp) |
顺序写为主,容量消耗随 run 数与数据量线性增长;失败 run 的残留不会被自动清理 | 必须与系统盘、数据库盘分开挂载 | df 看到剩余空间按天掉;回填跑一夜后盘满 | 临时目录落在系统盘,盘满后 SSH 都登不上;容器化部署时镜像层与构建缓存把系统盘吃掉 |
Dagster 有三层并发控制,管的东西完全不同,混着讲一定会配错。
第一层是 run 并发,由 QueuedRunCoordinator 的 max_concurrent_runs 控制,管的是"同一时刻最多有几个 run 在执行"。这是最粗的一层,也是必须设的一层。
第二层是 tag 并发,tag_concurrency_limits 允许你按 tag 分槽,比如给"重活"这一类打 tag,限制它同时只能跑 2 个,而"轻活"这一类可以跑 10 个。回填还会自动带上 backfill 相关的 tag,可以用它专门给回填限流。
第三层是 op 并发,管的是"一个 run 内部最多同时跑几个 op"。在 multiprocess executor 里由 executor 的 max_concurrent 控制;而 op 级别的 concurrency key(给 op 打上并发标签、跨 run 限流)需要 Celery 一类的分布式执行器才生效,纯多进程下不生效。这一点必须提前知道,否则你会配了半天发现没反应。
开源版默认链路是 DefaultRunCoordinator 加默认 run launcher 加默认 executor,这条链路不对并发设任何上限——具体行为以你所用版本的配置文档为准,但普遍现象是:多个 schedule 恰好同时到点,就同时起那么多 run,每个 run 内部又按 executor 的默认策略 fork 出若干 op 子进程。
于是进程数是相乘的。假设凌晨三点有 8 个 schedule 同时触发,每个 run 内部有 4 个 op 可以并行,每个 op 峰值 1.5G,那么这一瞬间的内存需求是 8 × 4 × 1.5 = 48G。一台 32G 的机器在这个瞬间必然 OOM,而它平时闲得要死。这就是"加内存管两周"的机制——你加到 64G,下周 schedule 多配两个,又回到原点。
所以正解不是加内存,是把乘积里的两个因子都显式设成固定值:QueuedRunCoordinator 的 max_concurrent_runs 设上,executor 的 max_concurrent 设上。设完之后内存需求才是可预测的,也才谈得上算规格。
把需要的内存拆成常驻和峰值两部分:
M_total = M_base + R × O × M_op,再乘上 (1 + α) 的安全系数
其中:
代入一个典型的小团队场景:M_base 按 8G 算(系统 1G、webserver 1G、daemon 0.5G、两个 code location 共 1.5G、Postgres 3G、父进程 0.5G、杂项),设 R = 4、O = 2,实测 M_op = 2G,那么:
8 + 4 × 2 × 2 = 24G,乘 1.4 之后约 33.6G。这台机器至少要 32G,而且 32G 是踩着线跑,建议直接上 64G 或者把 R 降到 3。
M_op 怎么测才靠谱:别用"感觉",用 /usr/bin/time -v 跑一次真实 op,看 Maximum resident set size 那一行;或者用 cgroup 的 memory.peak 读峰值;长期监控用 node exporter 加 process exporter,按进程名看常驻集的 P99。注意要测最大分区那一次的峰值,不是平均值——平均值测出来永远够用,峰值才是 OOM 那一下。
分区和回填是并发最容易被放大的地方。一个按天分区的 asset,回填一年就是 365 个 run 一次性提交进队列。如果没给回填专门设 tag 并发限制,队列会按 max_concurrent_runs 放,而这个值通常是按日常负载配的,回填一来就是持续几小时的满负荷。
估算方法很直接:峰值内存 = 回填并发槽数 × 单 run 峰值,而"单 run 峰值"又等于 O × M_op。也就是说回填期间这台机器要扛的,是平时峰值的 R 倍持续负载,而不是尖峰。持续负载的杀伤力更大,因为它同时把磁盘写满、把 Postgres 写爆、把 CPU 占死。
三条实操:给回填单独设 tag 并发,值设成日常并发的一半或更低;大回填按季度分批提交,别一次丢一年的进去;回填窗口避开线上跑批时段,因为回填和日常任务抢的是同一批槽位,抢起来两边都慢。
同一份代码、同一台机器,换一个 IO 管理器,内存占用可以差好几倍。这件事在 Dagster 里比在别的编排框架里更关键,因为 Dagster 是 asset / op 显式传值的模型——上游 op 的返回值要交给下游 op,中间一定有个"放在哪"的问题。
in_process executor 在同一个进程内跑完所有 op,默认搭配的是内存 IO 管理器。所有中间产物都留在同一个进程的地址空间里,直到整个 run 结束才释放。好处是快,没有任何序列化开销;代价是 run worker 的内存占用等于"整个 DAG 里同时在世的所有中间对象的总和"。
一个具体形态:op A 读一张 3G 的宽表返回 DataFrame,op B 和 op C 都消费它。内存 IO 管理器下,这个 DataFrame 会一直被引用着,直到 B 和 C 都跑完。如果 B 和 C 各自又产出一个同量级的中间结果,那么三个大对象同时在内存里,峰值就是 9G 往上。这不是"某个 op 太重",是 IO 管理器的默认行为决定的。
默认的 fs_io_manager 把中间产物 pickle 到本地盘(位置在 DAGSTER_HOME 下的存储目录或 run 的临时目录里)。内存占用的形态立刻变了:op 跑完就把结果序列化出去,本地内存压力大幅下降,代价是磁盘写入量与序列化耗时。
pickle 出来的文件体积大致等于对象在内存里序列化后的大小,一个 2G 的 DataFrame pickle 出来也在 G 量级。所以这一步是实打实的磁盘换算——你以为省了内存,实际上是把账单转给了磁盘。
还有个必须知道的细节:run 失败时,中间产物默认是保留的,方便你重跑时复用;也正因为保留,失败 run 的残留会一直堆在盘上。跑过几百次失败回填的机器,storage 目录长到几十 G 很常见。
把中间产物丢到 S3 兼容的对象存储,是数据团队规模上去之后的标准做法。好处是本地盘彻底解放、多个 worker 之间可以共享中间结果、重跑不用重算上游。
这里有个选择要做:pickle 还是列式格式。pickle 保真、省事,但不可跨版本、不可被别的工具读、体积大;parquet 一类的列式格式体积小、可列裁剪、可被外部工具直接查,代价是对复杂 Python 对象不友好,需要你写序列化逻辑。做数据分析的团队,中间产物用 parquet 几乎总是更划算——下游只要三列的时候,读三列和读全表完全不是一个量级。
代价是两笔开销要提前算进服务器选型:API 调用成本与网络吞吐。op 粒度切得太碎,一次 run 产生几百个小文件,请求数会打满,延迟也会从毫秒级涨到几十毫秒级累积;以及本地仍然需要一块临时盘,对象存储 IO 管理器在读写前后通常还是要在本地落一次。
把 DAGSTER_HOME 下的 storage、compute log、临时目录单独挂一块数据盘,是这一步最该做的事,没有之一。理由不是性能,是隔离:临时目录写满的时候,死的应该是执行层,不是系统盘,更不是数据库。
具体要盯三样。compute log,每个 run 的标准输出都往里写,配置里可以把 compute log 的位置改到独立挂载点甚至对象存储,别让它躺在系统盘。临时目录,可用 DAGSTER_TMP 一类环境变量指到数据盘,并且挂一个按 mtime 清理的定时任务,超过三到七天的直接删。容器化部署时还要加上 image 与 volume 的清理,容器退出之后留下的悬空镜像和匿名卷不会自己消失,跑几个月的机器能在上面堆出几十 G。
Dagster 的状态全在 Postgres 里,分成三套逻辑存储:event log storage、run storage、schedule storage。可以合成一个库也可以分开,但小团队通常是一个库三套表在长。
event_logs 是绝对的大头。每一个 op 的开始、成功、失败,每一次 asset 物化,每一条你从 op 里 yield 出来的日志,都在这里占一行,事件本身以 JSON 列存着。一个中等复杂度的作业跑一次,产生几百到几千行 event 是很正常的。
runs 与 run_tags 记的是 run 的元信息,行数等于 run 数,长得慢,但每行带索引,删的时候要小心。
jobs 与 job_ticks 记的是调度与 sensor 的每次 tick,这一套最容易被忽略。一个 30 秒 interval 的 sensor,一天就是将近 3000 行,一年就是百万行量级,而且是纯写入、从不读取。它单独看不大,架不住时间长。
不要套别人的经验值,用你自己库里的三个数字代进去:
SELECT count(*) FROM event_logs WHERE timestamp > now() - interval '1 day';SELECT avg(pg_column_size(dagster_event)) FROM event_logs WHERE run_id = '<某次真实 run 的 id>';。STEP 类事件通常几百字节,带大量 metadata 的物化事件可以上 KB。SELECT pg_size_pretty(pg_total_relation_size('event_logs'));然后用"日均行数 × 单行平均字节 × 天数"得到数据量的粗估,再乘上一个索引膨胀系数——Postgres 的索引通常能占到数据的三成到一倍以上,具体看索引数量和填充因子,这个值也得从 pg_total_relation_size 减去 pg_relation_size 的差额里反推出来。
这套算法的用处不在于算得多准,而在于把"半年后会不会撑不住"变成一个可以在第一周就回答的问题。第一周测出日均行数,乘以 180,加上当前的库大小,就知道该给数据盘配 200G 还是 1T,也知道该在第三个月还是第六个月开始归档。
开源部署没有开箱即用的自动清理任务(具体能力以你所用版本的发布说明为准),所以这件事要自己搭。四条思路按优先级排:
第一条,按 run 分批删,别一次性删大表。event_logs 上有 run_id 索引,正确的姿势是先用 GraphQL 或者数据库查出要清理的 run 列表,然后按 run_id 分批 DELETE,每批几千到几万行,批与批之间留间隔。一次性 DELETE FROM event_logs WHERE timestamp < ... 会开一个巨大的长事务,锁表、撑爆 WAL、把在线的写入一起拖慢。
第二条,先归档再删。把要清理的那批 run 的 event 用 pg_dump 或者 COPY 导出成文件,压缩后丢到对象存储,确认可恢复之后再执行删除。这一步省不掉——事件日志是事后复盘"三个月前那次数据为什么错了"的唯一证据。
第三条,把 compute log 从数据库里彻底挪走。compute log 默认落在本地磁盘的 DAGSTER_HOME 目录下,可以配置独立的 ComputeLogManager 指到数据盘或者对象存储。它是纯文本、量大、几乎不查询,放在数据库盘上纯属浪费。
第四条,删完必须处理膨胀。这是最常被漏掉的一步:Postgres 的 DELETE 只标记删除,表文件一点都不会变小,autovacuum 在持续高写入的库里往往跟不上节奏。所以清理之后要在业务低峰做一次 VACUUM (FULL) 或者用 pg_repack 一类的在线重组工具,并 REINDEX。不做这一步,你删了 50G 数据,磁盘占用可能一点没降。
容量是线性增长的,好预测;性能不是。event_logs 涨到几千万行之后,run 详情页的查询从秒级掉到十几秒,asset 的物化历史加载不出来,webserver 的连接池被几个慢查询占满,然后连调度都开始卡顿。这时候加内存是没用的,加 CPU 也只是缓解,真正的解法是把历史数据搬走。
所以判断"该不该清理"的信号不是磁盘使用率,而是两个指标:详情页打开耗时和 autovacuum 是否长期处于运行状态。前者到了秒级以上说明索引已经不好使了,后者说明死元组产生的速度超过了回收速度。
把前面的分析落到机器上,小团队实际只有两种拓扑值得考虑。没有中间状态——半分离的部署比全合并还难管。
webserver、daemon、code location 的 gRPC server、run worker、Postgres 全在一台。这是绝大多数 3 到 6 人团队的起点,也是开源版默认配置最自然的落地方式。
规格取向:内存第一,磁盘第二,CPU 够用就行。按前面的公式,把 R 设成 2 到 3、O 设成 2,M_op 控制在 2G 以内,M_base 按 8G 算,32G 内存是这条路的实用起点,能往上到 64G 更从容。CPU 给 8 到 16 核足够,因为并发槽已经把并行度限死了,多出来的核只在回填时有用。
磁盘必须分三块,这是拓扑 A 的生存底线:系统盘 100G 左右装系统和容器镜像;一块数据盘放 Postgres 的数据目录,200G 起步,要求随机写性能别太差;一块数据盘放 DAGSTER_HOME 与临时目录,容量按"日均 run 数 × 单次中产物体积 × 保留天数"估,SSD 优先但顺序写为主,容量比 IOPS 重要。
拓扑 A 的问题不是性能,是故障域:任何一个组件出问题,整条数据链路都停。OOM 会杀死 daemon,盘满会拖死数据库,一次失控的回填能把全部三件事同时触发。所以选拓扑 A 就一定要把三件硬约束配上去——并发上限、临时目录独立挂载、日志与中产物的定时清理。少了任何一条,这台机器活不过半年。
调度层一台:webserver、daemon、Postgres、以及调度侧需要的 code location gRPC server。执行层一台或多台:run worker 与执行侧的 code location。
拆开的收益很具体。内存故障域隔离——run worker OOM 时,daemon 在另一台机器上照常 tick,调度不中断,UI 上能立刻看到失败而不是卡在 STARTED。CPU 不再互相抢——sensor 的 evaluation 与跑批分开算,调度延迟消失。规格可以分开选——调度层要的是稳定和小内存,执行层要的是大内存和强 CPU,不用再取折中。
调度层的规格反而可以很小:4 到 8 核、16G 内存、一块 200G 到 500G 的高 IOPS 盘放 Postgres。它不吃内存,吃的是磁盘响应时间和不被打断的 CPU。省下来的预算全部堆到执行层上:16 核以上、64G 到 128G 内存、大容量数据盘,而且执行层是唯一值得横向加机器的角色——跑不动了再加一台,把 tag 并发按机器分槽就行。
这里有个很实际的问题:两台机器必须在同一个内网段里互通,run worker 要连调度层的 Postgres,跨公网走会同时带来延迟和暴露面。一万网络深耕 IDC 19 年(成立于 2007 年),在同一机房内开多台机器可以直接划进同一内网段,BGP 多线 + CN2 GIA 的出口留给 webserver 那一台给人访问,执行层那台不需要大公网带宽,这正好是分离拓扑最舒服的落地方式;7×24 中文工单、平均 5 分钟响应,加机器时不用排长队,免费的系统盘每日 3 份快照对调度层尤其有用——Postgres 的数据目录在做表重组之前有快照兜底,比任何口头备份承诺都实在。
具体到选型动作,拓扑 B 下两台机器的钱要花在不同地方。调度层选大陆节点的入门档就够,官网公示的起步价如华东 ¥699、华南 ¥799、华北 ¥899 起,弹性云一万云 ¥25 起(均以官网实时价为准);真正该投入的是独立数据盘与快照,而不是核数。
执行层建议直接看裸金属。官网公示的裸金属标准档里,E5-2698v4×2 / 32G / 1T 是 ¥3999 起(以官网实时价为准),32G 这一档正好卡在前面公式算出来的并发起步线上。但说句实话,这个配置对 run worker 是偏科的——双路 E5 给到几十个核,而并发槽设完之后你根本用不到这么多核,反倒是 32G 内存在 R = 4、O = 2 的场景下踩着线跑。所以这台机器的正确动作是加内存而不是加核:官网人工定制 GPU 机型页面公示的升级项里,内存加到 128G 是 +¥600/月、硬盘加 1T 是 +¥300/月(以官网实时价为准),这两个升级项对执行层的收益远大于任何 CPU 升级。
需要容器化部署的团队,还要在这台机器上多留 100G 到 200G 给镜像层与构建缓存。这部分开销不在任何配置清单里,但它每年都会让你清一次盘。
为什么坑:内存需求是"并发 run 数 × 每 run 并发 op 数 × 单 op 峰值"这个乘积,你不限制前两个因子,等于让需求随时间自由增长。加到 64G,下个月多配两个 schedule、多切一个 code location,OOM 就回来了,而且这次连哪条任务会死都不固定,排查成本更高。
怎么避:把 QueuedRunCoordinator 的 max_concurrent_runs 和 executor 的 max_concurrent 都设成明确数字,设完之后用公式重算一遍内存。加内存只能作为"在固定并发下余量不够"的手段,不能作为"让并发继续涨"的手段。顺序不能反。
为什么坑:这两个角色的故障模式正好互相放大。run worker 的临时目录和 Postgres 的数据目录抢同一块盘的 IO 与容量,回填一夜把盘写满,数据库直接进入只读甚至损坏状态,而这时候你连 SSH 都可能登不上去。反过来,Postgres 在跑大清理或者 autovacuum 时把 IO 占满,run worker 的每一步都在等磁盘,表现为所有任务莫名变慢。
怎么避:最低要求是分开挂载——Postgres 数据目录一块盘,DAGSTER_HOME 与临时目录另一块盘,系统盘第三块。一万网络这边能配的是独立数据盘与系统盘快照(免费系统盘每日 3 份、30 秒回滚),把数据目录单独挂一块,即使盘写满也只死执行层不数据库。量再大一点就把 Postgres 直接搬到调度层那台机器上,走拓扑 B。
为什么坑:in_process 执行器默认搭配内存 IO 管理器,中间产物全部留在同一进程的地址空间,直到整个 run 结束才释放。你的 op 可能只吃 1G,但 DAG 里三个大对象同时在世的时候峰值能到 9G。这种问题在代码审查里看不出来,因为每个 op 单独跑都正常。
怎么避:数据量大或者 op 链条长的作业,用 multiprocess executor 配 fs_io_manager,让中间产物离开内存;规模再往上走就换对象存储 IO 管理器并用 parquet 一类的列式格式。判断标准很简单——看 run worker 的内存曲线是不是"阶梯式上涨、run 结束才断崖下跌",是阶梯就说明中间结果在内存里堆着。
为什么坑:按天分区回填一年就是三百多个 run 一次性进队列,队列按日常配的 max_concurrent_runs 放,于是机器连续几个小时满负荷。它不是尖峰而是持续负载,同时把内存、磁盘、Postgres 三样一起压满,日常任务在这期间全部变慢,业务方先感知到的不是回填慢,是日报晚了。
怎么避:给回填相关的 tag 单独设并发上限,值取日常并发的一半甚至更低;大回填按季度分批提交,中间留出观察窗口;回填窗口避开线上跑批时段。回填前先按"分区数 × 单 run 峰值"估一次总时长,估出来超过一个维护窗口就分批。
为什么坑:容器隔离的是文件系统和依赖,不是内存。不给容器设 --memory 限制,它照样能把宿主机的内存吃光,OOM 之后的表现只是从"进程被杀"变成"容器 exit code 137",排查时反而更难看懂。而且容器化还多了一层冷启动开销,op 切得太碎的时候启动时间会超过计算时间。
怎么避:容器化之后照样要设并发槽,并且给每个容器显式设内存上限——设了上限之后,超内存的 run 会以可预期的方式失败并重试,而不是随机拖垮邻居。同时把镜像层与构建缓存的清理挂上定时任务,别让它吃系统盘。
差别在"谁在跑用户代码"。Airflow 的 worker 负责拉起任务,重活通常在外部系统(Spark、数据库、远程计算)里跑,worker 本身偏薄;Dagster 的 run worker 默认就在本机进程里执行你的 op,重活发生在编排机器自己的内存里。所以 Airflow 的服务器规格通常可以压得比较低,而 Dagster 的执行层必须按"你的数据有多大"来配。另一个差别是状态存储:Airflow 的元数据表相对轻,Dagster 的 event_logs 把每一步的事件都写进库里,写入量高一个量级,Postgres 那块盘要按写入型负载选,不能按容量选。还有一点容易被忽略:Dagster 的 code location 是常驻 gRPC 进程,Airflow 的 DAG 解析是周期性的,前者的常驻内存是"一直占着",后者是"解析时才尖一下"。
要调,这是 Dagster 部署里第一件该做的事,重要性高于加内存。开源版的默认 run coordinator 不排队、默认 executor 也不对并发设硬上限,结果是"同时到点的 run 数 × 每个 run 内部并行的 op 数"这个乘积完全失控。日常风平浪静是因为 schedule 到点时间错开了,一旦两个 schedule 撞到一起、或者有人提交一次大回填,机器就会在几秒内被打爆。调的动作是两步:把 run coordinator 换成 QueuedRunCoordinator 并设 max_concurrent_runs,把 executor 的 max_concurrent 设成明确数字。两个都设完之后,内存需求才从"随缘"变成"可计算",你才能按公式去选机器。
不给通用数字,因为日增长完全取决于你的 op 数和日志级别,给错了比不给更害人。但方法是可以固定的:跑 SELECT count(*) FROM event_logs WHERE timestamp > now() - interval '1 day' 得到日均行数,再用 SELECT avg(pg_column_size(dagster_event)) FROM event_logs WHERE run_id = '某次真实 run' 得到单行平均字节,两者相乘就是日增数据量,再乘 1.5 到 2 倍算上索引开销,乘 180 就是半年后的规模。第一周就能测出来,测完就知道数据盘该配 200G 还是 1T。要留意的变量是日志级别——op 里 yield 的每一条日志都是一行 event,把 DEBUG 关掉通常能让日增长降一个数量级。
小团队早期可以,但要清楚代价。Dagster 的 event 写入是持续、高频、小批量的,跟业务库的读写混在一个实例里,会互相影响 buffer cache 和 IO,而且 Dagster 做大清理或者 VACUUM 的时候业务库的响应时间会明显抖。真要放一起,至少做三件事:单独建一个 database 甚至单独的表空间、把 Dagster 的清理作业严格限制在低峰并分批、给两边设不同的连接数上限。一旦你发现业务接口偶尔慢一下但查不到原因,第一件事就是把 Dagster 的库拆出去。执行层和业务库更不要放一起——run worker 的临时目录和数据库的数据目录挤在一块盘上,盘满就是双杀。
会,而且是 Dagster 里最容易造成"生产事故"的操作。回填不是尖峰而是持续满负荷,它同时吃内存、写满临时盘、把 event_logs 的写入量抬高几倍,日常任务在这期间全部变慢。控制手段是三条一起上:给回填相关的 tag 设专门的并发上限,值取日常并发的一半甚至更低,这是最有效的一条;大回填按季度分批提交,中间留出观察窗口看内存和磁盘;回填窗口避开线上跑批时段,因为两者抢的是同一批槽位。提交之前先按"分区数 × 单 run 峰值内存"估一下并发期间的持续内存压力,确认机器扛得住再放。
按你的依赖复杂度选,不是按性能选。多进程执行器在本地 fork 子进程,冷启动几乎为零,适合 op 短小、依赖统一、一台机器上只有一个项目的场景,代价是依赖冲突只能靠 venv 或者多台机器解决。Docker 执行器每个 run 或每个 op 一个容器,依赖彻底隔离,适合多个团队共用一套 Dagster、或者不同作业要用冲突依赖的场景,代价是冷启动几秒到几十秒、镜像层吃掉大量磁盘、以及失败排查多一层。要注意的是:换了 Docker 执行器并不解决内存问题,容器不隔离内存,必须显式设容器内存上限,否则 OOM 只是从"进程被杀"变成"容器 exit 137",更难看懂。
取决于你的并发槽设成多少,而不是取决于你的团队人数。按 M_base 加 R × O × M_op 再乘 1.3 到 1.5 的公式算:M_base 常驻开销(系统、webserver、daemon、code location、Postgres、父进程)小团队算 6G 到 8G;R 设 2 到 4;O 设 2;M_op 必须实测,用 /usr/bin/time -v 看 Maximum resident set size。如果 M_op 是 1G 左右,32G 很宽裕;如果 M_op 是 2G 到 3G,32G 就是踩线,64G 才从容。所以正确答案不是"32G 够不够",而是"先把 M_op 测出来,再决定"。这里还要提一句,核多内存少的机型对 run worker 是偏科的——并发槽限死之后核根本用不完,钱应该花在内存上。
四个指标,按重要性排。第一是常驻内存的 P99,用 node exporter 加 process exporter 按进程名采集,看的是峰值不是平均——平均值永远够用,峰值才是 OOM 那一下。第二是 run 从入队到开始执行的等待时长,它直接反映并发槽够不够,持续上升说明槽位设小了或者执行层该加机器。第三是 run 实际开始时间减去计划时间的差值,这个指标异常说明 daemon 的 CPU 被抢了,是"调度层和执行层挤一台"的典型信号,往往在 OOM 之前几周就出现了。第四是临时目录与数据盘的剩余空间按天的变化率,它是磁盘会不会在下个月写满的先行指标。这四个指标配上之后,绝大多数 Dagster 的资源问题都能在变成故障之前被看见。
把这篇里最实用的那条公式再钉一遍:M_total = (M_base + R × O × M_op) × (1 + α)。R 是 QueuedRunCoordinator 的 max_concurrent_runs,O 是 executor 的 max_concurrent,M_op 是单个 op 的实测峰值常驻内存,M_base 是系统加 webserver 加 daemon 加各 code location 加 Postgres 的常驻开销,α 取 0.3 到 0.5。
这套换算的价值不在于算得准,而在于它把"内存不够"从一个感觉问题变成一个可以谈判的问题。你可以明确地回答业务方:要再上两个日常任务,要么把 R 从 4 降到 3 接受排队变长,要么把内存从 32G 加到 64G,二选一,不存在"既不加钱又不排队"的选项。反过来,如果有人要求把 op 峰值从 1G 涨到 3G(比如换了个更大的分区粒度),你立刻能算出这需要把并发槽砍到多少,或者需要多大的机器。
还有一个反过来的用法值得记住:当内存实在加不上去时,调小 R 和 O 的代价远小于把任务跑挂。并发槽调小,任务只是排队久一点,数据最终还是对的;并发槽不设,机器在峰值那一刻随机杀掉几条任务,数据就得靠人去补。这两者的成本差着一个数量级。所以真正的优先级是——先设并发槽,再测 M_op,最后才是挑内存多大的机器。顺序搞反了,加多少内存都是在给下一次 OOM 攒时间。
我的立场是:3 到 5 人、日均 run 数在几十到一两百、单 op 峰值 1G 以内的团队,先用 all-in-one,但按分离的要求去部署——也就是并发上限设好、三块盘分开挂、清理任务配齐。这时候分离的收益不足以抵消多一台机器的运维成本。
越过下面任何一条线,就该拆了:出现第一次"调度延迟"(run 实际开始时间持续晚于计划时间),说明 daemon 的 CPU 已经被执行层抢走了;出现第一次"OOM 杀掉了 daemon 或 code location",说明故障域已经串了;单 op 峰值超过 2G 且并发要 4 个以上,说明执行层的规格已经和调度层完全不在一个量级,合在一台机器上必然是取折中,两头都不舒服;有人开始提"我们要支持另一个团队的项目",说明 code location 数量要涨,依赖隔离的需求也跟着来了。
拆的时候钱怎么花是有明确答案的:调度层给小内存、好磁盘、稳定 CPU,执行层给大内存、大本地盘、可横向扩展。调度层 4 到 8 核加 16G 加高 IOPS 盘就完全够,它吃的是磁盘响应时间和不被打断的时间片;执行层是唯一值得堆内存和加机器的角色,因为并发槽一限满,加机器是线性提升吞吐量的。而执行层那台机器上,正确的升级动作永远是加内存和加数据盘,不是加核——几十个核在并发槽设完之后根本用不到,这是我在裸金属配置上最想提醒的一句话。
最后说一句不那么技术的:Dagster 这套东西的复杂度不在安装,在于它把"调度"和"执行"两件资源诉求完全不同的事,塞进了一个看起来像单一服务的产品里。你把它当成两个系统来规划服务器,绝大部分资源问题在选型阶段就消失了;你把它当成一个系统,那这台机器会在半年之内用三种不同的方式各崩一次。
本文数据与来源说明。文中关于 Dagster 四类进程、run launcher 与 executor 的职责划分、QueuedRunCoordinator 与 tag_concurrency_limits、fs_io_manager 与内存 IO 管理器的行为、event_logs / runs / job_ticks 三套表的用途,参考 Dagster 官方文档中 Deployment、Run Coordinator、Executors、IO Managers 与 Postgres Storage 相关章节,2026 年可查;具体配置项名称与默认值以你实际使用的版本文档为准,开源版的功能边界随版本变化,本文未对任何未明示的行为做确定性表述。
文中所有内存与存储数字,包括 M_base 的常驻开销取值、α 余量系数、单次 op 的峰值内存、日均 event 行数与单行字节,全部是需要你自己实测的量,文中给出的取值与代入过程只是演算示例,不构成任何性能承诺。测量方法已在文中给出具体命令与思路,包含 /usr/bin/time -v 的 Maximum resident set size、cgroup 的 memory.peak、以及针对 event_logs 的三条 SQL。
文中涉及的服务器规格参考与价格,来自一万网络官网 https://www.idc10000.net 的公开页面,包含裸金属服务器标准档(E5-2698v4×2 / 32G / 1T ¥3999 起)、人工定制 GPU 机型页面公示的升级项(内存 128G +¥600/月、硬盘 1T +¥300/月)、大陆节点起步价(华东 ¥699、华南 ¥799、华北 ¥899 起)与一万云 ¥25 起,以官网实时价为准。品牌信息中"深耕 IDC 19 年(成立于 2007 年)"来自官网公示。文中未列出价格的规格、线路、加盘与增值项,一律需实时询价,价格主要受 CPU、内存、存储、带宽、IP 和线路影响。
需要说明本文未做推断的部分:具体的 op 峰值内存、单次 run 的 event 行数、Postgres 索引膨胀系数、容器镜像层的实际占用,这几项在不同业务间差异极大,本文一律标注为需实测,未给出任何未经测量的确定数值。网络链路质量与跨机房内网延迟同样需实测,本文未给出未经测量的延迟数字。具体以签约时最新报价与合同为准。
Copyright © 2013-2020 idc10000.net. All Rights Reserved. 一万网络 科技有限公司 版权所有 深圳市科技有限公司 粤ICP备07026347号
本网站的域名注册业务代理北京新网数码信息技术有限公司的产品