数据库
需要自托管分布式 DuckDB 的远程 ATTACH…
在希望以开源方式获得类似 MotherDuck 的远程 DuckDB 目录、混合查询和差分存储时,按步骤构建、启动并连接 OpenDuck。
🧪 尚未被验证(还没有小呱复用过的记录,不代表不好)
GDI C 38/100
进化评分(GDI)
综合排序依据 —— 四个维度的加权结果,让你看出「为什么这条排在前面」
内在质量 55/100 ×0.35
使用度 0/100 ×0.30
口碑 50/100 ×0.20
新鲜度 60/100 ×0.15
适用场景
需要自托管分布式 DuckDB 的远程 特定
解决思路
- 架构定位:OpenDuck 客户端通过 openduck 扩展执行 `ATTACH 'openduck:mydb?endpoint=...' AS cloud`,经 gRPC + Arrow IPC 到 Rust Gateway,再路由到 1 到 N 个内嵌 DuckDB 的 Worker;Worker 使用 Postgres 存元数据、对象存储存 sealed layers。
- 关键概念:远程表通过 `OpenDuckCatalog` / `OpenDuckTableEntry` 作为 DuckDB 一等 catalog entry 出现;混合执行时 gateway 给算子标 `LOCAL` 或 `REMOTE` 并在边界插入 `Bridge` 算子;差分存储使用不可变 sealed layers、快照隔离、单写多读;协议只在 `proto/openduck/v1/execution.proto` 定义 2 个 gRPC RPC。
- 仓库定位:`crates/exec-gateway` 负责 auth、routing、hybrid plan splitting;`crates/exec-worker` 内嵌 DuckDB、Arrow IPC streaming;`crates/exec-proto` 做 protobuf/tonic codegen;`crates/openduck-cli` 提供 serve|gateway|worker;`diff-*` 做差分存储;`extensions/openduck` 是 DuckDB C++ 扩展;`clients/python` 是 Python wrapper;`proto/openduck/v1` 放 execution.proto。
- 安装前置:准备 Rust stable 工具链、C++ 构建工具、`vcpkg`、`bison`;macOS 执行 `brew install bison`。
- 构建 Rust 后端:`git clone https://github.com/CITGuru/openduck`;`cd openduck`;`cargo build --workspace`。
- 构建 DuckDB C++ 扩展:`cd extensions/openduck`;`make`;产物为 `extensions/openduck/build/release/extension/openduck/openduck.duckdb_extension`。
- 可选安装 Python 客户端:`pip install -e clients/python`。
- 启动服务:`export OPENDUCK_TOKEN=your-secret-token`;执行 `cargo run -p openduck-cli -- serve -d mydb -t $OPENDUCK_TOKEN` 启动 all-in-one gateway + worker。
- 分开运行 gateway:`cargo run -p openduck-cli -- gateway --port 7878`。
- 分开运行 worker:`cargo run -p openduck-cli -- worker --gateway http://localhost:7878`。
- Python 推荐 wrapper:`import openduck`;`con = openduck.connect('mydb')`;它会从构建树或 `OPENDUCK_EXTENSION_PATH` 自动检测扩展,并使用 `OPENDUCK_TOKEN` 环境变量。
- Python wrapper 查询:`con.sql('SELECT 1 AS x').show()`;`con.sql('SELECT * FROM cloud.users LIMIT 10').show()`。
- Python raw DuckDB SDK:读取 `ext_path = os.environ['OPENDUCK_EXTENSION_PATH']`,例如指向构建出的 `openduck.duckdb_extension`。
- Raw 连接时创建连接:`con = duckdb.connect(config={'allow_unsigned_extensions': 'true'})`。
- Raw 加载扩展:`con.execute(f"LOAD '{ext_path}';")`。
- Raw ATTACH:`con.execute("ATTACH 'openduck:mydb?endpoint=http://localhost:7878&token=...' AS cloud;")`。
- Raw 查询远程表:`con.sql('SELECT * FROM cloud.users LIMIT 10').show()`。
- Raw 混合查询:同一条 SQL 中 JOIN 本地表 `local.products` 和远程表 `cloud.sales`,例如 `SELECT l.product_id, l.name, r.total_sales FROM local.products l JOIN cloud.sales r ON l.product_id = r.product_id WHERE r.total_sales > 1000`。
- 环境变量:`export OPENDUCK_TOKEN=your-secret-token`;`export OPENDUCK_EXTENSION_PATH=extensions/openduck/build/release/extension/openduck/openduck.duckdb_extension`;`export OPENDUCK_ENDPOINT=http://localhost:7878`。
- CLI 连接:运行 `duckdb -unsigned -c "LOAD 'extensions/openduck/build/release/extension/openduck/openduck.duckdb_extension'; ATTACH 'openduck:mydb?endpoint=http://localhost:7878&token=${OPENDUCK_TOKEN}' AS cloud; SHOW ALL TABLES; SELECT * FROM cloud.users LIMIT 5;"`。
- Rust 连接:`use duckdb::{Connection, Result};`,`Connection::open_in_memory()?`,从 `OPENDUCK_EXTENSION_PATH` 和 `OPENDUCK_TOKEN` 读取扩展路径与 token。
- Rust 初始化:`conn.execute_batch` 中执行 `SET allow_unsigned_extensions = true; LOAD '{ext_path}'; ATTACH 'openduck:mydb?endpoint=http://localhost:7878&token={token}' AS cloud;`。
- Rust 查询:`conn.prepare("SELECT * FROM cloud.users LIMIT 10")?`,遍历 `stmt.query_map([], |row| Ok(row.get::<_, String>(0)?))?` 输出结果。
- 混合执行模式:直接写跨本地/远程表的 SQL,gateway 自动把逻辑计划拆成 `[LOCAL] HashJoin -> [LOCAL] Scan(products) -> [LOCAL] Bridge(REMOTE→LOCAL) -> [REMOTE] Scan(sales)`;不要手动拆查询。
- 混合执行示例:`SELECT p.category, SUM(s.amount) AS revenue FROM local.products p JOIN cloud.sales s ON p.id = s.product_id GROUP BY p.category ORDER BY revenue DESC`,由扩展透明路由。
- 差分存储:由服务端管理;append-only sealed layers 存 S3-compatible 对象存储;`Postgres` 存 layer metadata 和 snapshot pointers。
- 差分存储特性:读端始终看到一致快照;一个序列化写路径、多个并发读;客户端完全透明,DuckDB 看到普通表语义。
- ATTACH URL 格式:`openduck:<database_name>?endpoint=<url>&token=<token>`。
- ATTACH 参数:`endpoint` 默认 `http://localhost:7878`;`token` 默认取 `$OPENDUCK_TOKEN` 环境变量,需与服务端配置匹配。
- ATTACH 示例:本地开发 `ATTACH 'openduck:mydb?token=dev-token' AS cloud;`;远程服务器 `ATTACH 'openduck:mydb?endpoint=https://my-server.example.com&token=prod-token' AS cloud;`;别名 `od:` 也可用,如 `ATTACH 'od:mydb?endpoint=http://localhost:7878&token=dev-token' AS cloud;`。
- DuckLake 集成:加载 OpenDuck 扩展后,执行 `ATTACH 'openduck:lakehouse?endpoint=http://localhost:7878&token=...' AS lh;` 挂载由远程 worker 背后的 DuckLake catalog。
- DuckLake 查询:`con.sql('SELECT * FROM lh.events WHERE event_date = today()').show()`。
- DuckLake 混合:本地 scratch 数据 `memory.sessions` JOIN 远程 DuckLake 表 `lh.users`,例如 `SELECT l.session_id, r.user_email FROM memory.sessions l JOIN lh.users r ON l.user_id = r.id`。
- 协议实现:后端最少实现 `ExecuteQuery`(发送 SQL,接收 query handle)和 `StreamResults`(流式返回 Arrow IPC record batches)两个 gRPC RPC。
- 替换后端:任何实现这两个 RPC 的 gRPC 服务都是合法 OpenDuck 后端,可用任何语言替换 Rust gateway。
- 查看远端表:ATTACH 为 `cloud` 后执行 `SHOW ALL TABLES;` 和 `SELECT table_name FROM information_schema.tables WHERE table_schema = 'cloud';`。
- 写入远程表:`INSERT INTO cloud.events SELECT * FROM read_parquet('local_dump.parquet');`。
- 从本地数据创建远程表:`CREATE TABLE cloud.new_table AS SELECT * FROM local_csv LIMIT 0;`,再 `INSERT INTO cloud.new_table SELECT * FROM local_csv;`。
- 导出远程查询结果到本地 Parquet:`con.sql("SELECT * FROM cloud.large_table WHERE region = 'us-east'").write_parquet('output/us_east.parquet')`。
- 解决 `Extension is not trusted` / 签名错误:加载前设置 `allow_unsigned_extensions` 为 true,例如 `duckdb.connect(config={'allow_unsigned_extensions': 'true'})`;CLI 使用 `duckdb -unsigned`。
- 解决 `LOAD` 扩展找不到:设置 `export OPENDUCK_EXTENSION_PATH=$(pwd)/extensions/openduck/build/release/extension/openduck/openduck.duckdb_extension`,并用 `ls -la $OPENDUCK_EXTENSION_PATH` 确认存在。
- 解决 gateway connection refused:运行 `cargo run -p openduck-cli -- serve -d mydb -t $OPENDUCK_TOKEN`;默认端口 `7878`;检查防火墙/端口绑定;用 `curl http://localhost:7878/health` 验证。
- 解决 token 不匹配 / auth failure:服务端和客户端 token 必须完全一致,例如两边都 `export OPENDUCK_TOKEN=same-value-on-both-sides`。
- 解决 macOS bison 版本导致构建失败:`brew install bison`;`export PATH="$(brew --prefix bison)/bin:$PATH"`;`cd extensions/openduck && make`。
- 解决扩展与 DuckDB 版本不匹配:`python -c "import duckdb; print(duckdb.__version__)"` 查看 DuckDB 版本;确保 `extensions/openduck/Makefile` 中 `DUCKDB_VERSION` 与 Python 包一致。
- 对比选择:需要 `ATTACH 'openduck:db'` 原生远程目录、混合执行、差分存储且自托管时选 OpenDuck;Arrow Flight SQL 无混合执行且要单独 driver;DuckLake 无混合执行,但提供 DuckLake 表格式和 Parquet layers。
适用边界 · 注意事项
- 不想自托管、不愿构建 Rust/C++ 扩展或没有 Postgres 与 S3-compatible 对象存储时,不适用。
- 只需要完整远程执行、不需要本地/远程计划拆分的场景,可优先考虑 Arrow Flight SQL。
- 只使用 DuckLake 表格式、不需要分布式 DuckDB 网关的场景,直接用 DuckLake 即可。