超10万字整理完k8s的volume卷之本地卷和网络卷详细说明,代码和理论都超详细,建议跟着做一遍实验【emptyDir、hostPath、nfs共享的网络卷】【1】

说明【必看】

在这里插入图片描述

  • 总数将近11万,存储一共有4大类,本地卷、网络卷、持久性存储和动态卷供应 ,为了能够直观理解,所以我分成了3篇来发布;
    初次看的时候,建议3篇都打开,按顺序学习和使实验,有助于理解哈。 后面查阅的时候看标题,点进相应的文章哈
  • 这篇是第一篇

持久性存储 【必看】【2】

看这篇博客【上面的本地卷和网络卷是基础知识,足够用了,如果想更深学习,就去这篇博客】:
超10万字整理完k8s的volume卷之持久性存储-超详细说明,代码和理论都超详细,建议跟着做一遍实验【2】

动态卷供应 【必看】【3】

看这篇博客【分开存放是因为volume内容太多了,放一起很容易导致页面卡顿,上面的本地卷和网络卷是基础知识,足够用了,如果想更深学习,就去这篇博客】【先学持久性存储】:
超10万字整理完k8s的volume卷之动态卷供应-超详细说明,代码和理论都超详细,建议跟着做一遍实验【3】

说明

  • 如果基础不好的,看这边文章,可能会看不懂,我博客的k8s分类中有k8s详细的流程,建议去我分类中从第一片文章整体学习一遍。

容器磁盘上的文件的生命周期是短暂的,这就使得在容器中运行重要应用时会出现一些问题。首先,当容器崩溃 时,kubelet 会重启它,但是容器中的文件将丢失——容器以干净的状态(镜像最初的状态)重新启动。其次,在 Pod 中同时运行多个容器时,这些容器之间通常需要共享文件。Kubernetes 中的 Volume 抽象就很好的解决了 这些问题

测试环境准备

  • 先搭建一套集群【没有的去我k8s分类中找到k8s集群搭建跟着搭一套】
    我现在的集群是一个master和2个node节点。
[root@master volume]# kubectl get nodes
NAME     STATUS   ROLES    AGE   VERSION
master   Ready    master   35d   v1.21.0
node1    Ready    <none>   35d   v1.21.0
node2    Ready    <none>   35d   v1.21.0
[root@master volume]# 
  • 为了测试方便,新建一个volume目录,后续所有的配置文件都放在这里面,然后新建一个volume的ns空间,后续所有的pod都创建在这个ns空间下。
[root@master ~]# mkdir volume
[root@master ~]# 
[root@master ~]# kubectl create ns volume
namespace/volume created
[root@master ~]# kubens volume
Context "context" modified.
Active namespace is "volume".
[root@master ~]# 
[root@master ~]# kubectl config  get-contexts 
CURRENT   NAME           CLUSTER   AUTHINFO   NAMESPACE
*         context        master    ccx        volume
          context1-new   master1   ccx1       default
[root@master ~]# 
[root@master ~]# kubectl get pods
No resources found in volume namespace.
[root@master ~]# 
  • 然后在这个volume路径下生成pod文件并增加一个0秒删除pod的内容
[root@master ~]# cd volume/
[root@master volume]# 
[root@master volume]# kubectl run pod1 --image=nginx --image-pull-policy=IfNotPresent --dry-run=client -o yaml > pod1.yaml
[root@master volume]# 
[root@master volume]# vim pod1.yaml                               
[root@master volume]# cat pod1.yaml
apiVersion: v1
kind: Pod
metadata:
  creationTimestamp: null
  labels:
    run: pod1
  name: pod1
spec:
  terminationGracePeriodSeconds: 0
  containers:
  - image: nginx
    imagePullPolicy: IfNotPresent
    name: pod1
    resources: {}
  dnsPolicy: ClusterFirst
  restartPolicy: Always
status: {}
[root@master volume]# 

# 如果node节点里面没有nginx镜像,创建的pod的状态就不会为running啊
[root@node2 ~]# docker images | grep nginx
nginx                                                             latest     d1a364dc548d   2 months ago    133MB
[root@node2 ~]# 

临时卷

  • 临时卷就是创建的pod,这pod里面的数据是临时存储在本地物理机上的,无果将pod删除,那么这个本地数据也会跟着删除。
    简单来说,就是本地卷的数据是随着pod的存在而存在,当我们删除了pod之后,我们往pod里缩写的数据都被删除掉了,这样的容器是不存储任何数据的——这种我们称之为:无状态的容器【stateless】
    有时我们需要让pod能够存储数据,这样的容器叫做有状态的容器【statefull】

  • 现在我们来创建一个pod测试
    现在先创建一个pod并查看其运行在哪个node节点上

[root@master volume]# kubectl apply -f pod1.yaml 
pod/pod1 created
[root@master volume]# 
[root@master volume]# kubectl get pods -o wide
NAME   READY   STATUS    RESTARTS   AGE   IP               NODE    NOMINATED NODE   READINESS GATES
pod1   1/1     Running   0          5s    10.244.166.164   node1   <none>           <none>
[root@master volume]# 
  • 通过上面我们可以看到pod是运行在node1节点上的,那么我们现在去node1这台主机上,并执行find / -name aaa.txt【这个我们后面会在容器内创建】
    可以看到是没有任何内容的
[root@node1 ~]# find / -name aaa.txt
[root@node1 ~]#
  • 现在我们回到master节点上,进入到创建的这个pod里面,然后创建一个aaa.txt文件
    然后再去到node1节点上执行上面命令,可以看到本地就会生成相应的文件了,这个文件就是临时的卷的文件,用来存储pod里面数据的
[root@master volume]# 
[root@master volume]# kubectl exec -it pod1 -- bash
root@pod1:/# 
root@pod1:/# touch aaa.txt
root@pod1:/# 
# 下面是node1节点上
[root@node1 ~]# find / -name aaa.txt
/var/lib/docker/overlay2/00168907f2abaeffd3c5534c80c870e0e30725d89d51dcaba8d4643a50de08ac/diff/aaa.txt
/var/lib/docker/overlay2/00168907f2abaeffd3c5534c80c870e0e30725d89d51dcaba8d4643a50de08ac/merged/aaa.txt
[root@node1 ~]# 
  • 这时候我们把pod1删除,然后再去node1上查看,可以发现,这个临时生成的文件也会跟着消失
