K8s in Action 阅读笔记——【6】Volumes: attaching disk storage to containers

K8s in Action 阅读笔记——【6】Volumes: attaching disk storage to containers

在前三章中,我们介绍了Pods以及它们与ReplicationControllers、ReplicaSets、DaemonSets、Jobs和Services等Kubernetes资源的交互。现在,我们将回到Pod内部,学习如何让容器访问外部磁盘存储和/或共享存储。

我们曾说过,Pods类似于逻辑主机,其中运行的进程共享CPU、RAM、网络接口等资源。尽管大多数人会期望这些进程也共享磁盘,但实际上并非如此。Pod中的每个容器都有自己的独立文件系统,由容器的镜像提供

每个新的容器都从构建时添加到镜像中的确切文件集合开始运行。由于Pod中的容器可能会重启(因为进程挂掉或活动探针向Kubernetes发出容器已不健康的信号),新容器不会看到前面容器写入文件系统的任何内容,即使新容器在同一Pod中运行。

在某些情况下,例如重启物理机器上的进程,你希望新容器从上一个容器结束的地方继续运行。你可能不需要(或者不想要)持久性保存整个文件系统,但想要保留包含实际数据的目录。

为了解决这个问题,Kubernetes定义了存储卷(Volumes)。存储卷不像Pods一样是顶层资源,而是作为Pod的一部分定义,并与Pod共享生命周期。这意味着,在启动Pod时会创建一个卷,在删除Pod时会销毁该卷。因此,在容器重新启动时,卷的内容将继续存在。新容器重新启动后,便可以看到先前容器写入卷中的所有文件。此外,如果Pod中包含多个容器,那么它们都可以同时使用卷。

6.1 Introducing volumes

6.1.1 Explaining volumes in an example

假设你有一个包含三个容器的Pod(如图6.1所示)。其中一个容器运行一个Web服务器,从/var/htdocs目录提供HTML页面,并将访问日志存储到/var/logs。第二个容器运行一个代理,创建HTML文件并将它们存储在/var/html中。第三个容器处理在/var/logs目录中找到的日志(轮换、压缩、分析或其他操作)。

image-20230529152316396

每个容器都有一个良好定义的单一职责,但单独使用时,没有一个容器有太大的用处。创建一个没有共享磁盘存储的这三个容器的Pod是没有任何意义的。

但是,如果向Pod添加两个卷并在三个容器内部的适当路径上挂载它们(如图6.2所示),你将创建一个远远超出其部件总和的系统。Linux允许你在文件树中的任意位置挂载文件系统。这样做后,挂载文件系统的内容就可以在挂载到的目录中访问。通过在两个容器中挂载相同的卷,它们可以对同一组文件进行操作。在你的情况下,你在三个容器中挂载了两个卷。通过这样做,你的三个容器可以共同工作并做一些有用的事情。

image-20230529152425645

首先,pod中有一个名为publicHtml的卷。该卷在WebServer容器中挂载到/var/htdocs下,因为这是Web服务器提供文件的目录。同样的卷也在ContentAgent容器中挂载,但被挂载到/var/html下,因为代理将文件写到该位置。通过这样挂载单个卷,Web服务器将服务于内容代理生成的内容。

类似地,该pod还有一个日志存储卷logVol。该卷在WebServer和LogRotator容器中都被挂载到/var/logs下。请注意,它不在ContentAgent容器中挂载。即使容器和卷都是pod的一部分,容器也无法访问其文件。如果想让容器能够访问卷中的内容,不仅需要在pod中定义该卷,还需要在容器的规范中定义VolumeMount

在这个例子中,这两个卷都可以最初为空,因此可以使用名为emptyDir的卷类型。Kubernetes还支持其他类型的卷,这些卷在卷的初始化过程中从外部源填充,或者已存在的目录被挂载到卷内。在启动pod的容器之前,会执行填充或挂载卷的操作。

