MariaDB & K8s: 使用持久卷部署 MariaDB 和 WordPress

在之前的博客文章 MariaDB & K8s: 创建 Secret 并在 MariaDB 部署中使用它 中,我们使用 Secrets 资源来隐藏机密的 root 用户数据。而在该系列更早的一篇博客文章 MariaDB & K8s: 容器/部署之间的通信 中,我们在一个 Pod 中创建了 2 个容器(即 MariaDB 和 phpmyadmin)。那种部署方式没有使用任何持久卷。

在本篇博客文章中,我们将为 MariaDB 和 WordPress 应用程序创建单独的 Deployment,并为两者创建一个 Service 以便连接它们。此外,我们还将在 MariaDB Deployment 的 Pod 中创建 Volume。

配置文件

在前一篇博客文章和再前一篇博客文章的基础上,我们已经创建了 MariaDB Secret 和 MariaDB ConfigMap,这里我们假设它们已经在集群中存在。

$ kubectl describe secret/mariadb-secret
Name:         mariadb-secret
Namespace:    default
Labels:       <none>
Annotations:  <none>

Type:  Opaque

Data
====
mariadb-root-password:  6 bytes

$ kubectl describe cm mariadb-configmap
Name:         mariadb-configmap
Namespace:    default
Labels:       <none>
Annotations:  <none>

Data
====
database_url:
----
mariadb-internal-service

BinaryData
====

Events:  <none>

让我们添加一个用于 MariaDB Deployment 和 Service 的配置文件(GitHub 文件)。

apiVersion: v1
kind: Service
metadata:
  name: mariadb-internal-service
spec:
  selector:
    app: mariadb
  ports:
    - protocol: TCP
      port: 3306
      targetPort: 3306
  clusterIP: None
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: mariadb-deployment
spec: # specification for deployment resource
  replicas: 1
  selector:
    matchLabels:
      app: mariadb
  template: # blueprint for Pod
    metadata:
      labels:
        app: mariadb # service will look for this label
    spec: # specification for Pod
      containers:
      - name: mariadb
        image: mariadb
        ports:
        - containerPort: 3306 #default one
        env:
        - name: MARIADB_ROOT_PASSWORD
          valueFrom:
            secretKeyRef:
              name: mariadb-secret
              key: mariadb-root-password
        - name: MARIADB_DATABASE
          value: wordpress

创建的 Service 是一个Headless 类型的 Service,没有分配集群 IP,也不需要负载均衡。与之前的博客文章唯一不同的是在容器启动期间创建了数据库“wordpress”,这是 WordPress Deployment 的一个要求。

让我们添加一个用于 WordPress Deployment 和 Service 的配置文件(GitHub 文件)。

apiVersion: v1
kind: Service
metadata:
  name: wordpress
spec:
  selector:
    app: wordpress
  ports:
    - port: 80
      targetPort: 80
      protocol: TCP #default
      nodePort: 31000
  type: LoadBalancer
---

apiVersion: apps/v1
kind: Deployment
metadata:
  name: wordpress-deployment
spec: # specification for deployment resource
  replicas: 1
  selector:
    matchLabels:
      app: wordpress
  template: # blueprint for Pod
    metadata:
      labels:
        app: wordpress
    spec: # specification for Pod
      containers:
      - name: wordpress
        image: wordpress:latest 
        ports:
        - containerPort: 80
        env:
        - name: WORDPRESS_DB_HOST
          valueFrom:
            configMapKeyRef:
              name: mariadb-configmap
              key: database_url
        - name: WORDPRESS_DB_PASSWORD
          valueFrom:
            secretKeyRef:
              name: mariadb-secret
              key: mariadb-root-password
        - name: WORDPRESS_DB_USER
          value: root
        - name: WORDPRESS_DEBUG
          value: "1"

创建的 Service 是一个LoadBalancer 类型(Minikube 支持它),用于平衡 Service 的负载。我们暴露了一个固定的节点端口,用作外部端口。此端口号应在 30000-32767 之间。创建的 Deployment 使用 ConfigMap 中的信息作为环境变量 WORDPRESS_DB_HOST 的值,使用 Secret 中的信息作为环境变量 WORDPRESS_DB_PASSWORD 的值,并引用容器端口 80。

创建资源并验证

$ kubectl apply -f mariadb-configmap.yaml 
configmap/mariadb-configmap created

$ kubectl apply -f mariadb-secret.yaml 
secret/mariadb-secret created

$ kubectl apply -f mariadb-deployment-pvc.yaml 
service/mariadb-internal-service created
deployment.apps/mariadb-deployment created

$ kubectl apply -f wordpress-deployment-pvc.yaml 
service/wordpress created
deployment.apps/wordpress-deployment created

$ minikube service wordpress
|-----------|-----------|-------------|---------------------------|
| NAMESPACE |   NAME    | TARGET PORT |            URL            |
|-----------|-----------|-------------|---------------------------|
| default   | wordpress |          80 | http://192.168.49.2:31000 |
|-----------|-----------|-------------|---------------------------|
🎉  Opening service default/wordpress in default browser...

