K8s 运维系列 | 第 29 天:生产实践——K8s 上运行数据库与中间件

引言

Kubernetes(K8s)的核心优势在于编排无状态的容器化应用,但当业务落地到生产环境,几乎不可避免地要面对有状态服务:关系型数据库(MySQL、PostgreSQL)、缓存(Redis)、消息队列(Kafka、RabbitMQ)、搜索引擎(Elasticsearch)等中间件。这类组件对数据持久化、高可用、副本一致性、主从切换、滚动升级都有严苛要求,是 K8s 生产实践最难啃的硬骨头。

本文作为《K8s 运维系列》第 29 天,聚焦如何在 K8s 上稳定、安全、可演进地部署数据库与中间件。我们会拆解三大范式(原生 StatefulSet、Operator、官方 Helm Chart),并给出 MySQL、Redis、Kafka、Elasticsearch 的实战方案与常见坑点,帮你把”裸装中间件”升级为”生产级运维”。

K8s 运维 第29天

核心概念

在把有状态服务搬到 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-0mysql-1mysql-2,Pod 重建后名字不变;
  • 稳定的网络:配合 Headless Service,DNS 名 mysql-0.mysql-headless 稳定解析;
  • 稳定的存储:通过 volumeClaimTemplates 为每个 Pod 创建独立 PVC;
  • 有序操作:默认按 ordinal 升序创建、降序删除;
  • Pod Management PolicyOrderedReady(默认)或 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 podLast StateReason: 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 rollbackkubectl 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、备份、升级、监控、安全)以声明式方式沉淀到清单里。推荐路径:

  1. 简单组件(Redis 单机/哨兵、Nginx)用官方 Helm Chart 或原生 StatefulSet;
  2. 复杂组件(MySQL 高可用、Kafka、ES、TiDB)首选官方 Operator;
  3. 无论哪种,PDB、资源 Request/Limit、NetworkPolicy、备份 CronJob 是生产基线,缺一不可;
  4. 镜像版本走灰度策略,保留回滚能力;
  5. 用 Prometheus + exporter 接入监控,用 Loki/EFK 收日志。

下一篇我们将进入系列收官之作《Kubernetes 精通之路——总结与进阶规划》,带你回顾全 30 篇主线、梳理进阶学习路线、推荐认证与社区参与方式,并讨论 K8s 与云原生生态的未来趋势。

下期预告

第 30 天:Kubernetes 精通之路——总结与进阶规划

全 30 天回顾 | 学习路线与认证建议 | Operator 与 Operator 之上的新范式 | 云原生与 AI 推理的结合 | 运维工程师成长路径


本系列由星尘数据技术团队整理,欢迎转载,转载请注明出处。

微信二维码
© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

取消
昵称表情代码图片快捷回复

    暂无评论内容