从0到1使用Kubernetes系列(六):数据持久化实战


本文是从 0 到 1 使用 Kubernetes 系列第六篇,上一篇《从 0 到 1 使用 Kubernetes 系列(五):Kubernetes Scheduling》介绍了 Kubernetes 调度器如何进行资源调度,本文将为大家介绍几种常用储存类型。

默认情况下 Pod 挂载在磁盘上的文件生命周期与 Pod 生命周期是一致的,若 Pod 出现崩溃的情况,kubelet 将会重启它,这将会造成 Pod 中的文件将丢失,因为 Pod 会以镜像最初的状态重新启动。在实际应用当中,开发者有很多时候需要将容器中的数据保留下来,比如在 Kubernetes 中部署了 MySQL,不能因为 MySql 容器挂掉重启而上面的数据全部丢失;其次,在 Pod 中同时运行多个容器时,这些容器之间可能需要共享文件。也有的时候,开发者需要预置配置文件,使其在容器中生效,例如自定义了 mysql.cnf 文件在 MySQL 启动时就需要加载此配置文件。这些都将是今天将要实战解决的问题。如果你想和更多Kubernetes技术专家交流,可以加我微信liyingjiese,备注『加群』。群里每周都有全球各大公司的最佳实践以及行业最新动态

今天这篇文将讲解下面几种常用存储类型:

SECRET

Secret 对象允许您存储和管理敏感信息,例如密码,OAuth 令牌和 ssh 密钥。将此类信息放入一个 secret 中可以更好地控制它的用途,并降低意外暴露的风险。

使用场景

鉴权配置文件挂载

使用示例

在 CI 中 push 构建好的镜像就可以将 docker 鉴权的 config.json 文件存入 secret 对象中,再挂载到 CI 的 Pod 中,从而进行权限认证。

首先创建 secret:
$ kubectl create secret docker-registry docker-config  --docker-server=https://hub.docker.com --docker-username=username --docker-password=password
secret/docker-config created

新建 docker-pod.yaml 文件,粘贴以下信息:
apiVersion: v1
kind: Pod
metadata:
name: docker
spec:
containers:
- name: docker
image: docker
command:
  - sleep
  - "3600"
volumeMounts:
- name: config
  mountPath: /root/.docker/
volumes:
- name: config
secret:
  secretName: docker-config
  items:
  - key: .dockerconfigjson
    path: config.json
    mode: 0644

Docker Pod 挂载 secret:
$ kubectl apply -f docker-pod.yaml
pod/docker created

查看挂载效果:
$ kubectl exec docker -- cat /root/.docker/config.json
{"auths":{"https://hub.docker.com":{"username":"username","password":"password","auth":"dXNlcm5hbWU6cGFzc3dvcmQ="
}}}
清理环境:
$ kubectl delete pod docker
$ kubectl delete secret docker-config

ConfigMap

许多应用程序会从配置文件、命令行参数或环境变量中读取配置信息。这些配置信息需要与 docker image 解耦 ConfigMap API 给我们提供了向容器中注入配置信息的机制,ConfigMap 可以被用来保存单个属性,也可以用来保存整个配置文件。

使用场景

配置信息文件挂载。

使用示例

使用 ConfigMap 中的数据来配置 Redis 缓存。

创建 example-redis-config.yaml 文件,粘贴以下信息:
apiVersion: v1
kind: ConfigMap
metadata:
name: example-redis-config
data:
redis-config: |
maxmemory 2b
maxmemory-policy allkeys-lru

创建 ConfigMap:
$ kubectl apply -f example-redis-config.yaml
configmap/example-redis-config created

创建 example-redis.yaml 文件,粘贴以下信息:
apiVersion: v1
kind: Pod
metadata:
name: redis
spec:
containers:
- name: redis
image: kubernetes/redis:v1
env:
- name: MASTER
  value: "true"
ports:
- containerPort: 6379
resources:
  limits:
    cpu: "0.1"
volumeMounts:
- mountPath: /redis-master-data
  name: data
- mountPath: /redis-master
  name: config
volumes:
- name: data
  emptyDir: {}
- name: config
  configMap:
    name: example-redis-config
    items:
    - key: redis-config
      path: redis.conf

Redis Pod 挂载 ConfigMap 测试:
$ kubectl apply -f example-redis.yaml
pod/redis created

查看挂载效果:
$ kubectl exec -it redis redis-cli
$ 127.0.0.1:6379> CONFIG GET maxmemory
1) "maxmemory"
2) "2097152"
$ 127.0.0.1:6379> CONFIG GET maxmemory-policy
1) "maxmemory-policy"
2) "allkeys-lru"

清理环境:
$ kubectl delete pod redis
$ kubectl delete configmap example-redis-config

EmptyDir

