alitrack

一个 YAML 搞定 DuckDB 集群——我写了个 Quack 进程管家,170K QPS 零故障

一个 YAML 搞定 DuckDB 集群——我写了个 Quack 进程管家,170K QPS 零故障

quack-proxy:633 行 Go,管理 N 个 DuckDB+Quack 进程,自动健康检查 + 重启,一把梭生成 HAProxy 配置。

DuckDB 团队 5 月 12 号发布了 Quack 协议,原生解决了多客户端并发写入的问题。这是好事。但要用好 Quack,你还得自己写 shell 脚本、systemd unit、健康检查、负载均衡——每个 DuckDB 文件都得配一套。
没人在乎运维,直到凌晨两点收到告警。
这就是 quack-proxy 要做的事。

为什么需要这个?

假设你有三个 DuckDB 数据库文件,想把它们都暴露成 Quack 端点:

1# 现状:手动管理
2duckdb orders_2024.db -c "INSTALL quack; LOAD quack; CALL quack_serve('quack:0.0.0.0:9491')" &
3duckdb orders_2025.db -c "INSTALL quack; LOAD quack; CALL quack_serve('quack:0.0.0.0:9492')" &
4duckdb customers.db  -c "INSTALL quack; LOAD quack; CALL quack_serve('quack:0.0.0.0:9493')" &
5
6# 还得自己写健康检查
7while true; do
8  curl -f http://localhost:9491/ || restart_duckdb 9491
9  curl -f http://localhost:9492/ || restart_duckdb 9492
10  sleep 5
11done
12
13# 然后手搓 HAProxy 配置...

三个文件就这么烦了。生产环境 20 个文件呢?
quack-proxy 的做法:

1# quack-proxy.yaml
2shards:
3  - name: orders_2024
4    database: /data/orders_2024.db
5  - name: orders_2025
6    database: /data/orders_2025.db
7  - name: customers
8    database: /data/customers.db
1quack-proxy start  # 一条命令,全部搞定

自动分配端口(9491、9492、9493),自动装 Quack 扩展,自动生成 token,自动健康检查,崩了自动重启。

架构

1quack-proxy (Go daemon, ~10MB RSS)
2├── Process Supervisor
3│   ├── duckdb orders_2024.db → Quack :9491
4│   ├── duckdb orders_2025.db → Quack :9492
5│   └── duckdb customers.db  → Quack :9493
6├── Health Check Loop (每 5 秒)
7│   └── HTTP GET / → unhealthy? kill → restart
8├── Signal Handler
9│   ├── SIGHUP  → 热重载配置
10│   └── SIGTERM → 优雅关闭所有子进程
11└── HAProxy Config Generator
12    └── quack-proxy gen-proxy → 一键生成配置

跟 systemd 的哲学一样:管生管养管埋。每个 DuckDB 子进程的生命周期完全由 quack-proxy 控制——启动、监控、重启、优雅关闭。

压力测试

我在这上面跑了四轮压测。环境:WSL2,8 vCPU,两个 shard。工具是 hey,全打 HTTP 层(Quack RPC 端点,非 SQL 查询)。

单 shard 吞吐
并发
QPS
P50
P99
1
19,649
0.0ms
0.1ms
10
83,369
0.1ms
0.6ms
50
33,520
0.1ms
14.5ms

c=10 是甜点,8 核机器上 83K QPS。c=50 反而掉到 33K——上下文切换开销超过了并发收益。

双 shard 并发

两个 shard 同时打,各 c=20,持续 30 秒,每个 shard 100 万请求:

Shard
QPS
P99
错误数
analytics
95,404
1.4ms
2 / 1,000,000
logs
73,511
1.5ms
1 / 1,000,000
合计168,915
—
3 / 2,000,000

两百万请求,3 个超时,错误率 0.00015%。QPS 差异 ~23% 只是因为两个进程抢 8 个核——内核调度不均匀,正常现象。

故障恢复

直接 kill -9 DuckDB 进程:

  • 杀一个 shard:~2 秒恢复
  • 杀两个 shard:~6 秒全部恢复

quack-proxy 的健康检查每 5 秒跑一次,检测到进程没了立刻重启。DuckDB 重新安装 Quack 扩展需要 ~5 秒。

30 分钟长稳

c=10 持续压两个 shard,30 分钟:

  • 零健康检查失败
  • 零非预期重启
  • DuckDB 内存稳定在 38MB RSS/shard,无增长
  • quack-proxy 内存 ~10MB,无泄漏

代码量

一共 633 行 Go,5 个源文件:

文件
行数
职责
cmd/main.go
209
CLI 入口,信号处理
internal/config/config.go
130
YAML 解析 + 校验
internal/supervisor/supervisor.go
213
进程管理 + 健康检查 + 自动重启
internal/health/health.go
26
HTTP 健康检查
internal/proxy/haproxy.go
55
HAProxy 配置生成

对比一下同类工具:Rust 写的进程管理器通常 2000+ 行起。Go 的 goroutine 天然映射子进程管理——os/exec 开一个 goroutine 就是一个 DuckDB 实例,不需要自己写事件循环。

和 duckdb_fdw 的配合

quack-proxy 是我写 duckdb_fdw v2.0 时顺带做出来的。duckdb_fdw 让 PostgreSQL 能通过 Quack 协议访问远程 DuckDB,但它不负责管 Quack 服务器本身。
配合用法:

1-- PG 中配置 duckdb_fdw,指向 quack-proxy 管理的 HAProxy VIP
2CREATE SERVER quack_cluster FOREIGN DATA WRAPPER duckdb_fdw
3OPTIONS (quack_host 'localhost:9490');
4
5CREATE USER MAPPING FOR current_user SERVER quack_cluster
6OPTIONS (quack_token 'token_from_status_json');
7
8IMPORT FOREIGN SCHEMA "remote" FROM SERVER quack_cluster INTO public;

quack-proxy 管 DuckDB 进程,HAProxy 做负载均衡,duckdb_fdw 在 PG 里提供 FDW 接口——三层协作,各司其职。

不是什么

说清楚不做什么,比说做什么更重要:

  • 不是分布式数据库
    。quack-proxy 只是进程管理器。跨 shard 查询靠 DuckDB 的 ATTACH 语句,单事务只能写一个 ATTACH 数据库。
  • 不做自动分片
    。用户自己决定数据怎么分,quack-proxy 只管进程。
  • 不做 Web UI
    。CLI 就够。五个命令:start / stop / status / reload / gen-proxy。
  • 不做跨机器端点发现
    。v0.1 专注单机多文件管理。

获取

1go install github.com/alitrack/quack-proxy/cmd/quack-proxy@latest

或者直接 clone:

1git clone https://github.com/alitrack/quack-proxy.git
2cd quack-proxy && go build -o quack-proxy ./cmd/...

MIT 协议,随便用。
要求:Go 1.21+,DuckDB ≥ 1.5.2(带 Quack 扩展)。

Quack 协议把 DuckDB 从嵌入式引擎变成了真正的客户端-服务器数据库。但 DuckDB 团队只给了协议,没给运维工具。quack-proxy 补的就是这最后一公里。
周末项目,633 行 Go。写得糙,但能用。