go-service-release

Category: DevOps Risk: High risk niuwoai/skills CC-BY-4.0
destructive_filesystemshell_executionnetwork_accessfilesystem_access

name: go-service-release
description: Go 后端服务发布检查单与回滚预案。当要上线一个 Go 服务、要确认版本号和迁移是否就位、要做灰度或回滚、要配健康检查时使用。触发词:发版、上线、部署、发布检查、灰度、回滚、健康检查、版本号对不上、迁移没跑、systemd、二进制替换。不负责代码实现和性能调优,也不负责 Kubernetes 编排(走 docker-compose-hardening 或专门的 K8s 技能)。

Go 服务发布检查单

发布事故的分布很稳定:配置错、迁移错、版本不一致、没法回滚,四类占九成。这份检查单就是围着这四类设计的。

一、发布前

版本一致性

后端二进制、前端产物、API 响应头三处版本号必须一致。不一致的后果是排障时看到的版本是假的。

# 编译期注入,不要硬编码在多个地方
go build -ldflags "-X main.version=$(cat VERSION)" -o bin/app ./cmd/app

服务必须通过响应头暴露版本,比如 X-App-Version。上线后第一件事就是 curl -I 确认这个头。

版本号规则:功能新增才动正式版本位并把 rc 重置为 rc1;纯 bugfix 只递增 rc 号。改了版本号必须同步 CHANGELOG,两者不同步等于没有版本管理。

配置与密钥

  • 配置和密钥不进构建产物,走环境变量或密钥管理。
  • 新增的环境变量,在目标机器上确认已经存在。缺失时服务应当启动失败并明确报错,而不是用零值默认跑起来。
  • 检查配置项的默认值在生产是否合理。开发环境的超时 30 秒,生产可能需要 3 秒。

数据库迁移

  • 迁移文件 up/down 成对,down 实际执行测试过。
  • 服务启动时自动执行一次 migrate up,失败即阻止启动,并输出迁移版本、文件名和错误上下文。
  • 多实例部署时迁移必须加锁,否则并发启动会互相踩。
  • 高风险迁移不随启动自动执行:大表 DDL、不可逆的数据变更、删字段删表,这三类走人工审批和单独窗口。
  • 迁移必须向后兼容一个版本。也就是说新迁移跑完后,旧二进制仍然能正常工作。做不到就说明这次发布无法回滚,需要拆成两次发。

后一条最容易被忽略,也最致命。「加字段」是兼容的,「改字段名」不是。改名要拆成三步:加新字段并双写 → 迁移数据并切读 → 下个版本删旧字段。

构建与产物

CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -trimpath \
  -ldflags "-s -w -X main.version=$(cat VERSION)" -o dist/app ./cmd/app
  • CGO_ENABLED=0 得到静态二进制,避免目标机 glibc 版本问题。
  • -trimpath 去掉本机路径,产物可复现且不泄露目录结构。
  • 产物打上版本号或时间戳,别叫 app

测试

  • go test ./... 全绿。
  • go vet ./... 和 linter 无新增告警。
  • go build 在目标平台交叉编译成功。
  • 核心链路有集成测试跑过一遍。

二、发布中

备份优先

替换二进制之前,先把旧产物拷到带版本号的目录,至少保留最近三个版本。

install -d /opt/app/releases
cp /opt/app/bin/app /opt/app/releases/app-$(date +%F-%H%M%S)-$(cat /opt/app/VERSION)
ls -t /opt/app/releases | tail -n +4 | xargs -r -I{} rm -f /opt/app/releases/{}

没有这一步,回滚就只能靠重新编译,而故障当下你未必编得动。

部署脚本要幂等

同一个脚本跑十次和跑一次结果一样。不幂等的脚本在重试时会制造脏状态,而故障时你一定会重试。

健康检查后才承载流量

新版本起来后,依次确认:

层次 检查
进程 systemctl is-active <svc> 为 active
端口 ss -lntp | grep <port> 在监听
健康接口 /healthz 返回 200,且真的检查了依赖
版本接口 响应头里的版本号是新版本
关键业务接口 至少一条真实业务路径跑通

健康接口要真检查依赖(数据库能连、缓存能通),只返回 ok 的健康检查毫无价值。

优雅退出

Go 服务必须处理 SIGTERM:停止接受新请求,等待在途请求完成(给一个上限,比如 30 秒),关闭数据库连接池,然后退出。systemd 的 TimeoutStopSec 要大于这个上限。

没有优雅退出,每次发布都会给用户丢一批 502。

三、发布后

立刻确认这几项,不要等用户报障:

  • 错误率和 P99 延迟与发布前对比。
  • 日志里有没有新出现的 ERROR 模式。
  • 迁移是否执行成功,schema_migrations 的版本和 dirty 标志。
  • 内存曲线,观察 10 分钟看有没有泄漏迹象。

反馈给相关人的信息必须包含五样:版本号、部署结果、迁移结果、备份路径、具体的回滚命令。少一样,故障时就要现查。

四、回滚

回滚预案在发布前就要写好,不是出事了现想。

优先回滚应用:恢复旧产物并重启。

systemctl stop app
cp /opt/app/releases/app-<上一个版本> /opt/app/bin/app
systemctl start app
curl -sI http://127.0.0.1:<port>/healthz | grep -i x-app-version

迁移的 down 排在最后。只有在确认「备份可恢复」且「新迁移与旧版本不兼容」时才手动执行,且要人工确认影响范围。绝大多数情况下,回滚应用就够了,因为迁移本来就该向后兼容。

特别注意:迁移失败会把 schema_migrations 置为 dirty,此时旧二进制也起不来。这时回滚二进制是无效的,必须先清掉 dirty 标志(migrate force <上一个版本>),再决定是修复前进还是执行 down。

五、绝对不做的事

  • 直接在 main 或 develop 上 commit,然后手工编译上线。
  • 静默失败:部署、迁移、重启、健康检查任何一步失败都必须明确报告原因和下一步建议,不能吞掉。
  • 在没有备份确认的前提下执行 migrate down
  • .env、证书、密钥提交进仓库或打进产物。
  • 高峰期做大表 DDL。
  • 发布后不看监控就下班。