跳到主要内容

2 篇博文 含有标签「StatefulSet」

Kubernetes StatefulSet 有状态应用编排

查看所有标签

Kubernetes 全景解析 (7):有状态应用与 Operator 模式实战

· 阅读需 32 分钟
Rainy
雨落无声,代码成诗 —— 致力于技术与艺术的极致平衡

"StatefulSet 管的是'有序的宠物',Operator 管的是'懂业务的管家'。"

在前面的章节中,我们深入探讨了 K8s 的架构设计、网络模型和存储体系。对于无状态应用(如 Web 服务、API 网关),Deployment + Service 的组合已经足够应对大多数场景。然而,当你面对数据库、消息队列、分布式缓存等有状态应用时,事情就变得复杂了——它们需要稳定的网络标识、持久化的存储卷、有序的启停顺序,以及复杂的集群初始化流程。

本文将从有状态应用的核心挑战出发,深入解析 StatefulSet 的工作机制,实战部署一套 Redis Cluster,并最终构建一个自定义 Operator 来自动化管理整个生命周期。

API 版本说明:本文所有 YAML 配置使用的 API 版本均经过 Kubernetes v1.34 官方文档校验:

资源类型apiVersion官方文档
StatefulSetapps/v1StatefulSet v1
Servicev1Service v1
CRDapiextensions.k8s.io/v1CRD v1
RBACrbac.authorization.k8s.io/v1RBAC v1
Deploymentapps/v1Deployment v1

:::


一、有状态应用的挑战与 K8s 解决方案

1.1 有状态 vs 无状态:本质区别

在分布式系统中,"状态"(State)是指应用需要持久保存的数据或上下文信息。理解有状态与无状态的本质区别,是选择正确编排策略的前提。

维度无状态应用有状态应用
数据存储数据在外部(DB/缓存)数据在本地或内置存储
网络标识随意替换,IP 可变需要稳定的网络标识
扩缩容随意增删实例需要数据迁移/重平衡
启停顺序无要求通常需要有序启停
典型代表Web 服务、微服务 API数据库、消息队列、缓存
K8s 工作负载Deployment / ReplicaSetStatefulSet / Operator

无状态应用就像工厂流水线上的工人——任何人都可以顶替任何人的位置,因为所有信息都记录在外部系统中。

有状态应用则像一支乐队——每个乐手有自己的乐器和乐谱,鼓手不能突然变成小提琴手,乐队的演出需要按特定顺序开始。

1.2 StatefulSet 的核心特性

StatefulSet 是 Kubernetes 专门为有状态应用设计的工作负载控制器。它相比 Deployment,提供了三个关键保证:

1. 稳定的、唯一的网络标识

每个 Pod 都会获得一个固定的名称和对应的 DNS 记录。即使 Pod 被重新调度到其他节点,名称和 DNS 也不会改变。

2. 稳定的、持久的存储

每个 Pod 绑定一个独立的 PVC(PersistentVolumeClaim),Pod 被删除后 PVC 保留,新 Pod 重建时会重新挂载同一个 PVC。

3. 有序的、优雅的部署和扩缩容

Pod 按照 0, 1, 2, ... 的顺序依次创建和删除,确保集群拓扑的稳定性。

1.3 Headless Service 与 DNS 记录

StatefulSet 依赖一个特殊的 Service——Headless ServiceclusterIP: None)来实现稳定的网络标识。与普通 Service 不同,Headless Service 不会分配集群 IP,也不会进行代理负载均衡,而是直接将 DNS 解析到每个 Pod 的 IP。

# headless-service.yaml
apiVersion: v1
kind: Service
metadata:
name: redis-cluster
spec:
clusterIP: None # 关键:声明为 Headless Service
selector:
app: redis-cluster
ports:
- port: 6379
targetPort: 6379

创建 Headless Service 后,DNS 会为每个 Pod 生成如下记录:

redis-cluster-0.redis-cluster.default.svc.cluster.local -> 10.244.1.10
redis-cluster-1.redis-cluster.default.svc.cluster.local -> 10.244.2.15
redis-cluster-2.redis-cluster.default.svc.cluster.local -> 10.244.3.20

这种稳定的 DNS 记录使得有状态应用可以通过固定的主机名互相发现和通信,而不需要依赖可能变化的 Pod IP。

DNS 命名规则

完整的 DNS 名称格式为:<pod-name>.<headless-service-name>.<namespace>.svc.cluster.local

在同一个命名空间内,可以简写为 <pod-name>.<headless-service-name>


二、实战 1:Redis Cluster 部署

2.1 Redis Cluster 架构设计

Redis Cluster 是 Redis 官方提供的分布式解决方案,采用去中心化的 Hash Slot 架构。一个 Redis Cluster 由多个主节点(Master)组成,每个主节点负责一部分 Hash Slot(共 16384 个),主节点可以挂载从节点(Slave)用于故障转移。

2.2 完整部署 YAML

下面是一套可直接运行的 Redis Cluster 部署配置。我们将部署 6 个节点(3 主 3 从),使用 StatefulSet 保证有序部署和稳定标识。

API 版本说明:StatefulSet 使用 apps/v1,Headless Service 使用 v1,均为 Kubernetes v1.34 稳定版本。详见官方文档:StatefulSetService

文件路径:redis-cluster/redis-cluster.yaml

---
# Headless Service:为 StatefulSet 提供稳定的网络标识
apiVersion: v1
kind: Service
metadata:
name: redis-cluster
labels:
app: redis-cluster
spec:
clusterIP: None
selector:
app: redis-cluster
ports:
- name: redis
port: 6379
targetPort: 6379
- name: cluster-bus
port: 16379
targetPort: 16379

---
# StatefulSet:管理 6 个 Redis 节点
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: redis-cluster
labels:
app: redis-cluster
spec:
serviceName: redis-cluster # 必须关联 Headless Service
replicas: 6
selector:
matchLabels:
app: redis-cluster
template:
metadata:
labels:
app: redis-cluster
spec:
terminationGracePeriodSeconds: 30
containers:
- name: redis
image: redis:7.2.4
ports:
- containerPort: 6379
name: redis
- containerPort: 16379
name: cluster-bus
command:
- /bin/sh
- -c
- |
redis-server \
--cluster-enabled yes \
--cluster-config-file /data/nodes.conf \
--cluster-node-timeout 5000 \
--appendonly yes \
--bind 0.0.0.0 \
--port 6379 \
--cluster-announce-ip $(hostname).redis-cluster.default.svc.cluster.local \
--cluster-announce-port 6379 \
--cluster-announce-bus-port 16379
env:
- name: POD_NAME
valueFrom:
fieldRef:
fieldPath: metadata.name
volumeMounts:
- name: redis-data
mountPath: /data
livenessProbe:
exec:
command:
- redis-cli
- ping
initialDelaySeconds: 15
periodSeconds: 5
readinessProbe:
exec:
command:
- redis-cli
- ping
initialDelaySeconds: 5
periodSeconds: 3
resources:
requests:
cpu: 100m
memory: 256Mi
limits:
cpu: 500m
memory: 512Mi
# 每个 Pod 独立的 PVC
volumeClaimTemplates:
- metadata:
name: redis-data
spec:
accessModes:
- ReadWriteOnce
storageClassName: standard # 根据集群实际情况修改
resources:
requests:
storage: 1Gi
生产环境注意事项
  1. cluster-announce-ip 必须使用 Pod 的完整 DNS 名称,否则集群节点之间无法正确发现对方。
  2. cluster-announce-bus-port 必须显式声明,Redis Cluster 节点间通过 Bus 端口进行 gossip 通信。
  3. storageClassName 需要根据你的 K8s 集群实际可用的 StorageClass 进行修改。生产环境建议使用 SSD 存储类。
  4. 建议配置 PodDisruptionBudget 以防止维护操作时同时驱逐过多节点。

2.3 部署与验证

执行部署:

# 创建命名空间(可选)
kubectl create namespace redis

# 部署 Redis Cluster
kubectl apply -f redis-cluster/redis-cluster.yaml -n redis

# 观察 Pod 的有序创建过程
kubectl get pods -n redis -l app=redis-cluster -w

你应该能看到 Pod 按顺序逐个启动:

NAME READY STATUS RESTARTS AGE
redis-cluster-0 1/1 Running 0 30s
redis-cluster-1 1/1 Running 0 15s
redis-cluster-2 1/1 Running 0 5s
redis-cluster-3 0/1 Pending 0 0s
...

验证 DNS 解析:

# 进入任意 Pod 验证 DNS
kubectl exec -it redis-cluster-0 -n redis -- nslookup redis-cluster-1.redis-cluster.redis.svc.cluster.local

# 验证各节点可以互相通信
kubectl exec -it redis-cluster-0 -n redis -- redis-cli -h redis-cluster-1.redis-cluster.redis.svc.cluster.local ping

三、Redis Cluster 初始化与验证

说明:Redis Cluster 初始化使用 redis-cli --cluster create 命令,这是 Redis Cluster 的官方初始化方式。详见官方文档:Redis Cluster Tutorial

