一文读懂 Kubernetes 存储设计

在 Docker 的设计中,容器内的文件是临时存放的,并且随着容器的删除,容器内部的数据也会一同被清空。不过,我们可以通过在 docker run 启动容器时,使用 --volume/-v 参数来指定挂载卷,这样就能够将容器内部的路径挂载到主机,后续在容器内部存放数据时会就被同步到被挂载的主机路径中。这样做可以保证保证即便容器被删除,保存到主机路径中的数据也仍然存在。

与 Docker 通过挂载卷的方式就可以解决持久化存储问题不同,K8s 存储要面临的问题要复杂的多。因为 K8s 通常会在多个主机部署节点,如果 K8s 编排的 Docker 容器崩溃,K8s 可能会在其他节点上重新拉起容器,这就导致原来节点主机上挂载的容器目录无法使用。

当然也是有办法解决 K8s 容器存储的诸多限制,比如可以对存储资源做一层抽象,通常大家将这层抽象称为卷(Volume)。

K8s 支持的卷基本上可以分为三类:配置信息、临时存储、持久存储。

配置信息

无论何种类型的应用,都会用到配置文件或启动参数。而 K8s 将配置信息进行了抽象,定义成了几种资源,主要有以下三种:

  • ConfigMap

  • Secret

  • DownwardAPI

ConfigMap

ConfigMap 卷通常以一个或多个 key: value 形式存在,主要用来保存应用的配置数据。其中 value 可以是字面量或配置文件。

不过,因为ConfigMap 在设计上不是用来保存大量数据的,所以在 ConfigMap 中保存的数据不可超过 1 MiB(兆字节)。

ConfigMap 有两种创建方式:

  • 通过命令行创建

  • 通过 yaml 文件创建

通过命令行创建

在创建 Configmap 的时候可以通过 --from-literal 参数来指定 key: value,以下示例中 foo=bar 即为字面量形式,bar=bar.txt 为配置文件形式。

$ kubectl create configmap c1 --from-literal=foo=bar --from-literal=bar=bar.txt

bar.txt 内容如下:

baz

通过 kubectl describe 命令查看新创建的名称为 c1 的这个 Configmap 资源内容。

$ kubectl describe configmap c1
Name:         c1
Namespace:    default
Labels:       <none>
Annotations:  <none>

Data
====
bar:
----
baz
foo:
----
bar
Events:  <none>

通过 yaml 文件创建

创建 configmap-demo.yaml 内容如下:

kind: ConfigMap
apiVersion: v1
metadata:
  name: c2
  namespace: default
data:
  foo: bar
  bar: baz

通过 kubectl apply 命令应用这个文件。

$ kubectl apply -f configmap-demo.yaml

$ kubectl get configmap c2
NAME   DATA   AGE
c2     2      11s

$ kubectl describe configmap c2
Name:         c2
Namespace:    default
Labels:       <none>
Annotations:  <none>

Data
====
foo:
----
bar
bar:
----
baz
Events:  <none>

得到的结果跟通过命令行方式创建的 Configmap 没什么两样。

使用示例

完成 Configmap 创建后,来看下如何使用。

创建好的Configmap 有两种使用方法:

  • 通过环境变量将 Configmap 注入到容器内部

  • 通过卷挂载的方式直接将 Configmap 以文件形式挂载到容器。

通过环境变量方式引用

创建 use-configmap-env-demo.yaml 内容如下:

apiVersion: v1
kind: Pod
metadata:
  name: "use-configmap-env"
  namespace: default
spec:
  containers:
    - name: use-configmap-env
      image: "alpine"
      # 一次引用单个值
      env:
        - name: FOO
          valueFrom:
            configMapKeyRef:
              name: c2
              key: foo
      # 一次引用所有值
      envFrom:
        - prefix: CONFIG_  # 配置引用前缀
          configMapRef:
            name: c2
      command: ["echo", "$(FOO)", "$(CONFIG_bar)"]

可以看到我们创建了一个名为 use-configmap-env 的 Pod,Pod 的容器将使用两种方式引用 Configmap 的内容。

第一种是指定 spec.containers.env,它可以为容器引用一个 Configmap 的 key: value 对。其中valueFrom. configMapKeyRef 表明我们要引用 Configmap ,Configmap 的名称为 c2 ,引用的 key 为 foo 。

第二种是指定 spec.containers.envFrom ,只需要通过 configMapRef.name 指定 Configmap 的名称,它就可以一次将 Configmap 中的所有 key: value 传递给容器。其中 prefix 可以给引用的 key 前面增加统一前缀。

Pod 的容器启动命令为 echo $(FOO) $(CONFIG_bar) ,可以分别打印通过 env 和 envFrom 两种方式引用的 Configmap 的内容。

# 创建 Pod
$ kubectl apply -f use-configmap-env-demo.yaml
# 通过查看 Pod 日志来观察容器内部引用 Configmap 结果
$ kubectl logs use-configmap-env
bar baz

结果表明,容器内部可以通过环境变量的方式引用到 Configmap 的内容。

通过卷挂载方式引用

创建 use-configmap-volume-demo.yaml 内容如下:

apiVersion: v1
kind: Pod
metadata:
  name: "use-configmap-volume"
  namespace: default
spec:
  containers:
    - name: use-configmap-volume
      image: "alpine"
      command: ["sleep", "3600"]
      volumeMounts:
        - name: configmap-volume
          mountPath: /usr/share/tmp  # 容器挂载目录
  volumes:
    - name: configmap-volume
      configMap:
        name: c2

这里创建一个名为 use-configmap-volume 的 Pod。通过 spec.containers.volumeMounts 指定容器挂载,name 指定挂载的卷名,mountPath 指定容器内部挂载路径(也就是将 Configmap 挂载到容器内部的指定目录下)。spec.volumes 声明一个卷,而configMap.name 表明了这个卷要引用的 Configmap 名称。

然后可通过如下命令创建 Pod 并验证容器内部能否引用到 Configmap。

# 创建 Pod
$ kubectl apply -f use-configmap-volume-demo.yaml
# 进入 Pod 容器内部
$ kubectl exec -it use-configmap-volume -- sh
# 进入容器挂载目录
/ # cd /usr/share/tmp/
# 查看挂载目录下的文件
/usr/share/tmp # ls
bar  foo
# 查看文件内容
/usr/share/tmp # cat foo
bar
/usr/share/tmp # cat bar
baz

