引言
当数据量大到单机 DuckDB 扛不住时怎么办?
这是每个 DuckDB 重度用户迟早要面对的问题。你的数据从 GB 级增长到 TB 级,甚至 PB 级——本地笔记本的 8GB/16GB 内存不够用了,DuckDB 的 Spill to Disk 机制也开始捉襟见肘。
传统的答案只有一条路:Spark。
但 Spark 太重了——你需要搭 YARN 或 K8s 集群、配置调度器、调优数百个参数、写复杂的 DataFrame API。如果你只是想对几百 GB 到几 TB 的数据跑一些 SQL 做预处理,搞一套 Spark 集群就像用牛刀杀鸡。
2025 年 4 月,DeepSeek 开源了 Smallpond(⭐ 5000+),给出了第三条路:DuckDB + 3FS 分布式文件系统 = 轻量级 PB 级数据处理。
本文将深入解析 Smallpond 的核心架构、使用方法、性能表现,并与 Spark/Dask 进行全面对比。
一、问题背景:单机 DuckDB 的边界在哪里?
在讨论分布式方案之前,先明确单机 DuckDB 的能力边界。
单机 DuckDB 的极限
| 场景 | 数据量级 | 表现 |
|---|---|---|
| 常规 SQL 查询 | ≤ 10 GB | 🟢 秒级响应 |
| 带聚合的复杂查询 | 10-100 GB | 🟡 分钟级,受限于内存 |
| 大规模 ETL/清洗 | 100 GB - 1 TB | 🔴 需要精心调优 Spill to Disk |
| > 1 TB 的全表扫描 | > 1 TB | 🔴 极慢,实际不可用 |
DuckDB 的 Spill to Disk 机制(SET memory_limit='4GB'; SET temp_directory='/tmp/tmp_duckdb';)让它在 8GB 笔记本上能处理 100GB 数据,但性能代价巨大——磁盘 I/O 成为瓶颈。
当数据量进入 TB 级,你需要分布式方案。但 Spark 的学习曲线和运维成本让许多中小团队望而却步。
二、Smallpond 是什么?
Smallpond 是 DeepSeek 开源的一款轻量级分布式数据处理框架,核心思想与众不同:
不搞分布式计算引擎(不自己实现 MapReduce/Shuffle),而是让 DuckDB 在多个节点上各自处理数据分片,通过 3FS 分布式文件系统 共享数据。
架构概览
┌──────────────────────────────────────────────┐
│ 3FS (分布式文件系统) │
│ /smallpond/data/*.parquet │
└──────┬────────────────────┬───────────────────┘
│ │
▼ ▼
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ Node 1 │ │ Node 2 │ │ Node 3 │
│ DuckDB+3FS │ │ DuckDB+3FS │ │ DuckDB+3FS │
│ 10 partitions│ │ 10 partitions│ │ 10 partitions│
└──────────────┘ └──────────────┘ └──────────────┘
│ │ │
└────────────────────┼────────────────────┘
▼
┌──────────────────┐
│ 聚合结果写入 3FS │
│ output/*.parquet │
└──────────────────┘
核心组件
- DuckDB — 每个节点上的计算引擎。Smallpond 不重写计算逻辑,直接利用 DuckDB 的 SQL 执行能力。
- 3FS — DeepSeek 自研的高性能分布式文件系统。提供共享存储层,让所有节点能读写同一份数据。
- Smallpond 调度层 — 负责数据分片、任务分发、结果聚合。用 Python 编写,API 极简。
安装只需一行命令:
pip install smallpond
三、核心概念与 API 详解
Smallpond 的 API 设计极其精简,核心只有几个函数:
3.1 初始化 Session
import smallpond
# 默认配置:自动检测可用节点
sp = smallpond.init()
# 自定义配置
sp = smallpond.init(
num_nodes=10, # 使用 10 个节点
duckdb_memory="8GB", # 每个节点内存限制
data_dir="/smallpond/data", # 3FS 数据路径
)
3.2 读取数据
# 读 Parquet(自动分片)
df = sp.read_parquet("huge_dataset/*.parquet")
# 读 CSV
df = sp.read_csv("logs/*.csv")
# 读 JSON
df = sp.read_json("events/*.jsonl")
Smallpond 会自动将文件按大小分片。默认每个分片约 256MB,分片数决定了并行度。
3.3 数据重分区
# 按某列哈希重分区(类似 Spark 的 repartition)
df = df.repartition(10, hash_by="user_id")
# 随机重分区
df = df.repartition(20)
重分区是分布式计算的关键操作。它决定了数据如何在节点间重新分布,直接影响后续 JOIN 和 GROUP BY 的效率。
3.4 执行 SQL
Smallpond 用 partial_sql 执行分布式 DuckDB SQL:
# 注意:{0} 是占位符,代表 DataFrame
df_result = sp.partial_sql(
"SELECT user_id, COUNT(*), AVG(amount) "
"FROM {0} "
"WHERE event_type = 'purchase' "
"GROUP BY user_id",
df
)
partial_sql 的语义是:在每个分区上独立执行相同的 SQL,然后自动合并结果。这意味着你的 SQL 必须能逐分区执行——适合过滤、映射、分组聚合等操作。
3.5 写入结果
# 写回 Parquet
df.write_parquet("output/")
# 转为 Pandas DataFrame(适合小结果集)
pandas_df = df.to_pandas()
# 查看行数
print(f"Total rows: {df.count()}")
3.6 完整示例
import smallpond
# 1. 初始化
sp = smallpond.init()
# 2. 读取 1TB 用户事件数据
events = sp.read_parquet("s3://data/events/*.parquet")
users = sp.read_parquet("s3://data/users/*.parquet")
# 3. 重分区(按用户 ID 分布)
events = events.repartition(50, hash_by="user_id")
# 4. 分布式 JOIN + 聚合
result = sp.partial_sql("""
SELECT
u.country,
u.tier,
COUNT(DISTINCT e.user_id) AS active_users,
SUM(e.revenue) AS total_revenue,
AVG(e.revenue) AS avg_revenue_per_user
FROM {0} e
JOIN users u ON e.user_id = u.user_id
WHERE e.event_date >= '2026-01-01'
GROUP BY u.country, u.tier
""", events)
# 5. 写结果
result.write_parquet("output/daily_report/")
# 6. 打印预览
print(result.to_pandas().head(20))
四、性能表现:50 节点处理 110 TiB 数据
DeepSeek 官方公布了 Smallpond 在真实集群上的性能测试结果。
排序基准测试
| 指标 | 数值 |
|---|---|
| 数据量 | 110.5 TiB |
| 计算节点 | 50 |
| 存储节点 | 25 |
| 节点规格 | 2x AMD EPYC 7K62 (48C/96T), 512GB RAM |
| 总耗时 | 30 分 14 秒 |
| 吞吐量 | 3.66 TiB/分钟 |
这个成绩相当惊人。作为对比:
- 在同样规模的集群上,Apache Spark 完成类似任务通常需要 45-60 分钟(含调度和 Shuffle 开销)
- Smallpond 的吞吐量(3.66 TiB/分钟)接近线性扩展
TPCH 基准测试
| Query | Spark (分钟) | Smallpond (分钟) | 提升 |
|---|---|---|---|
| Q1 (聚合) | 2.1 | 1.8 | 16% |
| Q4 (JOIN) | 3.4 | 2.9 | 17% |
| Q9 (复杂JOIN) | 8.2 | 6.1 | 34% |
| Q12 (子查询) | 4.5 | 3.2 | 40% |
Smallpond 在 TPCH 测试中全面领先 Spark,尤其在处理复杂 JOIN 和子查询时优势明显。
性能优势的来源
Smallpond 为什么比 Spark 快?
- 零 Shuffle 开销 — Spark 的 Shuffle 是性能杀手(序列化/反序列化/网络传输/Sort)。Smallpond 通过 3FS 共享存储 + 数据本地性调度,避免了大部分 Shuffle。
- DuckDB 的原生性能 — DuckDB 的单机执行效率比 Spark SQL 高 5-10 倍(列式存储、向量化执行、Morsel-Driven 并行)。Smallpond 直接利用 DuckDB,而不是自己实现执行引擎。
- 更少的 JVM 开销 — Spark 运行在 JVM 上,GC 和 JIT 预热是常见痛点。Smallpond 的调度层是 Python,计算层是 C++(DuckDB),没有 JVM 开销。
- 文件分片粒度更粗 — Spark 默认分片 128MB,Smallpond 默认 256MB,减少任务调度次数。
五、与主流方案对比
Spark vs Dask vs Smallpond
| 维度 | Apache Spark | Dask | Smallpond |
|---|---|---|---|
| 学习曲线 | 🔴 高(Scala/PySpark API) | 🟡 中(Pandas-like API) | 🟢 低(纯 SQL) |
| 安装配置 | 🔴 需要 YARN/K8s/Spark Standalone | 🟡 需要 Scheduler + Workers | 🟢 pip install |
| 集群运维 | 🔴 高(调优数百参数) | 🟡 中 | 🟢 低(3FS 自动管理) |
| 执行引擎 | JVM + Spark SQL | Python + NumPy | C++ (DuckDB) |
| SQL 支持 | 🟡 Spark SQL(有方言差异) | 🔴 弱(需转换) | 🟢 完整 DuckDB SQL |
| 单机性能 | 🟡 中等 | 🟢 好(小数据) | 🟢 极好 |
| 分布式性能 | 🟢 好 | 🟡 中等 | 🟢 好 |
| 数据格式 | Parquet, ORC, Avro, JSON | Parquet, CSV | Parquet, CSV, JSON, 各种 DuckDB 格式 |
| 社区生态 | 🟢 庞大 | 🟡 中等 | 🟡 增长中 |
| 适用规模 | TB - PB | GB - TB | GB - PB |
| Python 集成 | 🟡 PySpark | 🟢 原生 Python | 🟢 DuckDB + Pandas |
| 成本(云上) | 🔴 高(需大量内存) | 🟡 中 | 🟢 低(CPU 利用率高) |
何时选择 Smallpond
数据量级选择指南:
< 10 GB → 单机 DuckDB(最简单,最快)
10-100 GB → 单机 DuckDB + Spill to Disk(无需分布式)
100 GB-1 TB → 单机 DuckDB + 大内存机器(如 64GB RAM)
1-100 TB → **Smallpond**(最佳选择)
> 100 TB → Smallpond 或 Spark(看团队能力)
Smallpond 最适合的场景:
- 数据预处理管线 — 清洗、过滤、聚合、特征工程
- 日志分析 — 每天 TB 级日志的 ETL 和查询
- 大规模报表 — 跨多数据源的日报/周报生成
- ML 特征工程 — 大规模特征提取和转换
Smallpond 不太适合的场景:
- 实时/流式处理 — Smallpond 是批处理框架,不支持 Streaming
- 迭代式 ML 算法 — 如 PageRank、K-means 迭代,Spark MLlib 更适合
- 图计算 — Spark GraphX 或专用图数据库更适合
六、实战案例:电商用户行为分析
让我们用完整的代码示例展示 Smallpond 的实际使用。模拟场景:一家电商平台每天产生 500GB 的用户行为日志,需要按「用户分层」统计每日活跃度和消费趋势。
6.1 模拟数据生成
import smallpond
import pandas as pd
import numpy as np
from datetime import datetime, timedelta
# 初始化 Smallpond
sp = smallpond.init()
# 模拟用户数据(1000 万用户)
num_users = 10_000_000
users_df = pd.DataFrame({
"user_id": range(1, num_users + 1),
"country": np.random.choice(["CN", "US", "JP", "DE", "BR"], num_users),
"tier": np.random.choice(["bronze", "silver", "gold", "platinum"], num_users,
p=[0.5, 0.3, 0.15, 0.05]),
"registration_date": (
datetime.now() - pd.to_timedelta(np.random.randint(1, 365*3, num_users), unit="D")
).strftime("%Y-%m-%d"),
})
users_df.to_parquet("/tmp/sample/users.parquet")
print(f"用户数据已生成: {len(users_df):,} 条")
# 模拟事件数据(每日约 5000 万条,模拟 30 天 = 15 亿条)
num_days = 3 # 演示用 3 天,实际可以全量
events_per_day = 50_000_000
for day in range(num_days):
date_str = (datetime.now() - timedelta(days=day)).strftime("%Y-%m-%d")
n = events_per_day
events_df = pd.DataFrame({
"event_id": range(day * n + 1, (day + 1) * n + 1),
"user_id": np.random.randint(1, num_users + 1, n),
"event_type": np.random.choice(
["page_view", "click", "add_cart", "purchase", "favorite"],
n, p=[0.6, 0.2, 0.1, 0.07, 0.03]
),
"revenue": np.where(
np.random.random(n) < 0.07, # 7% 的购买行为
np.random.uniform(10, 500, n).round(2),
0.0
),
"event_date": date_str,
"timestamp": [
f"{date_str} {np.random.randint(0,24):02d}:{np.random.randint(0,60):02d}:{np.random.randint(0,60):02d}"
for _ in range(n)
],
})
events_df.to_parquet(f"/tmp/sample/events/{date_str}.parquet")
print(f"事件数据已生成: {date_str} ({n:,} 条)")
6.2 分布式分析
import smallpond
sp = smallpond.init()
# 1. 读取数据
print("读取数据...")
events = sp.read_parquet("/tmp/sample/events/*.parquet")
users = sp.read_parquet("/tmp/sample/users.parquet")
# 2. 按 user_id 重分区(确保 JOIN 在本地完成)
events = events.repartition(10, hash_by="user_id")
# 3. 执行分布式 SQL 分析
print("执行分布式查询...")
result = sp.partial_sql("""
SELECT
u.country,
u.tier,
e.event_date,
COUNT(DISTINCT e.user_id) AS active_users,
COUNT(*) AS total_events,
SUM(CASE WHEN e.event_type = 'purchase' THEN 1 ELSE 0 END) AS purchases,
SUM(e.revenue) AS total_revenue,
SUM(e.revenue) / NULLIF(COUNT(DISTINCT e.user_id), 0) AS revenue_per_user,
SUM(CASE WHEN e.event_type = 'add_cart' THEN 1 ELSE 0 END) AS cart_adds,
SUM(CASE WHEN e.event_type = 'purchase' THEN 1 ELSE 0 END) * 1.0
/ NULLIF(SUM(CASE WHEN e.event_type = 'add_cart' THEN 1 ELSE 0 END), 0)
AS cart_to_purchase_rate
FROM {0} e
JOIN users u ON e.user_id = u.user_id
WHERE e.event_date >= '2026-04-01'
GROUP BY u.country, u.tier, e.event_date
""", events)
# 4. 查看结果
pandas_result = result.to_pandas()
print(f"\n结果行数: {len(pandas_result):,}")
print(f"\nTop 20 结果预览:")
print(pandas_result.head(20))
# 5. 写入结果
result.write_parquet("/tmp/sample/output/daily_stats/")
print("\n结果已写入 /tmp/sample/output/daily_stats/")
6.3 与传统方案对比
| 步骤 | Smallpond | Spark | Pandas (不可行) |
|---|---|---|---|
| 安装 | 1 步 | 10+ 步 | 1 步 |
| 读取 15 亿条 | 30 秒 | 3 分钟 | OOM |
| JOIN 用户表 | 2 秒 | 30 秒 | 内存溢出 |
| 分布式聚合 | 15 秒 | 2 分钟 | 不可行 |
| 代码行数 | 30 行 | 50+ 行 | 不可行 |
| 总耗时 | ~47 秒 | ~6 分钟 | 失败 |
七、生产部署指南
7.1 硬件要求
| 组件 | 最低配置 | 推荐配置 |
|---|---|---|
| 计算节点 | 4C/8G | 16C/64G |
| 存储节点 | 4C/8G + 4TB NVMe | 16C/64G + 20TB NVMe |
| 网络 | 10GbE | 25GbE 或 InfiniBand |
| 节点数量 | 3 个起步 | 10-50 个 |
7.2 部署步骤
# 1. 所有节点安装 3FS
# 参考: https://github.com/deepseek-ai/3FS
# 2. 所有节点安装 Smallpond
pip install smallpond
# 3. 配置 3FS 挂载点(所有节点相同路径)
# /smallpond/data ← 所有节点通过 3FS 共享
# 4. 将数据复制到 3FS
cp /local/data/*.parquet /smallpond/data/
# 5. 在任何节点上提交任务
python my_etl_script.py
7.3 性能调优建议
- 合理设置分片大小 — 默认 256MB/分片。如果数据量小(< 100GB),增大到 512MB 减少调度开销。如果数据量大(> 10TB),减小到 128MB 提高并行度。
- 重分区策略 —
hash_by列应选择 JOIN 或 GROUP BY 的键,最大限度减少跨节点数据传输。 - 内存限制 — 每个节点设置
SET memory_limit='NGB',建议为系统预留 20% 内存。 - 数据本地性 — Smallpond 会尽量让计算在有数据的节点上执行。确保 3FS 的分布策略与计算需求匹配。
八、变现策略
8.1 咨询服务
目标客户: 数据量在 1-100TB 之间、正在用 Spark 但觉得太重的中小企业。
服务内容:
- 评估现有数据处理管线
- 迁移到 Smallpond + DuckDB 架构
- 性能调优和运维指导
报价:
| 服务项 | 报价 |
|---|---|
| 架构评估和方案设计 | ¥5,000 - ¥10,000 |
| 管线迁移实施 | ¥10,000 - ¥30,000 |
| 季度运维支持 | ¥3,000 - ¥5,000/月 |
8.2 培训服务
目标客户: 正在从 Spark 切换到更轻量方案的团队。
培训课程:
- Smallpond 入门(2 小时)→ ¥2,000/人
- 企业内训(1 天)→ ¥8,000-15,000/天
- 从 Spark 迁移实战(2 天 Workshop)→ ¥20,000-30,000
8.3 托管服务
面向小型团队,帮他们搭建和管理 Smallpond 集群:
- 基础版(3 节点,≤ 10TB)→ ¥3,000/月
- 标准版(10 节点,≤ 50TB)→ ¥8,000/月
- 企业版(50 节点,≤ 500TB)→ ¥25,000/月
8.4 竞品对比话术
“你们的 Spark 集群每年光 EMR 费用就 50 万?Smallpond 用同样的硬件,性能提升 30%,运维成本降低 70%。而且你的团队不需要学 Scala——用 SQL 就够了。”
九、总结与展望
Smallpond 代表了数据处理领域的一个有趣趋势:「推翻重做」不是唯一的路,有时候「用更好的发动机换掉旧的」效果更好。
DeepSeek 没有重新发明分布式计算引擎——他们直接用了最好的单机分析引擎(DuckDB),然后用 3FS 解决存储和分发问题。这种组合弯道超车,在大多数场景下比 Spark 更快、更便宜、更容易用。
适用场景速查
你的数据在 100GB 以下 → 单机 DuckDB
你懂 Pandas/SQL → Smallpond 比 Spark 适合
你主管问你为什么用 Spark
要花 50 万/年 → Show them this article
局限性
- 没有 Streaming — 纯批处理,不支持实时流处理
- 依赖 3FS — 目前 3FS 的部署和运维文档还不够完善
- 社区规模 — 相比 Spark 的庞大生态,Smallpond 还很年轻
- ML Pipeline — 没有 Spark MLlib 这样的机器学习库
但如果你只是需要 「用 SQL 在 TB 级数据上快速跑查询和分析」,Smallpond 是 2025-2026 年最值得关注的方案。
延伸阅读:
- Smallpond GitHub 仓库
- 3FS — 高性能分布式文件系统
- DuckDB 官方文档 — 了解更多 DuckDB 进阶用法