97、数据库容器化:Docker部署、Kubernetes编排、持久化存储、弹性伸缩

说实话,数据库容器化这个话题,我犹豫了很久才决定放进这100例里。

为什么犹豫?因为很多老派的DBA一听“数据库上容器”就摇头。我自己早年也踩过不少坑——容器一重启,数据丢了;Pod一漂移,连不上了;流量一上来,磁盘不够了……

但这两年,随着Kubernetes生态成熟,加上StatefulSet、CSI、Operator这些技术的完善,数据库容器化已经不是“能不能做”的问题,而是“怎么做才稳”的问题。今天我就把实战中沉淀下来的一套方法,掰开了讲给你听。

一、先画张图:容器化数据库的整体架构

动手之前,脑子里得有个全局图。我习惯把数据库容器化拆成四个层次:

数据库容器化四层架构 应用层 业务Pod / 微服务 / 连接池 编排层 Kubernetes StatefulSet / Operator / Service 存储层 PVC / PV / CSI驱动 / 分布式存储 基础设施层 Docker / 宿主机 / 网络 / 监控

你看,从下往上,每一层解决一个核心问题:基础设施层提供计算资源,存储层保证数据不丢,编排层管理生命周期,应用层负责接入。嗯,这个分层思路,我建议你记下来,后面所有操作都围绕它展开。

二、Docker部署:先让数据库跑起来

容器化第一步,就是把数据库装进Docker镜像里。别小看这一步,我见过有人直接把官方镜像拉下来就跑,结果字符集不对、时区不对、数据目录没挂载……

我个人习惯,写一个干净的Dockerfile:

# Dockerfile - 以MySQL 8.0为例
FROM mysql:8.0.33

# 设置时区
ENV TZ=Asia/Shanghai

# 复制自定义配置文件
COPY my.cnf /etc/mysql/conf.d/my.cnf

# 初始化脚本(可选)
COPY init.sql /docker-entrypoint-initdb.d/

EXPOSE 3306

VOLUME ["/var/lib/mysql"]

对应的my.cnf,我一般会加上这几行:

[mysqld]
character-set-server=utf8mb4
collation-server=utf8mb4_unicode_ci
max_connections=500
innodb_buffer_pool_size=2G

启动命令很简单:

docker run -d \
  --name mysql-prod \
  -e MYSQL_ROOT_PASSWORD=MyStr0ng!Pass \
  -v /data/mysql:/var/lib/mysql \
  -p 3306:3306 \
  mysql:8.0.33
⚠️ 注意: 这里的 -v /data/mysql:/var/lib/mysql 是持久化的关键。我曾经有一次忘记挂载卷,容器重启后数据库直接变空——那感觉,就像写了一天代码没保存。所以,生产环境必须挂载宿主机目录或使用命名卷

三、Kubernetes编排:用StatefulSet管理有状态服务

单机Docker跑数据库,只能算“尝鲜”。真正上生产,得用Kubernetes编排。这里有个关键点:数据库是有状态的,不能用Deployment,得用StatefulSet

为什么?Deployment的Pod是无状态的,重启后名字、IP都会变。而StatefulSet保证每个Pod有稳定的网络标识和持久化存储。我当年第一次用Deployment部署MySQL,Pod一重启,从库找不到主库了……后来换成StatefulSet,世界清净了。

来看一个精简的StatefulSet定义:

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: mysql
spec:
  serviceName: mysql-headless
  replicas: 3
  selector:
    matchLabels:
      app: mysql
  template:
    metadata:
      labels:
        app: mysql
    spec:
      containers:
      - name: mysql
        image: mysql:8.0.33
        env:
        - name: MYSQL_ROOT_PASSWORD
          valueFrom:
            secretKeyRef:
              name: mysql-secret
              key: password
        ports:
        - containerPort: 3306
          name: mysql
        volumeMounts:
        - name: data
          mountPath: /var/lib/mysql
  volumeClaimTemplates:
  - metadata:
      name: data
    spec:
      accessModes: ["ReadWriteOnce"]
      resources:
        requests:
          storage: 50Gi
💡 小技巧: 注意 volumeClaimTemplates 这个字段。它会为每个Pod自动创建独立的PVC。Pod-0对应PVC-data-mysql-0,Pod-1对应PVC-data-mysql-1。这样每个实例的数据是隔离的,不会互相干扰。