创建完成后,通过 kubectl exec 命令可以进入容器内部。查看容器 /usr/share/tmp/ 目录,可以看到两个以 Configmap 中 key 为名称的文本文件(foo 、bar), key 所对应的 value 内容即为文件内容。

以上就是两种将 Configmap 的内容注入到容器内部的方式。容器内部的应用则可以分别通过读取环境变量、文件内容的方式使用配置信息。

Secret

熟悉了 Configmap 的用法,接下来看下 Secret 的使用。Secret 卷用来给 Pod 传递敏感信息,例如密码、SSH 密钥等。因为虽然Secret 与 ConfigMap 非常类似,但是它会对存储的数据进行 base64 编码。

Secret 同样有两种创建方式:

  • 通过命令行创建

  • 通过 yaml 文件创建

通过命令行创建

Secret 除了通过 --from-literal 参数来指定 key: value,还有另一种方式。即通过 --form-file 参数直接从文件加载配置,文件名即为 key,文件内容作为 value。

# generic 参数对应 Opaque 类型,既用户定义的任意数据
$ kubectl create secret generic s1 --from-file=foo.txt

foo.txt 内容如下:

foo=bar
bar=baz

可以看到与 Configmap 不同,创建 Secret 需要指明类型。上面的示例为命令通过指定 generic 参数来创建类型为 Opaque 的 Secret ,这也是 Secret 默认类型。需要注意的是除去默认类型,Secret 还支持其他类型,可以通过官方文档查看。不过初期学习阶段只使用默认类型即可,通过默认类型就能够实现其他几种类型的功能

$ kubectl describe secret s1
Name:         s1
Namespace:    default
Labels:       <none>
Annotations:  <none>

Type:  Opaque

Data
====
foo.txt:  16 bytes

另外一点与 Configmap 不同的是,Secret 仅展示 value 的字节大小,而不直接展示数据,这是为了保存密文,也是Secret 名称的含义。

通过 yaml 文件创建

创建 secret-demo.yaml 内容如下:

apiVersion: v1
kind: Secret
metadata:
  name: s2
  namespace: default
type: Opaque  # 默认类型
data:
  user: cm9vdAo=
  password: MTIzNDU2Cg==

通过 kubectl apply 命令应用这个文件。


$ kubectl apply -f secret-demo.yaml

$ kubectl get secret s2
NAME   TYPE     DATA   AGE
s2     Opaque   2      59s

$ kubectl describe secret s2
Name:         s2
Namespace:    default
Labels:       <none>
Annotations:  <none>

Type:  Opaque

Data
====
password:  7 bytes
user:      5 bytes

同样能够正确创建出 Secret 资源。但是可以看到通过 yaml 文件创建 Secret 时,指定的 data 内容必须经过 base64 编码,比如我们指定的 user 和 password 都是编码后的结果。

data:
  user: cm9vdAo=
  password: MTIzNDU2Cg==

除此外也可以使用原始字符串方式,这两种方式是等价,示例如下:

data:
  stringData:
   user: root
   password: "123456"

相对而言,我更推荐使用 base64 编码的方式。

使用示例

同 Configmap 使用方式一样,我们也可以通过环境变量或卷挂载的方式来使用 Secret 。以卷挂载方式为例。首先创建 use-secret-volume-demo.yaml 内容如下:

apiVersion: v1
kind: Pod
metadata:
  name: "use-secret-volume-demo"
  namespace: default
spec:
  containers:
    - name: use-secret-volume-demo
      image: "alpine"
      command: ["sleep", "3600"]
      volumeMounts:
        - name: secret-volume
          mountPath: /usr/share/tmp # 容器挂载目录
  volumes:
    - name: secret-volume
      secret:
        secretName: s2

即创建一个名为 use-secret-volume-demo 的 Pod,而 Pod 的容器通过卷挂载方式引用 Secret 的内容。

# 创建 Pod
$ kubectl apply -f use-secret-volume-demo.yaml

# 进入 Pod 容器内部
$ kubectl exec -it use-secret-volume-demo -- sh
# 进入容器挂载目录
/ # cd /usr/share/tmp/
# 查看挂载目录下的文件
/usr/share/tmp # ls
password  user
# 查看文件内容
/usr/share/tmp # cat password 
123456
/usr/share/tmp # cat user 
root

可以发现被挂载到容器内部以后,Secret 的内容将变成明文存储。容器内部应用可以同使用 Configmap 一样来使用 Secret 。

作为可以存储配置信息的 Configmap 和 Secret , Configmap 通常存放普通配置, Secret 则存放敏感配置。

值得一提的是,使用环境变量方式引用 Configmap 或 Secret ,当 Configmap 或 Secret 内容变更时,容器内部引用的内容不会自动更新;使用卷挂载方式引用 Configmap 或 Secret ,当 Configmap 或 Secret 内容变更时,容器内部引用的内容会自动更新。如果容器内部应用支持配置文件热加载,那么通过卷挂载对的方式引用 Configmap 或 Secret 内容将是推荐方式。

DownwardAPI

DownwardAPI 可以将 Pod 对象自身的信息注入到 Pod 所管理的容器内部。

使用示例

创建 downwardapi-demo.yaml 内容如下:

apiVersion: v1
kind: Pod
metadata:
  name: downwardapi-volume-demo
  labels:
    app: downwardapi-volume-demo
  annotations:
    foo: bar
spec:
  containers:
    - name: downwardapi-volume-demo
      image: alpine
      command: ["sleep", "3600"]
      volumeMounts:
        - name: podinfo
          mountPath: /etc/podinfo
  volumes:
    - name: podinfo
      downwardAPI:
        items:
          # 指定引用的 labels
          - path: "labels"
            fieldRef:
              fieldPath: metadata.labels
          # 指定引用的 annotations
          - path: "annotations"
            fieldRef:
              fieldPath: metadata.annotations
# 创建 Pod
$ kubectl apply -f downwardapi-demo.yaml
pod/downwardapi-volume-demo created

# 进入 Pod 容器内部
$ kubectl exec -it downwardapi-volume-demo -- sh
# 进入容器挂载目录
/ # cd /etc/podinfo/
# 查看挂载目录下的文件
/etc/podinfo # ls
annotations  labels
# 查看文件内容
/etc/podinfo # cat annotations 
foo="bar"
kubectl.kubernetes.io/last-applied-configuration="{\"apiVersion\":\"v1\",\"kind\":\"Pod\",\"metadata\":{\"annotations\":{\"foo\":\"bar\"},\"labels\":{\"app\":\"downwardapi-volume-demo\"},\"name\":\"downwardapi-volume-demo\",\"namespace\":\"default\"},\"spec\":{\"containers\":[{\"command\":[\"sleep\",\"3600\"],\"image\":\"alpine\",\"name\":\"downwardapi-volume-demo\",\"volumeMounts\":[{\"mountPath\":\"/etc/podinfo\",\"name\":\"podinfo\"}]}],\"volumes\":[{\"downwardAPI\":{\"items\":[{\"fieldRef\":{\"fieldPath\":\"metadata.labels\"},\"path\":\"labels\"},{\"fieldRef\":{\"fieldPath\":\"metadata.annotations\"},\"path\":\"annotations\"}]},\"name\":\"podinfo\"}]}}\n"
kubernetes.io/config.seen="2022-03-12T13:06:50.766902000Z"
/etc/podinfo # cat labels
app="downwardapi-volume-demo"

不难发现,DownwardAPI 的使用方式同 Configmap 和 Secret 一样,都可以通过卷挂载方式挂载到容器内部以后,在容器挂载的目录下生成对应文件,用来存储 key: value。不同的是 ,因为DownwardAPI能引用的内容已经都在当前 yaml 文件中定义好了,所以DownwardAPI 不需要预先定义,可以直接使用。

小结

ConfigMap 、Secret 、DownwardAPI 这三种 Volume 存在的意义不是为了保存容器中的数据,而是为了给容器传递预先定义好的数据。

临时卷

接下来我们要关注的是临时卷,即临时存储。K8s 支持的临时存储中最常用的就是如下两种:

  • EmptyDir

  • HostPath

临时存储卷会遵从 Pod 的生命周期,与 Pod 一起创建和删除。

EmptyDir

先来看 emptyDir 如何使用。EmptyDir 相当于通过 --volume/-v 挂载时的隐式 Volume 形式使用 Docker。K8s 会在宿主机上创建一个临时目录,被挂载到容器所声明的 mountPath 目录上,即不显式的声明在宿主机上的目录。

使用示例

创建 emptydir-demo.yaml 内容如下:

apiVersion: v1
kind: Pod
metadata:
  name: "emptydir-nginx-pod"
  namespace: default
  labels:
    app: "emptydir-nginx-pod"
spec:
  containers:
    - name: html-generator
      image: "alpine:latest"
      command: ["sh", "-c"]
      args:
       - while true; do
          date > /usr/share/index.html;
          sleep 1;
         done
      volumeMounts:
        - name: html
          mountPath: /usr/share
    - name: nginx
      image: "nginx:latest"
      ports:
        - containerPort: 80
          name: http
      volumeMounts:
        - name: html
          # nginx 容器 index.html 文件所在目录
          mountPath: /usr/share/nginx/html
          readOnly: true
  volumes:
    - name: html
      emptyDir: {}

这里创建一个名为 emptydir-nginx-pod 的 Pod,它包含两个容器 html-generator 和 nginx 。html-generator 用来不停的生成 html 文件,nginx 则是用来展示 html-generator 生成的 index.html 文件的 Web 服务。

具体流程为,html-generator 不停的将当前时间写入到 /usr/share/index.html 下,并将 /usr/share 目录挂载到名为 html 的卷中,而 nginx 容器则将 /usr/share/nginx/html 目录挂载到名为 html 的卷中,这样两个容器通过同一个卷 html 挂载到了一起。

现在通过 kubectl apply 命令应用这个文件:

# 创建 Pod
$ kubectl apply -f emptydir-demo.yaml
pod/emptydir-nginx-pod created

# 进入 Pod 容器内部
$ kubectl exec -it pod/emptydir-nginx-pod -- sh
# 查看系统时区
/ # curl 127.0.0.1
Sun Mar 13 08:40:01 UTC 2022
/ # curl 127.0.0.1
Sun Mar 13 08:40:04 UTC 2022

根据 nginx 容器内部 curl 127.0.0.1 命令输出结果可以发现,nginx 容器 /usr/share/nginx/html/indedx.html 文件内容即为 html-generator 容器 /usr/share/index.html 文件内容。

能够实现此效果的原理是,当我们声明卷类型为 emptyDir: {} 后,K8s 会自动在主机目录上创建一个临时目录。然后将 html-generator 容器 /usr/share/ 目录和 nginx 容器 /usr/share/nginx/html/ 同时挂载到这个临时目录上。这样两个容器的目录就能够实现数据同步。

需要注意的是,容器崩溃并不会导致 Pod 被从节点上移除,因此容器崩溃期间 emptyDir 卷中的数据是安全的。另外,emptyDir.medium 除了可以设成 {},还可以设成 Memory 表示内存挂载。

HostPath

与 emptyDir 不同,hostPath 卷能将主机节点文件系统上的文件或目录挂载到指定的 Pod 中。并且当 Pod 删除时,与之绑定的 hostPath 并不会随之删除。新创建的 Pod挂载到上一个 Pod 使用过的 hostPath时,原 hostPath 中的内容仍然存在。但这仅限于新的 Pod 和已经删除的 Pod 被调度到同一节点上,所以严格来讲 hostPath 仍然属于临时存储。

hostPath 卷的典型应用是将主机节点上的时区通过卷挂载的方式注入到容器内部, 进而保证启动的容器和主机节点时间同步。

使用示例

创建 hostpath-demo.yaml 内容如下:

apiVersion: v1
kind: Pod
metadata:
  name: "hostpath-volume-pod"
  namespace: default
  labels:
    app: "hostpath-volume-pod"
spec:
  containers:
    - name: hostpath-volume-container
      image: "alpine:latest"
      command: ["sleep", "3600"]
      volumeMounts:
        - name: localtime
          mountPath: /etc/localtime
  volumes:
    - name: localtime
      hostPath:
        path: /usr/share/zoneinfo/Asia/Shanghai

要实现时间同步,只需要将主机目录 /usr/share/zoneinfo/Asia/Shanghai 通过卷挂载的方式挂载到容器内部的 /etc/localtime 目录即可。

可以使用 kubectl apply 命令应用这个文件,然后进入 Pod 容器内部使用 date 命令查看容器当前时间。

# 创建 Pod
$ kubectl apply -f hostpath-demo.yaml
pod/hostpath-volume-pod created
# 进入 Pod 容器内部
$ kubectl exec -it hostpath-volume-pod -- sh
# 执行 date 命令输出当前时间
/ # date
Sun Mar 13 17:00:22 CST 2022  # 上海时区