3.1 集群初始化脚本

StatefulSet 只负责创建 Pod,Redis Cluster 的初始化(节点握手、槽位分配)需要手动执行。我们编写一个初始化脚本来自动化这个过程。

文件路径:redis-cluster/init-cluster.sh

#!/bin/bash
set -euo pipefail

NAMESPACE="${1:-redis}"
SERVICE="redis-cluster"
REPLICAS=6

echo "=== 等待所有 Redis Pod 就绪 ==="
for i in $(seq 0 $((REPLICAS - 1))); do
echo -n "等待 redis-cluster-${i} ... "
kubectl wait --for=condition=ready pod/redis-cluster-${i} \
-n ${NAMESPACE} --timeout=120s
echo "就绪"
done

echo ""
echo "=== 收集节点地址 ==="
NODES=""
for i in $(seq 0 $((REPLICAS - 1))); do
POD_FQDN="redis-cluster-${i}.${SERVICE}.${NAMESPACE}.svc.cluster.local"
NODES="${NODES} ${POD_FQDN}:6379"
done

echo "节点列表:${NODES}"
echo ""

echo "=== 创建 Redis Cluster ==="
kubectl exec -it redis-cluster-0 -n ${NAMESPACE} -- \
redis-cli --cluster create ${NODES} --cluster-replicas 1

echo ""
echo "=== 验证集群状态 ==="
kubectl exec -it redis-cluster-0 -n ${NAMESPACE} -- \
redis-cli --cluster check redis-cluster-0.${SERVICE}.${NAMESPACE}.svc.cluster.local:6379

执行初始化:

chmod +x redis-cluster/init-cluster.sh
./redis-cluster/init-cluster.sh redis

3.2 槽位分配与节点握手

初始化成功后,你应该能看到类似以下输出:

>>> Performing hash slots allocation on 6 nodes...
Master[0] -> Slots 0 - 5460
Master[1] -> Slots 5461 - 10922
Master[2] -> Slots 10923 - 16383
M: xxx... redis-cluster-0.redis-cluster.redis.svc.cluster.local:6379
S: yyy... redis-cluster-3.redis-cluster.redis.svc.cluster.local:6379
M: zzz... redis-cluster-1.redis-cluster.redis.svc.cluster.local:6379
S: aaa... redis-cluster-4.redis-cluster.redis.svc.cluster.local:6379
M: bbb... redis-cluster-2.redis-cluster.redis.svc.cluster.local:6379
S: ccc... redis-cluster-5.redis-cluster.redis.svc.cluster.local:6379
Can I set the above configuration? (type 'yes' to accept): yes
>>> Nodes configuration updated
>>> Cluster state ok

3.3 数据读写验证

# 写入数据
kubectl exec -it redis-cluster-0 -n redis -- \
redis-cli SET greeting "Hello from Redis Cluster"

# 读取数据(从任意节点)
kubectl exec -it redis-cluster-1 -n redis -- \
redis-cli GET greeting

# 查看集群信息
kubectl exec -it redis-cluster-0 -n redis -- \
redis-cli CLUSTER INFO

# 查看节点列表
kubectl exec -it redis-cluster-0 -n redis -- \
redis-cli CLUSTER NODES

3.4 故障转移测试

# 查看当前主从关系
kubectl exec -it redis-cluster-0 -n redis -- redis-cli CLUSTER NODES

# 模拟主节点故障(删除 redis-cluster-0)
kubectl delete pod redis-cluster-0 -n redis

# 等待 StatefulSet 重建 Pod
kubectl get pods -n redis -l app=redis-cluster -w

# 重建完成后,检查集群状态
# 对应的从节点应该已经提升为主节点
kubectl exec -it redis-cluster-1 -n redis -- redis-cli CLUSTER NODES

# 验证数据仍然可用
kubectl exec -it redis-cluster-1 -n redis -- redis-cli GET greeting
故障转移注意事项
  1. Redis Cluster 的故障转移由集群内部自动完成,不需要外部干预。
  2. 当原主节点恢复后,它会自动变为新主节点的从节点,不会抢回主节点角色。
  3. 在生产环境中,建议设置 cluster-node-timeout 为合理值(通常 5-15 秒),以平衡故障检测速度和误判率。

四、Operator 模式深度解析

4.1 Operator 的设计哲学

通过前面的实战,你可能已经发现一个问题:Redis Cluster 的初始化是手动执行的。StatefulSet 只负责创建 Pod,但集群的初始化、扩缩容、故障恢复等操作仍然需要人工介入。

这正是 Operator 模式要解决的核心问题。

Operator 的本质是将人类运维特定应用的知识编码为自动化软件。

CoreOS(现 Red Hat)在 2016 年提出了 Operator 模式的概念。其核心思想是:

  1. 人类运维人员知道如何部署、扩缩容、备份、恢复一个特定应用
  2. 将这些领域知识编码为 Kubernetes 控制器
  3. 控制器通过 CRD(Custom Resource Definition) 扩展 K8s API,引入应用特定的资源类型
  4. 通过 Reconciliation Loop 持续调谐实际状态与期望状态

简单来说,Operator = CRD + Controller + 特定领域知识

4.2 Operator 核心组件

一个完整的 Operator 由以下三个核心组件构成:

组件职责类比
CRD(Custom Resource Definition)定义自定义资源的 Schema(结构)数据库的表结构
ControllerWatch 资源变化,执行调谐逻辑数据库的触发器
Reconciliation Loop持续将实际状态趋近期望状态自动驾驶的反馈控制

4.3 开发框架对比

目前主流的 Operator 开发框架有三个:

特性controller-runtimeKubebuilderOperator SDK
定位底层库脚手架工具全功能 SDK
语言GoGoGo / Ansible / Helm
复杂度中等
灵活性
项目脚手架无(需手动搭建)内置 kubebuilder init内置 operator-sdk init
Webhook 支持手动注册自动生成脚手架自动生成脚手架
关系Kubebuilder 和 Operator SDK 的底层依赖基于 controller-runtime基于 controller-runtime
框架选择建议
  • 学习 Operator 原理:使用 controller-runtime,理解底层机制
  • 快速开发生产级 Operator:使用 Kubebuilder,脚手架完善、社区活跃
  • 非 Go 技术栈:使用 Operator SDK 的 Ansible/Helm 模式

本文的实战部分将使用 controller-runtime,帮助你理解 Operator 的核心原理。


五、实战 2:自定义 Redis Operator

5.1 项目初始化

# 创建项目目录
mkdir redis-operator && cd redis-operator

# 初始化 Go Module
go mod init github.com/rainlib/redis-operator

# 安装依赖
# controller-runtime v0.18.0 与 Kubernetes v1.34 API 兼容
go get sigs.k8s.io/controller-runtime@v0.18.0
go get k8s.io/api@v0.30.0
go get k8s.io/apimachinery@v0.30.0

依赖版本说明:controller-runtime v0.18.0 与 Kubernetes v1.34 API 完全兼容。详见官方文档:controller-runtime

5.2 CRD 定义

文件路径:api/v1alpha1/rediscluster_types.go

package v1alpha1

import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

// RedisClusterSpec 定义用户期望的集群状态
type RedisClusterSpec struct {
// 主节点数量(必须为奇数)
MasterCount int32 `json:"masterCount"`

// 每个主节点的从节点数量
ReplicasPerMaster int32 `json:"replicasPerMaster"`

// Redis 镜像
Image string `json:"image"`

// 资源请求与限制
Resources ResourceRequirements `json:"resources,omitempty"`
}

type ResourceRequirements struct {
CPURequest string `json:"cpuRequest,omitempty"`
MemoryRequest string `json:"memoryRequest,omitempty"`
CPULimit string `json:"cpuLimit,omitempty"`
MemoryLimit string `json:"memoryLimit,omitempty"`
}

// ClusterPhase 表示集群当前阶段
type ClusterPhase string

const (
ClusterPhaseCreating ClusterPhase = "Creating"
ClusterPhaseInitializing ClusterPhase = "Initializing"
ClusterPhaseRunning ClusterPhase = "Running"
ClusterPhaseScaling ClusterPhase = "Scaling"
ClusterPhaseFailed ClusterPhase = "Failed"
)

// RedisClusterStatus 反映集群的实际状态
type RedisClusterStatus struct {
// 集群当前阶段
Phase ClusterPhase `json:"phase,omitempty"`

// 各节点的就绪状态
ReadyNodes int32 `json:"readyNodes,omitempty"`

// 集群是否已初始化
ClusterInitialized bool `json:"clusterInitialized,omitempty"`

// 最近一次操作的消息
Message string `json:"message,omitempty"`
}

// +kubebuilder:object:root=true
// +kubebuilder:subresource:status
// +kubebuilder:resource:shortName=rc

// RedisCluster 是自定义资源的顶层类型
type RedisCluster struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`

Spec RedisClusterSpec `json:"spec,omitempty"`
Status RedisClusterStatus `json:"status,omitempty"`
}

// +kubebuilder:object:root=true

// RedisClusterList 用于列表操作
type RedisClusterList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty"`

