引言
Kubernetes(K8s)的核心优势在于编排无状态的容器化应用,但当业务落地到生产环境,几乎不可避免地要面对有状态服务:关系型数据库(MySQL、PostgreSQL)、缓存(Redis)、消息队列(Kafka、RabbitMQ)、搜索引擎(Elasticsearch)等中间件。这类组件对数据持久化、高可用、副本一致性、主从切换、滚动升级都有严苛要求,是 K8s 生产实践最难啃的硬骨头。
本文作为《K8s 运维系列》第 29 天,聚焦如何在 K8s 上稳定、安全、可演进地部署数据库与中间件。我们会拆解三大范式(原生 StatefulSet、Operator、官方 Helm Chart),并给出 MySQL、Redis、Kafka、Elasticsearch 的实战方案与常见坑点,帮你把”裸装中间件”升级为”生产级运维”。

核心概念
在把有状态服务搬到 K8s 上之前,必须先理解三个原生的能力边界:
1. 为什么有状态服务难托管
K8s 的 Pod 是”易失的”:Pod 被驱逐、节点宕机时,Pod 会被重新调度到其他节点。如果数据只放在 Pod 的容器本地盘上,重建后数据就丢了。有状态服务需要:
- 持久化存储:数据要绑定到稳定的 PVC,不能依赖 Pod 本地路径;
- 稳定的网络标识:Pod 重建后访问地址不能变(K8s 用 Headless Service + StatefulSet 的 ordinal Pod 名解决);
- 稳定的存储标识:Pod 与某个 PV 强绑定,不能随机漂移;
- 有序部署与有序删除:数据库集群通常要求副本依次启动、依次下线。
2. StatefulSet 的关键特性
StatefulSet 是 K8s 面向有状态服务的工作负载控制器,与普通 Deployment 最大的区别是:
- 稳定的 Pod 名:
mysql-0、mysql-1、mysql-2,Pod 重建后名字不变; - 稳定的网络:配合 Headless Service,DNS 名
mysql-0.mysql-headless稳定解析; - 稳定的存储:通过
volumeClaimTemplates为每个 Pod 创建独立 PVC; - 有序操作:默认按 ordinal 升序创建、降序删除;
- Pod Management Policy:
OrderedReady(默认)或Parallel。
3. 三种部署范式
| 范式 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| 原生 StatefulSet + PVC | 简单中间件(Redis 单机) | 易理解、依赖少 | 主从切换、备份需手写脚本 |
| 官方 Helm Chart | 主流组件(MySQL、Redis、Kafka、ES) | 上手快、社区维护 | 深度定制有限 |
| Operator + CRD | 复杂组件(TiDB、CockroachDB、ClickHouse) | 自动化主从切换、扩缩容、备份 | 学习成本高 |
实战步骤
下面以最常用的四类组件为例,给出可落地的部署方式。所有示例均可作为生产起点,再按业务需求增强。
步骤 1:MySQL 通过 Bitnami Helm Chart 部署主从集群
Bitnami 的 mysql Chart 是社区最成熟的 MySQL 部署方案,默认一主一从,支持密码 Secret、PVC 大小、镜像版本等参数化。
# 添加 Bitnami 仓库
helm repo add bitnami https://charts.bitnami.com/bitnami
helm repo update
# 创建密码 Secret
kubectl -n mysql create secret generic mysql-secret
--from-literal=rootpassword=RootPass#2026
--from-literal=password=AppPass#2026
--from-literal=dbuser=app
--from-literal=dbname=orderdb
# 部署:1 主 2 从,PVC 各 20Gi
helm install orderdb bitnami/mysql
--namespace mysql --create-namespace
--set auth.rootPassword='RootPass#2026'
--set auth.database=orderdb
--set auth.user=app
--set auth.password='AppPass#2026'
--set primary.persistence.size=20Gi
--set primary.persistence.storageClass=ssd-block
--set replica.replicas=2
--set replica.persistence.size=20Gi
--set replica.persistence.storageClass=ssd-block
--set primary.podDisruptionBudget.enabled=true
--set resources.requests.memory=2Gi
--set resources.limits.memory=4Gi
步骤 2:Redis 通过 StatefulSet 部署哨兵高可用
Redis 官方推荐用”主 + 哨兵”模式实现自动故障转移。以下是一个最小可运行的 Redis Sentinel StatefulSet 骨架,重点是Headless Service 和 稳定的 ordinal 名:
apiVersion: v1
kind: Service
metadata:
name: redis-headless
labels:
app: redis
spec:
clusterIP: None # Headless Service
selector:
app: redis
ports:
- name: redis
port: 6379
- name: sentinel
port: 26379
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: redis
spec:
serviceName: redis-headless
replicas: 3
selector:
matchLabels:
app: redis
template:
metadata:
labels:
app: redis
spec:
containers:
- name: redis
image: bitnami/redis-sentinel:7.2
ports:
- containerPort: 6379
- containerPort: 26379
volumeMounts:
- name: data
mountPath: /bitnami/redis
volumeClaimTemplates:
- metadata:
name: data
spec:
accessModes: ["ReadWriteOnce"]
storageClassName: ssd-block
resources:
requests:
storage: 10Gi
注意:Sentinel 启动时需要配置
sentinel-announce-ip,可通过 Pod 的$HOSTNAME自动识别,避免手动 IP 漂移。
步骤 3:Kafka 通过 Strimzi Operator 部署
Kafka 是典型的多组件有状态系统(Controller、Broker、ZooKeeper/KRaft),强烈建议用 Strimzi Operator 托管。Strimzi 提供 KRaft 模式(无 ZooKeeper)的官方支持。
apiVersion: kafka.strimzi.io/v1beta2
kind: Kafka
metadata:
name: my-cluster
namespace: messaging
spec:
kafka:
version: 3.7.0
replicas: 3
storage:
type: persistent-claim
size: 50Gi
class: ssd-block
config:
log.retention.hours: 168
num.partitions: 8
authentication:
type: tls
zookeeper: # 3.5+ 可省略,默认走 KRaft
replicas: 3
storage:
type: persistent-claim
size: 10Gi
entityOperator:
topicOperator: {}
userOperator: {}
---
apiVersion: kafka.strimzi.io/v1beta2
kind: KafkaTopic
metadata:
name: order-events
namespace: messaging
labels:
strimzi.io/cluster: my-cluster
spec:
partitions: 8
replicas: 3
cleanupPolicy: delete
步骤 4:Elasticsearch 通过 ECK Operator 部署
Elasticsearch 集群需要节点角色分离(master/data/coord)、滚动升级、快照备份,最省事的方式是安装 Elastic 官方 ECK Operator:
apiVersion: elasticsearch.k8s.elastic.co/v1
kind: Elasticsearch
metadata:
name: es-prod
spec:
version: 8.14.0
nodeSets:
- name: master
count: 3
config:
node.roles: [master]
node.store.allow_mmap: true
podTemplate:
spec:
affinity:
podAntiAffinity: # 反亲和,避免同一节点
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
podAffinityTerm:
labelSelector:
matchExpressions:
- key: elasticsearch.k8s.elastic.co/cluster-name
operator: In
values: [es-prod]
topologyKey: kubernetes.io/hostname
- name: data
count: 3
config:
node.roles: [data]
volumeClaimTemplates:
- metadata:
name: elasticsearch-data
spec:
accessModes: ["ReadWriteOnce"]
storageClassName: ssd-block
resources:
requests:
storage: 100Gi
podTemplate:
spec:
resources:
requests:
memory: 8Gi
limits:
memory: 8Gi
关键点:ES 要求
-Xms == -Xms,JVM 堆不能等于容器内存限制(需预留 native 内存约 25%),否则会被 K8s OOMKilled。
步骤 5:统一治理清单(生产必备)
无论用哪种方式,以下六件事在生产环境都必须配齐:
| 治理项 | 配置示例 | 目的 |
|---|---|---|
| PodDisruptionBudget | minAvailable: 1 |
节点维护时至少保一份 |
| 资源 Request/Limit | requests.memory=2Gi, limits.memory=4Gi |
防 OOM、防资源争抢 |
| 拓扑分布约束 | topologySpreadConstraints |
副本跨可用区/节点分布 |
| 备份 CronJob | 每日 mysqldump / snap / binlog | 数据可恢复 |
| 监控探针 | liveness/readiness + exporter | 异常自动重启 |
| NetworkPolicy | 仅允许应用 namespace 访问 | 东西向流量收敛 |
一个 MySQL PDB 示例:
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: mysql-pdb
spec:
minAvailable: 1
selector:
matchLabels:
app.kubernetes.io/name: mysql
常见问题
Q1:Pod 被 OOMKilled 怎么办?
容器内存被强制杀死 90% 的原因是 limits.memory 设得太小,或者 JVM/MySQL 配置没考虑 native 内存。排查思路:kubectl describe pod 看 Last State 的 Reason: OOMKilled;调大 limits.memory,并把 JVM 的 -Xmx 设到容器内存的 75%(预留 25% 给 Metaspace、线程栈、DirectByteBuffer)。
Q2:PVC 显示 Terminating 状态卡死?
这是 K8s 1.29 之前常见的问题。1.29+ 引入 pvc-auto-protection feature gate,默认保护非 StatefulSet 创建的 PVC。手动删除 PVC 会卡住。正确做法:不要手动删 PVC,等 StatefulSet 缩容自动触发;若真的卡死,需要删除底层 PV 或联系 CSI 驱动处理。
Q3:数据库升级时如何避免数据丢失?
永远不要直接修改 Helm release 的镜像 tag 升级,正确流程:
1. 先打快照(云盘快照 / mysqldump / pg_dump);
2. 灰度 1 个副本升级,验证业务;
3. 观察主从延迟、慢查询指标;
4. 批量升级剩余副本;
5. 保留旧版本镜像可回滚(helm rollback 或 kubectl rollout undo)。
Q4:中间件跨可用区部署,跨 AZ 网络延迟会影响一致性吗?
对于 MySQL 半同步复制、Kafka ISR,跨 AZ(同一 Region 内)通常 1~3ms 延迟完全可接受;跨 Region 复制必须使用异步复制(MySQL Group Replication、Debezium + Kafka Connect),并接受 5~30 秒的 RPO。原则:单 Region 内做高可用,跨 Region 做灾备。
Q5:为什么推荐 Operator 而不是裸装 StatefulSet?
裸装 StatefulSet 能跑,但主从切换、扩缩容、备份恢复、证书轮换都要写脚本,运维成本高。Operator 把这些自动化为 CRD 字段,一个 kubectl apply 就能完成主从切换、在线扩容、自动备份。经验法则:组件越复杂、运维越多,越应该上 Operator。
总结
把数据库与中间件搬上 K8s 的关键不是”能跑”,而是把生产级运维能力(HA、备份、升级、监控、安全)以声明式方式沉淀到清单里。推荐路径:
- 简单组件(Redis 单机/哨兵、Nginx)用官方 Helm Chart 或原生 StatefulSet;
- 复杂组件(MySQL 高可用、Kafka、ES、TiDB)首选官方 Operator;
- 无论哪种,PDB、资源 Request/Limit、NetworkPolicy、备份 CronJob 是生产基线,缺一不可;
- 镜像版本走灰度策略,保留回滚能力;
- 用 Prometheus + exporter 接入监控,用 Loki/EFK 收日志。
下一篇我们将进入系列收官之作《Kubernetes 精通之路——总结与进阶规划》,带你回顾全 30 篇主线、梳理进阶学习路线、推荐认证与社区参与方式,并讨论 K8s 与云原生生态的未来趋势。
下期预告
第 30 天:Kubernetes 精通之路——总结与进阶规划
全 30 天回顾 | 学习路线与认证建议 | Operator 与 Operator 之上的新范式 | 云原生与 AI 推理的结合 | 运维工程师成长路径
本系列由星尘数据技术团队整理,欢迎转载,转载请注明出处。



















暂无评论内容