看到输出结果为 Sun Mar 13 17:00:22 CST 2022 ,其中 CST 代表了上海时区,也就是主机节点的时区。如果不通过卷挂载的方式将主机时区挂载到容器内部,则容器默认时区为 UTC 时区。

小结

临时卷内容介绍了 K8s 的临时存储方案以及应用,其中emptyDir 适用范围较少,可以当作临时缓存或者耗时任务检查点等。

需要注意的是,绝大多数 Pod 应该忽略主机节点,不应该访问节点上的文件系统。尽管有时候 DaemonSet 可能需要访问主机节点的文件系统,而且hostPath 可以用来同步主机节点时区到容器,但其他情况下使用较少,特别hostPath 的最佳实践就是尽量不使用 hostPath。

持久卷

临时卷的生命周期与 Pod 相同,当 Pod 被删除时,K8s 会自动删除 Pod 挂载的临时卷。而当 Pod 中的应用需要将数据保存到磁盘,且即使 Pod 被调度到其他节点数据也应该存在时,我们就需要一个真正的持久化存储了。

K8s 支持的持久卷类型非常多,以下是 v1.24 版本支持的卷类型的一部分:

  • awsElasticBlockStore - AWS 弹性块存储(EBS)

  • azureDisk - Azure Disk

  • azureFile - Azure File

  • cephfs - CephFS volume

  • csi - 容器存储接口 (CSI)

  • fc - Fibre Channel (FC) 存储

  • gcePersistentDisk - GCE 持久化盘

  • glusterfs - Glusterfs 卷

  • iscsi - iSCSI (SCSI over IP) 存储

  • local - 节点上挂载的本地存储设备

  • nfs - 网络文件系统 (NFS) 存储

  • portworxVolume - Portworx 卷

  • rbd - Rados 块设备 (RBD) 卷

  • vsphereVolume - vSphere VMDK 卷

看到这么多持久卷类型不必恐慌,因为 K8s 为了让开发者不必关心这背后的持久化存储类型,所以对持久卷有一套独有的思想,即开发者无论使用哪种持久卷,其用法都是一致的。

K8s 持久卷设计架构如下:

Node1 和 Node2 分别代表两个工作节点,当我们在工作节点创建 Pod 时,可以通过 spec.containers.volumeMounts 来指定容器挂载目录,通过 spec.volumes 来指定挂载卷。之前我们用挂载卷挂载了配置信息和临时卷,而挂载持久卷也可以采用同样的方式。每个 volumes 则指向的是下方存储集群中不同的存储类型。

为了保证高可用,我们通常会搭建一个存储集群。通常通过 Pod 来操作存储, 因为 Pod 都会部署在 Node 中,所以存储集群最好跟 Node 集群搭建在同一内网,这样速度更快。而存储集群内部可以使用任何 K8s 支持的持久化存储,如上图的 NFS 、CephFS 、CephRBD 。

使用NFS

持久化挂载方式与临时卷大同小异,我们同样使用一个 Nginx 服务来进行测试。这次我们用 NFS 存储来演示 K8s 对持久卷的支持(NFS 测试环境搭建过程可以参考文章结尾的附录部分),创建 nfs-demo.yaml 内容如下:

apiVersion: v1
kind: Pod
metadata:
  name: "nfs-nginx-pod"
  namespace: default
  labels:
    app: "nfs-nginx-pod"
spec:
  containers:
    - name: nfs-nginx
      image: "nginx:latest"
      ports:
        - containerPort: 80
          name: http
      volumeMounts:
        - name: html-volume
          mountPath: /usr/share/nginx/html/
  volumes:
    - name: html-volume
      nfs:
        server: 192.168.99.101  # 指定 nfs server 地址
        path: /nfs/data/nginx  # 目录必须存在

将容器 index.html 所在目录 /usr/share/nginx/html/ 挂载到 NFS 服务的 /nfs/data/nginx 目录下,在 spec.volumes 配置项中指定 NFS 服务。其中server 指明了 NFS 服务器地址,path 指明了 NFS 服务器中挂载的路径,当然这个路径必须是已经存在的路径。然后通过 kubectl apply 命令应用这个文件。

$ kubectl apply -f nfs-demo.yaml

接下来我们查看这个 Pod 使用 NFS 存储的结果:

在 NFS 节点中我们准备一个 index.html 文件,其内容为 hello nfs。

使用 curl 命令直接访问 Pod 的 IP 地址,即可返回 Nginx 服务的 index.html 内容,结果输出为 hello nfs ,证明 NFS 持久卷挂载成功。

登入 Pod 容器,通过 df -Th 命令查看容器目录挂载信息。可以发现,容器的 /usr/share/nginx/html/ 目录被挂载到 NFS 服务的 /nfs/data/nginx 目录。

现在,当我们执行 kubectl delete -f nfs-demo.yaml 删除 Pod 后,NFS 服务器上数据盘中的数据依然存在,这就是持久卷。

持久卷使用痛点

虽然通过使用持久卷,可以解决临时卷数据易丢失的问题。但目前持久卷的使用方式还存在以下痛点:

  • Pod 开发人员可能对存储不够了解,却要对接多种存储

  • 安全问题,有些存储可能需要账号密码,这些信息不应该暴露给 Pod

因此为了解决这些不足,K8s 又针对持久化存储抽象出了三种资源 PV、PVC、StorageClass。三种资源定义如下:

  • PV 描述的是持久化存储数据卷

  • PVC 描述的是 Pod 想要使用的持久化存储属性,既存储卷申明

  • StorageClass 作用是根据 PVC 的描述,申请创建对应的 PV

PV 和 PVC 的概念可以对应编程语言中的面向对象思想,PVC 是接口,PV 是具体实现。

有了这三种资源类型后,Pod 就可以通过静态供应和动态供应这两种方式来使用持久卷。

静态供应

静态供应不涉及 StorageClass,只涉及到 PVC 和 PV。其使用流程图如下:

使用静态供应时,Pod 不再直接绑定持久存储,而是会绑定到 PVC 上,然后再由 PVC 跟 PV 进行绑定。这样就实现了 Pod 中的容器可以使用由 PV 真正去申请的持久化存储。

使用示例

创建 pv-demo.yaml 内容如下:

apiVersion: v1
kind: PersistentVolume
metadata:
  name: nfs-pv-1g
  labels:
    type: nfs