Items []RedisCluster `json:"items"`
}

func init() {
SchemeBuilder.Register(&RedisCluster{}, &RedisClusterList{})
}

5.3 CRD YAML 生成

API 版本说明:CRD 使用 apiextensions.k8s.io/v1,这是 Kubernetes v1.34 中的稳定版本。apiextensions.k8s.io/v1beta1 已在 v1.22 中移除,请确保使用 v1。详见官方文档:CustomResourceDefinition v1

文件路径:config/crd/redisclusters.rainlib.io.yaml

apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
name: redisclusters.rainlib.io
spec:
group: rainlib.io
names:
kind: RedisCluster
listKind: RedisClusterList
plural: redisclusters
shortNames:
- rc
singular: rediscluster
scope: Namespaced
versions:
- name: v1alpha1
served: true
storage: true
schema:
openAPIV3Schema:
type: object
properties:
spec:
type: object
required:
- masterCount
- replicasPerMaster
- image
properties:
masterCount:
type: integer
minimum: 1
description: 主节点数量
replicasPerMaster:
type: integer
minimum: 0
description: 每个主节点的从节点数量
image:
type: string
description: Redis 镜像地址
resources:
type: object
properties:
cpuRequest:
type: string
memoryRequest:
type: string
cpuLimit:
type: string
memoryLimit:
type: string
status:
type: object
properties:
phase:
type: string
enum:
- Creating
- Initializing
- Running
- Scaling
- Failed
readyNodes:
type: integer
clusterInitialized:
type: boolean
message:
type: string
subresources:
status: {}

5.4 Controller 实现

文件路径:controllers/rediscluster_controller.go

package controllers

import (
"context"
"fmt"
"time"

redisv1alpha1 "github.com/rainlib/redis-operator/api/v1alpha1"
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/api/resource"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
"sigs.k8s.io/controller-runtime/pkg/log"
)

const (
finalizerName = "rainlib.io/rediscluster-finalizer"
)

// RedisClusterReconciler 调谐 RedisCluster 资源
type RedisClusterReconciler struct {
client.Client
Scheme *runtime.Scheme
}

// +kubebuilder:rbac:groups=rainlib.io,resources=redisclusters,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=rainlib.io,resources=redisclusters/status,verbs=get;update;patch
// +kubebuilder:rbac:groups=rainlib.io,resources=redisclusters/finalizers,verbs=update
// +kubebuilder:rbac:groups=apps,resources=statefulsets,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=core,resources=services,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=core,resources=pods,verbs=get;list;watch
// +kubebuilder:rbac:groups=core,resources=events,verbs=create;patch

func (r *RedisClusterReconciler) Reconcile(
ctx context.Context,
req ctrl.Request,
) (ctrl.Result, error) {
logger := log.FromContext(ctx)

// 1. 获取 RedisCluster 实例
var rc redisv1alpha1.RedisCluster
if err := r.Get(ctx, req.NamespacedName, &rc); err != nil {
if errors.IsNotFound(err) {
return ctrl.Result{}, nil
}
return ctrl.Result{}, err
}

// 2. 处理 Finalizer(删除逻辑)
if !rc.DeletionTimestamp.IsZero() {
if controllerutil.ContainsFinalizer(&rc, finalizerName) {
logger.Info("执行清理逻辑")
// 这里可以添加资源清理逻辑(如外部存储清理)
controllerutil.RemoveFinalizer(&rc, finalizerName)
if err := r.Update(ctx, &rc); err != nil {
return ctrl.Result{}, err
}
}
return ctrl.Result{}, nil
}

// 3. 添加 Finalizer
if !controllerutil.ContainsFinalizer(&rc, finalizerName) {
controllerutil.AddFinalizer(&rc, finalizerName)
if err := r.Update(ctx, &rc); err != nil {
return ctrl.Result{}, err
}
}

// 4. 创建/更新 Headless Service
if err := r.reconcileService(ctx, &rc); err != nil {
logger.Error(err, "调和 Service 失败")
r.updateStatus(ctx, &rc, redisv1alpha1.ClusterPhaseFailed, 0, false, err.Error())
return ctrl.Result{}, err
}

// 5. 创建/更新 StatefulSet
if err := r.reconcileStatefulSet(ctx, &rc); err != nil {
logger.Error(err, "调和 StatefulSet 失败")
r.updateStatus(ctx, &rc, redisv1alpha1.ClusterPhaseFailed, 0, false, err.Error())
return ctrl.Result{}, err
}

// 6. 检查 Pod 就绪状态
readyNodes, err := r.getReadyNodeCount(ctx, &rc)
if err != nil {
return ctrl.Result{}, err
}

totalNodes := rc.Spec.MasterCount * (1 + rc.Spec.ReplicasPerMaster)

if readyNodes < totalNodes {
logger.Info("等待所有节点就绪",
"ready", readyNodes, "total", totalNodes)
r.updateStatus(ctx, &rc, redisv1alpha1.ClusterPhaseCreating, readyNodes, false,
fmt.Sprintf("等待节点就绪 %d/%d", readyNodes, totalNodes))
return ctrl.Result{RequeueAfter: 5 * time.Second}, nil
}

// 7. 初始化集群(如果尚未初始化)
if !rc.Status.ClusterInitialized {
logger.Info("开始初始化 Redis Cluster")
r.updateStatus(ctx, &rc, redisv1alpha1.ClusterPhaseInitializing, readyNodes, false,
"正在初始化集群")

if err := r.initializeCluster(ctx, &rc); err != nil {
logger.Error(err, "初始化集群失败")
r.updateStatus(ctx, &rc, redisv1alpha1.ClusterPhaseFailed, readyNodes, false, err.Error())
return ctrl.Result{RequeueAfter: 10 * time.Second}, nil
}

r.updateStatus(ctx, &rc, redisv1alpha1.ClusterPhaseRunning, readyNodes, true,
"集群已就绪")
}

return ctrl.Result{RequeueAfter: 30 * time.Second}, nil
}

// reconcileService 创建或更新 Headless Service
func (r *RedisClusterReconciler) reconcileService(
ctx context.Context,
rc *redisv1alpha1.RedisCluster,
) error {
logger := log.FromContext(ctx)

desired := &corev1.Service{
ObjectMeta: metav1.ObjectMeta{
Name: rc.Name,
Namespace: rc.Namespace,
},
Spec: corev1.ServiceSpec{
ClusterIP: "None",
Selector: map[string]string{
"app": rc.Name,
"managed-by": "redis-operator",
"redis-cluster": rc.Name,
},
Ports: []corev1.ServicePort{
{Name: "redis", Port: 6379, TargetPort: intstr.FromInt(6379)},
{Name: "bus", Port: 16379, TargetPort: intstr.FromInt(16379)},
},
},
}

// 设置 OwnerReference
if err := controllerutil.SetControllerReference(rc, desired, r.Scheme); err != nil {
return err
}

var existing corev1.Service
err := r.Get(ctx, types.NamespacedName{Name: rc.Name, Namespace: rc.Namespace}, &existing)

if errors.IsNotFound(err) {
logger.Info("创建 Headless Service")
return r.Create(ctx, desired)
} else if err != nil {
return err
}

// Service 已存在,更新
existing.Spec.Selector = desired.Spec.Selector
existing.Spec.Ports = desired.Spec.Ports
return r.Update(ctx, &existing)
}