执行最后一个命令后,我们将获得一个 URL,可以通过该 URL 安装 WordPress。

添加持久卷

当 Pod 崩溃时,kubectl 会重启容器并从一个干净的状态开始,这可能会导致数据一致性问题。为了解决这个问题,存在与 Pod 相关的 Volume 抽象。Pod 可以有多种卷类型,例如默认且与 Pod 生命周期绑定的临时卷类型、其数据可以通过 Pod 中的文件消费的 ConfigMapSecret 类型,以及本篇博客关注的 PersistentVolumeClaim 类型,它表示在 Pod 中挂载 PersistentVolume 的请求。

一个 PersistentVolume (PV) 资源是集群中的一块存储,它由管理员手动配置,或者由 Kubernetes 使用 StorageClass 进行动态配置(集群中有一个默认的 StorageClass 使用 hostPath provisioner)。

一个 PersistentVolumeClaim (PVC) 是用户对存储的请求,可以由 PV 满足。Claim 请求特定的容量和访问模式。

PersistentVolumes 和 PersistentVolumeClaims 独立于 Pod 的生命周期,并且在 Pod 重启、重新调度甚至删除后保留数据。

为了验证这一点,让我们添加一些数据并重启 MariaDB Pod。

$ kubectl get pods -l app=mariadb
NAME                                  READY   STATUS    RESTARTS   AGE
mariadb-deployment-74f8c57cbf-t4whv   1/1     Running   0          73m
$ kubectl exec mariadb-deployment-74f8c57cbf-t4whv -- mariadb -uroot -psecret -e "create database if not exists mytest;use mytest; create table t(t int); insert into t values (1),(2); select * from t";
t
1
2
a

# Watch in first terminal state of Pods (MariaDB Pod will be restarted, wordpress Pod will be running)
$ kubectl get pods -w
NAME                                    READY   STATUS    RESTARTS   AGE
mariadb-deployment-74f8c57cbf-t4whv     1/1     Running   0          78m
wordpress-deployment-79697d4fd5-4qsn7   1/1     Running   0          78m
wordpress-deployment-79697d4fd5-s4gc9   1/1     Running   0          78m
mariadb-deployment-74f8c57cbf-t4whv     1/1     Terminating   0          79m
mariadb-deployment-74f8c57cbf-t4whv     0/1     Terminating   0          79m
mariadb-deployment-74f8c57cbf-t4whv     0/1     Terminating   0          79m

# Scaling deployment replicas to 0, will restart the Pod. 
# Start command in the second terminal
$ kubectl scale deployment mariadb-deployment --replicas=0
deployment.apps/mariadb-deployment scaled

# Watch again MariaDB Pod creation before starting command to scale new replicas
# wordpress Pod is again in running state, not affected
$ kubectl get pods -w
NAME                                    READY   STATUS    RESTARTS   AGE
wordpress-deployment-79697d4fd5-4qsn7   1/1     Running   0          83m
wordpress-deployment-79697d4fd5-s4gc9   1/1     Running   0          83m
mariadb-deployment-74f8c57cbf-qd786     0/1     Pending   0          0s
mariadb-deployment-74f8c57cbf-qd786     0/1     Pending   0          0s
mariadb-deployment-74f8c57cbf-qd786     0/1     ContainerCreating   0          0s
mariadb-deployment-74f8c57cbf-qd786     1/1     Running             0          3s

# In other terminal 
$ kubectl scale deployment mariadb-deployment --replicas=1
deployment.apps/mariadb-deployment scaled

# Check existence of already created database 'mytest'
$ kubectl exec svc/mariadb-internal-service -- mariadb -uroot -psecret -e "show databases"
Database
information_schema
mysql
performance_schema
sys
wordpress

如上所示,在对 ReplicaSet 进行零扩容(zero scaling)时,旧的 Pod 被终止,然后创建了一个新的 Pod,这类似于 Pod 的重启。

在扩容到单个副本之前,可以看到只有 WordPress Pod 在运行,没有 MariaDB Pod;在扩容到单个副本之后,我们获得了一个 MariaDB Pod,其 Status 列显示了 Pod 生命周期的创建阶段。

现在让我们修改脚本,添加 PersistentVolumeClaim,然后运行相同的场景。

首先让我们删除旧的 MariaDB Deployment

$ kubectl delete -f mariadb-deployment-pvc.yaml 
service "mariadb-internal-service" deleted
deployment.apps "mariadb-deployment" deleted

许多集群环境都安装了默认的 StorageClass 。当 PersistentVolumeClaim 中未指定 StorageClass 时,将使用集群的默认 StorageClass。我们可以检查 StorageClass

$ kubectl get storageclass
NAME                 PROVISIONER                RECLAIMPOLICY   VOLUMEBINDINGMODE   ALLOWVOLUMEEXPANSION   AGE
standard (default)   k8s.io/minikube-hostpath   Delete          Immediate           false                  145d

创建 PersistentVolumeClaim 时,将根据 StorageClass 配置动态配置一个 PersistentVolume。