spec:
  capacity:
    storage: 1Gi
  accessModes:
    - ReadWriteOnce
  storageClassName: nfs-storage
  nfs:
    server: 192.168.99.101
    path: /nfs/data/nginx1
---
apiVersion: v1
kind: PersistentVolume
metadata:
  name: nfs-pv-100m
  labels:
    type: nfs
spec:
  capacity:
    storage: 100m
  accessModes:
    - ReadWriteOnce
  storageClassName: nfs-storage
  nfs:
    server: 192.168.99.101
    path: /nfs/data/nginx2
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: pvc-500m
  labels:
    app: pvc-500m
spec:
  storageClassName: nfs-storage
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 500m
---
apiVersion: v1
kind: Pod
metadata:
  name: "pv-nginx-pod"
  namespace: default
  labels:
    app: "pv-nginx-pod"
spec:
  containers:
    - name: pv-nginx
      image: "nginx:latest"
      ports:
        - containerPort: 80
          name: http
      volumeMounts:
        - name: html
          mountPath: /usr/share/nginx/html/
  volumes:
    - name: html
      persistentVolumeClaim:
        claimName: pvc-500m

其中 yaml 文件定义了如下内容:

  • 两个 PV:申请容量分别为 1Gi 、100m ,通过 spec.capacity.storage 指定,并且他们通过 spec.nfs 指定了 NFS 存储服务的地址和路径。

  • 一个 PVC :申请 500m 大小的存储。

  • 一个 Pod:spec.volumes 绑定名为 pvc-500m 的 PVC,而不是直接绑定 NFS 存储服务。

通过 kubectl apply 命令应用该文件:

$ kubectl apply -f pv-demo.yaml

以上完成创建,结果查看操作则如下:

首先通过 kubectl get pod 命令查看新创建的 Pod,并通过 curl 命令访问 Pod 的 IP 地址,得到 hello nginx1 的响应结果。

然后通过 kubectl get pvc 查看创建的 PVC:

  • STATUS 字段:标识 PVC 已经处于绑定(Bound)状态,也就是与 PV 进行了绑定。

  • CAPACITY 字段:标识 PVC 绑定到了 1Gi 的 PV 上,尽管我们申请的 PVC 大小是 500m ,但由于我们创建的两个 PV 大小分别是 1Gi 和 100m ,K8s 会帮我们选择满足条件的最优解。因为没有刚好等于 500m 大小的 PV 存在,而 100m 又不满足,所以 PVC 会自动与 1Gi 大小的 PV 进行绑定。

通过 kubectl get pv 来查询创建的 PV 资源,可以发现 1Gi 大小的 PV STATUS 字段为 Bound 状态。CLAIM 的值,则标识的是与之绑定的 PVC 的名字。

现在我们登录 NFS 服务器,确认NFS 存储上不同持久卷(PV)挂载的目录下文件内容。

可以看到,/nfs/data/nginx1 目录下的 index.html 内容为 hello nginx1 ,即为上面通过 curl 命令访问 Pod 服务的响应结果。

到此持久卷完成使用,我们总结下整个持久卷使用流程。首先创建一个 Pod, Pod 的 spec.volumes 中绑定 PVC。这里的 PVC 只是一个存储申明,代表我们的 Pod 需要什么样的持久化存储,它不需要标明 NFS 服务地址,也不需要明确要和哪个 PV 进行绑定,只是创建出这个 PVC 即可。接着我们创建两个 PV,PV 也没有明确指出要与哪个 PVC 进行绑定,只需要指出它的大小和 NFS 存储服务地址即可。此时 K8s 会自动帮我们进行 PVC 和 PV 的绑定,这样 Pod 就和 PV 产生了联系,也就可以访问持久化存储了。

其他

细心的你可能已经发现,前文提到静态供应不涉及 StorageClass,但是在定义 PVC 和 PV 的 yaml 文件时,还是都为其指定了 spec.storageClassName 值为 nfs-storage。因为这是一个便于管理的操作方式,只有具有相同 StorageClass 的 PVC 和 PV 才可以进行绑定,这个字段标识了持久卷的访问模式。在 K8s 持久化中支持四种访问模式:

  • RWO - ReadWriteOnce —— 卷可以被一个节点以读写方式挂载

  • ROX - ReadOnlyMany —— 卷可以被多个节点以只读方式挂载

  • RWX - ReadWriteMany —— 卷可以被多个节点以读写方式挂载

  • RWOP - ReadWriteOncePod —— 卷可以被单个 Pod 以读写方式挂载( K8s 1.22 以上版本)

只有具有相同读写模式的 PVC 和 PV 才可以进行绑定。

现在我们来继续实验,通过命令 kubectl delete pod pv-nginx-pod 删除 Pod,再次查看 PVC 和 PV 状态。