卷绑定到pod的生命周期,并且仅在pod存在时存在,但是根据卷类型,即使pod和卷消失后,卷中的文件可能仍然保持完好,可以稍后被挂载到新卷中。让我们看看有哪些卷类型存在。

6.1.2 Introducing available volume types

这里列出了几种可用的卷类型:

  • emptyDir——用于存储短暂数据的简单空目录。
  • hostPath——用于将工作节点文件系统中的目录挂载到pod中。
  • gitRepo——通过检出Git仓库的内容来初始化卷。
  • nfs——挂载到pod中的NFS共享。
  • gcePersistentDisk(Google Compute Engine持久磁盘)、awsElasticBlockStore(Amazon Web Services弹性块存储卷)、azureDisk(Microsoft Azure磁盘卷)——用于挂载云提供商特定的存储。
  • cinder、cephfs、iscsi、flocker、glusterfs、quobyte、rbd、flexVolume、vsphereVolume、photonPersistentDisk、scaleIO——用于挂载其他类型的网络存储。
  • configMap、secret、downwardAPI——特殊类型的卷,用于将某些Kubernetes资源和集群信息公开给pod。
  • persistentVolumeClaim——使用预配或动态预配的持久存储的方法。(我们将在本章的最后一部分中讨论它们。)

6.2 Using volumes to share data between containers

6.2.1 Using an emptyDir volume

最简单的卷类型是emptyDir卷,因此让我们在第一个例子中看一下如何在pod中定义卷。顾名思义,该卷最初是一个空目录。然后,在pod内运行的应用程序可以将任何它需要的文件写入该目录中。由于卷的生命周期与pod的生命周期绑定,因此删除pod时卷的内容也会丢失。

emptyDir卷对于在同一个pod中运行的容器之间共享文件特别有用。但是它也可以被单个容器使用,用于容器需要暂时将数据写入磁盘的情况。

让我们重新看一下之前的例子,其中Web服务器、内容代理和日志轮换器共享两个卷,但让我们简化一下。你将构建一个仅包含Web服务器容器和内容代理的pod,并使用单个卷来存储HTML内容。

你将使用Nginx作为Web服务器,并使用UNIX fortune命令生成HTML内容。fortune命令每次运行时都会打印出一个随机的引用。你将创建一个脚本,每10秒调用fortune命令,并将其输出存储在index.html中。你可以在Docker Hub上找到现有的Nginx镜像,并使用Docker Hub下luksa/fortune的镜像。

fortune镜像的创建过程

创建一个名为fortune的新目录,然后在其中创建一个名为fortuneloop.sh的shell脚本,其内容如下:

#!/bin/bash 
trap "exit" SIGINT 
mkdir /var/htdocs 
while : 
do 
	echo $(date) Writing fortune to /var/htdocs/index.html 
	/usr/games/fortune > /var/htdocs/index.html 
	sleep 10 
done

然后,在同一目录中,创建一个名为Dockerfile的文件,其中包含以下内容:

FROM ubuntu:latest 
RUN apt-get update ; apt-get -y install fortune 
ADD fortuneloop.sh /bin/fortuneloop.sh 
ENTRYPOINT /bin/fortuneloop.sh

准备好这两个文件后,使用以下两个命令构建并上传镜像到Docker Hub

$ docker build -t <your dockerhup id>/fortune .
$ docker push <your dockerhup id>/fortune

准备好镜像后,创建如下YAML文件:

# fortune-pod.yaml
apiVersion: v1
kind: Pod
metadata:
  name: fortune
spec:
  volumes:
  - name: html
    emptyDir: {
    
    }
  containers:
    - image: luksa/fortune
      name: html-generator
      volumeMounts:
        - name: html
          mountPath: /var/htdocs
    - image: nginx:alpine
      name: web-server
      volumeMounts:
        - name: html
          mountPath: /usr/share/nginx/html
          readOnly: true
      ports:
      - containerPort: 80      
        protocol: TCP

