当监控服务的请求体数据从 10KB 扩容至 100KB,数据库存储与备份成本急剧上升,业务面临一个经典抉择:继续堆砌数据库资源,还是将大对象剥离至专用存储。Healthchecks.io 给出了自己的答案 —— 将 ping 请求体迁移至 S3 兼容对象存储。这篇文章将完整呈现其迁移决策链、实现细节与自托管配置清单,为面临类似架构演进问题的团队提供可复用的工程参考。
背景:为什么需要脱离数据库
Healthchecks.io 的核心功能是接收并记录各类定时任务的「心跳」信号。当用户通过 HTTP POST 向 ping URL 发送请求时,服务端会捕获并存储请求体数据。这一特性允许用户将命令输出直接记录下来,供后续审查。例如使用 cowsay hello | curl --data-binary @- https://hc-ping.com/some-uuid-here 即可将命令输出存储为 ping 附件。早期版本将请求体限制在 10KB,后来放宽至 100KB。
问题在于,这些数据最初全部存储在 PostgreSQL 主数据库中。随着 100KB 限制的生效,数据量呈现线性增长态势,直接导致三类运维压力:备份文件体积膨胀、数据库网络 I/O 成为瓶颈、恢复时间显著延长。团队做过一个简单的成本估算 —— 若将所有 ping 体迁移至 AWS S3,按每秒 20 个对象计算,每月会产生约 5200 万次 PUT 请求,按 AWS 定价仅请求费用就高达 260 美元,还不包括流量与存储成本。
与此同时,ping 请求体可能包含敏感的个人数据,受限于 GDPR 与 Schrems II 法案要求,必须在数据离开系统前完成加密。这进一步增加了直接使用 AWS S3 的合规复杂度。
架构决策:同步上传还是异步上传
迁移方案的核心第一个分叉点是上传时机选择。同步方案是在 HTTP 请求处理周期内直接完成 S3 上传,架构简单、无需后台 worker,但 S3 API 延迟会直接影响请求响应时间。异步方案则是先将请求体写入本地文件系统或消息队列,由独立的后台 worker 负责上传,优点是解耦了 ping handler 的吞吐量瓶颈,但增加了系统复杂度 ——worker 可能异常、可能产生任务积压、需要额外监控。
Healthchecks.io 最终采用了双轨策略:开源版本使用同步上传以保持部署简洁,托管服务(healthchecks.io)则使用异步方案 ——ping handler 将接收到的数据写入本地文件系统,专门的 worker 进程异步抓取并上传至 S3。这种差异化的设计体现了对运维复杂度与性能的精准权衡。
在客户端选型上,团队曾尝试手写 AWS Signature 请求以减少依赖,但在完成签名验证实验后决定采用现成库。最终选择了 minio-py 作为 S3 客户端,其体积与依赖项远小于 boto3,更适合 Healthchecks 这类边缘服务场景。
数据模型调整与对象键设计
迁移前,ping 请求体在数据库中以文本字段存储。迁移后引入了二进制字段 body_raw 作为默认存储方式,避免了短内容存数据库、长内容存对象存储的数据不一致问题。团队还设置了一个 100 字节的阈值 —— 小于该阈值的请求体仍保留在数据库,只有超过阈值的大对象才会被 offload 至 S3。
对象键(Object Key)的命名设计也颇具匠心。最初采用直观的 /<uuid>/<n> 格式(如 /504eb741-1966-49fe-a6e7-4d3133d2b2bd/1),但在清理场景下遇到了问题:需要列出前缀下所有对象再在客户端过滤。团队引入了排序前缀技巧,使用自定义字符映射将序号转换为逆序字符串,使得 list_objects 的 start_after 参数可以直接定位分界点。例如 /504eb741-.../zi-1、/504eb741-.../zh-2 这样的键名,配合 start_after="yej" 即可高效获取 n<50 的所有对象,避免了全量列举。
S3 提供商选型:Scaleway 与 OVH 的真实对比
成本考量促使团队放弃原生 AWS S3,转向 S3 兼容的欧洲提供商。初步测试了 Scaleway,联系其技术支持确认了使用场景的合理性,但在实际测试中发现了严重问题:DeleteObjects API 响应时间经常达到数秒乃至数十秒,期间还遭遇了长达数小时的服务宕机,API 返回 "InternalError"。
随后转向 OVH 进行测试。OVH 明确表示无请求速率限制,DeleteObjects 通常在亚秒级完成,整体表现显著优于 Scaleway。但测试过程中也遇到了若干问题:OVH API 偶发返回 "ServiceUnavailable, Please reduce your request rate",官方解释为平台偶发操作导致。当对象数量超过 50 万时,OVH 管理控制台无法正常展示 bucket 内容(页面超时报错),但 API 层面仍可正常工作。
值得注意的是,团队还遇到了一个 minio-py 客户端的布尔序列化 bug:生成 DeleteObjects 请求时,minio-py 将布尔值序列化为 True/False(大写),而 AWS S3 可以接受,Scaleway 与 OVH 却拒绝接受。团队向 minio-py 提交了修复,同时分别联系了两家提供商,后者最终在其 S3 实现中兼容了大写布尔值。
安全与清理机制
由于 ping 请求体可能包含个人数据,Healthchecks.io 在将数据移交外部对象存储前进行了加密处理。这一设计满足了 GDPR 框架下的数据最小化与安全传输要求。
与 Django 生态下数据库记录的级联删除不同,S3 对象的生命周期管理需要自行维护。团队开发了 pruneobjects 管理命令,用于遍历 S3 bucket 并删除那些数据库中已不存在对应记录的孤立对象。这一机制对于用户注销账号、删除监控项等场景至关重要,避免了对象存储中的数据泄漏。
备份策略采用多层冗余设计:定时任务在独立 VPS 上使用 aws s3 sync 同步整个 bucket 内容,随后使用 tar 打包并通过 GPG 加密,最终将加密归档文件上传至另一家提供商的不同 bucket。这种跨提供商、加密归档的策略有效降低了单点故障与数据丢失风险。
自托管配置清单
对于自托管用户,Healthchecks 提供了完整的 S3 兼容存储支持,只需配置以下环境变量即可启用对象存储:
# S3 兼容存储凭据
export HEALTHCHECKS_SECRET_KEY=your-secret-key
export HEALTHCHECKS_S3_ACCESS_KEY=your-access-key
export HEALTHCHECKS_S3_SECRET_KEY=your-secret-key
export HEALTHCHECKS_S3_BUCKET=your-bucket-name
export HEALTHCHECKS_S3_REGION=your-region
export HEALTHCHECKS_S3_ENDPOINT_URL=https://your-s3-compatible-endpoint
# 可选:自定义域名前缀
export HEALTHCHECKS_S3_PREFIX=optional-path-prefix
支持的 S3 兼容提供商包括但不限于 AWS S3、MinIO、Backblaze B2、Cloudflare R2、OVH Object Storage、Scaleway Object Storage。配置完成后,超过 100 字节的 ping 请求体将自动存储至指定的对象存储服务,而非数据库。
若需手动触发孤立对象清理,可运行:
python manage.py pruneobjects
该命令会扫描 bucket 并删除数据库中已不存在对应记录的 ping 体对象,建议定期执行以控制存储成本。
工程经验总结
Healthchecks.io 的这次迁移揭示了几个可复用的架构原则。第一,上传策略应根据部署规模差异化选择 —— 自托管场景优先考虑同步上传以降低运维复杂度,规模化托管服务则可通过异步 worker 解耦性能瓶颈。第二,对象键设计应优先考虑后续清理与查询的效率,适当的排序前缀可以显著降低 list 操作的客户端过滤开销。第三,供应商测试不能仅依赖官方承诺的 SLA,必须通过实际负载测试验证 API 行为,尤其是删除操作的响应延迟。第四,跨平台兼容性问题往往隐藏在细节中,布尔值大小写、XML 格式细微差异都可能导致集成失败 —— 使用成熟的客户端库并在多提供商环境下进行端到端测试是必要投入。
对于正在评估类似迁移的团队,建议先从小规模试点开始,监控 S3 API 的错误率与延迟表现,再逐步扩大迁移范围。同时务必建立对象生命周期管理机制,避免存储成本不受控制地增长。
资料来源:Healthchecks.io 官方博客《We Moved Some Data to S3》(2022 年 4 月)。