Skip to main content

10. 准备部署

从本地到生产​

上一章 make run 把 Controller 跑在本地——你 Ctrl+C 它就停。生产环境不能这样,得跑成集群里的 Pod,挂了自动重启,多副本做高可用。

这一章解决三个问题:

  1. 怎么打包:Controller 变成镜像,跑成 Pod
  2. 怎么高可用:多副本时多个 Controller 抢着调谐怎么办
  3. 怎么观测:跑起来后怎么知道它健康不健康、性能怎么样

镜像构建​

Kubebuilder 生成的项目自带 Dockerfile:

# 多阶段构建:builder 阶段编译,runner 阶段只跑二进制
FROM golang:1.22 AS builder
WORKDIR /workspace
COPY go.mod go.sum ./
RUN go mod download
COPY api/ api/
COPY cmd/ cmd/
COPY internal/ internal/
RUN CGO_ENABLED=0 GOOS=linux go build -o manager cmd/main/main.go

FROM gcr.io/distroless/static:nonroot
WORKDIR /
COPY --from=builder /workspace/manager .
USER nonroot:nonroot
ENTRYPOINT ["/manager"]

几个关键点:

决策原因
多阶段构建builder 阶段有 Go 工具链几百 MB,runner 阶段只拷二进制,镜像从 800MB 砍到 20MB
CGO_ENABLED=0纯静态二进制,不依赖 glibc,能在 distroless 里跑
distroless/static没有 shell、没有包管理器,攻击面小
nonroot用非 root 用户跑,符合 Pod Security Standards

构建+推送:

# 用 Makefile 封装好的
make docker-build docker-push IMG=yourrepo/mysql-operator:v0.1.0

# 或手动
docker build -t yourrepo/mysql-operator:v0.1.0 .
docker push yourrepo/mysql-operator:v0.1.0

部署 manifest​

Kubebuilder 生成的 config/manager/manager.yaml:

apiVersion: apps/v1
kind: Deployment
metadata:
name: mysql-operator
namespace: mysql-operator-system
spec:
replicas: 1 # 见后文 Leader Election
selector:
matchLabels:
control-plane: controller-manager
template:
metadata:
labels:
control-plane: controller-manager
spec:
serviceAccountName: mysql-operator # RBAC 绑的账号
containers:
- name: manager
image: yourrepo/mysql-operator:v0.1.0
args:
- --metrics-bind-address=:8080
- --health-probe-bind-address=:8081
- --leader-elect # 启用领导选举
ports:
- containerPort: 8080
name: metrics
- containerPort: 8081
name: health
livenessProbe:
httpGet:
path: /healthz
port: 8081
initialDelaySeconds: 15
periodSeconds: 20
readinessProbe:
httpGet:
path: /readyz
port: 8081
initialDelaySeconds: 5
periodSeconds: 10
resources:
limits:
cpu: 500m
memory: 256Mi
requests:
cpu: 100m
memory: 128Mi
terminationGracePeriodSeconds: 10

逐项看为什么这么配。

serviceAccountName​

Controller 用这个 ServiceAccount 调 API Server。上一章生成的 RBAC ClusterRole 绑到这个账号上——Controller 才有权限读写 MySQL、StatefulSet 这些资源。

# RBAC 绑定
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: mysql-operator-binding
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: mysql-operator-role # 上一章 make manifests 生成的
subjects:
- kind: ServiceAccount
name: mysql-operator
namespace: mysql-operator-system

不打这个绑定,Controller 启动后第一个 API 调用就 403。

健康检查​

两种 probe 用途不同:

probe用途失败了干嘛
liveness"Controller 还活着吗"重启容器
readiness"能接活了吗"暂时从 Service endpoints 摘掉(不影响 Controller,但影响被 Service 暴露的场景)

/healthz 和 /readyz 是 controller-runtime 自动注册的路径——你的 Manager 启动时就有,不用自己写。

/readyz 返回 ok 表示缓存同步好了、可以处理事件了。/healthz 返回 ok 表示主循环没死。

resources​

Controller 不吃资源,但必须给 limit。原因:

  • Controller 内存泄漏(Informer 缓存失控、goroutine 泄漏)时,limit 是兜底
  • 没有 limit 的 Pod 可能吃光节点内存,触发 OOM 杀别的 Pod

给个小 limit 就行——100m CPU + 128Mi 内存对大部分 Controller 够用。管大规模集群(几万资源)时再调大。

