Go 服务启动 panic:对象存储 400 不应该阻塞 API 启动

  1. 1. 问题现象
  2. 2. 排查过程
    1. 2.1. 先确认“MinIO”到底是什么
    2. 2.2. 找到真正的网络请求
  3. 3. 根因
  4. 4. 解决方案
    1. 4.1. 启动只加载配置,不探测远端
    2. 4.2. 把严格探测放进“测试配置”
    3. 4.3. 校验 R2 的两个账户字段
  5. 5. 验证与回归
  6. 6. 常见误区与边界
  7. 7. 检查清单

对象存储的临时故障不应该让整个 API 进程无法启动。本文记录一次 Go 服务启动时检查 S3 Bucket 收到 400 Bad Request、容器随后退出的真实排查过程,并给出一套适用于 S3 兼容存储的启动策略。

问题现象

开发环境使用 Docker Compose 启动视频 API。数据库、Redis 和队列都已经初始化成功,但 API 在构建服务依赖时退出:

1
2
3
4
panic: checking private object storage: checking S3 bucket: 400 Bad Request

goframeddd/internal/bootstrap.BuildVideoServer
/workspace/server/internal/bootstrap/bootstrap.go:125

表面上看像是 MinIO 没启动。实际上 Compose 中没有 MinIO 服务,应用使用的是管理员在数据库中配置的 Cloudflare R2。

排查过程

先确认“MinIO”到底是什么

代码的 S3 实现使用了 github.com/minio/minio-go/v7。这里的 MinIO 是 Go 客户端 SDK,不等于系统正在运行 MinIO 服务。这个 SDK 同样可以访问 Cloudflare R2、阿里云 OSS、腾讯 COS 和其他 S3 兼容服务。

可以先检查服务编排和依赖:

1
2
rg -n "minio|MINIO|S3_ENDPOINT" compose.yaml server
rg -n "minio-go|EnsureBucket" server/internal

如果只找到 minio-go 依赖,却找不到 MinIO 容器,排查方向就应该转向数据库里的对象存储配置,而不是给 Compose 临时加一个 MinIO。

找到真正的网络请求

启动调用链是:

1
2
3
4
5
BuildVideoServer
-> buildRuntimeStorage
-> NewS3Store
-> EnsureBucket
-> minio.Client.BucketExists

原来的 buildRuntimeStorage 在创建客户端之后立刻执行 EnsureBucket。任何远端 4xx、网络超时或凭据错误都会返回到 BuildVideoServer,而 BuildVideoServer 对错误直接 panic

数据库中保存的是一条启用的 cloudflare_r2 配置。检查时只读取非敏感字段即可:

1
2
3
SELECT enabled, provider, endpoint, account_id, region, bucket, path_prefix
FROM video_storage_setting
WHERE id = 1;

这次配置的 Endpoint 和 Account ID 也不一致。R2 官方 Endpoint 通常包含账户 ID;两个字段不匹配时,即使网络可达,也不能说明签名请求一定正确。

根因

直接原因不是“MinIO 容器缺失”,而是启动阶段把远端 Bucket 连通性当成了 API 的硬依赖:

1
2
3
4
5
6
7
store, err := storageinfra.NewS3Store(options)
if err != nil {
return runtimeStorage{}, err
}
if err := store.EnsureBucket(ctx); err != nil {
return runtimeStorage{}, fmt.Errorf("checking private object storage: %w", err)
}

NewS3Store 只负责校验 Endpoint、凭据格式并创建客户端;真正的网络探测发生在 EnsureBucket。把两者放在同一个启动路径里,会产生一个很难恢复的锁定问题:对象存储配置越坏,管理员越无法进入后台修正它。

解决方案

启动只加载配置,不探测远端

启动时保留本地配置校验和 S3 客户端创建,但删除 EnsureBucket

1
2
3
4
5
6
7
8
store, err := storageinfra.NewS3Store(options)
if err != nil {
return runtimeStorage{}, fmt.Errorf("configuring private object storage: %w", err)
}

selection.Objects = store
selection.Provider = setting.Provider
return selection, nil

这样做并不是把错误吞掉。后续真正的上传、下载和删除仍然会返回对象存储错误;只是暂时故障不会阻止 API 提供管理页面和健康检查。

把严格探测放进“测试配置”

管理后台的测试接口使用独立的短超时,依次执行:

  1. 检查 Bucket 是否存在;
  2. 写入一个随机测试对象;
  3. 读取并校验对象内容;
  4. 删除测试对象。

这比只执行一次 HEAD Bucket 更接近资产真实使用所需的权限。测试失败时,错误会回到管理员页面,而不是让 API 进程退出。

校验 R2 的两个账户字段

当同时填写官方 R2 Endpoint 和 Account ID 时,后台现在会检查二者是否属于同一账户。Endpoint 可以留空并由 Account ID 自动生成,也可以填写完整 Endpoint,但不能保存互相矛盾的组合。

验证与回归

新增了一个回归测试,使用不可达的测试 Endpoint 构建云存储配置,确认启动阶段不会发起网络探测:

1
2
cd server
go test ./internal/bootstrap -run TestBuildRuntimeStorageDoesNotProbeCloudProviderAtStartup

本次修复还实际运行了整仓验证:

1
2
3
4
make test
make lint
make build
curl -fsS http://localhost:8080/health

结果是 Go 与前端测试全部通过,静态检查和构建通过,健康检查返回数据库、Redis、队列均为 healthy。API 日志显示仍选择已配置的 cloudflare_r2,没有悄悄切换到本地存储。

常见误区与边界

  • minio-go 是 SDK,不代表必须部署 MinIO。是否部署 MinIO 取决于 Endpoint,而不是 Go 包名。
  • 启动不探测远端并不等于对象存储可用。发布前仍应点击后台的“测试配置”。
  • 未配置、禁用或选择 local 时才使用本机存储;已启用的云存储配置错误时不会静默回退,否则会造成资产散落在两个后端。
  • R2 的 path-style、Region、Endpoint 和密钥必须按供应商要求配置。测试按钮通过后,再重启 API 与 Worker 让新配置生效。

检查清单

  • Compose 中是否真的有 MinIO 服务,还是只引入了 MinIO 的 S3 SDK?
  • video_storage_setting 是否启用,Provider、Endpoint、Account ID、Bucket 是否对应?
  • 启动流程是否只做本地配置校验,没有强制 BucketExists
  • 是否提供了独立的 Bucket + 写入 + 读取 + 删除测试?
  • 保存云存储配置后,是否重启了 API 和 Worker?
投喂小莫
给快要饿死的小莫投喂点零食吧~
投喂小莫
分享
分享提示信息