// reconcileStatefulSet 创建或更新 StatefulSet
func (r *RedisClusterReconciler) reconcileStatefulSet(
ctx context.Context,
rc *redisv1alpha1.RedisCluster,
) error {
logger := log.FromContext(ctx)

totalReplicas := rc.Spec.MasterCount * (1 + rc.Spec.ReplicasPerMaster)

cpuReq := rc.Spec.Resources.CPURequest
memReq := rc.Spec.Resources.MemoryRequest
cpuLim := rc.Spec.Resources.CPULimit
memLim := rc.Spec.Resources.MemoryLimit

if cpuReq == "" {
cpuReq = "100m"
}
if memReq == "" {
memReq = "256Mi"
}
if cpuLim == "" {
cpuLim = "500m"
}
if memLim == "" {
memLim = "512Mi"
}

desired := &appsv1.StatefulSet{
ObjectMeta: metav1.ObjectMeta{
Name: rc.Name,
Namespace: rc.Namespace,
},
Spec: appsv1.StatefulSetSpec{
ServiceName: rc.Name,
Replicas: &totalReplicas,
Selector: &metav1.LabelSelector{
MatchLabels: map[string]string{
"app": rc.Name,
"managed-by": "redis-operator",
"redis-cluster": rc.Name,
},
},
Template: corev1.PodTemplateSpec{
ObjectMeta: metav1.ObjectMeta{
Labels: map[string]string{
"app": rc.Name,
"managed-by": "redis-operator",
"redis-cluster": rc.Name,
},
},
Spec: corev1.PodSpec{
Containers: []corev1.Container{
{
Name: "redis",
Image: rc.Spec.Image,
Command: []string{
"/bin/sh", "-c",
fmt.Sprintf(`
redis-server \
--cluster-enabled yes \
--cluster-config-file /data/nodes.conf \
--cluster-node-timeout 5000 \
--appendonly yes \
--bind 0.0.0.0 \
--port 6379 \
--cluster-announce-ip $(hostname).%s.%s.svc.cluster.local \
--cluster-announce-port 6379 \
--cluster-announce-bus-port 16379
`, rc.Name, rc.Namespace),
},
Ports: []corev1.ContainerPort{
{ContainerPort: 6379, Name: "redis"},
{ContainerPort: 16379, Name: "bus"},
},
VolumeMounts: []corev1.VolumeMount{
{Name: "data", MountPath: "/data"},
},
LivenessProbe: &corev1.Probe{
Exec: &corev1.ExecAction{
Command: []string{"redis-cli", "ping"},
},
InitialDelaySeconds: 15,
PeriodSeconds: 5,
},
ReadinessProbe: &corev1.Probe{
Exec: &corev1.ExecAction{
Command: []string{"redis-cli", "ping"},
},
InitialDelaySeconds: 5,
PeriodSeconds: 3,
},
Resources: corev1.ResourceRequirements{
Requests: corev1.ResourceList{
corev1.ResourceCPU: resource.MustParse(cpuReq),
corev1.ResourceMemory: resource.MustParse(memReq),
},
Limits: corev1.ResourceList{
corev1.ResourceCPU: resource.MustParse(cpuLim),
corev1.ResourceMemory: resource.MustParse(memLim),
},
},
},
},
},
},
VolumeClaimTemplates: []corev1.PersistentVolumeClaim{
{
ObjectMeta: metav1.ObjectMeta{
Name: "data",
},
Spec: corev1.PersistentVolumeClaimSpec{
AccessModes: []corev1.PersistentVolumeAccessMode{
corev1.ReadWriteOnce,
},
Resources: corev1.VolumeResourceRequirements{
Requests: corev1.ResourceList{
corev1.ResourceStorage: resource.MustParse("1Gi"),
},
},
},
},
},
},
}

if err := controllerutil.SetControllerReference(rc, desired, r.Scheme); err != nil {
return err
}

var existing appsv1.StatefulSet
err := r.Get(ctx, types.NamespacedName{Name: rc.Name, Namespace: rc.Namespace}, &existing)

if errors.IsNotFound(err) {
logger.Info("创建 StatefulSet", "replicas", totalReplicas)
return r.Create(ctx, desired)
} else if err != nil {
return err
}

// StatefulSet 已存在,检查是否需要更新
if *existing.Spec.Replicas != totalReplicas || existing.Spec.Template.Spec.Containers[0].Image != rc.Spec.Image {
logger.Info("更新 StatefulSet")
existing.Spec.Replicas = &totalReplicas
existing.Spec.Template.Spec.Containers[0].Image = rc.Spec.Image
return r.Update(ctx, &existing)
}

return nil
}

// getReadyNodeCount 统计就绪的 Pod 数量
func (r *RedisClusterReconciler) getReadyNodeCount(
ctx context.Context,
rc *redisv1alpha1.RedisCluster,
) (int32, error) {
var podList corev1.PodList
if err := r.List(ctx, &podList,
client.InNamespace(rc.Namespace),
client.MatchingLabels{
"app": rc.Name,
"redis-cluster": rc.Name,
},
); err != nil {
return 0, err
}

var ready int32
for _, pod := range podList.Items {
for _, cond := range pod.Status.Conditions {
if cond.Type == corev1.PodReady && cond.Status == corev1.ConditionTrue {
ready++
break
}
}
}
return ready, nil
}

// initializeCluster 执行 Redis Cluster 初始化
func (r *RedisClusterReconciler) initializeCluster(
ctx context.Context,
rc *redisv1alpha1.RedisCluster,
) error {
logger := log.FromContext(ctx)

// 在实际生产中,这里会通过 exec 进入 Pod 执行 redis-cli --cluster create
// 为简化示例,这里记录初始化事件
logger.Info("Redis Cluster 初始化完成",
"masters", rc.Spec.MasterCount,
"replicas", rc.Spec.ReplicasPerMaster)

// 生产环境实现示例(伪代码):
// 1. 收集所有 Pod 的 FQDN
// 2. exec 进入第一个 Pod 执行 redis-cli --cluster create
// 3. 验证集群状态

return nil
}

// updateStatus 更新 RedisCluster 的状态子资源
func (r *RedisClusterReconciler) updateStatus(
ctx context.Context,
rc *redisv1alpha1.RedisCluster,
phase redisv1alpha1.ClusterPhase,
readyNodes int32,
initialized bool,
message string,
) {
rc.Status.Phase = phase
rc.Status.ReadyNodes = readyNodes
rc.Status.ClusterInitialized = initialized
rc.Status.Message = message

if err := r.Status().Update(ctx, rc); err != nil {
log.FromContext(ctx).Error(err, "更新状态失败")
}
}

// SetupWithManager 注册 Controller
func (r *RedisClusterReconciler) SetupWithManager(
mgr ctrl.Manager,
) error {
return ctrl.NewControllerManagedBy(mgr).
For(&redisv1alpha1.RedisCluster{}).
Owns(&appsv1.StatefulSet{}).
Owns(&corev1.Service{}).
Complete(r)
}
注意

上面的 Controller 代码中使用了 intstr.FromInt,需要在 import 中添加 "k8s.io/apimachinery/pkg/util/intstr"

5.5 Reconciliation Loop 工作流程

下图展示了 Operator 的 Reconciliation Loop 在不同场景下的工作流程:

5.6 主入口

文件路径:main.go

package main

import (
"os"

redisv1alpha1 "github.com/rainlib/redis-operator/api/v1alpha1"
"github.com/rainlib/redis-operator/controllers"
"k8s.io/apimachinery/pkg/runtime"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
_ "k8s.io/client-go/plugin/pkg/client/auth/gcp"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/healthz"
"sigs.k8s.io/controller-runtime/pkg/log/zap"
metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server"
)

var (
scheme = runtime.NewScheme()
)

func init() {
utilruntime.Must(clientgoscheme.AddToScheme(scheme))
utilruntime.Must(redisv1alpha1.AddToScheme(scheme))
}

func main() {
ctrl.SetLogger(zap.New(zap.UseDevMode(true)))

mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{
Scheme: scheme,
Metrics: metricsserver.Options{
BindAddress: ":8080",
},
HealthProbeBindAddress: ":8081",
LeaderElection: true,
LeaderElectionID: "redis-operator.rainlib.io",
})
if err != nil {
setupLog.Error(err, "无法创建 Manager")
os.Exit(1)
}

if err := (&controllers.RedisClusterReconciler{
Client: mgr.GetClient(),
Scheme: mgr.GetScheme(),
}).SetupWithManager(mgr); err != nil {
setupLog.Error(err, "无法创建 Controller")
os.Exit(1)
}

if err := mgr.AddHealthzCheck("healthz", healthz.PingHealthz); err != nil {
setupLog.Error(err, "无法设置健康检查")
os.Exit(1)
}

setupLog.Info("启动 Manager")
if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil {
setupLog.Error(err, "Manager 运行错误")
os.Exit(1)
}
}

六、Operator 部署与测试

6.1 RBAC 配置

API 版本说明:RBAC 资源使用 rbac.authorization.k8s.io/v1,这是 Kubernetes v1.34 中的稳定版本。详见官方文档:RBAC v1

文件路径:config/rbac/rbac.yaml

---
apiVersion: v1
kind: ServiceAccount
metadata:
name: redis-operator
namespace: redis-operator

---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: redis-operator-role
rules:
# 管理 RedisCluster CRD
- apiGroups: ["rainlib.io"]
resources: ["redisclusters"]
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
- apiGroups: ["rainlib.io"]
resources: ["redisclusters/status"]
verbs: ["get", "update", "patch"]
- apiGroups: ["rainlib.io"]
resources: ["redisclusters/finalizers"]
verbs: ["update"]
# 管理 StatefulSet
- apiGroups: ["apps"]
resources: ["statefulsets"]
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
# 管理 Service
- apiGroups: [""]
resources: ["services"]
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
# 读取 Pod 状态
- apiGroups: [""]
resources: ["pods"]
verbs: ["get", "list", "watch"]
# 创建 Event
- apiGroups: [""]
resources: ["events"]
verbs: ["create", "patch"]

---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: redis-operator-rolebinding
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: redis-operator-role
subjects:
- kind: ServiceAccount
name: redis-operator
namespace: redis-operator

6.2 部署 Operator

API 版本说明:Deployment 使用 apps/v1,ServiceAccount 使用 v1,均为 Kubernetes v1.34 稳定版本。详见官方文档:Deployment v1ServiceAccount v1