terminationGracePeriodSeconds​

terminationGracePeriodSeconds: 10

Pod 被删时,kubelet 先发 SIGTERM,等 10 秒还没退就 SIGKILL。controller-runtime 的 SetupSignalHandler() 监听 SIGTERM,收到后:

  1. 停止接收新事件
  2. 等队列里的 key 处理完
  3. 退出

10 秒对大部分场景够。如果你的 Reconcile 调外部系统(慢),可能要调大。

Leader Election:多副本高可用​

为什么不能多副本直接跑​

假设 replicas: 3,三个 Controller 同时跑。一个 MySQL 资源变了,三个 Controller 都收到事件,都去 reconcile——同时创建 StatefulSet,要么冲突要么重复。

像三个厨师看到同一张订单,都去炒同一盘菜——端上来三份,客人傻眼。

Leader Election 怎么解决​

多副本还是跑,但只有一个干活,其他待命。干活的叫 leader,挂了待命的顶上。

┌────────────────────────────────────────┐
│ Lease 对象(etcd 里) │
│ holder: mysql-operator-pod-1 │
│ renewTime: 2026-07-01T10:00:05Z │
└────────────────────────────────────────┘
▲
│ 每 2 秒续约
│
┌───────────────┼────────────────────────┐
│ pod-1 (leader) ──→ 干活 │
│ pod-2 ──→ 待命,每 1 秒检查 lease │
│ pod-3 ──→ 待命 │
└────────────────────────────────────────┘

机制:

  1. 三个 Pod 启动,都尝试创建/更新一个 Lease 对象
  2. 抢到的成为 leader,开始 Reconcile
  3. leader 每隔几秒续约(更新 renewTime)
  4. 其他 Pod 周期性查 lease——renewTime 还新鲜,继续待命
  5. leader 挂了,续约停止,待命的发现 lease 过期,抢着接任

启用​

main.go 里:

mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{
LeaderElection: true,
LeaderElectionID: "mysql-operator.example.com", # Lease 名字
LeaderElectionNamespace: "mysql-operator-system", # Lease 在哪个 namespace
})

或命令行:

args:
- --leader-elect

Lease 的几个参数​

LeaderElectionReleaseOnCancel: true, # leader 退出时主动释放 lease
LeaseDuration: 15 * time.Second, # lease 有效期
RenewDeadline: 10 * time.Second, # 续约超时
RetryPeriod: 2 * time.Second, # 重试间隔
  • LeaseDuration:lease 多久没续约算过期。设太短——网络抖动就换主;设太长——leader 真挂了要等很久才切换
  • RenewDeadline:leader 续约失败的重试窗口
  • RetryPeriod:非 leader 检查 lease 的频率

默认值(15s/10s/2s)对大部分场景合适。网络不稳的集群适当调大。

replicas 给多少​

spec:
replicas: 2 # 或 3

给 1——单点,挂了就停。 给 2——挂一个还有一个顶。够用。 给 3——更稳,但浪费资源。除非真的极高可用要求。

不要给 5 或 10——leader 选举的性能跟副本数有关,副本越多选举越慢,收益递减。

指标暴露​

controller-runtime 自带 Prometheus 指标,默认在 :8080/metrics:

controller_runtime_reconcile_total{controller="mysql"} 42
controller_runtime_reconcile_errors_total{controller="mysql"} 3
controller_runtime_reconcile_time_seconds{controller="mysql"} 0.15
workqueue_depth{controller="mysql"} 0
workqueue_adds_total{controller="mysql"} 42
workqueue_retries_total{controller="mysql"} 5
指标含义看什么
reconcile_totalreconcile 调用次数趋势,看是不是疯狂调谐
reconcile_errors_total失败次数不为 0 就查日志
reconcile_time_seconds单次耗时p99 太高就要优化
workqueue_depth队列积压持续 > 0 说明处理跟不上
workqueue_retries_total重试次数高说明有持续失败的资源

ServiceMonitor 暴露给 Prometheus​

apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
name: mysql-operator
namespace: mysql-operator-system
spec:
selector:
matchLabels:
control-plane: controller-manager
endpoints:
- port: metrics
path: /metrics
interval: 30s

配好后在 Grafana 建面板:

  • Reconcile 速率(QPS)
  • 错误率
  • 队列深度
  • Reconcile 延迟 p50/p95/p99

自定义指标​