配合一个Headless Service,让Pod之间通过DNS直连:

apiVersion: v1
kind: Service
metadata:
  name: mysql-headless
spec:
  clusterIP: None
  selector:
    app: mysql
  ports:
  - port: 3306
    targetPort: 3306

这样一来,mysql-0.mysql-headless.default.svc.cluster.local 就能稳定访问到第一个实例。主从复制配置里,直接写这个地址就行。

四、持久化存储:数据不能丢

容器化数据库最大的痛点,就是存储。Pod可以漂,但数据必须原地不动。

Kubernetes里,持久化存储靠PVC和PV。我建议你用动态存储供应,而不是手动创建PV。比如用阿里云的NAS、AWS的EBS,或者自建的Rook/Ceph。

下面是一个StorageClass的例子:

apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: fast-ssd
provisioner: kubernetes.io/aws-ebs
parameters:
  type: gp3
  fsType: ext4
reclaimPolicy: Retain

🔑 关键配置: reclaimPolicy: Retain 表示PVC删除后,PV保留。这样即使误删了StatefulSet,数据还在。我建议生产环境都用Retain,别用Delete——别问我怎么知道的,说多了都是泪。

另外,备份策略一定要提前规划。我常用的做法是:

  • 每天凌晨用mysqldump全量备份,存到对象存储
  • 每6小时做一次binlog增量备份
  • 备份脚本以CronJob形式跑在K8s集群里

一个简单的备份CronJob:

apiVersion: batch/v1
kind: CronJob
metadata:
  name: mysql-backup
spec:
  schedule: "0 2 * * *"
  jobTemplate:
    spec:
      template:
        spec:
          containers:
          - name: backup
            image: mysql:8.0.33
            command:
            - /bin/sh
            - -c
            - |
              mysqldump -h mysql-0.mysql-headless \
                -u root -p$MYSQL_ROOT_PASSWORD \
                --all-databases > /backup/mysql-$(date +%Y%m%d).sql
          restartPolicy: OnFailure

五、弹性伸缩:流量大了怎么办?

数据库不像Web服务,不能随便加Pod。加个只读副本,还得同步数据。但Kubernetes的HPA(Horizontal Pod Autoscaler)配合StatefulSet,还是能实现一定程度的弹性。

我一般这样设计:

  1. 主库:不参与伸缩,保持单点写入
  2. 从库:根据CPU或连接数自动扩缩
  3. 读写分离:应用层通过Proxy(如ProxySQL)路由

HPA配置示例:

apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: mysql-read-hpa
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: StatefulSet
    name: mysql-read
  minReplicas: 2
  maxReplicas: 8
  metrics:
  - type: Resource
    resource:
      name: cpu
      target:
        type: Utilization
        averageUtilization: 70
⚠️ 注意: 从库扩容后,新Pod需要时间同步数据。如果同步延迟大,查询可能读到旧数据。我建议设置readinessProbe,等同步完成后再加入服务。另外,不要对主库做HPA,主库扩缩容易引发数据一致性问题。

六、避坑指南:我踩过的几个坑

最后,分享几个实战中遇到的坑,希望能帮你省点时间:

  • 坑一:容器内文件权限。MySQL镜像默认用mysql用户运行,但挂载的宿主机目录可能是root权限。解决办法:在Dockerfile里用chown,或者在宿主机上chown -R 999:999 /data/mysql
  • 坑二:Pod重启后IP变了。主从复制配置里如果写死了IP,Pod重启就废了。一定要用StatefulSet的DNS名称,比如mysql-0.mysql-headless
  • 坑三:磁盘空间不足。数据库日志增长很快,尤其是binlog。我建议在StatefulSet里加上resources.limits.ephemeral-storage,并配置日志自动清理。
  • 坑四:网络延迟。跨AZ部署时,主从同步延迟可能飙升。如果业务对一致性要求高,建议把主从放在同一个可用区。

好了,数据库容器化这条路,说难不难,说简单也不简单。核心就三句话:存储要持久,网络要稳定,扩缩要谨慎。把这三点拿捏住,你的数据库在容器里跑个三五年没问题。


公众号:蓝海资料掘金营,微信deep3321