当使用 emptyDir 卷的 Pod 在节点创建时,会在该节点创建一个新的空目录,只要该 Pod 运行在该节点,该目录会一直存在,Pod 内的所有容器可以将改目录挂载到不同的挂载点,但都可以读写 emptyDir 内的文件。当 Pod 不论什么原因被删除,emptyDir 的数据都会永远被删除(一个 Container Crash 并不会在该节点删除 Pod,因此在 Container crash 时,数据不会丢失)。默认情况下,emptyDir 支持任何类型的后端存储:disk、ssd、网络存储。也可以通过设置 emptyDir.medium 为 Memory,kubernetes 会默认 mount 一个 tmpfs(RAM-backed filesystem),因为是 RAM Backed,因此 tmpfs 通常很快。但是会在容器重启或者 crash 时,数据丢失。

使用场景

同一 Pod 内各容器共享存储。

使用示例

在容器 a 中生成 hello 文件,通过容器 b 输出文件内容。

创建 test-emptydir.yaml 文件,粘贴以下信息:
apiVersion: v1
kind: Pod
metadata:
name: test-emptydir
spec:
containers:
- image: alpine
name: container-a
command:
  - /bin/sh
args:
  - -c
  - echo 'I am container-a' >> /cache-a/hello && sleep 3600
volumeMounts:
- mountPath: /cache-a
  name: cache-volume
- image: alpine
name: container-b
command:
  - sleep
  - "3600"
volumeMounts:
- mountPath: /cache-b
  name: cache-volume
volumes:
- name: cache-volume
emptyDir: {
}
创建 Pod:
kubectl apply -f test-emptydir.yaml
pod/test-emptydir created

测试:
$ kubectl exec test-emptydir -c container-b -- cat /cache-b/hello
I am container-a

清理环境:
$ kubectl delete pod test-emptydir

HostPath

将宿主机对应目录直接挂载到运行在该节点的容器中。使用该类型的卷,需要注意以下几个方面:
  1. 使用同一个模板创建的 Pod,由于不同的节点有不同的目录信息,可能会导致不同的结果
  2. 如果 kubernetes 增加了已知资源的调度,该调度不会考虑 hostPath 使用的资源
  3. 如果宿主机目录上已经存在的目录,只可以被 root 可以写,所以容器需要 root 权限访问该目录,或者修改目录权限


使用场景

运行的容器需要访问宿主机的信息,比如 Docker 内部信息/var/lib/docker 目录,容器内运行 cadvisor,需要访问/dev/cgroups。

使用示例

使用 Docker socket binding 模式在列出宿主机镜像列表。

创建 test-hostpath.yaml 文件,粘贴以下信息:
apiVersion: v1
kind: Pod
metadata:
name: test-hostpath
spec:
containers:
- image: docker
name: test-hostpath
command:
  - sleep
  - "3600"
volumeMounts:
- mountPath: /var/run/docker.sock
  name: docker-sock
volumes:
- name: docker-sock
hostPath:
  path: /var/run/docker.sock
  type: Socket

创建 test-hostpath Pod:
$ kubectl apply -f test-hostpath.yaml
pod/test-hostpath created

测试是否成功:
$ kubectl exec test-hostpath docker images
REPOSITORY      IMAGE ID        CREATED         SIZE
docker          639de9917ae1    13 days ago     171MB
...

NFS 存储卷

NFS 卷允许将现有的 NFS(网络文件系统)共享挂载到您的容器中。不像 emptyDir,当删除 Pod 时,nfs 卷的内容被保留,卷仅仅是被卸载。这意味着 nfs 卷可以预填充数据,并且可以在 pod 之间共享数据。 NFS 可以被多个写入者同时挂载。

重要提示:您必须先拥有自己的 NFS 服务器然后才能使用它。

使用场景

不同节点 Pod 使用统一 nfs 共享目录。

使用示例

创建 test-nfs.yaml 文件,粘贴以下信息:
apiVersion: apps/v1
kind: Deployment
metadata:
name: test-nfs
spec:
selector:
matchLabels:
  app: store
replicas: 2
template:
metadata:
  labels:
    app: store
spec:
  volumes:
  - name: data
    nfs:
      server: nfs.server.com
      path: /
  affinity:
    podAntiAffinity:
      requiredDuringSchedulingIgnoredDuringExecution:
      - labelSelector:
          matchExpressions:
          - key: app
            operator: In
            values:
            - store
        topologyKey: "kubernetes.io/hostname"
  containers:
  - name: alpine
    image: alpine
    command:
      - sleep
      - "3600"
    volumeMounts:
    - mountPath: /data
      name: data

创建测试 deployment:
$ kubectl apply -f test-nfs.yaml
deployment/test-nfs created

查看 Pod 运行情况:
$ kubectl get po -o wide
NAME                        READY     STATUS    RESTARTS   AGE       IP              NODE      NOMINATED NODE
test-nfs-859ccfdf55-kkgxj   1/1       Running   0          1m        10.233.68.245   uat05     <none>
test-nfs-859ccfdf55-aewf8   1/1       Running   0          1m        10.233.67.209   uat06     <none>