controller-runtime 默认指标偏运维视角(reconcile 次数、队列长度)。业务指标——比如"当前管着几个 MySQL 实例"——要自己加:

import (
"github.com/prometheus/client_golang/prometheus"
"sigs.k8s.io/controller-runtime/pkg/metrics"
)

var mysqlCount = prometheus.NewGauge(
prometheus.GaugeOpts{
Name: "mysql_operator_managed_instances",
Help: "当前管理的 MySQL 实例数",
},
)

func init() {
metrics.Registry.MustRegister(mysqlCount)
}

func (r *MySQLReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
// ...
mysqls, _ := r.List(ctx, &mygroupv1.MySQLList{})
mysqlCount.Set(float64(len(mysqls.Items)))
// ...
}

metrics.Registry 是 controller-runtime 暴露给 /metrics 的注册表——你注册的指标自动出现在端点里。

部署到集群​

# 1. 建 namespace
kubectl create namespace mysql-operator-system

# 2. 装 CRD
make install

# 3. 装 RBAC
kubectl apply -f config/rbac/

# 4. 部署 Operator
make deploy IMG=yourrepo/mysql-operator:v0.1.0

# 5. 看 Pod
kubectl get pods -n mysql-operator-system
# NAME READY STATUS RESTARTS AGE
# mysql-operator-xxx-yyy 1/1 Running 0 1m

完整部署清单​

把这一章的组件拼起来:

┌─ Namespace: mysql-operator-system ──────────────────────┐
│ │
│ ┌─ Deployment ─────────────────────────┐ │
│ │ replicas: 2 │ │
│ │ ┌──────────────────────────┐ │ │
│ │ │ Pod 1 (leader) │ │ │
│ │ │ - manager 容器 │ │ │
│ │ │ 健康检查: /healthz │ │ │
│ │ │ 指标: :8080/metrics │ │ │
│ │ │ args: --leader-elect │ │ │
│ │ └──────────────────────────┘ │ │
│ │ ┌──────────────────────────┐ │ │
│ │ │ Pod 2 (standby) │ │ │
│ │ └──────────────────────────┘ │ │
│ └───────────────────────────────────────┘ │
│ │
│ ┌─ ServiceAccount ──┐ ┌─ ClusterRole ──────┐ │
│ │ mysql-operator │←──│ mysql-operator-role │ │
│ └───────────────────┘ └────────────────────┘ │
│ │
│ ┌─ Lease ───────────────────────────────┐ │
│ │ mysql-operator.example.com │ │
│ │ holder: mysql-operator-pod-1 │ │
│ └────────────────────────────────────────┘ │
│ │
└──────────────────────────────────────────────────────────┘

前置阅读​

  • 使用Kubebuilder — 本章前置,部署的是上一章写的 Controller
  • 工作负载管理 — Starter 第 5 章,Deployment 是 Controller 部署的基础
  • 安全 — Starter 第 8 章,ServiceAccount/RBAC 是权限基础
  • 超卖与QoS — Starter 第 10 章,resources 配置影响 QoS 等级
  • 弹性伸缩 — Starter 第 9 章,HPA 也用 Leader Election 做高可用

思考题​

问题 1:Leader Election 的 LeaseDuration 设成 1 秒会怎样?设成 1 小时呢?

问题 2:Deployment 的 replicas 给 2,但忘了开 --leader-elect,会发生什么?

问题 3:Controller Pod 被 OOM 杀了,正在处理的 Reconcile 会不会丢?


解答​

问题 1:1 秒——网络抖动一次就换主,频繁切换 leader,每个 Pod 都在抢 lease 不干活。1 小时——leader 真挂了,要等 1 小时待命的才发现,期间没人 reconcile。合理值是 15-60 秒,跟网络稳定性挂钩。

问题 2:两个 Pod 同时跑,同时 Watch 同一个 MySQL 资源,同时 reconcile——重复创建 StatefulSet 导致冲突,或重复调谐外部系统造成副作用。必须开 leader-elect 保证只有一个干活。

问题 3:不会丢。Workqueue 是基于 etcd 的——key 在处理中时被标记为"processing",Pod 挂了,Done() 没被调用,key 在 lease 过期后被其他 Pod 重新取出处理。controller-runtime 的 Reconcile 失败 requeue 机制保证最终一致。

下一章​

Controller 写完也部署了。最后一章我们跳出 K8S——看看 Controller 这种模式在 K8S 之外的应用。