2026年8月10日 1 分钟阅读

别把 DuckDB 当成完整分析平台:用 Arc 把写入、Parquet 压实与数据保留收进一个二进制

tinyash 0 条评论

做事件分析、日志检索或设备遥测时,团队常常先用对象存储里的 Parquet 文件加 DuckDB 查询。这个组合很适合临时分析:SQL 熟悉,文件可携带,也不用先搭数据仓库。但当数据从“偶尔导入一批文件”变成持续写入,系统还要面对另一组问题:小文件如何合并?旧数据何时删除?谁来管理 schema、认证和查询任务?如果每一项都另选组件,原本轻量的方案很快就需要维护一条数据管道。

Arc 是一个以列式分析为定位的数据库。它把写入、Parquet 落盘、后台 compaction、SQL 查询、保留策略和连续查询放进同一个服务;查询层使用 DuckDB,但项目并不把自己描述为 DuckDB 的包装器。更准确的理解是:DuckDB 负责 SQL 执行,而 Arc 尝试补齐持续运行的分析系统所需的数据生命周期。

本文不把它当作 ClickHouse 或云数仓的替代品清单,而是从一个更具体的场景出发:一个服务不断上报 HTTP 请求事件,希望用最少的部署部件保留 30 天数据、按小时统计错误率,并且最终留下自己可读取的 Parquet 文件。

先区分两件事:查询引擎与数据系统

DuckDB 很擅长直接扫描文件和执行分析 SQL,但“有人持续写数据”并不会天然得到稳定的文件布局。若每个批次都生成一个小 Parquet 文件,查询会逐渐被文件枚举、元数据与打开开销拖慢;若应用自己合并文件,又需要协调写入与查询。保留策略也不只是一个 DELETE:对面向列存和对象存储的系统,更常见的做法是让过期分区或文件按规则淘汰。

Arc 的路径是先接收数据、自动 flush 为 Parquet,再在后台把多个小文件压实为更大的优化文件。README 给出的 compaction 示例将 43 个文件合并为 1 个文件;这是项目方在特定测试数据上的展示,不应当把其中的体积或性能数字外推到生产环境。不过它清楚说明了该机制解决的对象:避免小文件长期堆积,并让后续扫描面对更少的文件。

因此,适合 Arc 的不是“我要在笔记本上查一个 CSV”,而是“我需要一个常驻进程接住事件流,同时仍希望数据最终以开放的 Parquet 形式留在自己的存储里”。它也提供持续查询、认证、备份恢复与 MQTT 摄取等运行期能力;是否需要这些能力,才是选择它而非直接 DuckDB 的关键。

用 Docker 起一个可验证的最小实例

项目 README 提供了容器镜像。先使用命名卷启动服务,并调用健康检查,而不是一上来就把它接进生产采集器:

docker run -d \
  --name arc \
  -p 8000:8000 \
  -v arc-data:/app/data \
  ghcr.io/basekick-labs/arc:latest

curl http://localhost:8000/health

这里的命名卷是最容易被忽略的部分。没有它,容器删除或重建时,默认数据目录中的 Parquet 文件也会随之丢失。验证健康检查成功,只能证明进程可响应;上线前还应明确宿主机或对象存储的备份策略,并确认升级镜像时数据目录和配置的兼容性。

如果要从源码构建,README 也给出 make build./arc 的路径。生产部署更建议优先选定发布版本、记录镜像 digest 或包版本,而不是长期追踪 latest。截至本文核查时,项目最新 GitHub release 是 v26.06.3;项目声明采用 AGPL-3.0,若要把它嵌入、修改或作为网络服务再分发,应先让法务和工程团队评估相应义务。

把“每小时错误率”留给 SQL,而不是业务代码

当事件已经落到一个请求表后,错误率计算不需要在应用中维护第二套聚合逻辑。下面的 SQL 来自 Arc README 所展示的 HTTP 日志分析模式,表名和字段名必须与你实际写入的 schema 一致:

SELECT
  service_name,
  DATE_TRUNC('hour', timestamp) AS hour,
  COUNT(*) AS total_requests,
  SUM(CASE WHEN status >= 500 THEN 1 ELSE 0 END) AS errors,
  (SUM(CASE WHEN status >= 500 THEN 1 ELSE 0 END)::FLOAT / COUNT(*)) * 100 AS error_rate