[root@master volume]# kubectl delete pod pod1
pod "pod1" deleted
[root@master volume]# 


# 上面pod删除以后,node1上的文件会隔一会才会被自动删除
[root@node1 ~]# find / -name aaa.txt
/var/lib/docker/overlay2/00168907f2abaeffd3c5534c80c870e0e30725d89d51dcaba8d4643a50de08ac/diff/aaa.txt
[root@node1 ~]# find / -name aaa.txt
[root@node1 ~]# 

本地卷

说明

  • 在容器中的文件在磁盘上是临时存放的,当容器关闭时这些临时文件也会被一并清除。这给容器中运行的特殊应用程序带来一些问题。

  • 首先,当容器崩溃时,kubelet 将重新启动容器,容器中的文件将会丢失——因为容器会以干净的状态重建。

  • 其次,当在一个 Pod 中同时运行多个容器时,常常需要在这些容器之间共享文件。
    Kubernetes 抽象出 Volume 对象来解决这两个问题。

  • Kubernetes Volume卷具有明确的生命周期——与包裹它的 Pod 相同。 因此,Volume比 Pod 中运行的任何容器的存活期都长,在容器重新启动时数据也会得到保留。 当然,当一个 Pod 不再存在时,Volume也将不再存在。更重要的是,Kubernetes 可以支持许多类型的Volume卷,Pod 也能同时使用任意数量的Volume卷。

  • 使用卷时,Pod 声明中需要提供卷的类型 (.spec.volumes 字段)和卷挂载的位置 (.spec.containers.volumeMounts 字段).

  • 定义卷格式为:

...
spec:
  # 定义一个挂载
  volumes:
    - name: 自定义卷名称
      类型:
        卷的参数
      #类型有emptyDir和hostPath 两种,我下面分别展示
...

# 下面是emptyDir类型
...
spec:
  # 定义一个挂载
  volumes:
    - name: 自定义卷名称
     #下面这个就是表示在pod所属的node节点上随机生成一个存储路径
      emptyDir: {}
...

# 下面是hostPath类型
...
spec:
  # 定义一个挂载
  volumes:
    - name: 自定义卷名称
      hostsPath:
        
...

  • 任何在容器里引用卷:
    volumeMounts:
    - name: 上面定义的卷名
      mountPath: /自定义目录【这里面的数据就会同步到node节点上

# 如下
# 我多放2行代码,是让你知道这个片段是放哪里的
  - image: nginx
    imagePullPolicy: IfNotPresent
    name: pod-emp1
    resources: {}
    #下面这是挂载,name是上面的name名称,mountpath是容器内的存储路径
    volumeMounts:
    - name: v1
      mountPath: /aa
  dnsPolicy: ClusterFirst

emptyDir卷

emptyDir——以内存为介质的,数据不会被永久存储,pod删除,数据就跟着删除了

emptyDir的常规使用

配置文件编辑和创建pod
  • 我们先cp一份之前生成的配置文件并编辑该配置文件,最后创建一个pod
[root@master volume]# cp pod1.yaml pod-emp1.yaml
[root@master volume]# vim pod-emp1.yaml     
[root@master volume]# cat pod-emp1.yaml
apiVersion: v1
kind: Pod
metadata:
  creationTimestamp: null
  labels:
    run: pod-emp1
  name: pod-emp1
spec:
  terminationGracePeriodSeconds: 0
  # 定义一个挂载
  volumes:
     # 自定义卷名称
    - name: v1
     #下面这个就是表示在pod所属的node节点上随机生成一个存储路径
      emptyDir: {}
      # 如果需要定义多个,直接复制下面2行即可
    - name: v2
      emptyDir: {}
  containers:
  - image: nginx
    imagePullPolicy: IfNotPresent
    name: pod-emp1
    resources: {}
    #下面这是挂载,name是上面的name名称,mountpath是容器内的存储路径
    volumeMounts:
    - name: v1
      mountPath: /aa
  dnsPolicy: ClusterFirst
  restartPolicy: Always
status: {}
[root@master volume]#                      
[root@master volume]# kubectl apply -f pod-emp1.yaml 
pod/pod-emp1 created
[root@master volume]#
[root@master volume]# kubectl get pods -o wide
NAME       READY   STATUS    RESTARTS   AGE    IP               NODE    NOMINATED NODE   READINESS GATES
pod-emp1   1/1     Running   0          102s   10.244.166.165   node1   <none>           <none>
[root@master volume]# 
查看pod节点自动生成的pod存储路径
  • 可以看到该pod是运行在node1节点上的,那么我们去node1节点上查看该卷,流程如下
  • 1、先用docker在pod所属节点上过滤出该pod的ID
  • 2、通过pod的ID查看详细属性,使用Mount参数过滤,得到Source【主机路径】和Destination【pod挂载路径】
  • 3、查看Source主机路径文件和pod内/aa文件是否一致