文件路径:config/deployment/deployment.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
name: redis-operator
namespace: redis-operator
labels:
app: redis-operator
spec:
replicas: 1
selector:
matchLabels:
app: redis-operator
template:
metadata:
labels:
app: redis-operator
spec:
serviceAccountName: redis-operator
containers:
- name: redis-operator
image: rainlib/redis-operator:latest
args:
- --leader-elect
ports:
- containerPort: 8080
name: metrics
- containerPort: 9443
name: webhook
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 500m
memory: 256Mi
livenessProbe:
httpGet:
path: /healthz
port: 8081
initialDelaySeconds: 15
periodSeconds: 20
readinessProbe:
httpGet:
path: /readyz
port: 8081
initialDelaySeconds: 5
periodSeconds: 10
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
runAsNonRoot: true

部署步骤:

# 1. 创建命名空间
kubectl create namespace redis-operator

# 2. 安装 CRD
kubectl apply -f config/crd/redisclusters.rainlib.io.yaml

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

# 4. 部署 Operator
kubectl apply -f config/deployment/deployment.yaml

# 5. 验证 Operator 运行状态
kubectl get pods -n redis-operator
kubectl logs -n redis-operator -l app=redis-operator

6.3 创建 RedisCluster 实例

文件路径:config/samples/rediscluster-sample.yaml

apiVersion: rainlib.io/v1alpha1
kind: RedisCluster
metadata:
name: my-redis
namespace: redis
spec:
masterCount: 3
replicasPerMaster: 1
image: redis:7.2.4
resources:
cpuRequest: "200m"
memoryRequest: "512Mi"
cpuLimit: "1000m"
memoryLimit: "1Gi"
# 创建命名空间
kubectl create namespace redis

# 创建 RedisCluster 实例
kubectl apply -f config/samples/rediscluster-sample.yaml

# 观察 Operator 行为
kubectl get rediscluster -n redis -w

# 查看 Operator 日志
kubectl logs -n redis-operator -l app=redis-operator -f

# 查看创建的资源
kubectl get statefulset,service,pod -n redis

6.4 验证 Operator 行为

自动扩缩容测试:

# 修改 replicasPerMaster 从 1 到 2
kubectl patch rediscluster my-redis -n redis --type=merge \
-p '{"spec":{"replicasPerMaster":2}}'

# 观察 Operator 自动更新 StatefulSet
kubectl get pods -n redis -w
kubectl logs -n redis-operator -l app=redis-operator -f

故障恢复测试:

# 模拟 Pod 故障
kubectl delete pod my-redis-0 -n redis

# 观察 StatefulSet 自动重建 Pod
kubectl get pods -n redis -w

# 验证 RedisCluster 状态
kubectl get rediscluster my-redis -n redis -o yaml

七、高级 Operator 模式

7.1 Finalizer 机制

Finalizer 是 Kubernetes 中用于防止资源被意外删除的机制。当资源带有 Finalizer 时,K8s 不会立即删除该资源,而是先设置 deletionTimestamp,等待 Controller 执行清理逻辑后再移除 Finalizer,最终完成删除。

// 处理删除逻辑
if !rc.DeletionTimestamp.IsZero() {
if controllerutil.ContainsFinalizer(&rc, finalizerName) {
// 1. 执行自定义清理逻辑
if err := r.cleanupExternalResources(ctx, &rc); err != nil {
return ctrl.Result{}, err
}
// 2. 移除 Finalizer,允许 K8s 完成删除
controllerutil.RemoveFinalizer(&rc, finalizerName)
if err := r.Update(ctx, &rc); err != nil {
return ctrl.Result{}, err
}
}
return ctrl.Result{}, nil
}
生产环境注意事项

Finalizer 中的清理逻辑必须保证幂等性(Idempotent),因为 Reconcile 可能会被多次调用。如果清理逻辑失败且不移除 Finalizer,资源将一直处于"正在删除"状态,成为僵尸资源。

7.2 OwnerReference 与级联删除

通过 controllerutil.SetControllerReference 设置的 OwnerReference 实现了级联删除:当 RedisCluster 被删除时,其创建的所有 StatefulSet、Service 等子资源会自动被 K8s 回收。

// 设置 OwnerReference
if err := controllerutil.SetControllerReference(rc, desired, r.Scheme); err != nil {
return err
}

OwnerReference 的传播策略有两种:

策略行为适用场景
Background(默认)立即删除父资源,后台异步删除子资源大多数场景
Foreground先删除所有子资源,再删除父资源需要确保子资源被正确清理的场景

7.3 Webhook(Admission / Mutating)

Webhook 允许 Operator 在资源创建或更新时进行拦截和修改,实现更精细的控制。

API 版本说明:ValidatingWebhookConfiguration 和 MutatingWebhookConfiguration 使用 admissionregistration.k8s.io/v1,这是 Kubernetes v1.34 中的稳定版本。详见官方文档:ValidatingWebhookConfigurationMutatingWebhookConfiguration

Validating Webhook(验证 webhook):拒绝不合法的请求。

// 验证 masterCount 必须为奇数
func (r *RedisCluster) ValidateCreate() (admission.Warnings, error) {
if r.Spec.MasterCount%2 == 0 {
return nil, fmt.Errorf("masterCount must be an odd number, got %d", r.Spec.MasterCount)
}
return nil, nil
}

func (r *RedisCluster) ValidateUpdate(old runtime.Object) (admission.Warnings, error) {
oldRC := old.(*RedisCluster)
if oldRC.Spec.MasterCount != r.Spec.MasterCount {
return nil, fmt.Errorf("masterCount is immutable after creation")
}
return nil, nil
}

Mutating Webhook(变更 webhook):自动修改请求内容。

// 自动设置默认值
func (r *RedisCluster) Default() {
if r.Spec.Image == "" {
r.Spec.Image = "redis:7.2.4"
}
if r.Spec.ReplicasPerMaster == 0 {
r.Spec.ReplicasPerMaster = 1
}
}

7.4 Leader Election(高可用)

当 Operator 部署多个副本时,Leader Election 确保只有一个实例在执行 Reconcile,其余实例处于 Standby 状态。

mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{
Scheme: scheme,
LeaderElection: true, // 启用 Leader Election
LeaderElectionID: "redis-operator.rainlib.io", // Leader Election 的标识
LeaderElectionNamespace: "redis-operator", // 选举的命名空间
LeaseDuration: &metav1.Duration{Seconds: 15}, // 租约时长
RenewDeadline: &metav1.Duration{Seconds: 10}, // 续约截止时间
RetryPeriod: &metav1.Duration{Seconds: 2}, // 重试间隔
})
Leader Election 原理

Leader Election 基于 K8s 的 Lease(租约)资源实现。Manager 启动时会尝试创建一个 Lease 对象,创建成功的实例成为 Leader 并定期续约。如果 Leader 宕机无法续约,其他 Standby 实例会竞争成为新 Leader。


八、生产环境最佳实践

8.1 Operator 开发规范

规范说明
最小权限原则RBAC 仅授予必要的权限,避免使用 cluster-admin
幂等 Reconcile所有调谐逻辑必须幂等,确保重复执行结果一致
状态子资源分离使用 Status 子资源记录实际状态,与 Spec 分离
事件记录关键操作必须通过 EventRecorder 记录事件,便于审计和排查
错误处理区分暂时性错误(Requeue)和永久性错误(不 Requeue)
日志规范使用结构化日志,包含关键上下文(资源名、命名空间等)

8.2 监控与日志

// 使用 Prometheus 指标暴露 Operator 运行状态
import (
"sigs.k8s.io/controller-runtime/pkg/metrics"
"github.com/prometheus/client_golang/prometheus"
)

var (
reconcileTotal = prometheus.NewCounterVec(
prometheus.CounterOpts{
Name: "redis_operator_reconcile_total",
Help: "Total number of reconciliation operations",
},
[]string{"namespace", "name"},
)
reconcileErrors = prometheus.NewCounterVec(
prometheus.CounterOpts{
Name: "redis_operator_reconcile_errors_total",
Help: "Total number of reconciliation errors",
},
[]string{"namespace", "name"},
)
)

func init() {
metrics.Registry.MustRegister(reconcileTotal, reconcileErrors)
}

推荐的监控大盘指标:

  • Reconcile 成功/失败次数
  • Reconcile 耗时分布
  • 每个 RedisCluster 的 Phase 分布
  • Redis 节点的内存、连接数、命令处理量
  • PVC 使用率

8.3 版本升级策略

API 版本说明:CRD 版本管理使用 apiextensions.k8s.io/v1,支持多版本共存、版本转换和弃用标记。详见官方文档:CRD Versioning

# 使用 CRD 版本管理 API 演进
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
name: redisclusters.rainlib.io
spec:
versions:
- name: v1alpha1
served: true # 仍然提供服务
storage: false # 不再作为存储版本
deprecated: true # 标记为已弃用
deprecationWarning: "rainlib.io/v1alpha1 RedisCluster is deprecated; use rainlib.io/v1"
- name: v1
served: true
storage: true # 作为存储版本
schema:
openAPIV3Schema:
type: object
properties:
spec:
type: object
properties:
masterCount:
type: integer
minimum: 1
# v1 新增字段
persistence:
type: object
properties:
enabled:
type: boolean
storageClass:
type: string
size:
type: string
CRD 版本升级注意事项
  1. 不可删除存储版本:一旦某个版本被标记为 storage: true,就不能直接删除该版本,必须先创建新版本并迁移数据。
  2. Webhook 版本转换:如果需要在不同 CRD 版本间自动转换,需要实现 Conversion Webhook。
  3. 向后兼容:新增字段应该有默认值,确保旧版本 CR 在新版本 Controller 下仍能正常工作。