进入 Pod 中进行测试:
# 进入uat05节点的pod中
$ kubectl exec -it test-nfs-859ccfdf55-kkgxj sh
# 创建文件
$ echo "uat05" > /data/uat05
# 退出uat05节点的pod
$ edit
# 进入uat06节点的pod中
$ kubectl exec -it test-nfs-859ccfdf55-aewf8 sh
# 查看文件内容
$ cat /data/uat05
uat05

清理环境:
$ kubectl delete deployment test-nfs

PersistentVolumeClaim

上面所有例子中我们都是直接将存储挂载到的 pod 中,那么在 kubernetes 中如何管理这些存储资源呢?这就是 Persistent Volume 和 Persistent Volume Claims 所提供的功能。

PersistentVolume 子系统为用户和管理员提供了一个 API,该 API 将如何提供存储的细节抽象了出来。为此,我们引入两个新的 API 资源:PersistentVolume 和 PersistentVolumeClaim。

PersistentVolume(PV)是由管理员设置的存储,它是群集的一部分。就像节点是集群中的资源一样,PV 也是集群中的资源。 PV 是 Volume 之类的卷插件,但具有独立于使用 PV 的 Pod 的生命周期。此 API 对象包含 Volume 的实现,即 NFS、iSCSI 或特定于云供应商的存储系统。
PersistentVolumeClaim(PVC)是用户存储的请求。它与 Pod 相似。Pod 消耗节点资源,PVC 消耗 PV 资源。Pod 可以请求特定级别的资源(CPU 和内存)。声明可以请求特定的大小和访问模式(例如,可以以读/写一次或 只读多次模式挂载)。虽然 PersistentVolumeClaims 允许用户使用抽象存储资源,但用户需要具有不同性质(例如性能)的 PersistentVolume 来解决不同的问题。集群管理员需要能够提供各种各样的 PersistentVolume,这些 PersistentVolume 的大小和访问模式可以各有不同,但不需要向用户公开实现这些卷的细节。对于这些需求,StorageClass 资源可以实现。

在实际使用场景里,PV 的创建和使用通常不是同一个人。这里有一个典型的应用场景:管理员创建一个 PV 池,开发人员创建 Pod 和 PVC,PVC 里定义了 Pod 所需存储的大小和访问模式,然后 PVC 会到 PV 池里自动匹配最合适的 PV 给 Pod 使用。

使用示例

创建 PersistentVolume:
apiVersion: v1
kind: PersistentVolume
metadata:
name: mypv
spec:
capacity:
storage: 5Gi
volumeMode: Filesystem
accessModes:
- ReadWriteOnce
persistentVolumeReclaimPolicy: Recycle
storageClassName: slow
mountOptions:
- hard
- nfsvers=4.0
nfs:
path: /tmp
server: 172.17.0.2

创建 PersistentVolumeClaim:
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
name: myclaim
spec:
accessModes:
- ReadWriteOnce
volumeMode: Filesystem
resources:
requests:
  storage: 5Gi
volumeName: mypv

创建 Pod 绑定 PVC:
kind: Pod
apiVersion: v1
metadata:
name: mypod
spec:
containers:
- name: myfrontend
  image: nginx
  volumeMounts:
  - mountPath: "/var/www/html"
    name: mypd
volumes:
- name: mypd
  persistentVolumeClaim:
    claimName: myclaim

查看 Pod 运行情况验证绑定结果:
$ kubectl get po -o wide
NAME    READY     STATUS    RESTARTS   AGE       IP              NODE      NOMINATED NODE
mypod   1/1       Running   0          1m        10.233.68.249   uat05     <none>
$ kubectl exec -it mypod sh
$ ls /var/www/html

清理环境:
$ kubectl delete pv mypv
$ kubectl delete pvc myclaim
$ kubectl delete po mypod

总结

本次实战中使用了 secret 存储 Docker 认证凭据,更好地控制它的用途,并降低意外暴露的风险。

使用 ConfigMap 对 redis 进行缓存配置,这样即使 Redis 容器挂掉重启 ConfigMap 中的配置依然会生效。接着又使用 emptyDir 来使得同一 Pod 中多个容器的目录共享,在实际应用中开发者通常使用 initContainers 来进行预处理文件,然后通过 emptyDir 传递给 Containers。然后再使用 hostPath 来访问宿主机的资源,当网路 io 达不到文件读写要求时,可考虑固定应用只运行在一个节点上然后使用 hostPath 来解决文件读写速度的要求。

NFS 和 PersistentVolumeClaim 的例子实质上都是试容器挂载的 nfs 服务器共享目录,但这些资源一般都只掌握在了管理员手中,开发人员想要获取这部分资源那么就不是这么友好了,动态存储类(StorageClass)就能很好的解决此类问题。

原文链接:https://choerodon.io/zh/blog/k ... ence/

0 个评论

要回复文章请先登录注册