[root@node1 ~]# docker ps | grep pod-emp1
477b87b04edc   d1a364dc548d                                          "/docker-entrypoint.…"   3 hours ago    Up 3 hours              k8s_pod-emp1_pod-emp1_volume_cdf68c6e-4068-478f-bf22-f43a9a9f1345_0
9617a0a32d7b   registry.aliyuncs.com/google_containers/pause:3.4.1   "/pause"                 3 hours ago    Up 3 hours              k8s_POD_pod-emp1_volume_cdf68c6e-4068-478f-bf22-f43a9a9f1345_0
[root@node1 ~]# docker inspect 477b87b04edc | grep -A10 Mounts
        "Mounts": [
            {
                "Type": "bind",
                "Source": "/var/lib/kubelet/pods/cdf68c6e-4068-478f-bf22-f43a9a9f1345/volumes/kubernetes.io~empty-dir/v1",
                "Destination": "/aa",
                "Mode": "",
                "RW": true,
                "Propagation": "rprivate"
            },
            {
                "Type": "bind",
[root@node1 ~]# 

# 可以看到,现在主机路径是没有内容的
[root@node1 ~]# ls /var/lib/kubelet/pods/cdf68c6e-4068-478f-bf22-f43a9a9f1345/volumes/kubernetes.io~empty-dir/v1
[root@node1 ~]# 

数据测试
  • 现在回到master节点上,进入到该pod里面,在/aa路径上随便创建2个文件,然后回到node的主机路径,看文件是否同步了
[root@master volume]# kubectl exec -it pod-emp1 -- bash
root@pod-emp1:/# 
root@pod-emp1:/# touch /aa/aa.txt
root@pod-emp1:/#

# 回到node节点,可以看到aa.txt文件就是正常的
[root@node1 ~]# ls /var/lib/kubelet/pods/cdf68c6e-4068-478f-bf22-f43a9a9f1345/volumes/kubernetes.io~empty-dir/v1
aa.txt
[root@node1 ~]# 
  • 根据这个特性,同理,我们在node节点的这个存储路径上创建文件,我们在master中进入pod内部,/aa中也会出现才对
[root@node1 ~]# ls /var/lib/kubelet/pods/cdf68c6e-4068-478f-bf22-f43a9a9f1345/volumes/kubernetes.io~empty-dir/v1
aa.txt
[root@node1 ~]# cd /var/lib/kubelet/pods/cdf68c6e-4068-478f-bf22-f43a9a9f1345/volumes/kubernetes.io~empty-dir/v1
[root@node1 v1]# ls
aa.txt
[root@node1 v1]# 
[root@node1 v1]# touch bb.txt
[root@node1 v1]# 
[root@node1 v1]# ls
aa.txt  bb.txt
[root@node1 v1]#

# 进入到该容器内部,看到/aa内有文件,则正常
[root@master volume]# kubectl exec -it pod-emp1 -- bash
root@pod-emp1:/# cd /aa
root@pod-emp1:/aa# ls
aa.txt  bb.txt
root@pod-emp1:/aa#
  • 前面说过,这种模式的数据是随着pod的存在而存在的,如果pod删了,那么node节点上的数据也会随着消失,现在我们删除这个pod试试
[root@node1 ~]# ls /var/lib/kubelet/pods/cdf68c6e-4068-478f-bf22-f43a9a9f1345/volumes/kubernetes.io~empty-dir/v1
aa.txt  bb.txt
[root@node1 ~]#


# 现在去master节点删除pod
[root@master volume]# kubectl get pods -o wide
NAME       READY   STATUS    RESTARTS   AGE     IP               NODE    NOMINATED NODE   READINESS GATES
pod-emp1   1/1     Running   0          3h20m   10.244.166.165   node1   <none>           <none>
[root@master volume]# 
[root@master volume]# kubectl delete pod pod-emp1 
pod "pod-emp1" deleted
[root@master volume]# 

# 再次回到node节点,查看数据是否确实消失
[root@node1 ~]# ls /var/lib/kubelet/pods/cdf68c6e-4068-478f-bf22-f43a9a9f1345/volumes/kubernetes.io~empty-dir/v1
ls: cannot access /var/lib/kubelet/pods/cdf68c6e-4068-478f-bf22-f43a9a9f1345/volumes/kubernetes.io~empty-dir/v1: No such file or directory
[root@node1 ~]# 

同一个pod里2个容器共享数据配置【emptyDir】

  • 同一个pod里2个容器共享数据配置 我们称之为:sidecar
  • 简单来说,就是我们在一个pod里创建2个容器,比如:一个容器是nginx,另一个容器是fluentd用来分析日志,我们将这2个容器的挂载目录都指向同一个地址,这样就可以实现数据共享了
配置文件编辑和生成pod

一个pod中创建第二个容器,从第二个容器开始,必须自定义command,否则就会端口冲突报错,下面共享存储这个逻辑是这样的,2个容器的存储卷我们都指向的是v1,虽然容器中的挂载目录不一致,但存储卷是一致的,所以在容器c1中是可以看到容器c2写入内容的,数据时刻同步。

[root@master volume]# cat pod-emp2.yaml
apiVersion: v1
kind: Pod
metadata:
  creationTimestamp: null
  labels:
    run: pod-emp1
  name: pod-emp1
spec:
  terminationGracePeriodSeconds: 0
  volumes:
    - name: v1
      emptyDir: {}
    - name: v2
      emptyDir: {}
  containers:
  - image: nginx
    imagePullPolicy: IfNotPresent
    name: c1
    resources: {}
    volumeMounts:
    - name: v1
      mountPath: /aa
  - image: nginx
    imagePullPolicy: IfNotPresent
    command: ["sh","-c","sleep 10000"]
    name: c2
    resources: {}
    volumeMounts:
    - name: v1
      mountPath: /xx
  dnsPolicy: ClusterFirst
  restartPolicy: Always
status: {}
[root@master volume]# 
[root@master volume]# kubectl apply -f pod-emp2.yaml
pod/pod-emp1 created
[root@master volume]# kubectl get pods -o wide
NAME       READY   STATUS    RESTARTS   AGE   IP               NODE    NOMINATED NODE   READINESS GATES
pod-emp1   2/2     Running   0          12s   10.244.166.166   node1   <none>           <none>
[root@master volume]# 
查看pod节点自动生成的pod存储路径
  • 测试前,我们先找到该容器对应的节点存储位置
    多容器的查询和单容器可能略有不同,如果自己不能定位 路径的,可以参考我下面的方式。
    【注:查询结果可以看到,虽然容器挂载路径不一样,但主机存储的路径的一样的】
[root@node1 ~]# docker ps | grep pod-emp1
f80b7cc5b585   d1a364dc548d                                          "sh -c 'sleep 10000'"    8 minutes ago   Up 8 minutes             k8s_c2_pod-emp1_volume_184bb3f1-7e37-4b5b-ba30-78a6bf64a8d6_0
da9b3ca32495   d1a364dc548d                                          "/docker-entrypoint.…"   8 minutes ago   Up 8 minutes             k8s_c1_pod-emp1_volume_184bb3f1-7e37-4b5b-ba30-78a6bf64a8d6_0
3b043db3ba62   registry.aliyuncs.com/google_containers/pause:3.4.1   "/pause"                 8 minutes ago   Up 8 minutes             k8s_POD_pod-emp1_volume_184bb3f1-7e37-4b5b-ba30-78a6bf64a8d6_0
[root@node1 ~]#
[root@node1 ~]# docker inspect f80b7cc5b585 | grep -B3 /xx
        "ExecIDs": null,
        "HostConfig": {
            "Binds": [
                "/var/lib/kubelet/pods/184bb3f1-7e37-4b5b-ba30-78a6bf64a8d6/volumes/kubernetes.io~empty-dir/v1:/xx",
--
            {
                "Type": "bind",
                "Source": "/var/lib/kubelet/pods/184bb3f1-7e37-4b5b-ba30-78a6bf64a8d6/volumes/kubernetes.io~empty-dir/v1",
                "Destination": "/xx",
[root@node1 ~]# 
[root@node1 ~]# docker inspect da9b3ca32495 | grep -B3 /aa
        "ExecIDs": null,
        "HostConfig": {
            "Binds": [
                "/var/lib/kubelet/pods/184bb3f1-7e37-4b5b-ba30-78a6bf64a8d6/volumes/kubernetes.io~empty-dir/v1:/aa",
--
            {
                "Type": "bind",
                "Source": "/var/lib/kubelet/pods/184bb3f1-7e37-4b5b-ba30-78a6bf64a8d6/volumes/kubernetes.io~empty-dir/v1",
                "Destination": "/aa",

# 上面结果得知/xx对应的目录是/var/lib/kubelet/pods/184bb3f1-7e37-4b5b-ba30-78a6bf64a8d6/volumes/kubernetes.io~empty-dir/v1
# /aa对应的目录是/var/lib/kubelet/pods/184bb3f1-7e37-4b5b-ba30-78a6bf64a8d6/volumes/kubernetes.io~empty-dir/v1

# 现在这2个位置里面都是没有文件的
[root@node1 ~]# ls /var/lib/kubelet/pods/184bb3f1-7e37-4b5b-ba30-78a6bf64a8d6/volumes/kubernetes.io~empty-dir/v1
[root@node1 ~]# 
[root@node1 ~]# ls /var/lib/kubelet/pods/184bb3f1-7e37-4b5b-ba30-78a6bf64a8d6/volumes/kubernetes.io~empty-dir/v1
[root@node1 ~]# 
数据测试
  • 现在我们分别进入到这2个容器中创建文件,然后回到主机上查看这2个文件,看文件是否同步生成呢
[root@master volume]# kubectl exec -it pod-emp1 -c c1 -- bash
root@pod-emp1:/# 
root@pod-emp1:/# touch /aa/aa.txt
root@pod-emp1:/# 
root@pod-emp1:/# exit
exit
[root@master volume]# kubectl exec -it pod-emp1 -c c2 -- bash
root@pod-emp1:/# 
root@pod-emp1:/# touch /xx/xx.txt
root@pod-emp1:/# ls /xx
aa.txt  xx.txt
root@pod-emp1:/# 
# 上面可以看到在/xx里面是可以看到/aa里创建的文件的,说明文件同步是没问题的

# 回到node主机查看本地数据是否一致
[root@node1 ~]# ls /var/lib/kubelet/pods/184bb3f1-7e37-4b5b-ba30-78a6bf64a8d6/volumes/kubernetes.io~empty-dir/v1
aa.txt  xx.txt
[root@node1 ~]# 
  • 注意,上面这种数据依然不是永久存储的,容器没了,数据和其文件也就没了
[root@master volume]# kubectl delete pod pod-emp1 
pod "pod-emp1" deleted
[root@master volume]#

[root@node1 ~]# ls /var/lib/kubelet/pods/184bb3f1-7e37-4b5b-ba30-78a6bf64a8d6/volumes/kubernetes.io~empty-dir/v1
ls: cannot access /var/lib/kubelet/pods/184bb3f1-7e37-4b5b-ba30-78a6bf64a8d6/volumes/kubernetes.io~empty-dir/v1: No such file or directory
[root@node1 ~]# 

emptyDir以只读的形式创建pod

这个就是在配置文件中增加一行参数:readOnly: ture
不多说,直接看实例吧
【之前忘了说,现在回来补上的,下面代码是以hostPath的文件展示的哈,不过使用方式是一样的。】

[root@master volume]# cat pod-host.yaml
apiVersion: v1
kind: Pod
metadata:
  creationTimestamp: null
  labels:
    run: pod-host
  name: pod-host
spec:
  terminationGracePeriodSeconds: 0
  volumes:
    - name: v1
      hostPath: 
        path: /xx
  containers:
  - image: nginx
    imagePullPolicy: IfNotPresent
    name: pod-host
    resources: {}
    volumeMounts:
    - name: v1
      mountPath: /aa
      #只读参数加在这
      readOnly: true
  dnsPolicy: ClusterFirst
  restartPolicy: Always
status: {}
[root@master volume]# kubectl apply -f pod-host.yaml
pod/pod-host created
[root@master volume]# kubectl exec -it pod-host -- bash
root@pod-host:/# cd /aa
root@pod-host:/aa# ls    
aa.txt  memload-7.0-1.r29766.x86_64.rpm
root@pod-host:/aa# touch bb
touch: cannot touch 'bb': Read-only file system
root@pod-host:/aa# 

hostPath卷

  • hostPath卷的好处就是,数据会永久存在,不会随着容器的删除而消失。

  • hostPath 卷能将主机node节点文件系统上的文件或目录挂载到你的 Pod 中。 虽然这不是大多数 Pod 需要的,但是它为一些应用程序提供了强大的逃生舱。

  • hostPath 的一些用法有

    • 运行一个需要访问 Docker 引擎内部机制的容器;请使用 - hostPath 挂载 /var/lib/docker 路径。
    • 在容器中运行 cAdvisor 时,以 hostPath 方式挂载 /sys。
    • 允许 Pod 指定给定的 hostPath 在运行 Pod 之前是否应该存在,是否应该创建以及应该以什么方式存在。

支持类型参数说明

  • 定义卷格式为:
...
spec:
  # 定义一个挂载
  volumes:
    - name: 自定义卷名称
      类型:
        卷的参数
      #类型有emptyDir和hostPath 两种,我下面分别展示
...

# 下面是emptyDir类型
...
spec:
  # 定义一个挂载
  volumes:
    - name: 自定义卷名称
     #下面这个就是表示在pod所属的node节点上随机生成一个存储路径
      emptyDir: {}
...

# 下面是hostPath类型
...
spec:
  # 定义一个挂载
  volumes:
    - name: 自定义卷名称
      hostsPath:
        
...

  • 任何在容器里引用卷:
    volumeMounts:
    - name: 上面定义的卷名
      mountPath: /自定义目录【这里面的数据就会同步到node节点上

# 如下
# 我多放2行代码,是让你知道这个片段是放哪里的
  - image: nginx
    imagePullPolicy: IfNotPresent
    name: pod-emp1
    resources: {}
    #下面这是挂载,name是上面的name名称,mountpath是容器内的存储路径
    volumeMounts:
    - name: v1
      mountPath: /aa
  dnsPolicy: ClusterFirst

除了必需的 path 属性之外,用户可以选择性地为 hostPath 卷指定 type。支持的 type 值如下:

取值行为
空字符串(默认)用于向后兼容,这意味着在安装 hostPath 卷之前不会执行任何检查
DirectoryOrCreate如果指定的路径不存在,那么将根据需要创建空目录,权限设置为 0755,具有与 Kubelet 相同的组和所有权
Directory给定的路径必须存在
FileOrCreate如果给定路径的文件不存在,那么将在那里根据需要创建空文件,权限设置为 0644,具有与 Kubelet 相同的组和所有权【前提:文件所在目录必须存在;目录不存在则不能创建文件】
File给定路径上的文件必须存在
Socket在给定路径上必须存在的 UNIX 套接字
CharDevice在给定路径上必须存在的字符设备
BlockDevice在给定路径上必须存在的块设备
  • 注意事项
    当使用这种类型的卷时要小心,因为:
    • 具有相同配置(例如从 podTemplate 创建)的多个 Pod 会由于节点上文件的不同而在不同节点上有不同的行为。
    • 当 Kubernetes 按照计划添加资源感知的调度时,这类调度机制将无法考虑由 hostPath 卷使用的资源。
    • 基础主机上创建的文件或目录只能由 root 用户写入。需要在 特权容器 中以 root 身份运行进程,或者修改主机上的文件权限以便容器能够写入 hostPath 卷。

hostPath的常规使用

配置文件编辑和创建pod
  • 我们创建一个容器,创建规则看代码中2行注释内容吧,挺好理解的。
[root@master volume]# cat pod-host.yaml
apiVersion: v1
kind: Pod
metadata:
  creationTimestamp: null
  labels:
    run: pod-host
  name: pod-host
spec:
  terminationGracePeriodSeconds: 0
  # 下面这个意思就是,我们创建一个卷v1,对应主机的/xx目录
  volumes:
    - name: v1
      hostPath: 
        path: /xx
  containers:
  - image: nginx
    imagePullPolicy: IfNotPresent
    name: pod-host
    resources: {}
    volumeMounts:
    # 这个就是容器中的位置,使用v1卷,对应容器中的/aa目录
    - name: v1
      mountPath: /aa
  dnsPolicy: ClusterFirst
  restartPolicy: Always
status: {}
[root@master volume]# 
[root@master volume]# kubectl apply -f pod-host.yaml
pod/pod-host created
[root@master volume]# 
[root@master volume]# kubectl get pods -o wide 
NAME       READY   STATUS    RESTARTS   AGE   IP               NODE    NOMINATED NODE   READINESS GATES
pod-host   1/1     Running   0          9s    10.244.166.167   node1   <none>           <none>
[root@master volume]#
查看pod节点自动生成的pod存储路径

上面我们看到该pod运行在node1上,我们去node1上查看属性,可以看到/aa对应的路径是/xx了,而不是一堆自动生成的路径;
然后系统会自动给我们创建/xx这个目录,我下面有内容,可能是之前创建过并放了东西在里面,这个不影响哈

[root@node1 ~]# docker ps | grep pod-host
78c5a8144951   d1a364dc548d                                          "/docker-entrypoint.…"   2 minutes ago   Up 2 minutes             k8s_pod-host_pod-host_volume_fc4fdba6-6d95-48bc-95ff-04f7373c2924_0
c551e76f8ab8   registry.aliyuncs.com/google_containers/pause:3.4.1   "/pause"                 2 minutes ago   Up 2 minutes             k8s_POD_pod-host_volume_fc4fdba6-6d95-48bc-95ff-04f7373c2924_0
[root@node1 ~]# 
[root@node1 ~]# docker inspect 78c5a8144951| grep -A10  Mounts
        "Mounts": [
            {
                "Type": "bind",
                "Source": "/xx",
                "Destination": "/aa",
                "Mode": "",
                "RW": true,
                "Propagation": "rprivate"
            },
            {
                "Type": "bind",
[root@node1 ~]# 
[root@node1 ~]# ls /xx
memload-7.0-1.r29766.x86_64.rpm
[root@node1 ~]# 
数据测试

接下来我们进入到容器内创建个文件,再次回到主机,看文件是否会同步
注:容器内也可以看到node节点上给该目录存放的文件内容。

[root@master volume]# kubectl get pods -o wide 
NAME       READY   STATUS    RESTARTS   AGE   IP               NODE    NOMINATED NODE   READINESS GATES
pod-host   1/1     Running   0          9s    10.244.166.167   node1   <none>           <none>
[root@master volume]# 
[root@master volume]# kubectl exec -it pod-host -- bash
root@pod-host:/# 
root@pod-host:/# 
root@pod-host:/# ls /aa
memload-7.0-1.r29766.x86_64.rpm
root@pod-host:/# 
root@pod-host:/# touch /aa/aa.txt
root@pod-host:/# ls /aa/      
aa.txt  memload-7.0-1.r29766.x86_64.rpm
root@pod-host:/# 

# 回到node节点,查看该目录,确实可以看到有文件的
[root@node1 ~]# ls /xx
aa.txt  memload-7.0-1.r29766.x86_64.rpm
[root@node1 ~]# 
  • 前面说过,这种方式挂载的存储卷,不会随着容器的消失而消失,现在测试
[root@master volume]# kubectl delete  pod pod-host 
pod "pod-host" deleted
[root@master volume]# 
# 上面容器删除了,但节点上的数据依然存在
[root@node1 ~]# ls /xx
aa.txt  memload-7.0-1.r29766.x86_64.rpm
[root@node1 ~]# 

hostPath以只读的形式创建pod

这个就是在配置文件中增加一行参数:readOnly: ture
不多说,直接看实例吧

[root@master volume]# cat pod-host.yaml
apiVersion: v1
kind: Pod
metadata:
  creationTimestamp: null
  labels:
    run: pod-host
  name: pod-host
spec:
  terminationGracePeriodSeconds: 0
  volumes:
    - name: v1
      hostPath: 
        path: /xx
  containers:
  - image: nginx
    imagePullPolicy: IfNotPresent
    name: pod-host
    resources: {}
    volumeMounts:
    - name: v1
      mountPath: /aa
      #只读参数加在这
      readOnly: true
  dnsPolicy: ClusterFirst
  restartPolicy: Always
status: {}
[root@master volume]# kubectl apply -f pod-host.yaml
pod/pod-host created
[root@master volume]# kubectl exec -it pod-host -- bash
root@pod-host:/# cd /aa
root@pod-host:/aa# ls    
aa.txt  memload-7.0-1.r29766.x86_64.rpm
root@pod-host:/aa# touch bb
touch: cannot touch 'bb': Read-only file system
root@pod-host:/aa# 

文件备份

  • 文件备份其实就备份完整的yaml文件,我们后面可以通过该文件创建pod,创建出来的内容和备份时的node节点是一摸一样的。
    备份的好处就是,该pod是运行在哪个节点上的,恢复时候就一定会运行在该节点上,数据可以始终保持一致【但不能删主机上的数据,否则容器内数据也会跟着变化】
  • 语法:kubectl get pod pod名称 -o yaml > 自定义名称.yaml
[root@master volume]# kubectl get pod pod-host -o yaml > pod-copy.yaml
[root@master volume]# cat pod-copy.yaml 
apiVersion: v1
kind: Pod
metadata:
  annotations:
    cni.projectcalico.org/podIP: 10.244.166.169/32
    cni.projectcalico.org/podIPs: 10.244.166.169/32
    kubectl.kubernetes.io/last-applied-configuration: |
      {"apiVersion":"v1","kind":"Pod","metadata":{"annotations":{},"creationTimestamp":null,"labels":{"run":"pod-host"},"name":"pod-host","namespace":"volume"},"spec":{"containers":[{"image":"nginx","imagePullPolicy":"IfNotPresent","name":"pod-host","resources":{},"volumeMounts":[{"mountPath":"/aa","name":"v1"}]}],"dnsPolicy":"ClusterFirst","restartPolicy":"Always","terminationGracePeriodSeconds":0,"volumes":[{"hostPath":{"path":"/xx"},"name":"v1"}]},"status":{}}
  creationTimestamp: "2021-08-19T02:30:50Z"
  labels:
    run: pod-host
  name: pod-host
  namespace: volume
  resourceVersion: "5842284"
  uid: 853b07b8-bbd8-4df6-8c7c-3cbeee5d5712
spec:
  containers:
  - image: nginx
    imagePullPolicy: IfNotPresent
    name: pod-host
    resources: {}
    terminationMessagePath: /dev/termination-log
    terminationMessagePolicy: File
    volumeMounts:
    - mountPath: /aa
      name: v1
    - mountPath: /var/run/secrets/kubernetes.io/serviceaccount
      name: kube-api-access-mlfvg
      readOnly: true
  dnsPolicy: ClusterFirst
  enableServiceLinks: true
  nodeName: node1
  preemptionPolicy: PreemptLowerPriority
  priority: 0
  restartPolicy: Always
  schedulerName: default-scheduler
  securityContext: {}
  serviceAccount: default
  serviceAccountName: default
  terminationGracePeriodSeconds: 0
  tolerations:
  - effect: NoExecute
    key: node.kubernetes.io/not-ready
    operator: Exists
    tolerationSeconds: 300
  - effect: NoExecute
    key: node.kubernetes.io/unreachable
    operator: Exists
    tolerationSeconds: 300
  volumes:
  - hostPath:
      path: /xx
      type: ""
    name: v1
  - name: kube-api-access-mlfvg
    projected:
      defaultMode: 420
      sources:
      - serviceAccountToken:
          expirationSeconds: 3607
          path: token
      - configMap:
          items:
          - key: ca.crt
            path: ca.crt
          name: kube-root-ca.crt
      - downwardAPI:
          items:
          - fieldRef:
              apiVersion: v1
              fieldPath: metadata.namespace
            path: namespace
status:
  conditions:
  - lastProbeTime: null
    lastTransitionTime: "2021-08-19T02:30:51Z"
    status: "True"
    type: Initialized
  - lastProbeTime: null
    lastTransitionTime: "2021-08-19T02:30:53Z"
    status: "True"
    type: Ready
  - lastProbeTime: null
    lastTransitionTime: "2021-08-19T02:30:53Z"
    status: "True"
    type: ContainersReady
  - lastProbeTime: null
    lastTransitionTime: "2021-08-19T02:30:50Z"
    status: "True"
    type: PodScheduled
  containerStatuses:
  - containerID: docker://2a1d512a9a942867e2c58a57ee26191b6ec92bd2fc011714a5a0d4f32a1e6929
    image: nginx:latest
    imageID: docker://sha256:d1a364dc548d5357f0da3268c888e1971bbdb957ee3f028fe7194f1d61c6fdee
    lastState: {}
    name: pod-host
    ready: true
    restartCount: 0
    started: true
    state:
      running:
        startedAt: "2021-08-19T02:30:52Z"
  hostIP: 192.168.59.143
  phase: Running
  podIP: 10.244.166.169
  podIPs:
  - ip: 10.244.166.169
  qosClass: BestEffort
  startTime: "2021-08-19T02:30:51Z"
[root@master volume]# 
  • 恢复
    这个和创建pod是一样的方式,只是配置文件内容不一样而已
[root@master volume]# kubectl delete pod pod-host 
pod "pod-host" deleted
[root@master volume]# kubectl apply -f pod-copy.yaml 
pod/pod-host created
[root@master volume]# kubectl exec -it pod-host -- bash
root@pod-host:/# ls /aa          
aa.txt  memload-7.0-1.r29766.x86_64.rpm
root@pod-host:/# 

注意事项

我们先做一个测试,配置文件不变,我们重新创建一个pod,可以看到也是运行在node1上的,并且进入容器可以看到之前创建的文件内容,说明该容器数据具有持久性了,但这种说法不严谨,我们能看到之前创建的文件内容是因为该pod恰好也是运行在node1节点上的,如果该 pod重新创建过程中,他的节点不在node1上了,那么数据其实也就没 了【也就是说,我们现在看到的数据,其实是node1上的,而非容器内】。

[root@master volume]# kubectl apply -f pod-host.yaml
pod/pod-host created
[root@master volume]# kubectl get pods -o wide 
NAME       READY   STATUS    RESTARTS   AGE   IP               NODE    NOMINATED NODE   READINESS GATES
pod-host   1/1     Running   0          4s    10.244.166.168   node1   <none>           <none>
[root@master volume]# kubectl exec -it pod-host -- bash
root@pod-host:/# ls /a
ls: cannot access '/a': No such file or directory
root@pod-host:/# ls /aa
aa.txt  memload-7.0-1.r29766.x86_64.rpm
root@pod-host:/# 
  • 为了证实我刚才所说的,看到的数据是node1节点上,而非容器内的,我们现在删除该容器,并指定起节点为node1,我们再次看这个aa文件,里面就会没内容了。
[root@master volume]# cat pod-host.yaml 
apiVersion: v1
kind: Pod
metadata:
  creationTimestamp: null
  labels:
    run: pod-host
  name: pod-host
spec:
  nodeName: node2
  terminationGracePeriodSeconds: 0
  volumes:
    - name: v1
      hostPath: 
        path: /xx
  containers:
  - image: nginx
    imagePullPolicy: IfNotPresent
    name: pod-host
    resources: {}
    volumeMounts:
    - name: v1
      mountPath: /aa
  dnsPolicy: ClusterFirst
  restartPolicy: Always
status: {}
[root@master volume]# kubectl apply -f pod-host.yaml
pod/pod-host created
[root@master volume]# 
[root@master volume]# kubectl get pods -o wide
NAME       READY   STATUS    RESTARTS   AGE   IP              NODE    NOMINATED NODE   READINESS GATES
pod-host   1/1     Running   0          12s   10.244.104.41   node2   <none>           <none>
[root@master volume]# kubectl exec -it pod-host -- bash
root@pod-host:/# ls /aa
memload-7.0-1.r29766.x86_64.rpm
root@pod-host:/# 
root@pod-host:/# touch /aa/bb.txt
root@pod-host:/# ls /aa
bb.txt  memload-7.0-1.r29766.x86_64.rpm
root@pod-host:/# 

# 创建了bb,我们去node2上查看
[root@node2 ~]# ls /xx
bb.txt  memload-7.0-1.r29766.x86_64.rpm
[root@node2 ~]# 
  • 解决上面问题呢,我们可以使用网络卷

网络卷

说明

  • 我们上面本地卷中,hostpath是映射到主机本地目录的,这样的好处就是数据不会随着容器的消失而消失,但是也会发生一些极端情况,就是说当容器重建了,但运行在其他节点上了,那么数据也会没了,解决这种办法呢,就是创建卷的时候,我们映射到网络卷上,无论pod是运行在哪个容器上,卷对应的地址都是同一个网络卷,这样,无论pod如何重建,卷都是在网络卷上,所以数据不会有任何变化【我这样说能理解我想表达的意思吗?】
  • 这个网络卷,是一台单独的主机,我们在上面做一个共享目录,然后创建pod的时候卷就对应到这个共享目录上,因为卷是一台单独的主机,所以我们就不需要考虑卷是映射到哪台node节点上了。
  • 支持网络数据卷
    • nfs
    • iscsi
    • glusterfs
    • awsElasticBlockStore
    • cephfs
    • azureFileVolume
    • azureDiskVolume
    • vsphereVolume
  • 任何共享形式都行,我这使用nfs吧,这个简单,也容易理解。
    nfs中的配置文件格式如下
...
spec:
  terminationGracePeriodSeconds: 0
  volumes:
    - name: 【自定义名称】
      nfs:
        server: 【nfs主机ip】
        path: 【nfs共享目录】
...

nfs共享配置【服务端】

  • 先准备一台主机,这个主机可以不用是集群中的某一台,只要和集群能互通就行。
    我使用的是一台非集群内的虚拟机,ip是:192.168.59.156
  • 先安装nfs服务:yum install nfs-utils -y
[root@etcd1 ~]# yum install nfs-utils -y
。。。
已安装:
  nfs-utils.x86_64 1:1.3.0-0.61.el7                                             

作为依赖被安装:
  gssproxy.x86_64 0:0.7.0-21.el7           keyutils.x86_64 0:1.5.8-3.el7        
  libbasicobjects.x86_64 0:0.1.1-32.el7    libcollection.x86_64 0:0.7.0-32.el7  
  libevent.x86_64 0:2.0.21-4.el7           libini_config.x86_64 0:1.3.1-32.el7  
  libnfsidmap.x86_64 0:0.25-19.el7         libpath_utils.x86_64 0:0.2.1-32.el7  
  libref_array.x86_64 0:0.1.5-32.el7       libtirpc.x86_64 0:0.2.4-0.15.el7     
  libverto-libevent.x86_64 0:0.2.5-4.el7   quota.x86_64 1:4.01-17.el7           
  quota-nls.noarch 1:4.01-17.el7           rpcbind.x86_64 0:0.2.0-47.el7        
  tcp_wrappers.x86_64 0:7.6-77.el7        

更新完毕:
  selinux-policy.noarch 0:3.13.1-229.el7                                        

作为依赖被升级:
  libselinux.x86_64 0:2.5-14.1.el7                                              
  libselinux-python.x86_64 0:2.5-14.1.el7                                       
  libselinux-utils.x86_64 0:2.5-14.1.el7                                        
  libsemanage.x86_64 0:2.5-14.el7                                               
  libsepol.x86_64 0:2.5-10.el7                                                  
  policycoreutils.x86_64 0:2.5-29.el7                                           
  selinux-policy-targeted.noarch 0:3.13.1-229.el7                               

完毕!
[root@etcd1 ~]# 
  • 创建共享目录,添加权限
    可以创建任意目录,我使用/data
[root@etcd1 ~]# mkdir  /data
[root@etcd1 ~]# chmod 777 /data
[root@etcd1 ~]# 
  • 编辑配置文件:/etc/exports
    我对任何ip开放,所以用**可以替换为master节点IP,但没必要限制。
[root@etcd1 ~]# cat /etc/exports
# 添加目录给相应网段访问并添加读写权限
# no_root_squash——不修改root权限,一定要加,不加就有问题
/data *(rw,async,no_root_squash)
[root@etcd1 ~]#
  • 开启rpc服务
[root@etcd1 ~]# systemctl enable rpcbind --now
  • 启动服务并设置开机自启
[root@etcd1 ~]# systemctl enable nfs-server.service --now
Created symlink from /etc/systemd/system/multi-user.target.wants/nfs-server.service to /usr/lib/systemd/system/nfs-server.service.
[root@etcd1 ~]# 
  • 让共享立即生效:exportfs -arv
[root@etcd1 ~]# exportfs -arv
exporting *:/data
[root@etcd1 ~]# 

nfs客户端配置【node节点操作】

  • 安装服务:yum install nfs-utils -y
    【所有node节点操作】
[root@node1 ~]# yum install nfs-utils -y

[root@node2 ~]# yum install nfs-utils -y
  • 启动服务并设置开机自启
[root@node1 ~]# systemctl enable nfs --now
Created symlink from /etc/systemd/system/multi-user.target.wants/nfs-server.service to /usr/lib/systemd/system/nfs-server.service.
[root@node1 ~]# 


[root@node2 ~]# systemctl enable nfs --now
Created symlink from /etc/systemd/system/multi-user.target.wants/nfs-server.service to /usr/lib/systemd/system/nfs-server.service.
[root@node2 ~]# 
  • 验证【下面的ip是nfs共享主机的ip】
[root@node1 ~]# showmount -e 192.168.59.156
Export list for 192.168.59.156:
/data *
[root@node1 ~]# 

[root@node2 ~]# showmount -e 192.168.59.156
Export list for 192.168.59.156:
/data *
[root@node2 ~]#

配置文件编辑【master节点操作】

[root@master volume]# cat pod-nfs.yaml
apiVersion: v1
kind: Pod
metadata:
  creationTimestamp: null
  labels:
    run: pod-emp1
  name: pod-emp1
spec:
  terminationGracePeriodSeconds: 0
  volumes:
    # 自定义卷名称
    - name: v1 
      nfs:
        # nfs-主机ip【showmount -e nfsip可以查看的】
        server: 192.168.59.156
        # nfs共享目录【showmount -e nfsip可以查看的】
        path: /data
  containers:
  - image: nginx
    imagePullPolicy: IfNotPresent
    name: pod-emp1
    resources: {}
    volumeMounts:
    # 使用上面卷名称
    - name: v1
       # 这个是容器内的存储文件【自定义的】
      mountPath: /aa
  dnsPolicy: ClusterFirst
  restartPolicy: Always
status: {}
[root@master volume]# kubectl apply -f pod-nfs.yaml
pod/pod-emp1 created
[root@master volume]# 
[root@master volume]# kubectl get pods -o wide
NAME       READY   STATUS    RESTARTS   AGE   IP               NODE    NOMINATED NODE   READINESS GATES
pod-emp1   1/1     Running   0          46s   10.244.166.172   node1   <none>           <none>
[root@master volume]# 
[root@master volume]# kubectl exec -it pod-emp1 -- bash
root@pod-emp1:/# 
root@pod-emp1:/# ls
aa   boot  docker-entrypoint.d   etc   lib    media  opt   root  sbin  sys  usr
bin  dev   docker-entrypoint.sh  home  lib64  mnt    proc  run   srv   tmp  var
root@pod-emp1:/# ls /aa
root@pod-emp1:/# touch /aa/aa.txt
root@pod-emp1:/# ls /aa
aa.txt
root@pod-emp1:/# 

数据测试

我们进入该容器,创建一个文件,然后去nfs共享主机,查看共享目录,确定数据是否同步【同步以后为正常】

[root@master volume]# kubectl get pods -o wide
NAME       READY   STATUS    RESTARTS   AGE   IP               NODE    NOMINATED NODE   READINESS GATES
pod-emp1   1/1     Running   0          46s   10.244.166.172   node1   <none>           <none>
[root@master volume]# 
[root@master volume]# kubectl exec -it pod-emp1 -- bash
root@pod-emp1:/# 
root@pod-emp1:/# ls
aa   boot  docker-entrypoint.d   etc   lib    media  opt   root  sbin  sys  usr
bin  dev   docker-entrypoint.sh  home  lib64  mnt    proc  run   srv   tmp  var
root@pod-emp1:/# ls /aa
root@pod-emp1:/# touch /aa/aa.txt
root@pod-emp1:/# ls /aa
aa.txt
root@pod-emp1:/# 



# 回到nfs节点,有数据为正常,同理,这里面创建的数据,容器内部也能看到才对
[root@etcd1 ~]# ls /data/
aa.txt
[root@etcd1 ~]# 
[root@etcd1 ~]# touch /data/test_20210819
[root@etcd1 ~]# ls /data/
aa.txt  test_20210819
[root@etcd1 ~]# 

# 容器内部
root@pod-emp1:/# ls /aa
aa.txt  test_20210819
root@pod-emp1:/#

更换节点测试数据是否依然存在

刚才的默认创建是在node1节点上的,现在我们自定义到node2节点上,然后进入容器查看,如果有数据则正常

[root@master volume]# kubectl delete pod pod-emp1                         
[root@master volume]# cat pod-nfs.yaml
apiVersion: v1
kind: Pod
metadata:
  creationTimestamp: null
  labels:
    run: pod-emp1
  name: pod-emp1
spec:
  nodeName: node2
  terminationGracePeriodSeconds: 0
  volumes:
    - name: v1
      nfs:
        server: 192.168.59.156
        path: /data
  containers:
  - image: nginx
    imagePullPolicy: IfNotPresent
    name: pod-emp1
    resources: {}
    volumeMounts:
    - name: v1
      mountPath: /aa
  dnsPolicy: ClusterFirst
  restartPolicy: Always
status: {}
[root@master volume]# kubectl apply -f pod-nfs.yaml
pod/pod-emp1 created
[root@master volume]# 
[root@master volume]# kubectl get pods -o wide
NAME       READY   STATUS    RESTARTS   AGE   IP              NODE    NOMINATED NODE   READINESS GATES
pod-emp1   1/1     Running   0          9s    10.244.104.42   node2   <none>           <none>
[root@master volume]# 
[root@master volume]# kubectl exec -it pod-emp1 -- bash
root@pod-emp1:/# 
root@pod-emp1:/# ls /aa
aa.txt  test_20210819
root@pod-emp1:/# 
root@pod-emp1:/# touch /aa/node-test1
root@pod-emp1:/# ls /aa/          
aa.txt  node-test1  test_20210819
root@pod-emp1:/# 

# nfs节点
[root@etcd1 ~]# ls /data/
aa.txt  node-test1  test_20210819
[root@etcd1 ~]# 
  • 2
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

҉人间无事人

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值