九、本章小结与系列总结

核心知识回顾

本文从有状态应用的核心挑战出发,系统性地讲解了 StatefulSet 和 Operator 两个关键概念:

StatefulSet 解决的问题:

  • 稳定的网络标识(Headless Service + 固定 DNS)
  • 持久的存储(VolumeClaimTemplate + PVC)
  • 有序的部署与扩缩容(顺序创建/逆序删除)

Operator 解决的问题:

  • 将运维知识编码为自动化控制器
  • 通过 CRD 扩展 K8s API,引入应用特定的资源类型
  • 通过 Reconciliation Loop 持续调谐实际状态与期望状态
  • 自动处理集群初始化、扩缩容、故障恢复等复杂操作

Operator 高级模式:

  • Finalizer:确保资源删除前的清理逻辑被执行
  • OwnerReference:实现级联删除,避免资源泄漏
  • Webhook:在 API 层面进行验证和变更
  • Leader Election:支持多副本高可用部署

Kubernetes 全景解析系列总结

至此,Kubernetes 全景解析系列的核心内容已经覆盖完毕。让我们回顾整个系列的知识体系:

章节主题核心收获
(0)架构设计与核心概念理解 K8s 的设计哲学与控制面/数据面架构
(1)工作负载管理掌握 Pod/Deployment/StatefulSet/DaemonSet 的使用场景
(2)网络模型理解 CNI、Service、Ingress 的工作原理
(3)存储体系与配置管理掌握 PV/PVC/StorageClass/ConfigMap/Secret
(4)调度策略理解节点选择、亲和性、污点与容忍
(5)安全机制掌握 RBAC、NetworkPolicy、PodSecurity
(6)Helm 包管理学会使用 Helm 打包和分发应用
(7)有状态应用与 Operator掌握 StatefulSet 和 Operator 开发

Kubernetes 的学习是一个持续深化的过程。掌握了本系列的内容,你已经具备了在生产和开发环境中使用 K8s 的核心能力。下一步建议:

  1. 深入实践:选择一个真实的有状态应用(如 PostgreSQL、Kafka),尝试为其编写 Operator
  2. 参与社区:关注 CNCF 生态,学习优秀的开源 Operator 实现(如 Prometheus Operator、Argo CD Operator)
  3. 探索高级主题:Service Mesh(Istio/Linkerd)、Serverless(Knative)、GitOps(Argo CD/Flux)等

"Kubernetes 不是终点,而是起点。理解了它的设计哲学,你就能更好地理解整个云原生生态。"


系列导航:

Kubernetes 全景解析 (1):工作负载与 Pod 生命周期深度解析

· 阅读需 21 分钟
Rainy
雨落无声,代码成诗 —— 致力于技术与艺术的极致平衡

"Pods are the atomic unit of scheduling in Kubernetes — not containers."

在 Kubernetes 的世界里,工作负载 (Workload) 是你与应用交互的核心抽象。无论你是部署一个无状态的 Web 服务、一个有状态的数据库,还是在每个节点上运行监控 Agent,K8s 都提供了专门的工作负载资源来满足需求。而所有这些工作负载的基础,都建立在 Pod 之上。

本文将从 Pod 的本质出发,逐层深入解析 Kubernetes 中五大核心工作负载资源的设计理念、编排策略与最佳实践,帮助你在架构选型时做出正确的决策。


一、Pod:K8s 的原子调度单元

1.1 为什么 Pod 是最小部署单元而非容器

许多初学者会困惑:既然 Docker 已经有了容器概念,为什么 Kubernetes 还要引入 Pod?答案在于 "超亲密容器"(Hyper-privileged Containers) 的设计哲学。

在现实世界中,一个应用往往不是孤立运行的。例如:

  • 一个 Web 服务器需要配合一个日志采集 Sidecar
  • 一个主进程需要配合一个健康检查辅助进程
  • 一个数据管道需要同时运行 ingest 和 transform 两个紧密协作的进程

这些进程需要共享网络命名空间(可以通过 localhost 互相通信)、共享存储卷(可以读写同一份数据),并且需要作为一个原子单元被调度到同一个节点上。Pod 正是为了解决这一需求而诞生的。

核心原则

Pod 是 Kubernetes 中最小的可调度单元。一个 Pod 可以包含一个或多个容器,这些容器共享相同的网络和存储命名空间,始终被调度到同一个节点上,并作为一个整体进行生命周期管理。

1.2 Pod 的设计理念

Pod 的设计围绕三个核心能力展开:

能力说明典型场景
共享网络同一 Pod 内的所有容器共享同一个 IP 地址和端口空间,可以通过 localhost 互相访问主容器 + Sidecar 代理
共享存储Pod 可以声明多个 Volume,这些 Volume 可以被 Pod 内的任意容器挂载主容器写日志,Sidecar 读取并转发
原子调度Pod 内的所有容器作为一个整体被调度到同一个节点保证本地通信的低延迟

Sidecar 模式 是 Pod 多容器设计中最经典的模式。例如,Istio 服务网格通过在每个 Pod 中注入一个 Envoy Sidecar 代理来实现流量管理、安全通信和可观测性,而无需修改应用代码。

1.3 Pod 的 YAML 结构详解

下面是一个完整的 Pod YAML 示例,每个字段都附有详细注释:

apiVersion: v1 # API 版本,Pod 属于核心 v1 组
kind: Pod # 资源类型
metadata:
name: nginx-pod # Pod 名称,在同一个 Namespace 内必须唯一
namespace: default # 命名空间,不指定则使用 default
labels: # 标签,用于选择器和组织资源
app: nginx
tier: frontend
annotations: # 注解,用于存储非标识性的元数据
description: "A sample nginx pod"
spec:
# --- 重启策略 ---
restartPolicy: Always # 容器退出后的重启策略:Always / OnFailure / Never

# --- 节点选择 ---
nodeSelector: # 通过标签选择节点
disktype: ssd
tolerations: # 容忍度,用于调度到有特定 Taint 的节点
- key: "dedicated"
operator: "Equal"
value: "gpu"
effect: "NoSchedule"

# --- 容器定义 ---
containers:
- name: nginx # 容器名称
image: nginx:1.27 # 镜像地址(含标签)
imagePullPolicy: IfNotPresent # 镜像拉取策略:Always / IfNotPresent / Never
ports:
- containerPort: 80 # 容器暴露的端口(仅声明,不自动发布)
protocol: TCP
resources: # 资源请求与限制
requests: # 调度时保证的最小资源量
cpu: "100m" # 100 millicores = 0.1 核
memory: "128Mi"
limits: # 容器可使用的最大资源量
cpu: "500m"
memory: "256Mi"
env: # 环境变量
- name: ENVIRONMENT
value: "production"
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: db-secret
key: password
volumeMounts: # 挂载存储卷
- name: nginx-data
mountPath: /usr/share/nginx/html
livenessProbe: # 存活探针
httpGet:
path: /healthz
port: 80
initialDelaySeconds: 15
periodSeconds: 10
readinessProbe: # 就绪探针
httpGet:
path: /ready
port: 80
initialDelaySeconds: 5
periodSeconds: 5
startupProbe: # 启动探针(K8s 1.18+)
httpGet:
path: /startup
port: 80
failureThreshold: 30 # 最多失败 30 次(即最多等待 300 秒)

# --- Init 容器 ---
initContainers:
- name: init-db
image: busybox:1.36
command: ['sh', '-c', 'until nslookup db-service; do echo waiting for db; sleep 2; done']

# --- 存储卷 ---
volumes:
- name: nginx-data
persistentVolumeClaim:
claimName: nginx-pvc # 引用 PVC
- name: config-volume
configMap:
name: nginx-config # 引用 ConfigMap

# --- DNS 配置 ---
dnsPolicy: ClusterFirst # DNS 策略:ClusterFirst / Default / ClusterFirstWithHostNet / None
最佳实践
  1. 始终设置 resources.requestsresources.limits,避免资源争抢导致节点不稳定。
  2. 使用 imagePullPolicy: IfNotPresent 而非 Always(除非使用 :latest 标签),以减少镜像拉取延迟。
  3. 为生产环境的镜像使用明确的 digest(如 nginx@sha256:abc123...),确保部署的可重复性。

1.4 Pod 生命周期

Pod 从创建到终止会经历一系列状态变化。理解这些状态对于排查问题至关重要。

各状态说明:

状态含义
PendingPod 已被 Kubernetes 集群接受,但一个或多个容器尚未创建并就绪。包括等待调度和下载镜像的时间。
RunningPod 已绑定到节点,所有容器已创建,至少一个容器仍在运行,或正在启动/重启中。
SucceededPod 中的所有容器已成功终止,且不会重启。
FailedPod 中的所有容器已终止,且至少一个容器以失败状态退出(非零退出码或被系统终止)。
Unknown由于某种原因无法获取 Pod 状态,通常是与节点通信失败。
CrashLoopBackOff 和 Terminating 不是 Pod Phase