从上图可以看到, Pod 删除后 PVC 和 PV 还在,这说明 Pod 删除并不影响 PVC 的存在。而当 PVC 删除时 PV 是否删除,则可以通过设置回收策略来决定。PV 回收策略(pv.spec.persistentVolumeReclaimPolicy)有三种:

  • Retain —— 手动回收,也就是说删除 PVC 后,PV 依然存在,需要管理员手动进行删除

  • Recycle —— 基本擦除 (相当于 rm -rf /*)(新版已废弃不建议使用,建议使用动态供应)

  • Delete —— 删除 PV,即级联删除

现在通过命令 kubectl delete pvc pvc-500m 删除 PVC,查看 PV 状态。

可以看到 PV 依然存在,其 STATUS 已经变成 Released ,此状态下的 PV 无法再次绑定到 PVC,需要由管理员手动删除,这是由回收策略决定的。

注意:绑定了 Pod 的 PVC,如果 Pod 正在运行中,PVC 无法删除。

静态供应的不足

我们一起体验了静态供应的流程,虽然比直接在 Pod 中绑定 NFS 服务更加清晰,但静态供应依然存在不足。

  • 首先会造成资源浪费,如上面示例中,PVC 申请 500m,而没有刚好等于 500m 的 PV 存在,这 K8s 会将 1Gi 的 PV 与之绑定

  • 还有一个致命的问题,如果当前没有满足条件的 PV 存在,则这 PVC 一直无法绑定到 PV 处于 Pending 状态,Pod 也将无法启动,所以就需要管理员提前创建好大量 PV 来等待新创建的 PVC 与之绑定,或者管理员时刻监控是否有满足 PVC 的 PV 存在,如果不存在则马上进行创建,这显然是无法接受的

动态供应

因为静态供应存在不足,K8s 推出一种更加方便的持久卷使用方式,即动态供应。动态供的应核心组件就是 StorageClass——存储类。StorageClass 主要作用有两个:

  • 一是资源分组,我们上面使用静态供应时指定 StorageClass 的目前就是对资源进行分组,便于管理

  • 二是 StorageClass 能够帮我们根据 PVC 请求的资源,自动创建出新的 PV,这个功能是 StorageClass 中 provisioner 存储插件帮我们来做的。

其使用流程图如下:

相较于静态供应,动态供应在 PVC 和 PV 之间增加了存储类。这次 PV 并不需要提前创建好,只要我们申请了 PVC 并且绑定了有 provisioner 功能的 StorageClass,StorageClass 会帮我们自动创建 PV 并与 PVC 进行绑定。

我们可以根据提供的持久化存储类型,分别创建对应的 StorageClass,比如:

  • nfs-storage

  • cephfs-storage

  • rbd-storage

也可以设置一个默认 StorageClass, 通过在创建 StorageClass 资源时指定对应的 annotations 实现:

apiVersion: storage.K8s.io/v1
kind: StorageClass
metadata:
  annotations:
    storageclass.kubernetes.io/is-default-class: "true"
...

当创建 PVC 时不指定 spec.storageClassName ,这个 PVC 就会使用默认 StorageClass。

使用示例

仍然使用 NFS 来作为持久化存储。

首先需要有一个能够支持自动创建 PV 的 provisioner ,这可以在 GitHub 中找到一些开源的实现。示例使用 nfs-subdir-external-provisioner 这个存储插件,具体安装方法非常简单,只需要通过 kubectl apply 命令应用它提供的几个 yaml 文件即可。完成存储插件安装后,可以创建如下 StorageClass:

apiVersion: storage.K8s.io/v1
kind: StorageClass
metadata:
  name: nfs-storage
provisioner: K8s-sigs.io/nfs-subdir-external-provisioner
parameters:
  archiveOnDelete: "true"

这个 StorageClass 指定了 provisioner 为我们安装好的 K8s-sigs.io/nfs-subdir-external-provisioner。
Provisioner 本质上也是一个 Pod,可以通过 kubectl get pod 来查看。指定了 provisioner 的 StorageClass 就有了自动创建 PV 的能力,因为 Pod 能够自动创建 PV。

创建好 provisioner 和 StorageClass 就可以进行动态供应的实验了。首先创建 nfs-provisioner-demo.yaml 内容如下:

kind: PersistentVolumeClaim
apiVersion: v1
metadata:
  name: test-claim
spec:
  storageClassName: nfs-storage
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 1Mi
---
apiVersion: v1
kind: Pod
metadata:
  name: "test-nginx-pod"
  namespace: default
  labels:
    app: "test-nginx-pod"
spec:
  containers:
    - name: test-nginx
      image: "nginx:latest"
      ports:
        - containerPort: 80
          name: http
      volumeMounts:
        - name: html
          mountPath: /usr/share/nginx/html/
  volumes:
    - name: html
      persistentVolumeClaim:
        claimName: test-claim

这里我们只定义了一个 PVC 和一个 Pod,并没有定义 PV。其中 PVC 的 spec.storageClassName 指定为上面创建好的 StorageClass nfs-storage ,然后只需要通过 kubectl apply 命令来创建出 PVC 和 Pod 即可:

$ kubectl apply -f nfs-provisioner-demo.yaml
persistentvolumeclaim/test-claim created
pod/test-nginx-pod created

现在查看 PV、PVC 和 Pod,可以看到 PV 已经被自动创建出来了,并且它们之间实现了绑定关系。

然后登录 NFS 服务,给远程挂载的卷写入 hello nfs 数据。

在 K8s 侧,就可以使用 curl 命令验证挂载的正确性了。

此时如果你通过 kubectl delete -f nfs-provisioner-demo.yaml 删除 Pod 和 PVC,PV 也会跟着删除,因为 PV 的删除策略是 Delete 。不过删除后NFS 卷中的数据还在,只不过被归档成了以 archived 开头的目录。这是 K8s-sigs.io/nfs-subdir-external-provisioner 这个存储插件所实现的功能,这就是存储插件的强大。

完成全部操作后,我们可以发现通过定义指定了 provisioner 的 StorageClass,不仅实现了 PV 的自动化创建,甚至实现了数据删除时自动归档的功能,这就是 K8s 动态供应存储设计的精妙。也可以说动态供应是持久化存储最佳实践。

附录:NFS 实验环境搭建

NFS 全称 Network File System,是一种分布式存储,它能够通过局域网实现不同主机间目录共享。

以下为 NFS 的架构图:由一个 Server 节点和两个 Client 节点组成。

下面列出 NFS 在 Centos 系统中的搭建过程。

Server 节点

# 安装 nfs 工具
yum install -y nfs-utils

# 创建 NFS 目录
mkdir -p /nfs/data/

# 创建 exports 文件,* 表示所有网络上的 IP 都可以访问
echo "/nfs/data/ *(insecure,rw,sync,no_root_squash)" > /etc/exports

# 启动 rpc 远程绑定功能、NFS 服务功能
systemctl enable rpcbind
systemctl enable nfs-server
systemctl start rpcbind
systemctl start nfs-server

# 重载使配置生效
exportfs -r
# 检查配置是否生效
exportfs
# 输出结果如下所示
# /nfs/data      

Client 节点

# 关闭防火墙
systemctl stop firewalld
systemctl disable firewalld

# 安装 nfs 工具
yum install -y nfs-utils

# 挂载 nfs 服务器上的共享目录到本机路径 /root/nfsmount
mkdir /root/nfsmount
mount -t nfs 192.168.99.101:/nfs/data /root/nfsmount

相关推荐

3 分钟了解 NVIDIA 新出的 H200

英伟达在 2023 年全球超算大会上发布了备受瞩目的新一代 AI 芯片——H200 Tensor Core GPU。相较于上一代产品 H100,H200 在性能上实现了近一倍的提升,内存容量翻倍,带宽也显著增加……

github_36774378的博客 2346

深入了解浮点运算——CPU 和 GPU 算力是如何计算的

算力最基本的计量单位是 FLOPS,英文 Floating-point Operations Per Second,即每秒执行的浮点运算次数。浮点运算其实就是带小数的加减乘除运算。

github_36774378的博客 5033

加速计算,为何会成为 AI 时代的计算力“新宠”

加速计算是指使用专用硬件来执行某些类型的计算,其效率要比仅使用通用中央处理器(CPU)高。利用图形处理单元(GPU)、专用集成电路(ASIC)(包括张量处理单元(TPU))和现场可编程逻辑门阵列(FPGA)等设备的强大功能,以更高的速度执行计算,从而加速计算过程,一般我们也将这些设备称之为加速器。这些加速器尤其适用于可被分解为较小并行任务的项目,如高性能计算 (HPC)、深度学习、机器学习、人工智能和大数据分析。通过将指定类型的工作分派到这些专用加速计算硬件上,大大提高了系统的性能和效率。

github_36774378的博客 1055

NVIDIA RTX4090,你能用它做什么?

如果女生选择给男生送礼,我相信一块 RTX4090 就足以让他高兴得死去活来。RTX4090 到底是何方神圣?它凭什么赢得所有男生的“芳心”?

github_36774378的博客 2338

3 分钟看完 NVIDIA GPU 架构及演进

近期随着 AI 市场的爆发式增长,作为 AI 背后技术的核心之一 GPU(图形处理器)的价格也水涨船高。GPU 在人工智能中发挥着巨大的重要,特别是在计算和数据处理方面。目前生产 GPU 主流厂商其实并不多,主要就是 NVIDIA、AMD、Intel、高通等厂家。本文将主要聊聊 NVIDIA GPU 的核心架构及架构演进。

github_36774378的博客 1969

聊聊 GPU 产品选型那些事

随着人工智能的飞速崛起,随之而来的是算力需求的指数级增加,CPU 已经不足以满足深度学习、大模型计算等场景的海量数据处理需求。GPU 作为一种强大的计算工具,无论是高性能计算、图形渲染还是机器学习领域,在各个领域展现出了巨大的潜力和应用前景。说起 GPU,绕不过的当然是 NVIDIA 这个公司了,现在市面上火热的 A100/A800、H100 等 GPU 全是他家的产品。但当你有业务需求或者个人需求,想要采购 GPU 的时候,你会发现各个型号的 GPU 令你眼花缭乱。

github_36774378的博客 981

巅峰对决:英伟达 V100、A100/800、H100/800 GPU 对比

近期,不论是国外的 ChatGPT,还是国内诸多的大模型,让 AIGC 的市场一片爆火。而在 AIGC 的种种智能表现背后,均来自于堪称天文数字的算力支持。以 ChatGPT 为例,据微软高管透露,为 ChatGPT 提供算力支持的 AI 超级计算机,是微软在 2019 年投资 10 亿美元建造一台大型顶尖超级计算机,配备了数万个 NVIDIA A100 GPU,还配备了 60 多个数据中心总共部署了几十万个 NVIDIA GPU 辅助。

github_36774378的博客 1万+

InfiniBand 的前世今生

今年,以 ChatGPT 为代表的 AI 大模型强势崛起,而 ChatGPT 所使用的网络,正是 InfiniBand,这也让 InfiniBand 大火了起来。那么,到底什么是 InfiniBand 呢?下面,我们就来带你深入了解 InfiniBand。

github_36774378的博客 858

揭秘神秘的字符串匹配工具——正则表达式

每个正则表达式都有一个有限自动机(也称为状态机),它接受表达式指定的语言,并使用 Thompson 构造算法将正则表达式转化为一个与之等价的非确定有限状态自动机(NFA)。同时,对于每个有限自动机来说,还有一个描述该自动机所接受语言的正则表达式。该表达式可以通过克莱恩算法或高斯消元法生成。正则表达式的一个著名应用是文本编辑器中的搜索和替换功能,计算机先驱 Ken Thompson( UNIX 操作系统的开发者之一)首先在 20 世纪 60 年代的面向行编辑器 QED 中实现了该功能。

github_36774378的博客 379

深入了解 GPU 互联技术——NVLINK

随着人工智能和图形处理需求的不断增长,多 GPU 并行计算已成为一种趋势。对于多 GPU 系统而言,一个关键的挑战是如何实现 GPU 之间的高速数据传输和协同工作。然而,传统的 PCIe 总线由于带宽限制和延迟问题,已无法满足 GPU 之间通信的需求。为了解决这个问题,NVIDIA 于 2018 年推出了 NVLINK,以提高 GPU 之间的通信效率。

github_36774378的博客 3803

各快 100 倍?4G、5G、6G 相差这么多吗

二狗子今天晚上有点 emo,为什么呢?原来是二狗子心心念很久的一个手游上线了,二狗子兴冲冲地下载了 40 多分钟,终于下载完了游戏。结果打开游戏一看,发现游戏内部的更新写着预计 30 分钟完成更新。这就把二狗子搞得 emo 了。二狗子突然发现自己的室友已经在这款游戏的新手村大杀四方了。“你怎么这么快?”二狗子问。“我用手机流量下载的,5G,当然快。”室友都没看二狗子一眼,沉浸在自己的游戏世界里。5G 真的有这么快吗?好学的二狗子就让游戏保持着后台更新,在网上冲浪学习下知识充实自己。

github_36774378的博客 451

为什么大家都在用 WebP?

WebP 是谷歌在 2010 年提出的一种新型的图片格式,放到现在来讲,已经不算是“新”技术了,毕竟已经有了更新的 JPEG XL 和 AVIF。但是在日常工作中,大家时常会碰到保存下来的图片的后缀是 .webp。那么 WebP 到底有什么魔力,让越来越多的网站“抛弃”常用的 PNG、JPG 而青睐它呢?

github_36774378的博客 948

从驾考科目二到自动驾驶,聊聊 GPU 为什么对自动驾驶很重要

这些数据包含前车和本车的距离,左右两边的道路指示标志,周围汽车的距离,前方道路指示标志,红绿灯信号,马路边行人,非机动车等信息等,大量的图像和视频信息被转换成电脑能处理的二进制信息,这就需要 CPU 和 GPU 的共同加持才能完成安全稳定的自动驾驶工作。

github_36774378的博客 352

聊透 GPU 通信技术——GPU Direct、NVLink、RDMA

AI 技术对算力的需求是非常庞大的,虽然 GPU 的计算能力在持续提升,但是对于 AI 来说,单卡的计算能力就算再强,也是有极限的,这就需要多 GPU 组合。而 GPU 多卡的组合,主要分为单个服务器多张 GPU 卡和多个服务器,每个服务器多张卡这两种情况,无论是单机多卡还是多机多卡,GPU 之间需要有超强的通信支持。

github_36774378的博客 2553

这个 AI 机器人会怼人,它是怎么做到的?

人工智能可以定义为计算机科学的一个分支,其目标是创造与人类智力相当的技术。但智力到底是什么?如何利用技术来复制智力?这个疑问并不存在单一的理解,目前已经有了许多理论和方法来回答这些问题。人工智能的实现方式主要有两种:一种是基于规则的人工智能,通过预先编写的规则和逻辑来实现特定的任务;另一种是基于机器学习的人工智能,通过让计算机从数据中学习和优化算法,来实现特定的任务。

github_36774378的博客 711

如何评估计算机的算力:从 CPU 到 GPU 的演变

这就是 CPU 和 GPU 在计算能力上的区别,这是由它们的架构决定的,CPU 可以执行任何程序,而 GPU转为多任务而生,并发能力强。当然,上面的两张图并不是说 GPU 比 CPU 厉害,事实上,CPU 和 GPU 各自具有不同的特点和优势,选择哪种设备取决于具体的应用场景和需求。其中,CPU(中央处理器)和 GPU(图形处理器)是计算机中最为核心的两个组件,它们的算力和性能对于计算机的整体性能具有决定性的影响。CPU 是计算机的核心组件之一,它的主要功能是执行指令、控制计算机的运行和处理数据等任务。

github_36774378的博客 2376

浅谈 Linux 下 vim 的使用

底线模式的进入方法为:在命令模式下按":“键,Vim 窗口的左下方会出现一个”:"符号,这时就可以输入相关的指令进行操作了。在此模式下可以使用上、下、左、右键或者k、j、h、l键进行光标的移动,还可以对文件内容进行复制、粘贴、替换、删除等操作。当编辑完成,就可以按 Esc 退出插入模式,进入到命令模式。p : 黏贴,此命令是在命令模式下,按照上面的命令复制后,然后光标移动到想要复制的位置,然后按 p 黏贴。直接到某行:在底线模式下直接输入数字就可以到想到的行,如果输入的数字比文档的行数多就会到最后一行。

github_36774378的博客 1308

如何高效实现 MySQL 与 elasticsearch 的数据同步

MySQL 自身简单、高效、可靠,是又拍云内部使用最广泛的数据库。但是当数据量达到一定程度的时候,对整个 MySQL 的操作会变得非常迟缓。而公司内部 robin/logs 表的数据量已经达到 800w,后续又有全文检索的需求。这个需求直接在 MySQL 上实施是难以做到的。

github_36774378的博客 722

国密证书 VS 传统 SSL 证书,到底区别在哪?

国密算法,即国家商用密码算法。是我国自主研发创新的一套数据加密处理系列算法。从 SM1-SM4 分别实现了对称、非对称、摘要等算法功能。尤其适用于嵌入式物联网等相关领域,完成身份认证和数据加解密等功能。

github_36774378的博客 1304

又拍云邵海杨 - 25年Linux老兵,聊聊运维的“术”与“道”

邵海杨,从1998年开始使用Linux至今快25年了,资深Linux系统运维/架构师,DevOps八荣八耻倡导者,喜欢交流分享,活跃于社区,一直积极投身于开源活动的组织和传播。

github_36774378的博客 741

从 AI 绘画到 ChatGPT,聊聊生成式 AI

无论是 AI 绘画,还是现在风靡全球的 ChatGPT,它们都属于 AI 的一个分支——生成式AI(Generative AI)。相较于传统的 AI 模型,生成式 AI 是一种特定类型的 AI,它专注于创建新内容。

github_36774378的博客 5749

从 B 站出发,用 Chrome devTools performance 分析页面如何渲染

页面是如何渲染的?从 Chrome 的性能工具开始,具体看看一个页面是如何进行渲染的,以及进行页面优化时需要关注哪些指标。

github_36774378的博客 1122

【白话科普】聊聊网络架构变革的关键——SDN

SDN 即软件定义网络,它描述了一种网络架构,可以使用软件对单个硬件组件进行集中、智能的管理和控制,使用 OpenFlow 等开放协议允许访问网络设备,如交换机、路由器或防火墙等。

github_36774378的博客 1831

二狗子翻车了,只因上了这个网站……

通过 fetch API 或 XMLHttpRequest 等方式发起的 HTTP 请求,必须要遵守同源策略 ,那什么是同源策略呢?为什么这个策略会导致我们的信息在浏览器上被盗用。

github_36774378的博客 657

从实战出发,聊聊缓存数据库一致性

在云服务中,缓存是极其重要的一点。所谓缓存,其实是一个高速数据存储层。本文主要介绍更新数据库成功后,更新缓存和删除缓存这两个操作的区别和改进方案。

github_36774378的博客 580

当谈论 React hook,我们究竟说的是什么?

React hook是什么,所谓 React hook,就是增强函数组件能力特性的钩子,把不同的特性「钩」进纯函数。React hook 的由来,其实也可以看作是前端技术不断演进的结果。

github_36774378的博客 582

视频网站节约 30% 成本的秘密在这里

今年,爱奇艺宣布2022年第一季度首次实现季度盈利,长视频行业发展十几年来,一直深陷亏损旋涡。爱奇艺的首次实现季度盈利,也意味着降本增效将成为视频平台的发展方向。

github_36774378的博客 958

【白话科普】从“熊猫烧香”聊聊计算机病毒

到底什么是计算机病毒呢?它是什么途径传播的,又会带来哪些危害,我们又该如何在日常使用电脑中防止感染病毒呢?

github_36774378的博客 1633

【实操日记】使用 PyQt5 设计下载远程服务器日志文件程序

PyQt5 是一套 Python 绑定 Digia Qt5 应用的框架,是最强大的 GUI 库之一,使用 PyQt5 我们能够很容易的开发桌面应用,我们如何用 PyQt5 设计一个下载服务器指定日期日志文件的程序。

github_36774378的博客 1112

又拍云之 Keepalived 高可用部署

Keepalived 是什么,它一款基于 VRRP 协议的高可用软件,借助它可以让多台服务器能像路由器一样做 VIP 虚拟地址的冗余转移,从而提升后端服务器的高可靠性。

github_36774378的博客 1282
上一篇: 从实战出发,聊聊缓存数据库一致性
下一篇: 二狗子翻车了,只因上了这个网站……
又拍云 UPYUN
又拍云 UPYUN 又拍云官方账号 又拍云官方账号
博客等级 码龄10年 149粉丝 · 174原创
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值