该pod包含两个容器和一个单独的卷,该卷在这两个容器中都被挂载,但是在不同的路径下。当html-generator容器启动时,它开始每10秒向/var/htdocs/index.html文件写入fortune命令的输出。由于卷被挂载到了/var/htdocs下,index.html文件被写入到了卷而不是容器的顶层中。当web-server容器启动后,它开始服务于/usr/share/nginx/html目录中的所有HTML文件(这是Nginx默认提供文件的目录)。因为你将卷挂载在了该位置,Nginx将服务于由运行fortune循环的容器写入的index.html文件。最终的效果是发送HTTP请求到pod的80端口的客户端将获得当前的fortune消息作为响应。

接下来应用上述Pod,来看看效果

$ kubectl port-forward fortune 8080:80
Forwarding from 127.0.0.1:8080 -> 80
$ curl http://localhost:8080
Q:      How many elephants can you fit in a VW Bug?
A:      Four.  Two in the front, two in the back.

Q:      How can you tell if an elephant is in your refrigerator?
A:      There's a footprint in the mayo.

Q:      How can you tell if two elephants are in your refrigerator?
A:      There's two footprints in the mayo.

Q:      How can you tell if three elephants are in your refrigerator?
A:      The door won't shut.

Q:      How can you tell if four elephants are in your refrigerator?
A:      There's a VW Bug in your driveway.

等待10秒后,再看看,会受到不同的消息:

curl http://localhost:8080
A morgue is a morgue is a morgue.  They can paint the walls with aggressively
cheerful primary colors and splashy bold graphics, but it's still a holding
place for the dead until they can be parted out to organ banks.  Not that I
would have cared normally but my viewpoint was skewed.  The relentless
pleasance of the room I sat in seemed only grotesque.
                -- Pat Cadigan, "Mindplayers"

你使用的emptyDir作为卷是创建在托管pod的工作节点的实际磁盘上的,因此其性能取决于节点磁盘的类型。但是,你可以告诉Kubernetes在tmpfs文件系统上(在内存中而不是在磁盘上)创建emptyDir。为此,请将emptyDir的medium设置为Memory,如下所示:

volumes:
	- name: html
	  emptyDir:
	  	medium: Memory

6.2.2 Using a Git repository as the starting point for a volume

一个gitRepo卷基本上是一个emptyDir卷,当pod启动时(但在其容器创建之前),通过克隆Git仓库并检出特定的版本来填充它。图6.3展示了这个过程。

image-20230529171648337

在创建gitRepo卷之后,它并不会与所引用的仓库同步。当你将附加的提交推送到Git仓库时,卷中的文件将不会被更新。但是,如果你的pod由ReplicationController管理,删除该pod将导致一个新的pod被创建,这个新pod的卷将包含最新的提交。

在创建Pod之前,你需要实际的Git存储库,并在其中放置HTML文件。作者在GitHub上创建了一个存储库https://github.com/luksa/kubia-website-example.git。你需要fork它(在GitHub上创建自己的存储库副本),以便稍后可以将更改推送到其中。

创建完fork后,你可以开始创建Pod。这次,你只需要在Pod中使用单个Nginx容器和一个gitRepo卷(确保将gitRepo卷指向你自己的存储库),如下所示。

apiVersion: v1
kind: Pod
metadata:
  name: gitrepo-volume-pod
spec:
  volumes:
  - name: html
    gitRepo:
      # 将仓库改成自己fork后的,方便后续更改
      repository: https://gitee.com/yi-junquan/kubia-website-example.git
      revision: master
      directory: . # 希望将repo克隆到卷的根目录中
  containers:
    - image: nginx:alpine
      name: web-server
      volumeMounts:
        - name: html
          mountPath: /usr/share/nginx/html
          readOnly: true
      ports:
      - containerPort: 80      
        protocol: TCP


创建Pod时,卷首先被初始化为空目录,然后指定的Git存储库被克隆到其中。如果你没有将目录设置为“.”(点),则存储库将被克隆到kubia-website-example子目录中,这不是你想要的。你希望将存储库克隆到卷的根目录中。除了存储库外,你还指定希望Kubernetes在创建卷时检查主分支指向的任何修订版本。

