Kubernetes 全景解析 (7):有状态应用与 Operator 模式实战
"StatefulSet 管的是'有序的宠物',Operator 管的是'懂业务的管家'。"
在前面的章节中,我们深入探讨了 K8s 的架构设计、网络模型和存储体系。对于无状态应用(如 Web 服务、API 网关),Deployment + Service 的组合已经足够应对大多数场景。然而,当你面对数据库、消息队列、分布式缓存等有状态应用时,事情就变得复杂了——它们需要稳定的网络标识、持久化的存储卷、有序的启停顺序,以及复杂的集群初始化流程。
本文将从有状态应用的核心挑战出发,深入解析 StatefulSet 的工作机制,实战部署一套 Redis Cluster,并最终构建一个自定义 Operator 来自动化管理整个生命周期。
API 版本说明:本文所有 YAML 配置使用的 API 版本均经过 Kubernetes v1.34 官方文档校验:
| 资源类型 | apiVersion | 官方文档 |
|---|---|---|
| StatefulSet | apps/v1 | StatefulSet v1 |
| Service | v1 | Service v1 |
| CRD | apiextensions.k8s.io/v1 | CRD v1 |
| RBAC | rbac.authorization.k8s.io/v1 | RBAC v1 |
| Deployment | apps/v1 | Deployment v1 |
:::
一、有状态应用的挑战与 K8s 解决方案
1.1 有状态 vs 无状态:本质区别
在分布式系统中,"状态"(State)是指应用需要持久保存的数据或上下文信息。理解有状态与无状态的本质区别,是选择正确编排策略的前提。
| 维度 | 无状态应用 | 有状态应用 |
|---|---|---|
| 数据存储 | 数据在外部(DB/缓存) | 数据在本地或内置存储 |
| 网络标识 | 随意替换,IP 可变 | 需要稳定的网络标识 |
| 扩缩容 | 随意增删实例 | 需要数据迁移/重平衡 |
| 启停顺序 | 无要求 | 通常需要有序启停 |
| 典型代表 | Web 服务、微服务 API | 数据库、消息队列、缓存 |
| K8s 工作负载 | Deployment / ReplicaSet | StatefulSet / 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 Service(clusterIP: 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 名称格式为:<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 稳定版本。详见官方文档:StatefulSet、Service
文件路径: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
cluster-announce-ip必须使用 Pod 的完整 DNS 名称,否则集群节点之间无法正确发现对方。cluster-announce-bus-port必须显式声明,Redis Cluster 节点间通过 Bus 端口进行 gossip 通信。storageClassName需要根据你的 K8s 集群实际可用的 StorageClass 进行修改。生产环境建议使用 SSD 存储类。- 建议配置 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
- Redis Cluster 的故障转移由集群内部自动完成,不需要外部干预。
- 当原主节点恢复后,它会自动变为新主节点的从节点,不会抢回主节点角色。
- 在生产环境中,建议设置
cluster-node-timeout为合理值(通常 5-15 秒),以平衡故障检测速度和误判率。
四、Operator 模式深度解析
4.1 Operator 的设计哲学
通过前面的实战,你可能已经发现一个问题:Redis Cluster 的初始化是手动执行的。StatefulSet 只负责创建 Pod,但集群的初始化、扩缩容、故障恢复等操作仍然需要人工介入。
这正是 Operator 模式要解决的核心问题。
Operator 的本质是将人类运维特定应用的知识编码为自动化软件。
CoreOS(现 Red Hat)在 2016 年提出了 Operator 模式的概念。其核心思想是:
- 人类运维人员知道如何部署、扩缩容、备份、恢复一个特定应用
- 将这些领域知识编码为 Kubernetes 控制器
- 控制器通过 CRD(Custom Resource Definition) 扩展 K8s API,引入应用特定的资源类型
- 通过 Reconciliation Loop 持续调谐实际状态与期望状态
简单来说,Operator = CRD + Controller + 特定领域知识。
4.2 Operator 核心组件
一个完整的 Operator 由以下三个核心组件构成:
| 组件 | 职责 | 类比 |
|---|---|---|
| CRD(Custom Resource Definition) | 定义自定义资源的 Schema(结构) | 数据库的表结构 |
| Controller | Watch 资源变化,执行调谐逻辑 | 数据库的触发器 |
| Reconciliation Loop | 持续将实际状态趋近期望状态 | 自动驾驶的反馈控制 |
4.3 开发框架对比
目前主流的 Operator 开发框架有三个:
| 特性 | controller-runtime | Kubebuilder | Operator SDK |
|---|---|---|---|
| 定位 | 底层库 | 脚手架工具 | 全功能 SDK |
| 语言 | Go | Go | Go / 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 v1、ServiceAccount 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 中的稳定版本。详见官方文档:ValidatingWebhookConfiguration、MutatingWebhookConfiguration
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 基于 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
- 不可删除存储版本:一旦某个版本被标记为
storage: true,就不能直接删除该版本,必须先创建新版本并迁移数据。 - Webhook 版本转换:如果需要在不同 CRD 版本间自动转换,需要实现 Conversion Webhook。
- 向后兼容:新增字段应该有默认值,确保旧版本 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 的核心能力。下一步建议:
- 深入实践:选择一个真实的有状态应用(如 PostgreSQL、Kafka),尝试为其编写 Operator
- 参与社区:关注 CNCF 生态,学习优秀的开源 Operator 实现(如 Prometheus Operator、Argo CD Operator)
- 探索高级主题:Service Mesh(Istio/Linkerd)、Serverless(Knative)、GitOps(Argo CD/Flux)等
"Kubernetes 不是终点,而是起点。理解了它的设计哲学,你就能更好地理解整个云原生生态。"
系列导航: