Go 云原生实践:从单体到 K8s 部署的完整路径
单体 Go 应用迁上 Kubernetes,不是"改造完再上",而是"先原样上、再逐步拆"——按容器化 → 无状态化 → 声明式发布 → 可观测 → 渐进拆分这五步走,多数团队在前三步就能拿到 80% 的收益,剩下的微服务拆分反而是最容易踩坑、最不该着急的部分。
第一步:把单体容器化,但别急着改一行业务代码
结论:第一次上 K8s 的目标是"能跑起来",不是"架构漂亮",所以容器化阶段只做三件事——静态编译、多阶段构建、非 root 运行。
Go 的编译产物是静态二进制,这让镜像可以做得很干净。标准写法是多阶段构建,构建阶段用 `golang:1.22-alpine`,运行阶段用 `gcr.io/distroless/static` 或 `scratch`:
FROM golang:1.22-alpine AS build
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -trimpath -ldflags="-s -w" -o /app/server ./cmd/server
FROM gcr.io/distroless/static:nonroot
COPY --from=build /app/server /server
USER nonroot:nonroot
EXPOSE 8080
ENTRYPOINT ["/server"]
注意点有三个:`CGO_ENABLED=0` 是必须的,否则二进制依赖 glibc,换基础镜像就报 "no such file or directory";`USER nonroot` 让容器不以 root 运行,很多集群的 PodSecurity 策略会直接拒绝 root 容器;镜像不要塞配置文件,配置走环境变量或 ConfigMap,这是十二要素应用(12-Factor App,指把配置、日志、进程状态都外置的一套应用设计原则)里最容易被忽略的一条。
第二步:让进程变成"随时可被杀死"的
结论:K8s 会随时重启、迁移、终止你的 Pod,一个不能在 30 秒内优雅退出的 Go 进程,上线后必然出现请求 502。
需要实现两件事。一是信号处理,监听 `SIGTERM` 后停止接收新连接、等待存量请求处理完再退出:
srv := &http.Server{Addr: ":8080", Handler: mux}
go srv.ListenAndServe()
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGTERM, syscall.SIGINT)
<-quit
ctx, cancel := context.WithTimeout(context.Background(), 25*time.Second)
defer cancel()
srv.Shutdown(ctx)
二是健康检查拆成两个端点:`/healthz` 探活(进程还活着就返回 200),`/readyz` 探就绪(依赖的数据库、Redis 都通了才返回 200)。liveness 探针写错是最常见的自伤操作——如果把数据库连接也放进 liveness,数据库抖一下全体 Pod 会被集体重启,雪崩。参数上建议 `initialDelaySeconds: 5`、`periodSeconds: 10`、`failureThreshold: 3`,就绪探针可以更激进一些。
第三步:资源声明做对,才不会被 OOMKill 和限流
结论:Go 应用在容器里必须显式设置 `GOMAXPROCS` 和合理的 requests/limits,否则它会按宿主机核数调度协程、按宿主机内存做 GC,表现就是"同一条代码在容器里慢一倍"。
推荐直接用 Uber 的 `automaxprocs`,在 `main` 里 `import _ "go.uber.org/automaxprocs"` 即可,它会读取 cgroup 限额自动设置 `GOMAXPROCS`。资源上 requests 按实际 P95 用量给,limits 的 CPU 可以放宽甚至不设(CPU 是压缩型资源,超了只是变慢),但内存 limits 必须给——Go 的 GC 目标是"堆不超过 limits 的百分比",设了反而更稳。另外 Go 1.19+ 支持 `GOMEMLIMIT`,可以设成 limits 的 90% 左右作为兜底。
第四步:用声明式发布替代手工上线
结论:Deployment + 滚动更新是单体上 K8s 收益最直接的一环,"发布=改镜像 tag"比任何 CI 脚本都可靠。
滚动更新要关注三个参数:`maxUnavailable: 0` + `maxSurge: 1` 保证容量不下降(代价是发布期间多占一份资源);`minReadySeconds: 10` 避免 Pod 刚 Ready 就被打流量;再配一个 `preStop` 钩子 `sleep 5`,给 Service 摘除 Endpoint 留出传播时间——这一步能消掉绝大部分"发布瞬间零星 502"。多副本时记得加 PodDisruptionBudget,`minAvailable: 1`,防止节点维护时把副本全驱逐了。
第五步:可观测和配置分离补齐,再谈拆分
结论:上 K8s 之后如果还在 `kubectl logs` 里翻日志、靠 `kubectl exec` 改配置,迁移只完成了一半。
日志统一写 stdout,用 JSON 格式,采集交给 Loki 或 ELK;指标暴露 `/metrics` 给 Prometheus,Go 用 `prometheus/client_golang` 几行就够;链路追踪接 OpenTelemetry,出问题时能一眼看出是网关慢还是 DB 慢。配置用 ConfigMap 挂载、敏感信息用 Secret,数据库迁移写成独立的 Job(或 initContainer),不要塞进应用启动流程——多副本同时跑迁移脚本会互相打架。
做完这五步,你已经有了一个"能在 K8s 上稳定跑的单体",这本身就是一个完全体面的终点。至于拆分,判断信号很清晰:某个模块需要独立扩缩容、需要独立发布节奏、或者团队人数已经超过两个披萨能喂饱的规模。真到那一步,也优先考虑"模块化单体"——在同一个进程里把边界划清楚(包依赖单向、领域层不依赖基础设施层),等边界稳定了再拆进程,比一上来就搞服务网格要省钱得多。
一句话收尾:Go 上 K8s 的顺序是"先容器化、再优雅、再资源、再发布、再观测、最后拆分",跳过前四步去追微服务,大概率是在给自己制造分布式系统的坑。
转载请注明出处,版权归原作者所有。
星耀SVIP
管理员
黑卡会员