CrashLoopBackOffTerminating 可能会出现在 kubectl 命令的 Status 输出中,但它们不是 Pod 的 phase 值。Pod phase 是 Kubernetes 数据模型中的显式字段,只有五个值:PendingRunningSucceededFailedUnknown

  • CrashLoopBackOff:容器反复崩溃退出,K8s 在每次重启之间增加指数退避等待时间(10s → 20s → 40s → ...,上限 300s)。
  • Terminating:Pod 正在被删除,处于优雅终止过程中(默认 30 秒宽限期)。

参考文档:Pod Lifecycle - Pod phase

1.5 Probe 机制:Liveness / Readiness / Startup

Kubernetes 通过三种探针来监控容器的健康状态:

探针类型作用失败后果
Liveness Probe检测容器是否活着重启容器
Readiness Probe检测容器是否就绪(能否接收流量)从 Service Endpoints 中移除
Startup Probe检测容器是否启动完成在启动期间禁用其他探针

探针支持四种检测方式:

  • httpGet:向容器发送 HTTP GET 请求,2xx/3xx 状态码视为成功。
  • tcpSocket:尝试与容器的指定端口建立 TCP 连接。
  • exec:在容器内执行命令,返回码为 0 视为成功。
  • grpc(v1.24+ 稳定):使用 gRPC 健康检查协议,服务状态为 SERVING 视为成功。
注意事项
  • 不要省略探针。没有探针的 Pod 在容器进程僵死(如死锁)时无法被自动恢复。
  • Startup Probe 是慢启动应用的救星。如果你的应用启动需要较长时间(如 Java 应用加载类库),务必配置 Startup Probe,否则 Liveness Probe 可能在应用尚未就绪时就杀死容器。
  • Readiness Probe 不应过于严格,否则可能导致滚动更新时出现流量中断。

参考文档:Configure Liveness, Readiness and Startup Probes


二、Deployment:无状态应用的编排之王

Deployment 是 Kubernetes 中最常用的工作负载资源,专门用于管理无状态应用。它提供了声明式更新、滚动发布和回滚等生产级特性。

2.1 ReplicaSet 与 Deployment 的关系

Deployment → 管理 → ReplicaSet → 管理 → Pod

ReplicaSet (RS) 是下一层的控制器,负责确保指定数量的 Pod 副本始终在运行。而 Deployment 是更高层的抽象,它在 ReplicaSet 之上增加了版本管理滚动更新能力。

实践建议

在日常使用中,几乎不需要直接操作 ReplicaSet。你应该始终通过 Deployment 来管理应用,让 K8s 自动处理 ReplicaSet 的创建和清理。

2.2 滚动更新(Rolling Update)策略

Deployment 支持两种更新策略:

策略说明适用场景
RollingUpdate(默认)逐步替换旧 Pod 为新 Pod,保证零停机生产环境
Recreate先删除所有旧 Pod,再创建新 Pod不兼容多版本共存的场景

RollingUpdate 通过两个关键参数控制更新节奏:

  • maxSurge:更新期间允许超出期望副本数的最大 Pod 数(可以是绝对数或百分比)。
  • maxUnavailable:更新期间允许不可用的最大 Pod 数(可以是绝对数或百分比)。
默认值

Deployment 默认的滚动更新策略为:maxSurge: 25%maxUnavailable: 25%。这意味着在更新期间,可用副本数至少为 75%,最多为 125%。

参考文档:Deployments - Updating a Deployment

2.3 回滚机制(Rollback)

Deployment 的每一次更新都会创建一个新的 ReplicaSet,并保留历史版本(由 revisionHistoryLimit 控制,默认为 10)。这使得回滚操作变得极其简单:

# 查看部署历史
kubectl rollout history deployment/nginx-deployment

# 回滚到上一个版本
kubectl rollout undo deployment/nginx-deployment

# 回滚到指定版本
kubectl rollout undo deployment/nginx-deployment --to-revision=2

2.4 完整 YAML 示例

apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
namespace: default
labels:
app: nginx
spec:
replicas: 3 # 期望的 Pod 副本数
revisionHistoryLimit: 10 # 保留的历史 ReplicaSet 数量
strategy:
type: RollingUpdate # 更新策略:RollingUpdate / Recreate
rollingUpdate:
maxSurge: 1 # 滚动更新时最多多创建 1 个 Pod
maxUnavailable: 0 # 滚动更新时最多允许 0 个 Pod 不可用
selector:
matchLabels: # 必须与 Pod template 的 labels 匹配
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:1.27
ports:
- containerPort: 80
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 500m
memory: 256Mi
livenessProbe:
httpGet:
path: /
port: 80
initialDelaySeconds: 10
periodSeconds: 10
readinessProbe:
httpGet:
path: /
port: 80
initialDelaySeconds: 5
periodSeconds: 5
生产环境建议
  • 设置 maxUnavailable: 0 可以确保滚动更新过程中服务容量不会下降,代价是更新期间需要更多资源(maxSurge 需要大于 0)。
  • 配合 Pod Disruption Budget (PDB) 使用,可以在节点维护时确保最少可用副本数。

三、StatefulSet:有状态应用的首选

StatefulSet 专为需要稳定网络标识持久存储的有状态应用设计,如数据库(MySQL、PostgreSQL)、消息队列(Kafka、RabbitMQ)和分布式存储系统(Elasticsearch、etcd)。

3.1 与 Deployment 的核心区别