运行Pod后,你可以尝试通过端口转发、服务或从Pod内部执行curl命令(或集群内的任何其他Pod)来访问它。

$ curl http://localhost:8081
<html>
<body>
Hello there.
</body>
</html>

对index.html进行修改,然后删除Pod,可以看到原先的内容被修改了。

$ curl http://localhost:8081
<html>
<body>
I have changed it.
</body>
</html>

Git同步过程不应该在与Nginx Web服务器相同的容器中运行,而应该在第二个容器中运行—— 一个sidecar容器。Sidecar容器是增强Pod的主容器操作的容器。你可以向Pod中添加sidecar,以便使用现有的容器镜像,而不是将额外的逻辑塞入主应用程序的代码中,这会使其过于复杂且不可重用。

要查找一个现有的容器镜像,该镜像可以保持本地目录与Git存储库同步,请转到Docker Hub并搜索“git sync”。你会发现有许多镜像可以实现同步功能。然后在先前示例中的Pod中使用该镜像中的新容器,在新容器中挂载Pod的现有gitRepo卷,并配置Git同步容器以使文件与你的Git存储库同步。如果一切设置正确,你应该会看到Web服务器正在提供的文件与你的GitHub存储库保持同步。

gitRepo卷与emptyDir卷类似,基本上都是专门为包含该卷的Pod创建且仅被该Pod使用的专用目录。当Pod被删除时,卷及其内容也被删除。但是,其他类型的卷不会创建新目录,而是将现有的外部目录挂载到Pod的容器文件系统中。该卷的内容可以在多个Pod实例化之间保留。下一步,我们将学习这些类型的卷。

6.3 Accessing files on the worker node’s filesystem

大多数 Pod 应该对其宿主节点毫不知情,因此不应访问节点文件系统上的任何文件。但是,某些系统级别的 Pod(请记住,通常由 DaemonSet 管理)确实需要读取节点上的文件或使用节点文件系统通过文件系统访问节点设备。Kubernetes 通过 hostPath 卷使此成为可能。

6.3.1 Introducing the hostPath volume

hostPath 卷指向节点文件系统上特定的文件或目录(参见图 6.4)。在同一节点上运行并在其 hostPath 卷中使用相同路径的 Pod 可以看到相同的文件。

image-20230529174247409

hostPath 卷是我们正在介绍的第一种持久存储类型,因为 gitRepo 和 emptyDir 卷的内容在 Pod 被删除时被删除,而 hostPath 卷的内容不会被删除。如果删除 Pod 并且下一个 Pod 使用一个 hostPath 卷,指向主机上相同的路径,则新的 Pod 将看到前一个 Pod 留下的任何内容,但仅在它被调度到与第一个 Pod 相同的节点时。

如果你正在考虑使用 hostPath 卷作为存储数据库数据目录的位置,请再考虑一下。因为卷的内容存储在特定节点的文件系统上,当数据库 Pod 被重新调度到另一个节点时,它将不再看到数据。这就解释了为什么不应该为常规 Pod 使用 hostPath 卷,因为它会使 Pod 对所调度到的节点敏感

6.3.2 Examining system pods that use hostPath volumes

kube-system 命名空间中正在运行多个使用hostPath volumes的此类 Pod

$ kubectl get pod -n kube-system
NAME                                   READY   STATUS    RESTARTS   AGE
chaosblade-operator-64c9587579-9cj96   1/1     Running   0          6d21h
chaosblade-tool-488rv                  1/1     Running   1          15d
chaosblade-tool-bbzw7                  1/1     Running   0          15d
chaosblade-tool-rdwx9                  1/1     Running   3          15d
chaosblade-tool-wchxf                  1/1     Running   4          15d
coredns-59d64cd4d4-264v5               1/1     Running   0          14d
coredns-59d64cd4d4-q4p2m               1/1     Running   0          14d
coredns-59d64cd4d4-qszcl               1/1     Running   0          6d21h
etcd-yjq-k8s1                          1/1     Running   1          16d
kube-apiserver-yjq-k8s1                1/1     Running   1          16d
kube-controller-manager-yjq-k8s1       1/1     Running   2          15d
kube-proxy-2lnh2                       1/1     Running   1          16d
kube-proxy-422l7                       1/1     Running   4          16d
kube-proxy-8mw2q                       1/1     Running   5          16d
kube-proxy-vqx6j                       1/1     Running   1          16d

