把 Delta Lake 装到自己租的物理机上,和装一个 Spark 集群完全是两件事。Spark 是跑一次算一次,节点不够顶多是慢;Delta Lake 多了一层持续存在的事务日志和一套永远跑不完的后台维护动作——合并小文件、写 checkpoint、清 VACUUM。这些东西在业务低峰时段准时开动,把 CPU、内存、磁盘 IOPS 一块吃掉,白班值班的人第二天上班看到的是一串「昨天凌晨 OPTIMIZE 又超时了」的告警。
这份手册不打算再给你讲一遍 Delta Lake 的 ACID 事务有什么好处。假定你已经决定在自有或租用的机器上自建湖仓,现在只关心一件事:这台机器到底要几个核、多大内存、什么盘。下面按「CPU 账、内存账、磁盘 IO 账」三本账拆开算,每个结论都给具体数字和可以照抄的配置单。
先把五个最关键的判断摆出来:
1. OPTIMIZE 是纯 CPU 加内存的活,不是磁盘活。一次合并要把目标文件组整份读出来、解码、重新做行组编排、重新编码压缩,再整份写回去,最后写一条 Delta 事务日志才算数。这个过程中 CPU 负责压缩编码,磁盘只负责搬运字节。把 8 核机器换成全闪阵列,合并窗口不会因此缩短。
2. OPTIMIZE 和 Z-ORDER 完全不是一个价钱。OPTIMIZE 的打包逻辑会跳过已经达到目标大小的文件,只合并零散的小文件;Z-ORDER 是把命中范围的每一份数据都重写一遍。天天全表 Z-ORDER,是绝大多数自建湖仓白白烧掉一半 CPU 预算的头号原因。
3. 写入密度决定了合并窗口,而合并窗口决定了你要买几核。核心算式只有一条:单次需要重写的数据量 ÷(单核有效吞吐 × 有效核数)< 维护窗口时长。把这个算式算出来,采购单就有了依据;算不出来,买多买少都是在赌。
4. 小文件的真正代价不是存储容量,是元数据和打开句柄。每天 28 万个 8MB 文件听起来只有 2TB 出头,实际代价是 28 万次 Parquet footer 读取、28 万条 add action 挤进 driver 的 checkpoint、以及文件系统 dentry 缓存的一路膨胀。
5. 磁盘容量按峰值 ×1.3 买,别按逻辑数据量买。VACUUM 默认保留 7 天,被重写过的数据在这 7 天里是「逻辑已删、物理还在」的状态。一次性全表 OPTIMIZE 且不立刻 VACUUM,瞬时占用会接近两倍。
先说清楚 OPTIMIZE 到底干了什么。Delta 表里有一堆散落的 Parquet 文件,比如某个日期分区下躺着 3200 个平均 8MB 的文件。OPTIMIZE 做的第一件事是把这些文件按「装箱算法」归拢:把小于目标大小的文件挑出来,按每份不超过 delta.targetFileSize(常见设定为 128MB 到 1GB 之间)打包成若干个新文件,写完后在同一条事务日志里把旧文件标记删除、新文件登记入库。
关键在于「像整理抽屉一样原地挪一挪」这种理解是错的——Delta Lake 没有任何「原地修改文件」的能力,Parquet 也不支持往已有文件里追加。所以每一次合并都是:把旧文件的二进制内容完整读进内存,按列解码成 Spark 内部的列式格式,按新的行组边界重新切分,重新做字典编码、RLE、位打包,再选一种压缩算法压回去,最后写入新文件。读进来多少字节,就得吐出去差不多多少字节。
这中间的 CPU 消耗集中在三处:列解码再编码(占比最大)、字典构建(字符串列的字典重排很吃 CPU)、以及压缩算法本身。选 Snappy 的话,压缩这一环很轻,代价是文件略大;换成 ZSTD(spark.sql.parquet.compression.codec=zstd)能把落地体积再压掉 20% 到 35%(预估),但单核吞吐会掉一半左右——这是一笔纯粹的 CPU 换磁盘交易,后面算账时会反复出现。
很多人担心「每天压一次,是不是同一份数据被反复重写几十遍」。答案是不用担心,因为 OPTIMIZE 的装箱逻辑默认跳过已达目标大小的文件。一个 256MB 的文件不会因为在目标为 256MB 的表里待着而被重新读取;只有那些明显偏小的碎文件才会被挑进重写名单。
这条特性直接决定了 write amplification(写放大)的下界:正常情况下每天的合并写放大系数在 1.0 到 1.3 之间,也就是「读 230GB、写 230GB」。真正把写放大推上去的是另外两种情况:
第一种是目标文件大小设得太小。micro-batch 进来的是 8MB 的文件,你把 target 设成 32MB,那么一合并就得到一堆刚好 32MB 的文件,第二天再来一批新的碎文件,装箱时这 32MB 的文件离 target 已经很近、勉强不重写,但稍微波动就又被卷进来。来回几次,写放大能上到 2 倍以上。
第二种是全表 Z-ORDER。Z-ORDER 不看文件大小,它在选定范围内重新按 Z 值排列记录,因此范围内的每一份数据都会被重写一遍。这就是下面要单独拎出来说的那笔账。
算账必须有数字。在一台现代 Xeon 或 EPYC 机器上,跑「读 Parquet + 解码 + 可能排序 + 重编码压缩」这条完整流水线,实践中常见的单物理核吞吐区间是这样的(以下均为预估,取决于列数、字符串占比、压缩算法和 NVMe 队列深度):
Snappy 列压缩、30 到 60 列的中等宽度表,单核端到端大约 25 到 45 MB/s;换成 ZSTD 默认级别,掉到 15 到 28 MB/s;再叠加 Z-ORDER 的多维排序,因为多了 shuffle、外部归并排序和可能的 spill,在这个基础上再打六到七折,大约 12 到 25 MB/s。
这三个数字就是后面所有核数推算的基准。要提醒一句:超线程在这里不能按两倍算。压缩编码是典型的流水线密集型任务,开超线程通常只能多换 20% 到 30% 的吞吐,按 1.25 倍记比较稳妥。
真正决定买几核的是下面这个不等式:
单次需重写数据量(MB)÷[单核有效吞吐(MB/s)× 有效核数]< 可用维护窗口(秒)
其中「有效核数」不是纸面核数,要连减三刀:
第一刀扣操作系统和后台服务。Executor 所在节点上有 Spark shuffle service、日志采集、监控 agent、文件系统的 journal 线程,至少留 4 个核给他们,留 10% 更保险。
第二刀扣超线程折算。40 核 80 线程的机器,实际有效核数按 40 × 1.25 ≈ 50 记。
第三刀看输出 task 数能不能喂满。每次 OPTIMIZE 的输出文件数约等于「重写数据量 ÷ 目标文件大小」,每个输出文件对应一个 task。2TB、256MB 目标,得到 8192 个 task,喂 64 核毫无压力;但如果只有 300GB、目标却是 1GB,只有 300 个 task,在 64 核上跑也只能勉强铺满一轮,多出来的核基本在空转。
算题一:日增 230GB 的轻量湖仓。按每 10 分钟一个 micro-batch、每批约 8MB × 200 个 task = 1.6GB 算,一天 144 批。每天对当天新分区做一次 OPTIMIZE,重写量约 230GB,即 235,520MB。Snappy 按 32MB/s 单核算:235,520 ÷ 32 = 7,360 核秒 ≈ 2 核小时。给 12 个有效核就是 10 分钟出头。结论很朴素:这个体量根本不需要专门堆核,一台单路 16 核的机器绰绰有余,钱应该花在内存和 NVMe 上。
算题二:日增 2TB、周末还要 Z-ORDER 热点分区的主力生产档。每天增量合并:2,097,152MB ÷ 32 = 65,536 核秒 ≈ 18.2 核小时,32 个有效核跑大约 34 分钟。麻烦在周末那次 Z-ORDER:假设要对最近 30 天的热点分区重排,涉及 5TB 数据,单核按打七折后的 20MB/s 算:5,242,880 ÷ 20 = 262,144 核秒 ≈ 72.8 核小时。维护窗口给 3 小时,需要的核数是 72.8 ÷ 3 ≈ 24.3 核;但考虑到这是重 IO 混合负载,别忘了还要给 spill 和业务重试留冗余,实际建议按 2 倍取 48 到 64 个有效核。这就是采购单上的第一条硬约束。
算题三:全表 120TB、每周做一次全量 Z-ORDER 的重度档。按每次重写 40TB 算:41,943,040MB ÷ 20MB/s = 2,097,152 核秒 ≈ 582 核小时。窗口给 8 小时 → 需要约 73 核;加冗余取整数,128 到 160 个有效核,也就是三到四台 40 核双路裸金属。这种规模下我一般会反过来问一句:这 120TB 是不是真得每周重排一次?如果其中 80TB 是三个月没人碰过的冷分区,答案通常是「不该」,把冷分区排除掉,核数直接砍掉三分之二。
上面三道题都隐含「窗口越短越好」的假设。实际生产中,合并窗口本身是可以谈的。如果你的湖仓只对内服务、凌晨三点到七点没人查,那就放心把窗口按 4 小时甚至 6 小时算——核数需求直接砍到一半。反过来说,如果这是一个要 24 小时承接 ad-hoc 查询的湖仓,维护窗口被压到 1 小时内,那就必须靠核数硬扛。
这里有个明确的取舍判断:预算有限时,优先把窗口拉长,而不是后期补核。因为核数是按月付租金的持续成本,而窗口是免费的资源。把 OPTIMIZE 拆成「每天只压新增小文件、周末才做重活」的节奏,比一口气买两倍核数划算得多。
Z-ORDER 的思路不复杂:把多个列的值按位交织成一个单一的 Z 值,记录按这个 Z 值排序,多维上邻近的记录在物理上也大概率挨在一起。排序之后,每个文件携带的列统计信息(min/max)覆盖的值域变窄,查询时的 data skipping 就能跳过更多文件。
代价是三层叠加的。第一层:重写范围不受文件大小保护。前面说过 OPTIMIZE 会跳过肥文件,Z-ORDER 不认这套,范围内的全量重写。第二层:为了算出全局的 Z 值,必须做一轮 shuffle 与排序。排序意味着记录要跨 executor 交换,spark.shuffle 阶段的磁盘读写是整个 compaction 里最脏的一段随机 IO。第三层:内存装不下就 spill。排序缓冲区溢写到本地磁盘后,本来在内存里完成的归并变成多轮外部归并,CPU 和磁盘双双承压。
Z-ORDER 有个很刻薄的特性:排序效果主要集中在前面一到两列。你把三列写进 ZORDER BY (a, b, c),实际得到的是「a 聚得很好、b 勉强、c 基本没什么效果」。所以列数通常给 2 到 4 列就够了,再往上加只是在烧 CPU。
列的摆放顺序和基数也有讲究。
把低基数列放前面(比如「国家」「业务线」「事件类型」,取值几十到几百个),让记录先被粗粒度切成块;把高基数列(user_id、device_id、毫秒级时间戳)放在靠后位置,或者干脆不放。如果反过来把高基数列放在第一位,每一条记录都有自己的 Z 值区间,后面所有列的聚类效果会被彻底冲掉,等于花了四倍 CPU 买了个寂寞。
判断要不要做 Z-ORDER,我的标准很简单:看你的查询是不是带选择性过滤。如果这张表 90% 的消费方式是「拉取全表做 ETL」「整表聚合出日报」,Z-ORDER 一分钱都不该花,因为每条查询都要扫全量,skip 掉的文件比例永远是零。只有当查询确实是「按某个门店 ID、某个时间窗、某个事件类型点查或范围查」这类带明显过滤条件的场景,Z-ORDER 的投入才有回收期。
排序时的内存需求大致跟「单个 task 处理的数据量」成正比。经验做法是把 executor 切成 4 核一个的小格子:4 核 ×(12 到 16GB 堆内 + 6 到 8GB 堆外)。堆外那部分主要给 shuffle 的 Netty 直接内存和 Parquet 的列页缓冲区,这部分挪到堆外可以避免 GC 抖动放大。
判断内存够不够,别猜,看 Spark UI:如果 executor 页面的 Spill (Memory) 一列出现了非零值,说明排序缓冲区装不下,已经在往本地盘写。轻微 spill(几十 MB)可以接受,如果每个 task 都 spill 上百 MB,加内存比加核更有效。相反,如果 Spill 一直为零而 CPU 利用率已经打到 90% 以上,那才轮到加核。
我见过太多团队把「每天 OPTIMIZE + 每天 Z-ORDER 全表」写进 crontab 然后不管了。这套配置在一张 20TB 的表上意味着每天要重写 20TB,一个月 600TB 的写量,机房的电费和 SSD 寿命都烧在这里,而业务方查的最新数据可能只占其中 5%。
正确的做法是用分区把重排范围压下来:OPTIMIZE delta.`/path/tbl` WHERE dt >= '2026-10-01' ZORDER BY (shop_id, event_type),只对最近几天的数据做 Z-ORDER,历史分区在第一次上线时排一次就锁住不动。这样单次重写量从 20TB 降到 2TB 以内,所需核数跟着降一个数量级。
给 executor 划内存,最常见的错误是把内存全给了 heap。跑 compaction 时,内存有三个主要去处:
堆内负责 Spark 的执行内存和存储内存,默认按 spark.memory.fraction(0.6)从堆里切一块统一管理。 shuffle 期间的 spill 判断、拼接、hash 聚合都在这块。
堆外负责 Netty 的直接缓冲区、Parquet 读写的列页缓存。开启 spark.memory.offHeap.enabled=true 并给 spark.memory.offHeap.size 一个合理的值(每 executor 6 到 8GB),能在几乎不增加 GC 压力的前提下换来更稳的大页读写。
还有一块容易被忽略的是 spark.executor.memoryOverhead,它是 JVM 之外(线程栈、Netty、原生库)的安全垫。compaction 是多线程密集任务,给每个 executor 留 1.5 到 2GB 的 overhead 比较稳妥,宁厚勿薄。
照着这套切法,一台 40 核(80 线程)的机器可以这么排:
留出 4 核给操作系统、shuffle service 和监控 agent,剩 36 核给 executor;按每 executor 4 核算,正好 9 个 executor;堆内 12GB × 9 = 108GB,堆外 6GB × 9 = 54GB,overhead 2GB × 9 = 18GB,合计约 180GB。加 20% 的安全边际,这台机器应该配 192GB 到 256GB 内存。
注意这里藏着一个采购陷阱:很多 IDC 的裸金属起步档只带 32GB 内存,CPU 给得很足,看起来性价比极高。但按上面的算法,40 核只配 32GB 的话,你只能开 3 个 executor,而且堆外一开就 OOM。内存比 CPU 更容易成为瓶颈,且升级单价通常比加核贵,务必在签约前把内存配足。
driver 端的内存账很多人漏算。Delta 表的元数据都躺在表目录下的 _delta_log 里:每个 commit 写一个 JSON 文件(0000000000000000000000N.json),每 10 个 commit 写一次 Parquet 格式的 checkpoint(00000000000000000000010.checkpoint.parquet)。driver 要重建表状态,必须从最新的 checkpoint 出发,往后回放剩下那些 JSON。
checkpoint 里有什么?每条 add action 对应一个数据文件,记录了路径、大小、修改时间、以及 data skipping 用的列统计信息(默认给表的前 32 列收集统计)。一条带完整列统计的 add action,实际占用大概在 0.5KB 到 2KB 之间,取决于列数和列的复杂度。
把这个换算成具体规模:一张积累了 50 万个数据文件的表,checkpoint 文件的大小大约在 500MB 到 1GB。driver 把它读进来解析成内存对象,因为 Java 对象头、字符串池、HashMap 桶这些额外开销,实际占用通常是文件大小的 3 到 5 倍。这意味着 50 万文件对应 3GB 到 5GB 的常驻内存,100 万文件就要 6GB 到 10GB。加上 JVM 本身的 GC 余地和堆外占用,driver 给 16GB 起步、文件数超过 50 万给到 32GB 到 64GB,是比较靠谱的。
这里给一个能直接用的估算法:日志状态的常驻内存 ≈ 活跃文件数 × 1KB × 4(对象膨胀系数)× 1.5(余量)。50 万文件算下来约 3GB,100 万文件约 6GB。注意这只是重建 _delta_log 那一份的开销,driver 上还要跑 SparkContext、还要给 JVM 自己的 GC 留地方,所以整机落地依旧按「16GB 起步、50 万文件以上给 32–64GB」来配。
checkpoint 变慢有三个成因,逐个有对应的解法。
第一个是文件数爆炸。这个只能靠扎实的 compaction 解决——每周清一次 VACUUM、把目标文件大小设到 128MB 以上,把 add action 的总数压下来,这是治本。
第二个是列统计太贪。默认 32 列的统计信息在宽表(比如 200 列的埋点表)上会让每份 add action 膨胀好几倍。把 delta.dataSkippingNumIndexedCols 从 32 降到 8 到 12(只保留真正会被用来做过滤谓词的维度列),checkpoint 体积通常能缩小 30% 到 50%(预估)。代价是极少用到的那几列失去了 skip 能力,对大多数场景完全可以接受。
第三个是历史上留下的 JSON 从来没清。delta.logRetentionDuration 默认 30 天,清理日志的动作是跟着 VACUUM 走的。从来没跑过 VACUUM 的表,_delta_log 目录里堆着几万个小 JSON,driver 从头扫一遍要几分钟甚至更久。定期检查 _delta_log 的条目数量,超过 2000 就该安排一次带 RETAIN 参数的 VACUUM。
沿用算题一的节奏:每 10 分钟一个 micro-batch,每批 200 个 task,一天 144 批,每批落地 200 个文件,每个文件 8MB。
一天下来:144 × 200 = 28,800 个新文件,总容量 28,800 × 8MB = 230GB。一个月累计约 86 万个文件、6.9TB。
容量视角看这毫无问题,230GB/天 连一块 4TB 盘都撑不满一个月。真正的代价在下面三笔:
第一笔是打开文件的次数。每个 Parquet 文件被读取时,客户端要先读文件尾部的 footer(几 KB 到几十 KB)拿到 schema 和列块索引,再按需读列数据。86 万个文件意味着查询一个月数据时要做 86 万次 footer 请求。本地 NVMe 上一次小 IO 加一次 seek 大约 0.05 到 0.2 毫秒,合计 43 到 172 秒;在一块 7200 转机械盘上,一次 seek 就要 5 到 10 毫秒,光是打开 file handle 就要 1.2 到 2.4 小时;走对象存储的话,一次 GET 的端到端延迟在 5 到 20 毫秒,同样 86 万次就是 1.2 到 4.8 小时——这个数字足以让任何「秒级响应」的承诺破产。
第二笔是元数据的总量。86 万条 add action 要进 checkpoint,直接把 driver 顶到十几 GB;文件系统的 inode 和 dentry 缓存也要吃掉几 GB 的系统内存;ulimit -n(打开句柄数上限)如果还是默认的 1024,Spark 会大面积抛 Too many open files。
第三笔是 object storage 的请求计费。走 S3 兼容存储时,LIST 一次最多返回 1000 个 key,VACUUM 的批量删除一次上限也是 1000 个对象。86 万个文件的清理,光删除请求就是 860 次 API 调用;加上 compaction 期大量的 PUT 请求,这部分账单有时候比容量本身还显眼。
把 230GB 的日增量从「28,800 个 8MB 文件」合并成「约 900 个 256MB 文件」,以上三笔开支全部除以 32。这就是 compaction 的全部意义,跟「省磁盘」没有半点关系。
Delta 有个容易挨坑的机制:OPTIMIZE 完成之后,旧文件只是在事务日志里被标记为 remove,物理字节还躺在磁盘上。真正把它们删掉要靠 VACUUM,而 VACUUM 默认要求保留期不短于 7 天(delta.deletedFileRetentionDuration),这是为了保证时间旅行和长时间跑着的读事务不会因为底下的文件消失而炸掉。
于是容量规划要这么算:
峰值磁盘占用 ≈ 有效数据量 + 保留期内被重写且尚未回收的旧数据 + 保留期内新增数据
设定一个典型参数:某团队的策略是「每天只压当天分区 + 每周重排最近 30 天的热点分区」,那么七天的保留窗口内,被重写过的数据量大约是总盘的 20%;七天新增数据按日增 1TB 算是 7TB。代入:
峰值 ≈ D + 0.2D + 7TB = 1.2D + 7TB。
如果盘已经装到 D = 100TB,峰值就是 127TB 左右——这就是「按峰值 ×1.3 规划」的由来。剩下那 0.1 留给文件系统碎片、临时目录和 /tmp 里的 shuffle spill 残骸。
两种情况下 ×1.3 会不够,必须单独加码:
一是你打算做一次全表 OPTIMIZE。全表重写意味着同一时刻磁盘上同时存在旧副本和新副本,瞬时占用逼近 2 倍。要么临时扩容,要么把全表操作拆成分区批次,每批做完立刻 VACUUM 那批的老文件(只 VACUUM 已经过了保留期的那部分,效果有限,本质是时间换空间)。
二是 VACUUM 从来没排进 crontab。这种情况下 ×1.3 完全不够,历史文件会线性累积,三个月不看,表里的文件数一涨、checkpoint 一膨胀,两件事一起爆发。
compaction 阶段的磁盘负载是混合型的:输入侧是大量小文件的随机读(几 KB 到几 MB 的小 IO 铺面而来),shuffle 阶段是 spill 文件的随机写,输出侧才是一串串上百 MB 的顺序写。机械盘的随机 IOPS 只有一两百,遇到这种负载会直接跪;这也是为什么跑 Delta Lake 的机器,本地盘必须是 SSD,而且我建议直接上企业级 NVMe。
选盘时留意三点,比一味追求标称读取速度重要:随机写 IOPS(4K 随机写至少几万起步,spill 阶段能不能撑住看这个)、DWPD(每日整盘写入次数,compaction 是写密集型场景,消费级 TLC 盘的寿命会很快见底)、以及掉电保护(有 PLP 电容的企业级盘不会因为断电丢正在写的 Parquet 文件)。
文件系统也有讲究。ext4 在格式化时会按固定比例预分配 inode,文件数量上百万之后可能出现「磁盘还剩 800GB,但 inode 用光了」的窘境。要么在 mkfs.ext4 时用 -i 把 inode 密度调低(每 inode 对应更大的字节数区间),要么直接用 XFS——它的 inode 是动态分配的,在这种场景里几乎不用操心。
下面这张表把前面三本账合成一张可以直接拿去采购的对照表。三档分别对应「日增百 GB 级的验证型湖仓」「日增 TB 级的主力生产湖仓」「每周做全量重排的重度湖仓」。核数一列的数字都已经按「留 10% 给系统 + 超线程按 1.25 折算」扣过。
| 场景档(总规模 / 日增) | 单次 OPTIMIZE 重写量与频次 | 维护窗口 / 所需有效核数 | 建议整机配置 | 建议磁盘与 IO | 月度成本与取舍 |
|---|---|---|---|---|---|
| 轻量验证档 1–3TB / 100–300GB |
每日压增量 0.2–0.5TB;周末单次全表 1–3TB | 窗口 4 小时 / 8–12 核足够 | 裸金属 E5-2620 32G/1T,起步 ¥999/月(官网价,以官网实时价为准);内存务必加到 64–128G,增配部分以咨询为准 | 2×1T 企业级 NVMe 做本地数据目录;冷分区可挂 4T SATA SSD | 月度约 ¥1200–1800(预估,含内存与硬盘升级,以咨询为准)。这一档别买 GPU 机器,纯 CPU 活 |
| 主力生产档 10–30TB / 1–2TB |
每日增量 1–2TB;每周末热点分区 Z-ORDER 3–5TB | 窗口 3 小时 / 48–64 核 | 2 台裸金属 E5-2698v4×2(40 核),¥3999/台/月起(官网价,以官网实时价为准);每台配到 192–256G 内存 | 每台 2×1.92T 企业级 NVMe;节点间建议 10G 内网互联 | 月度约 ¥1.2–1.8 万(预估,含内存/硬盘升级与内网带宽,以咨询为准)。driver 建议独立一台,别和 executor 抢内存 |
| 重度 Z-ORDER 档 80–150TB / 3–5TB |
每日增量 3–5TB;每周一次全表 Z-ORDER 30–50TB | 窗口 8 小时 / 128–160 核 | 3–4 台 E5-2698v4×2 做 executor,另加 1 台 256G 内存节点专跑 driver 与 MetaStore | 每台 4×3.84T NVMe;超过 90 天的分区迁移到对象存储归档 | 月度约 ¥2–3 万(预估,以咨询为准)。先优化 dataSkipping 统计列数与分区策略,再谈加机器,通常能省两台 |
表里两个价格是官网已明示的确定档(裸金属 E5-2620 ¥999/月、E5-2698v4×2 ¥3999/月起),其余都是基于行业经验的推算,写了「预估」二字,别当成报价单。
为什么坑:每次 OPTIMIZE 之后磁盘占用会短暂翻倍,第一反应往往是把保留期从 7 天改成 1 小时甚至 0。改完之后,那些跑了一半的长查询、需要回溯消费的流式作业、以及正在做时间旅行的分析师,会在文件被物理删除后抛出 FileNotFound。这类故障特别难查,因为它是间歇性的——只有恰好跨越删除时刻的作业才会中招。
怎么避:保留期至少取「最长在读作业的运行时长 × 1.5」和「流式作业需要的重放窗口」两者中的较大值,通常 3 到 7 天是合理区间。真到了磁盘告急的地步,优先考虑扩容而不是砍保留期。
为什么坑:前面算过,全表 Z-ORDER 会把选定范围内所有数据重写一遍,写放大系数轻松上到 10 倍以上。一张 20TB 的表每天重写一遍,等于每月 600TB 的写量,NVMe 的消费级型号撑不过一年。
怎么避:用 WHERE 子句把 Z-ORDER 限制在最近 N 天的活跃分区;历史分区在首次入库时排一次之后锁住不动。同时审视那几个 Z-ORDER 列的必要性——如果控制台仪表盘的查询是全聚合,这两列可以直接删掉。
为什么坑:很多团队把写入作业配置成 200 甚至 1000 个 shuffle 分区,每批任务落地一堆 5MB 的文件,然后指望凌晨的 OPTIMIZE 收拾。结果就是 OPTIMIZE 每天要处理几十万个碎文件,自身也变成一场重量级作业,还会大幅拉长 write 后立即 query 的延迟。
怎么避:在写入侧就控制单次 task 的输出量,让每个 task 落地的数据不小于 64MB,理想是接近 128MB。目标文件大小设置要和日均写入量匹配——增量小却把 target 设成 1GB,等于永远合并不出来。
为什么坑:checkpoint 的体积是随文件数线性增长的,平时风平浪静,等到表积累了几十万个文件,某次重启需要从头回放时就炸了。而且 driver OOM 的后果比 executor OOM 严重得多——executor 挂了 Spark 会自动重试,driver 挂了整个作业直接失败。
怎么避:driver 内存按「活跃文件数」估算,16GB 起步,50 万文件以上给到 32–64GB。同时每月核对一次 checkpoint 文件大小,把它做成监控项:超过 1GB 就该安排一轮深度 compaction 和列统计瘦身。
为什么坑:小规模部署时最容易这么干——把 Spark 的 driver 和 executor 装在同一台 64GB 的机器上,平时没事,一到 Z-ORDER 高峰期,driver 要吃 checkpoint、executor 要吃排序缓冲区,两边一起往 swap 挤,整机性能断崖下跌。
怎么避:最低限度是把 driver 单独放一台机器,哪怕配置不高。driver 的 CPU 需求很低,4 到 8 核足够,但内存必须给够;反过来 executor 节点的 CPU 和磁盘要堆满,内存按前面 192–256GB 的算法配。这两类节点的资源曲线完全不同,混部只会互相拖累。
把三本账算完之后,落到具体的机器选型,我通常按「先定核数 → 再定内存 → 最后挑盘和网络」的顺序走。
主力生产档我给客户首推的是一万网络的裸金属 E5-2698v4×2,双路共 40 核 80 线程,官网 ¥3999 起/月(以官网实时价为准)。选它的理由很实在:双路 40 核正好对应上面算出来的 48–64 有效核区间(两台即可覆盖),无虚拟化开销,本地盘可以自己指定型号和组 RAID,跑 compaction 这种 IO 密集又有峰值的负载,比共享 vCPU 的云主机稳得多。
一万网络在这件事上的几个承诺是能落地的:深耕 IDC 19 年(成立于 2007 年),深圳自营机柜交付,不是层层转包的二手资源;7×24 中文工单平均 5 分钟响应,凌晨三点 OPTIMIZE 挂了也有人接;硬件故障 10 分钟自动迁移——对跑 checkpoint 的 driver 节点来说,这条比什么都重要,driver 单点故障意味着整个湖仓停用。另外免费的系统盘快照(每日 3 份、30 秒回滚),我强烈建议在动大版本 Delta/Spark 升级、或者要做全表 Z-ORDER 之前手动打一份,回滚窗口只有 30 秒,比起重建湖仓元数据划算太多。
内存一定要在下单时谈清楚:上面算过,一台 40 核节点跑满 compaction 需要 192–256GB,默认的 32G 档必须升级,升级部分的报价以咨询为准。这一步千万别省——CPU 不够只是慢,内存不够是直接 OOM,而 OOM 发生在凌晨三点。
driver 节点、MetaStore、监控和调度这几样,我用一万云弹性主机打底,¥25 起/月(官网价,以官网实时价为准),按月伸缩。driver 的负载很轻但要求稳定,云主机的好处是故障可以很快重建,搭配前面提到的免费系统盘快照,恢复时间可控。
如果这套湖仓的下游还挂着 GPU 推理或 embedding 生成,那可以看一眼人工定制 GPU 那条线:Tesla T4 整机 ¥900/月(官网价,以官网实时价为准),在此基础上 CPU 升级到 16 核是 +¥400/月、内存加到 128G 是 +¥600/月(均为官网明示的升级价,以官网实时价为准)。算下来 ¥1900/月 拿一台 16 核 128G 带一张 T4 的机器,这种配置适合「白天跑 compaction、夜里跑特征工程」的混合用法。如果完全不碰 GPU 相关业务,这张 T4 的钱就是白花,老老实实回到裸金属。顺带说明:官网明示的工程师 1 对 1 部署覆盖 CUDA/cuDNN/TensorRT/PyTorch/TensorFlow 这套环境,Spark/Delta Lake 这类中间件是否含在交付包里,建议下单前跟客服确认清楚范围。
网络部分,一万网络给的是 BGP 多线 + CN2 GIA 回国的组合,多节点覆盖华南/华东/华北/中国香港及海外。对跑 Hadoop/Spark 集群的场景,我个人更关注的是节点间有没有稳定的内网互联(建议谈 10G 内网,且确认内网流量不计入公网带宽),这部分属于定制项且各家政策不同,要以咨询为准。跨境同步的场景(比如把国内的湖仓增量回源到海外节点做异地容灾),CN2 GIA 回国的延迟优势能省下不少重试成本。
不用专门配,但也不能不跑。按上面的算式,50GB 的增量除以单核 32MB/s,只需要 1560 核秒,约 0.43 核小时——给 8 个核跑三分钟就完事了。这种体量下真正该买的不是 CPU 而是「稳定的 NVMe 和足够的内存」,一台裸金属 E5-2620 起步档(¥999/月,官网价,以官网实时价为准)内存加到 64G 就完全够用。重点反而是把 compaction 排进 crontab 别忘掉,小湖仓最常出的问题不是资源不够,是三个月没 VACUUM、文件数默默涨到几十万才发现 driver 已经 OOM 了。
取决于你的查询要多久能查到最新数据,以及每天新增文件数。经验规律:日新增文件数超过 5000 就该每天压一次;低于 2000 可以两天一次;如果每天的 micro-batch 只有几十次、且能接受查询延迟,三天到一周一次也行。真正的判别指标是「单分区下的文件平均大小」——低于 64MB 就该压了。别用日历上的周期当唯一依据,把每分区的平均文件大小做成监控曲线,让曲线决定频率。
两到四列,不要更多。排序效果主要集中在靠前的一两列,后面的列收益衰减得非常快。顺序上把低基数列(国家、业务线、事件类型,取值几十到几百那种)放前面,高基数列(用户主键、毫秒时间戳)放后面或者干脆不进。判断标准是你的查询里 WHERE 子句最常出现哪些列:如果 90% 的查询都带 shop_id 和 event_type,那就 ZORDER BY (shop_id, event_type),别把其他的都塞进来凑热闹。另外提醒一句,如果你的查询是全表扫描型的 ETL,Z-ORDER 一分钱都不要花。
我倾向 128MB 到 256MB,不要盲目追大。目标越大,单个文件的列统计范围越宽,data skipping 的精度越差(因为跳过的最小单位从 128MB 变成 1GB);同时每次重写最小单位变大,写放大增加。1GB 这种设置适合冷数据、按年访问几次的归档层。反过来,流式 ingestion 频繁、每次新增量只有几百 MB 的场景,设成 64MB 到 128MB 更容易达成,避免出现「永远凑不够一个满文件」的尴尬。
会冲突,但不是锁死。Delta 用乐观并发控制,OPTIMIZE 和业务写入如果动了同一批文件,一方会失败并重试,表现为 ConcurrentAppendException 或类似的冲突异常。实践中把 COMPACTION 放到业务低峰是最省事的做法;高度并发的场景建议给 OPTIMIZE 加重试循环,并且不要一次压全表——把重写范围切小,单次事务的冲突面也随之变小。还有一点容易被忽略:OPTIMIZE 本身的表版本提交也要和业务写入竞争同一份日志,写得太频繁(比如每分钟一次)会让冲突率明显上升。
先看你的活跃文件数。粗算一条:每个文件的 add action 连同列统计大约占 0.5 到 2KB,checkpoint 文件的大小 ≈ 文件数 × 1KB;driver 把它解析成对象后的内存占用大约是文件的 3 到 5 倍,再加上 1.5 的余量。折算下来:10 万文件给 8–16GB,50 万文件给 16–32GB,100 万文件以上直接给 32–64GB。如果这个数字算出来已经很难接受,说明你的文件合并不够彻底,该回头重做 compaction 策略,而不是无止境地给 driver 加内存。把 delta.dataSkippingNumIndexedCols 从默认的 32 降到 8 到 12,这一步通常能让 checkpoint 瘦身 30% 到 50%(预估)。
取决于盘的「热」。热数据(最近几天到几周、会被频繁重写的文件)一定要放本地 NVMe:compaction 的输入侧是海量随机小 IO,对象存储 5 到 20 毫秒的单次 GET 延迟会让 OPTIMIZE 慢到不可接受。冷数据(超过 90 天没人碰)迁到对象存储,成本能降一大截,而且不影响任何线上查询。折中方案是分层:本地盘跑热点分区,冷分区走对象存储。要注意的是,一旦走对象存储,VACUUM 会产生大量 LIST 和 DELETE 请求(每批上限 1000 个对象),这部分 API 请求费用在规划时要算进去。
常规够,两个例外。常规情况(每天只压增量、每周 Z-ORDER 热点分区、VACUUM 按时跑)峰值 ≈ 有效数据量的 1.2 倍加上七天的新增,再留 10% 给碎片和 /tmp,取整就是 1.3 倍。例外一是要一次性全表 OPTIMIZE——那一刻磁盘上旧副本和新副本并存,瞬时接近 2 倍,要么分批做每批做完立刻回收,要么临时扩一块盘。例外二是 VACUUM 压根没排进计划表,这种情况下历史文件线性累积,1.3 倍撑不过一个季度。把磁盘水位线设到 70% 就告警,比事后扩容体面。
写到这里,这篇手册想留下的其实只有一句话:Delta Lake 的服务器配置单不应该是「参考别家公司买了什么」,而应该是你自己的 OPTIMIZE 窗口反推出来的结果。
把单次重写的数据量估出来,除以单核有效吞吐,除以你愿意给的维护时长,得到的那个核数就是采购单上的第一行数字。从这个数字往上推——每 4 个核配 12–16GB 堆内加 6–8GB 堆外,算出整机的内存需求;driver 单独一台,按活跃文件数给到 16–64GB;本地盘按业务峰值 ×1.3 买企业级 NVMe,别用消费级 TLC 去扛 compaction 每天的写量。
如果算完发现核数需求是自己两倍,先别急着加机器,回头看看那三件事能不能动:目标文件大小是不是设得太小或太大、Z-ORDER 的范围能不能用 WHERE 压到最近几周、以及维护窗口能不能从 1 小时放宽到 4 小时。这三处调整基本不花钱,效果往往比多租两台机器明显。调完之后确实还差火力,那就照着配置单补——一万网络的裸金属 E5-2698v4×2(¥3999 起/月,以官网实时价为准)搭 executor、一万云弹性主机(¥25 起/月,以官网实时价为准)搭 driver 和调度,是一套在核数、内存和成本之间比较平衡的组合,深圳自营机柜、7×24 中文工单平均 5 分钟响应、硬件故障 10 分钟自动迁移、免费系统盘快照,这几条在跑「凌晨三点挂了」的时候最能体会到价值。
有一点立场要说明白:自建 Delta Lake 湖仓不是所有团队的最优解。日增量低于 100GB、团队里没有能扛住 Spark 调优的人,托管服务或者干脆用一个便宜的对象存储加调度器更务实。自建的意义在于数据不出机房、资源可独占、成本可控——这三件事成立的时候,才是按这篇手册去算账的时候。
本文配置参数、工艺口径与使用原则参考公开技术文档与运维实践整理,价格信息来自一万网络官网公开页面:裸金属 E5-2620 32G/1T ¥999/月、裸金属 E5-2698v4×2 32G/1T ¥3999 起/月、一万云 ¥25 起/月、人工定制 GPU Tesla T4 ¥900/月(均属官网明示价,以官网实时价为准),升级项与折扣政策以官网页面为准。
文中标注「(预估)」的数字,包括单核有效吞吐区间(Snappy 25–45MB/s、ZSTD 15–28MB/s、叠加 Z-ORDER 排序后 12–25MB/s)、checkpoint 体积与内存占用倍数(3–5 倍)、列统计列数下调带来的瘦身幅度(30%–50%)、各档月度组合成本(¥1200–1800 / ¥1.2–1.8 万 / ¥2–3 万),均为基于行业公开经验区间的推算值,非官方报价,实际以下单时核算为准。
Delta Lake 的版本迭代较快,delta.targetFileSize、delta.deletedFileRetentionDuration、delta.dataSkippingNumIndexedCols 等参数的默认值与行为在不同版本间可能存在差异,上机前请以你实际使用的 Delta Lake 和 Spark 版本官方文档为准。产品、报价与服务条款可能调整,具体以签约时最新报价与合同为准,详见 一万网络官网(https://www.idc10000.net/)。
Copyright © 2013-2020 idc10000.net. All Rights Reserved. 一万网络 科技有限公司 版权所有 深圳市科技有限公司 粤ICP备07026347号
本网站的域名注册业务代理北京新网数码信息技术有限公司的产品