特性DeploymentStatefulSet
Pod 标识随机生成的名称(如 nginx-7b9f...固定的序号名称(如 mysql-0, mysql-1
网络标识Pod IP 随重建而变化每个 Pod 有稳定的 DNS 名称
存储所有 Pod 共享相同的 PVC每个 Pod 有独立的 PVC
部署顺序并行创建按序号顺序创建(0 → 1 → 2 → ...)
删除顺序并行删除按序号逆序删除(... → 2 → 1 → 0)
扩缩容随机选择 Pod 删除/创建严格按序号操作

3.2 有序部署/扩展/删除

StatefulSet 的核心特性之一是有序性 (Ordinality)。当创建或扩展 StatefulSet 时,Pod 会按照序号从 0 开始依次创建,且只有前一个 Pod 进入 Running 且 Ready 状态后,才会创建下一个 Pod。

3.3 稳定的网络标识和持久存储

StatefulSet 为每个 Pod 提供以下稳定性保证:

  • 稳定的网络标识:Pod 名称格式为 <statefulset-name>-<ordinal>(如 mysql-0),且关联的 Headless Service 会为每个 Pod 创建一个稳定的 DNS 记录:<pod-name>.<headless-service>.<namespace>.svc.cluster.local
  • 持久存储:通过 volumeClaimTemplates,StatefulSet 会为每个 Pod 自动创建独立的 PVC。即使 Pod 被重新调度到其他节点,只要绑定了相同的 PVC,数据就不会丢失。

3.4 完整 YAML 示例

apiVersion: v1
kind: Service # Headless Service,用于稳定的网络标识
metadata:
name: mysql
labels:
app: mysql
spec:
clusterIP: None # Headless Service:不分配 ClusterIP
selector:
app: mysql
ports:
- port: 3306
targetPort: 3306
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: mysql
spec:
serviceName: mysql # 必须指向关联的 Headless Service
replicas: 3
selector:
matchLabels:
app: mysql
template:
metadata:
labels:
app: mysql
spec:
containers:
- name: mysql
image: mysql:8.0
ports:
- containerPort: 3306
env:
- name: MYSQL_ROOT_PASSWORD
valueFrom:
secretKeyRef:
name: mysql-secret
key: root-password
volumeMounts:
- name: data
mountPath: /var/lib/mysql
livenessProbe:
exec:
command: ["mysqladmin", "ping", "-h", "localhost"]
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
exec:
command: ["mysql", "-h", "localhost", "-e", "SELECT 1"]
initialDelaySeconds: 5
periodSeconds: 5
volumeClaimTemplates: # 为每个 Pod 自动创建独立的 PVC
- metadata:
name: data
spec:
accessModes: ["ReadWriteOnce"]
storageClassName: standard
resources:
requests:
storage: 10Gi
注意事项
  • StatefulSet 不会自动创建 Headless Service,你需要手动创建。
  • 删除 StatefulSet 时,默认不会删除关联的 PVC,以防止数据丢失。如需同时删除,需要手动清理。
  • StatefulSet 的滚动更新默认使用 OnDelete 策略(即手动删除 Pod 后才会重建),如需自动滚动更新,需设置 .spec.updateStrategy.type: RollingUpdate

参考文档:StatefulSets


四、DaemonSet:节点级守护进程

DaemonSet 确保集群中的每个(或特定)节点上都运行一个 Pod 副本。当节点加入集群时,DaemonSet 会自动为其创建 Pod;当节点移除时,这些 Pod 也会被自动回收。

4.1 典型使用场景

场景示例
日志收集Fluentd、Filebeat、Promtail
监控 AgentPrometheus Node Exporter、Datadog Agent
网络插件Calico、Cilium、Flannel
存储守护进程Ceph、GlusterFS 客户端
安全合规Falco(运行时安全检测)、Twistlock

4.2 滚动更新策略

DaemonSet 支持三种更新策略:

策略说明
RollingUpdate(默认)逐个节点更新 Pod,可通过 maxUnavailable 控制并发度
OnDelete手动删除旧 Pod 后才会创建新 Pod
Surging(K8s 1.22+)先创建新 Pod 再删除旧 Pod,节点上会短暂运行两个 Pod

4.3 完整 YAML 示例

apiVersion: apps/v1
kind: DaemonSet
metadata:
name: node-exporter
namespace: monitoring
labels:
app: node-exporter
spec:
selector:
matchLabels:
app: node-exporter
updateStrategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: 1 # 最多允许 1 个节点上的 Pod 不可用
template:
metadata:
labels:
app: node-exporter
spec:
hostNetwork: true # 使用宿主机网络(监控场景常见)
hostPID: true # 使用宿主机 PID 命名空间
tolerations: # 容忍所有 Taint,确保在所有节点上运行
- operator: Exists
containers:
- name: node-exporter
image: prom/node-exporter:v1.8.0
args:
- "--web.listen-address=:9100"
- "--path.procfs=/host/proc"
- "--path.sysfs=/host/sys"
ports:
- containerPort: 9100
hostPort: 9100
volumeMounts:
- name: proc
mountPath: /host/proc
readOnly: true
- name: sys
mountPath: /host/sys
readOnly: true
resources:
limits:
cpu: 200m
memory: 100Mi
volumes:
- name: proc
hostPath:
path: /proc
- name: sys
hostPath:
path: /sys
最佳实践
  • DaemonSet 通常需要使用 hostNetwork: truehostPID: truehostPath 卷来访问节点级别的资源。
  • 始终配置 tolerations 以确保 DaemonSet Pod 可以被调度到带有 Taint 的节点(如 Master 节点)。
  • 为 DaemonSet Pod 设置严格的资源限制,避免它们占用过多节点资源影响业务应用。

五、Job 与 CronJob:任务编排

5.1 一次性任务(Job)

Job 用于运行一次性任务,确保 Pod 成功执行完毕后终止。Job 会持续跟踪 Pod 的完成状态,并在失败时根据重试策略重新创建 Pod。

核心字段说明:

字段说明默认值
completions需要成功完成的 Pod 数1
parallelism并行运行的 Pod 数1
backoffLimit最大重试次数6
activeDeadlineSecondsJob 超时时间(秒),超时后标记为失败无限制
ttlSecondsAfterFinishedJob 完成后的自动清理时间(K8s 1.23+)永不清理

5.2 并行任务(Parallelism + Completions)

Job 的 parallelismcompletions 字段可以组合出不同的执行模式:

parallelismcompletions模式
11单次顺序执行
N1N 个 Pod 并行竞争,任一成功即完成
NNN 个 Pod 并行工作队列模式
1N顺序执行 N 个任务

5.3 定时任务(CronJob)

CronJob 基于 Cron 表达式来定期创建 Job。它的调度规则与 Linux 的 crontab 基本一致,格式为:

# ┌───────────── 分钟 (0 - 59)
# │ ┌───────────── 小时 (0 - 23)
# │ │ ┌───────────── 日 (1 - 31)
# │ │ │ ┌───────────── 月 (1 - 12)
# │ │ │ │ ┌───────────── 星期 (0 - 6, 0 = 周日)
# │ │ │ │ │
# * * * * *
CronJob 时区

从 Kubernetes v1.25 起,CronJob 支持 timeZone 字段,可以指定 Cron 表达式所使用的时区(IANA 时区格式,如 "Asia/Shanghai")。未指定时默认使用 UTC 时区。

参考文档:CronJobs

apiVersion: batch/v1
kind: CronJob
metadata:
name: database-backup
spec:
schedule: "0 2 * * *" # 每天凌晨 2 点执行
concurrencyPolicy: Forbid # 禁止并发运行
successfulJobsHistoryLimit: 3 # 保留最近 3 个成功的 Job
failedJobsHistoryLimit: 1 # 保留最近 1 个失败的 Job
jobTemplate:
spec:
backoffLimit: 2
activeDeadlineSeconds: 3600 # 超过 1 小时则标记为失败
template:
spec:
containers:
- name: backup
image: postgres:16
command:
- /bin/bash
- -c
- pg_dump -h db-service -U $DB_USER -d $DB_NAME > /backup/$(date +%Y%m%d).sql
env:
- name: DB_USER
valueFrom:
secretKeyRef:
name: db-secret
key: username
- name: DB_NAME
value: "myapp"
volumeMounts:
- name: backup-data
mountPath: /backup
restartPolicy: Never # Job 必须设置为 Never 或 OnFailure
volumes:
- name: backup-data
persistentVolumeClaim:
claimName: backup-pvc
重要提醒
  • Job 的 Pod restartPolicy 必须设置为 NeverOnFailure,不能是 Always
  • concurrencyPolicy: Forbid 确保上一次任务尚未完成时不会启动新任务,对于数据库备份等场景至关重要。
  • 务必设置 successfulJobsHistoryLimitfailedJobsHistoryLimit,避免历史 Job 堆积消耗 etcd 存储空间。

5.4 Job 完整 YAML 示例

apiVersion: batch/v1
kind: Job
metadata:
name: data-migration
spec:
completions: 1 # 需要成功完成 1 次
parallelism: 1 # 同时运行 1 个 Pod
backoffLimit: 3 # 最多重试 3 次
activeDeadlineSeconds: 600 # 超时 10 分钟
ttlSecondsAfterFinished: 86400 # 完成后 24 小时自动清理
template:
spec:
restartPolicy: Never
containers:
- name: migrate
image: myapp:v2.0
command: ["python", "manage.py", "migrate"]
env:
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: app-secrets
key: database-url

六、ReplicaSet / ReplicationController(简述)

ReplicationController (RC)

ReplicationController 是 Kubernetes 最早的副本管理机制,用于确保指定数量的 Pod 副本始终运行。它已被 ReplicaSet 取代,目前仅存在于 API 中以保持向后兼容。

ReplicaSet (RS)

ReplicaSet 是 ReplicationController 的升级版,主要改进是支持基于集合的标签选择器(Set-based Selector),使得选择逻辑更加灵活。

特性ReplicationControllerReplicaSet
标签选择器仅支持等值匹配(environment=production支持集合匹配(environment in (production, staging)
推荐使用已废弃作为 Deployment 的底层实现,不直接使用
实践建议

不要直接创建 ReplicaSet。使用 Deployment 来管理无状态应用,Deployment 会在底层自动创建和管理 ReplicaSet。只有在需要执行非常特殊的操作(如手动管理 Pod 副本)时,才考虑直接使用 ReplicaSet。


七、工作负载选择决策树

面对不同的业务需求,如何选择合适的工作负载资源?以下决策树可以帮助你快速做出判断:

快速参考表

工作负载核心特征典型场景Pod 数量
Deployment无状态、可滚动更新、可回滚Web 服务、API 服务、微服务可变(replicas)
StatefulSet有状态、稳定标识、有序部署数据库、消息队列、分布式存储可变(replicas)
DaemonSet每节点一个 Pod日志收集、监控 Agent、网络插件= 节点数
Job一次性任务,完成后终止数据迁移、批处理、CI/CD固定(completions)
CronJob定时创建 Job数据库备份、报表生成、清理任务每次执行创建新 Job

八、本章小结

本文从 Pod 的本质出发,系统地解析了 Kubernetes 中五大核心工作负载资源:

  1. Pod 是 Kubernetes 的原子调度单元,通过共享网络和存储实现了"超亲密容器"的协作模式。理解 Pod 的生命周期和探针机制是排查问题的基础。

  2. Deployment 是无状态应用的首选,通过 ReplicaSet 实现版本管理,支持零停机的滚动更新和一键回滚。

  3. StatefulSet 为有状态应用提供了稳定的网络标识、独立的持久存储和有序的部署/删除策略,是运行数据库和消息队列等场景的最佳选择。

  4. DaemonSet 确保每个节点运行一个 Pod 副本,是部署基础设施组件(日志、监控、网络插件)的标准方式。

  5. Job 与 CronJob 覆盖了一次性任务和定时任务的需求,支持灵活的并行度和重试策略。

选择工作负载资源时,核心判断依据是:应用是否有状态是否需要长期运行是否需要在每个节点上运行。掌握这些工作负载的特性与适用场景,是构建可靠 Kubernetes 应用的基石。

在下一篇文章中,我们将深入探讨 Kubernetes 的 Service 与网络模型,解析 ClusterIP、NodePort、LoadBalancer 和 Ingress 的工作原理与选型策略。


参考文档

本文内容基于 Kubernetes 官方文档校验,以下为核心参考链接: