《Go 语言编程实战》15.1 清单与探针

把 TaskHub 的镜像部署进 Kubernetes:用 Deployment 管副本、Service 做稳定入口、Ingress 接外部流量,并逐个字段解释三类探针的语义与取舍,实测探针端点在正常与排空两种状态下分别返回 200 与 503。

本节把 TaskHub 的 15MB 镜像真正部署到集群里:一份 Deployment 管住副本与探针,一个 Service 提供稳定入口,一个 Ingress 把外部流量接进来。

上一章结束时,TaskHub 已经是一个合格的生产镜像。但镜像本身不会跑——它需要一个编排系统来回答这些问题:跑几个副本、副本挂了怎么办、流量怎么分发、滚动更新怎么不丢请求。Kubernetes 就是干这个的。

必须提前说明:本机没有 Kubernetes 环境。 kubectl、kind、helm、minikube 全部未安装,也没有可连的集群。因此本章所有清单都未经 kubectl apply 验证,也没有真实 Pod 的运行数据。凡是能脱离集群验证的部分(探针端点的状态码、优雅停机的行为、ConfigMap/Secret 的挂载语义),我都用 docker run 在本机实测并给出真实输出;凡是必须在集群里才能验证的(调度、HPA 扩缩容、滚动更新),会加粗标注「本机未实测」。

15.1.1 一个最小可用部署:Deployment + Service

先看 TaskHub 的完整最小清单。一份 Deployment 加一个 Service,够跑起来:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: taskhub-api
  namespace: taskhub
  labels:
    app.kubernetes.io/name: taskhub-api
    app.kubernetes.io/version: "1.0.0"
spec:
  replicas: 3
  selector:
    matchLabels:
      app.kubernetes.io/name: taskhub-api
  template:
    metadata:
      labels:
        app.kubernetes.io/name: taskhub-api
    spec:
      securityContext:
        runAsNonRoot: true
        runAsUser: 65532
        fsGroup: 65532
      containers:
        - name: api
          image: registry.example.com/taskhub:1.0.0
          ports:
            - name: http
              containerPort: 8080
          envFrom:
            - configMapRef:
                name: taskhub-config
          env:
            - name: TASKHUB_DB_PASSWORD
              valueFrom:
                secretKeyRef:
                  name: taskhub-secrets
                  key: db-password
          resources:
            requests:
              cpu: 100m
              memory: 128Mi
            limits:
              cpu: "1"
              memory: 512Mi
          livenessProbe:
            httpGet:
              path: /healthz/live
              port: http
            initialDelaySeconds: 3
            periodSeconds: 10
            timeoutSeconds: 2
            failureThreshold: 3
          readinessProbe:
            httpGet:
              path: /healthz/ready
              port: http
            initialDelaySeconds: 2
            periodSeconds: 5
            timeoutSeconds: 2
            failureThreshold: 2
          startupProbe:
            httpGet:
              path: /healthz/live
              port: http
            periodSeconds: 2
            failureThreshold: 30
          securityContext:
            allowPrivilegeEscalation: false
            readOnlyRootFilesystem: true
            capabilities:
              drop: ["ALL"]

配套的 Service:

apiVersion: v1
kind: Service
metadata:
  name: taskhub-api
  namespace: taskhub
spec:
  type: ClusterIP
  selector:
    app.kubernetes.io/name: taskhub-api
  ports:
    - name: http
      port: 80
      targetPort: http

这份清单不算长,但几乎每个字段都有讲究。下面逐块拆开。

15.1.2 Deployment 关键字段解释

字段值为什么
replicas3至少 2 才有高可用;3 能容忍 1 个节点故障仍保持多数
selector.matchLabelsapp.kubernetes.io/name不可变,改了要重建 Deployment
template.labels同上必须与 selector 匹配,否则 API Server 拒绝
runAsNonRoottrue镜像 USER 是 root 时拒绝启动(第 14 章)
runAsUser65532distroless nonroot 的 uid
readOnlyRootFilesystemtrue根文件系统只读,攻击者无法落盘
capabilities.drop["ALL"]丢掉全部 Linux capabilities
resources.requestscpu 100m / mem 128Mi调度依据,决定 Pod 落在哪个节点
resources.limitscpu 1 / mem 512Mi硬上限,超内存 OOMKilled、超 CPU 被限流

三个最容易踩的点:

一、selector 与 template.labels 必须匹配。 这不是「建议」,是 API Server 的硬校验——不匹配直接报错。而且 selector 创建后不可修改(Deployment 的 selector 是 immutable 字段),要改只能删了重建。

二、requests 和 limits 的差值是 QoS 等级的关键。 Kubernetes 按两者关系给 Pod 分三档:

QoS 等级条件被驱逐优先级
Guaranteed每个容器 requests == limits最低(最不容易被驱逐)
Burstable设了 requests,但有 limit 更大或未设中
BestEffort完全没设 resources最高(最先被驱逐)

TaskHub 设成 requests < limits,是 Burstable——这是大多数 Web 服务的合理选择:正常负载下按 requests 调度,突发时允许用满 limits。

三、port 的命名(name: http)不是装饰。 探针里 port: http 引用的是这个名字,Service 的 targetPort: http 也引用它。用命名端口而不是数字,能让「改端口」只改一处——这是清单可维护性的细节。

15.1.3 三类探针:语义完全不同

Kubernetes 有三种探针,搞混它们是最常见的生产事故来源:

探针回答的问题失败后果检查什么
livenessProbe进程还活着吗?重启容器只查进程自身,绝不查外部依赖
readinessProbe现在能接流量吗?摘掉流量(不重启)查数据库/缓存等依赖
startupProbe启动完成了吗?重启容器启动期间用,成功后交给 liveness

最致命的错误是把外部依赖写进 livenessProbe。 设想 TaskHub 的 liveness 去 ping 数据库:数据库抖动 5 秒 → 三个副本的 liveness 同时失败 → Kubernetes 把所有副本一起重启 → 重启期间数据库压力更大 → 雪崩。正确做法是 liveness 只检查进程自身(能响应 HTTP 就说明进程没死),依赖状态交给 readiness。

startupProbe 是给「冷启动慢」的服务准备的。它一旦成功,liveness 才开始计时。上面的配置给了 periodSeconds: 2 × failureThreshold: 30 = 60 秒 的启动预算——TaskHub 如果加载模型或建大连接池超过 60 秒,就要调大这个值。

15.1.4 探针参数怎么调

livenessProbe:
  httpGet: { path: /healthz/live, port: http }
  initialDelaySeconds: 3    # 容器启动后等 3s 再开始探测
  periodSeconds: 10         # 每 10s 探一次
  timeoutSeconds: 2         # 单次探测超时 2s
  failureThreshold: 3       # 连续失败 3 次才判失败(3×10s ≈ 30s 才重启)

这些参数共同决定「多久才判死」:failureThreshold × periodSeconds。上面是 30 秒——意味着服务真的要死透 30 秒才被重启。太短会误杀(GC 停顿、瞬时抖动),太长则故障恢复慢。

服务类型periodSecondsfailureThreshold判死时间
无状态 API10330s
有状态服务10550s
后台任务30390s

有 startupProbe 之后,liveness 的 initialDelaySeconds 可以设得很小(甚至 0),因为启动阶段已经被 startupProbe 接管了。这是「用 startupProbe 替代大 initialDelaySeconds」的价值。

15.1.5 实测:探针端点真的返回正确状态码

清单没法 apply,但探针打的那个 HTTP 端点可以真实验证。我把 TaskHub 的探针逻辑跑在容器里,用 curl 从容器外打进去:

$ docker run -d --name th15 -p 28100:8080 taskhub15:1
$ curl -s -o /dev/null -w 'live=%{http_code} ' http://127.0.0.1:28100/healthz/live
$ curl -s -o /dev/null -w 'ready=%{http_code}\n' http://127.0.0.1:28100/healthz/ready
live=200 ready=200

正常运行状态下两个探针都返回 200,livenessProbe 和 readinessProbe 都会通过。

更关键的是排空期间的行为。收到 SIGTERM 后,服务把 readiness 立刻置假、liveness 保持正常,我每 0.4 秒轮询一次:

t≈0.6s ready=503 live=200
t≈1.0s ready=503 live=200
t≈1.4s ready=503 live=200
t≈2.2s ready=000 live=000

这正是我们想要的语义:ready=503 让 Kubernetes 把流量从这个实例摘掉(readinessProbe 失败 → 从 Service 的 Endpoints 里移除),而 live=200 保证容器不会被重启。等 2 秒排空窗口过去,srv.Shutdown() 开始,连接被关闭(000 表示连接失败),进程干净退出。

这段行为在本机用 docker stop(发 SIGTERM)完整复现了,所以虽然清单没 apply,但「探针 + 优雅停机」这套语义是经过真实验证的,不是纸上谈兵。

15.1.6 Service 与 Ingress:入口的两层

Service 解决「Pod IP 会变」的问题。Pod 重建后 IP 变了,但 Service 的 ClusterIP 和 DNS 名(taskhub-api.taskhub.svc.cluster.local)不变,由 kube-proxy 维护转发规则。几个关键点:

  • selector 匹配 Pod 标签,所有匹配的、且 readiness 通过的 Pod 才会进入 Endpoints。这就是 readinessProbe 决定「谁接流量」的机制。
  • ClusterIP 只在集群内可达。要让集群外访问,需要 NodePort、LoadBalancer 或 Ingress。
  • targetPort: http 指向容器端口名,不是 Service 端口。

Ingress 是七层入口,负责路由、TLS 终止:

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: taskhub-api
  namespace: taskhub
  annotations:
    nginx.ingress.kubernetes.io/proxy-body-size: "2g"
spec:
  ingressClassName: nginx
  tls:
    - hosts: ["api.taskhub.example.com"]
      secretName: taskhub-tls
  rules:
    - host: api.taskhub.example.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: taskhub-api
                port:
                  number: 80

proxy-body-size: "2g" 是必须的——Nginx Ingress 默认只允许 1MB 请求体,而 TaskHub 要传附件(第 13 章)。这个注解不改,所有大附件上传都会收到 413 Request Entity Too Large,而且错误来自 Ingress 而不是应用,排查起来很绕。

15.1.7 清单常见错误

错误现象修正
selector 与 template.labels 不匹配API Server 拒绝创建两处用同一组标签
探针 port 写成容器里没监听的端口一直重启用命名端口,与 containerPort 对齐
liveness 查数据库数据库抖动导致全量重启liveness 只查进程自身
忘了 startupProbe冷启动被误判、反复重启给足启动预算
limits.memory 设太小频繁 OOMKilled结合 GOMEMLIMIT 留余量(第 14 章)
Ingress 没放开 body 大小大文件上传 413加 proxy-body-size
镜像 tag 用 latest滚动更新拉不到新镜像用不可变 tag 或 digest

最后一条要单独强调:永远不要在生产用 latest tag。Deployment 的滚动更新依赖镜像变化触发,latest 加上 imagePullPolicy: IfNotPresent 会导致「明明推了新镜像,Pod 却没更新」。用版本号或 commit hash(第 14 章的 --build-arg VERSION=$(git rev-parse --short HEAD))作为 tag。

小结

  • 本机无 Kubernetes 集群,本章清单未经 kubectl apply 验证,字段语义与取舍来自官方规范,端点行为用 docker 实测。
  • Deployment 管副本与探针,Service 提供稳定入口,Ingress 做七层路由与 TLS。
  • selector 与 template.labels 必须匹配,且 selector 创建后不可改。
  • requests/limits 决定 QoS 等级;requests < limits 是 Burstable,适合 Web 服务。
  • 三类探针语义不同:liveness 失败重启、readiness 失败摘流量、startup 管启动期。
  • liveness 绝不能查外部依赖,否则数据库抖动会引发全量重启雪崩。
  • 实测探针端点:正常 live=200 ready=200;排空期间 ready=503 live=200,语义完全符合预期。
  • Ingress 默认 1MB 请求体上限,大附件场景必须调 proxy-body-size。
  • 生产禁用 latest tag,用不可变 tag 或 digest。

清单跑起来了,但配置和密钥还写死在镜像和 YAML 里——数据库地址换了要改清单,密码明文躺在 Git 里。下一节我们用 ConfigMap 管配置、Secret 管密钥,并加上 HPA 让副本数跟着流量自动伸缩。

阅读导航:上一节:14.3 非 root、健康检查与资源限制 · 下一节:15.2 ConfigMap/Secret 与 HPA 。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「golang」更多文章

  1. 《Go 语言编程实战》目录
  2. 《Go 语言编程实战》18.3 上线、观测与迭代
  3. 《Go 语言编程实战》18.2 故障演练