FROM logs.http_requests
WHERE timestamp > NOW() - INTERVAL '24 hours'
GROUP BY service_name, hour
HAVING error_rate > 1.0;

这个例子里有两个工程上的边界。第一,HAVING 过滤的是已完成聚合后的小时桶;如果要做实时告警,需要确认连续查询的运行频率、延迟容忍度和告警投递路径,而不是假设一条 SQL 会自动产生通知。第二,状态码并不总能代表业务失败。网关超时、客户端主动取消、重试后的成功请求是否计入错误,都应在采集字段定义中先约定,否则图表再精确也无法回答同一个问题。

数据保留与 compaction 要一起设计

只配置“保留 30 天”并不等于成本自然可控。保留周期决定何时淘汰旧数据;compaction 决定活跃数据在磁盘或对象存储中以多少文件存在。两者应和写入批次、查询时间范围一起测:高频小批次写入会提高小文件压力,超短的查询窗口则未必需要扫描长期历史。

在接入之前,先为事件定义一份尽量稳定的最小 schema。例如请求事件至少应有事件时间、服务名、状态码、请求路径或路由标识、延迟与请求 ID;用户标识、完整 URL、请求体这类高基数字段则应先判断是否真的用于分析。把不需要聚合的长文本无差别写入,会同时增加写入量、Parquet 文件体积和扫描成本。隐私字段也不应因为“以后可能有用”就进入分析库:哈希、截断、脱敏或在采集端直接丢弃,通常比事后治理更可靠。

另一个常见误区是把所有事件都做成同一种粒度。服务级的每次请求事件适合排障和钻取;按分钟或小时汇总的指标更适合长期趋势面板。连续查询可以承担部分派生汇总,但应把原始事件的保留期、汇总表的保留期和恢复策略分开制定。比如原始请求只保留 30 天,按服务和小时汇总的错误率可保留更久;发生事故时仍能回看近期明细,日常趋势又不必长期扫描全部明细。

一个实用的验收顺序是:先用压测流量写入一天的数据;观察 flush 后文件数量和查询延迟;再等待后台压实,比较文件布局与同一 SQL 的结果;最后让保留策略跨过一个测试边界,检查过期数据是否确实不可查询、备份是否符合预期。不要只看吞吐数字。Arc README 中“每秒记录数”和“每秒查询行数”来自 M3 Max、并发 worker、批量大小与指定协议的基准条件;你的网络、列宽、基数和存储后端不同,结果也会不同。

还应刻意测试失败路径:采集端短暂断网时会重发还是丢弃?服务重启中途的批次是否可能重复?时间戳由客户端提供还是在接收端写入?这几个选择会影响错误率、去重和按小时聚合的可信度。数据库能够保存数据,但不能替你定义事件语义。先写清这些约定,后面的 SQL、保留策略和容量估算才有可验证的基础。

容量评估也应从真实事件而不是平均值开始。把一次完整的写入批次导出,统计字段长度、空值比例和一天的峰值事件数;再分别测量原始数据与压实后的存储占用。这样得到的保留期成本才可用于预算。对于突发流量,优先确认写入端是否具备背压、批量大小上限与可观测的失败计数;否则数据库还没有成为瓶颈,采集队列就可能先默默丢失事件。

什么时候不该选 Arc

如果数据量很小、只在 CI 或本地脚本中偶发查询,直接用 DuckDB 读取 Parquet 通常更简单;如果团队已经有成熟的 Kafka、Spark、ClickHouse 或托管数仓,额外引入另一套摄取和生命周期语义也可能增加运维面。反过来,如果你需要多节点高可用、跨地域复制或极成熟的生态集成,也要逐项核对 Arc 的当前文档和版本状态,不能因为“单二进制”就假定这些能力已经满足。

Arc 的价值在于收敛边界:把持续写入、开放列式文件、文件压实、SQL 分析和数据寿命放进一个明确的运行单元。先在单一事件流上验证它是否降低了你的组件数量和排障成本;只有在这个收益成立时,再考虑把更多日志、产品事件或遥测数据迁进来。

相关链接

发表评论

你的邮箱地址不会被公开,带 * 的为必填项。