查看其中一个Pod的信息:kubectl describe pod kube-proxy-2lnh -n kube-system

Volumes:
  kube-proxy:
    Type:      ConfigMap (a volume populated by a ConfigMap)
    Name:      kube-proxy
    Optional:  false
  xtables-lock:
    Type:          HostPath (bare host directory volume)
    Path:          /run/xtables.lock
    HostPathType:  FileOrCreate
  lib-modules:
    Type:          HostPath (bare host directory volume)
    Path:          /lib/modules

hostPath 卷经常用于尝试在单节点集群中使用持久性存储

请记住,只有在需要读取或写入节点上的系统文件时才使用 hostPath 卷。永远不要使用它们来跨 Pod 持久化数据。

6.4 Using persistent storage

当运行在 Pod 中的应用程序需要将数据持久化到磁盘上,并且即使将 Pod 重新调度到另一个节点也需要有相同的数据时,你不能使用我们到目前为止提到的任何类型的卷。因为这些数据需要从任何集群节点都可以访问,所以必须将其存储在某种类型的网络附加存储(NAS)上。

6.5 Decoupling pods from the underlying storage technology

到目前为止,我们探讨的所有持久化卷类型都需要该 Pod 的开发人员了解集群中实际可用的网络存储基础架构。例如,要创建一个基于 NFS 的卷,开发人员必须知道 NFS所在的实际服务器。这与 Kubernetes 的基本思想相违背,Kubernetes 的目标是将实际基础架构对应用程序和开发人员都隐藏起来,使他们不必担心基础架构的细节,并使应用程序可在广泛的云提供商和本地数据中心之间实现可移植性

理想情况下,部署应用程序到 Kubernetes 上的开发人员不应该知道底层使用了什么存储技术,就像他们不必知道运行其 Pod 的物理服务器类型一样。与基础架构相关的处理应该是集群管理员的专属领域。

当开发人员需要为其应用程序获得一定量的持久存储时,他们可以像创建 Pod 时请求 CPU、内存等资源一样向 Kubernetes 请求。系统管理员可以配置集群,以便为应用程序提供其所需的资源。

6.5.1 Introducing PersistentVolumes and PersistentVolumeClaims

为了使应用程序能够在 Kubernetes 集群中请求存储而无需处理基础架构细节,引入了两个新的资源。它们是 PersistentVolumesPersistentVolumeClaims

image-20230529191832121

在 Pod 中使用 PersistentVolume 要比使用普通 Pod 卷更加复杂,因此让我们通过图 6.6 来说明 Pod、PersistentVolumeClaims、PersistentVolumes 和实际底层存储如何相互关联。

开发人员不再向其 Pod 添加技术特定的卷,而是将底层存储设置为集群管理员,并通过 Kubernetes API 服务器创建 PersistentVolume 资源将其注册在 Kubernetes 中。在创建 PersistentVolume 时,管理员指定其大小和支持的访问模式。

当集群用户需要在其某个 Pod 中使用持久化存储时,首先创建 PersistentVolumeClaim 清单,指定所需的最小大小和访问模式。然后,用户向 Kubernetes API 服务器提交 PersistentVolumeClaim 清单,Kubernetes 找到相应的 PersistentVolume 并将卷绑定到Pod中。

然后,PersistentVolumeClaim 可以用作 Pod 中的一个卷。其他用户无法使用相同的 PersistentVolume,直到绑定的 PersistentVolumeClaim 被删除并释放为止。

猜你喜欢

转载自blog.csdn.net/weixin_47692652/article/details/130934825