让我们添加一个名为“mariadb-pv-claim”的 PersistentVolumeClaim 资源,具有 RW 访问模式和 300 MB 的存储容量,并使用默认的 StorageClass(Persistent Volume 的动态配置)。

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: mariadb-pv-claim
  labels:
    app: mariadb
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 300M

此外,我们将使用以下内容更新容器规范

      containers:
      - name: mariadb
        image: mariadb
        ports:
        - containerPort: 3306 #default one
        env:
        - name: MARIADB_ROOT_PASSWORD
          valueFrom:
            secretKeyRef:
              name: mariadb-secret
              key: mariadb-root-password
        - name: MARIADB_DATABASE
          value: wordpress
        volumeMounts:
        - name: mariadb-pv
          mountPath: /var/lib/mysql
      volumes:
      - name: mariadb-pv
        persistentVolumeClaim:
          claimName: mariadb-pv-claim

在容器“mariadb”中,有一个名为“mariadb-pv”的挂载,用于容器内部的默认数据目录路径,其存储大小将等于 Claim 的大小,因为卷挂载名称“mariadb-pv”是“mariadb”容器特有的,并且类型为 persistentVolumeClaim,后者为 Pod 定义并引用了集群中已创建的 PersistentVolumeClaim 资源“mariadb-pv-claim”。

现在让我们应用此部署,验证结果,并再次尝试在 Pod 中创建数据库,重启 Pod,然后尝试获取旧数据。

$ kubectl apply -f mariadb-deployment-pvc.yaml 
persistentvolumeclaim/mariadb-pv-claim created
service/mariadb-internal-service created
deployment.apps/mariadb-deployment created

# Get information about pvc resource - pvc is bound to the pv, check kubectl get pv
$ kubectl get pvc -l app=mariadb
NAME               STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS   AGE
mariadb-pv-claim   Bound    pvc-57467ba5-bf6e-4b91-8741-c6f24e9c5861   300M       RWO            standard       31s
# Get the pods
$ kubectl get pods -l app=mariadb
NAME                                  READY   STATUS    RESTARTS   AGE
mariadb-deployment-58c7c4d75c-h45nl   1/1     Running   0          100s

# Watch in first terminal
$ kubectl get pods -w -l app=mariadb
NAME                                  READY   STATUS    RESTARTS   AGE
mariadb-deployment-58c7c4d75c-h45nl   1/1     Running   0          3m53s
mariadb-deployment-58c7c4d75c-h45nl   1/1     Terminating   0          4m13s
mariadb-deployment-58c7c4d75c-lsfdw   0/1     Pending       0          0s
mariadb-deployment-58c7c4d75c-lsfdw   0/1     Pending       0          0s
mariadb-deployment-58c7c4d75c-lsfdw   0/1     ContainerCreating   0          0s
mariadb-deployment-58c7c4d75c-h45nl   0/1     Terminating         0          4m15s
mariadb-deployment-58c7c4d75c-h45nl   0/1     Terminating         0          4m15s
mariadb-deployment-58c7c4d75c-h45nl   0/1     Terminating         0          4m15s
mariadb-deployment-58c7c4d75c-lsfdw   1/1     Running             0          4s

# Execute command in the second terminal - note above that new Pod has been created
$ kubectl scale deploy mariadb-deployment --replicas=0 && kubectl scale deploy mariadb-deployment --replicas=1
deployment.apps/mariadb-deployment scaled
deployment.apps/mariadb-deployment scaled

# Verify results from newly created Pod by inspecting data created in the old Pod
$ kubectl exec mariadb-deployment-58c7c4d75c-lsfdw -- mariadb -uroot -psecret -e "show databases like '%test%'; use mytest; select * from t;"
Database (%test%)
mytest
t
1
2

如上所示,我们能够获取到对 Pod 重启/终止具有抵抗力的数据。

对于一个 Deployment,我们可以拥有多个 PVC 资源。尝试为 WordPress Deployment 创建一个 PVC 资源,用于卷挂载路径 /var/www/html

通过这种方式,我们实现了数据一致性并部署了有状态应用(statefulset application)。

建议使用 Deployment 来部署有状态应用(statefulset applications),因为它们是为无状态应用(stateless applications)设计的,所以 Deployment 的所有副本共享同一个 PersistentVolumeClaim,只有 ReadOnlyMany 或 ReadWriteMany 模式的卷才能在这种设置下工作。即使是使用 ReadWriteOnce  卷的单副本 Deployment 也不推荐,因为默认的 Deployment 策略会导致第二个 Pod 被创建,可能会发生死锁(更多信息请参考此链接)。

结论与未来工作

本篇博客文章展示了如何创建 2 个 Deployment 以及如何通过 Service 将它们连接起来。我们还应用了之前学到的 K8s 概念,以便更充分地利用 K8s API。

在数据库示例中,我们展示了数据一致性的必要性和 Pod 的临时性,并介绍了用于在 K8s 集群上进行动态配置的 PersistentVolumeClaim 资源,它有助于解决这类问题。然而,本篇博客仅用于演示和学习概念。在接下来的博客文章中,我们将创建一个不使用 Deployment 的StatefulSet 应用,它用于确保数据一致性。

欢迎在 Zulip 上进行讨论。

阅读更多