共计 4787 个字符,预计需要花费 12 分钟才能阅读完成。
前言
临时性存储是容器的一个很大的买点。“根据一个镜像启动容器,随意变更,然后停止变更重启一个容器。你看,一个全新的文件系统又诞生了。”
在 docker 的语境下:
# docker run -it centos
[root@d42876f95c6a /]# echo “Hello world” > /hello-file
[root@d42876f95c6a /]# exit
exit
# docker run -it centos
[root@a0a93816fcfe /]# cat /hello-file
cat: /hello-file: No such file or directory
当我们围绕容器构建应用程序时,这个临时性存储非常有用。它便于水平扩展:我们只是从同一个镜像创建多个容器实例,每个实例都有自己独立的文件系统。它也易于升级:我们只是创建了一个新版本的映像,我们不必担心从现有容器实例中保留任何内容。它可以轻松地从单个系统移动到群集,或从内部部署移动到云:我们只需要确保集群或云可以访问 registry 中的镜像。而且它易于恢复:无论我们的程序崩溃对文件系统造成了什么损坏,我们只需要从镜像重新启动一个容器实例,之后就像从未发生过故障一样。
因此,我们希望容器引擎依然提供临时存储。但是从教程示例转换到实际应用程序时,我们确实会遇到问题。真实的应用必修在某个地方存储数据。通常,我们将状态保存到某个数据存储中(SQL 或是 NOSQL)。这也引来了同样的问题。数据存储也是位于容器中吗?理想情况下,答案是肯定的,这样我们可以利用和应用层相同的滚动升级,冗余和故障转移机制。但是,要在容器中运行我们的数据存储,我们再也不能满足于临时存储。容器实例需要能够访问持久存储。
如果使用 docker 管理持久性存储,有两种主流方案:我们可以在宿主机文件系统上指定一个目录,或者是由 Docker 管理存储:
# docker volume create data
data
# docker run -it -v data:/data centos
[root@5238393087ae /]# echo “Hello world” > /data/hello-file
[root@5238393087ae /]# exit
exit
# docker run -it -v data:/data centos
[root@e62608823cd0 /]# cat /data/hello-file
Hello world
Docker 并不会保留第一个容器的根目录,但是它会保留“data”卷。而该卷会被再次挂载到第二个容器上。所以该卷是持久存储。
在单节点系统上这样的方法是 ok 的。但是在一个容器集群环境下如 Kubernetes 或是 Docker Swarm,情况会变得复杂。如果我们的数据存储容器可能在上百个节点中的任意一个上启动,而且可能从一个节点随时迁移到另一个节点,我们无法依赖于单一的文件系统来存储数据,我们需要一个能够感知到容器的分部署存储的方案,从而无缝集成。
容器持久化的需求
在深入容器持久化的方案之前,我们应该先了解一下这个方案应该满足什么特性,从而更好的理解各种容器持久化方案的设计思路。
冗余
将应用移动到容器中并且将容器部署到一个编排环境的原因在于我们可以有更多的物理节点,从而可以支持部分节点当掉。同理,我们也希望持久化存储能够容忍磁盘和节点的崩溃并且继续支持应用运行。在持久化的场景下,冗余的需求更加重要了,因为我们无法忍受任何数据的丢失。
分布式
冗余的持久化驱动我们使用某种分布式策略,至少在磁盘层面上。但是我们还希望通过分布式存储来提高性能。当我们将容器水平扩展到成百上千个节点上是,我们不希望这些节点竞争位于同一个磁盘上的数据。所以当我们将服务部署到各个区域的环境上来减少用户延时时,我们还希望将存储也同时分布式部署。
动态的
容器架构持续变更。新版本不断的被构建,更新,应用被添加或是移除。测试用例被创建并启动,然后被删除。在这个架构下,需要能够动态的配置和释放存储。事实上,配置存储应当和我们声明容器实例,服务和网络连通性一样通过声明来实现。
灵活性
容器技术在飞速发展,我们需要能够引入新的存储策略,并且将应用移植到新的存储架构上。我们的存储策略需要能够支持任何底层架构,从开发人员用于测试的单节点到一个开放的云环境。
透明性
我们需要为各种类型的应用提供村塾,而且我们需要持续更新存储方案。这意味着我们不应该将应用强关联与一个存储方案。因此,存储需要看上去像是原生的,也就是对上层用户来说仿佛是一个文件系统,或者是某种现有的,易于理解的 API。
云原生存储
另一种说法是我们希望容器存储解决方案是“Cloud Native”(云原生的)。云原生计算组织(CNCF)定义了云原生系统的三个属性。这些属性也适用于存储:
容器打包:我们的物理或虚拟存储位于容器之外,但是我们希望它仅对特定容器课件(这样的话,容器就不会共享存储,除非特殊需求)。除此以外,我们可能希望容器化存储管理软件本身,从而利用容器化来管理和升级存储管理软件。
动态管理:对于有状态容器的持久部署,我们需要在无需管理员认为干预的情况下,分配存储给新的容器,并清理失效的存储。
面向微服务:当我们定义一个容器的时候,他应当明确的制定对存储的依赖。除此以外,存储管理软件本身应当基于微服务部署,从而更好的实现扩容和异地部署。
提供容器存储
为了满足容器持久化存储的需求,Kubernetes 和 Docker Swarm 提供了一组声明式资源来声明并绑定持久化存储至容器。这些持久化存储的功能构建与一些存储架构之上。我们首先来看一下这两种环境下是如何支持容器来声明对持久化存储的以来的。
Kubernetes
在 Kubernetes 中,容器存活于 Pods 中。每个 pod 包含一个或多个容器,它们共享网络栈和持久存储。持久化存储的定义位于 pod 定义的 volumn 字段下。该卷可以被挂在到 pod 的任意一个容器下。比如,一下有一个 Kubernetes 的 Pod 定义,它使用了一个 emptyDir 卷在容器间共享信息。emptyDir 卷初始为空,即使 pod 被迁移到另一个节点上仍将保存下来(这意味着容器的崩溃不会使其消失,但是 node 崩溃会将其删除)
apiVersion: v1
kind: Pod
metadata:
name: hello-storage
spec:
restartPolicy: Never
volumes:
– name: shared-data
emptyDir: {}
containers:
– name: nginx-container
image: nginx
volumeMounts:
– name: shared-data
mountPath: /usr/share/nginx/html
– name: debian-container
image: debian
volumeMounts:
– name: shared-data
mountPath: /pod-data
command: [“/bin/sh”]
args: [“-c”, “echo Hello from the debian container > /pod-data/index.html”]
如果你将以上内容保存到名为 two-containers.yaml 并使用 kubectl create -f two-containers.yaml 将其部署到 kubernetes 上,我们可以使用 pod 的 IP 地址来访问 NGINX 服务器,并获取新建的 index.html 文件。
这个例子说明了 Kubernetes 是如何支持在 pod 中使用 volumn 字段声明一个存储依赖的。但是,这不是真正的持久化存储。如果我们的 Kubernetes 容器使用 AWS EC2,yaml 文件如下:
apiVersion: v1
kind: Pod
metadata:
name: webserver
spec:
containers:
– image: nginx
name: nginx
volumeMounts:
– mountPath: /usr/share/nginx/html
name: web-files
volumes:
– name: web-files
awsElasticBlockStore:
volumeID: <volume-id>
fsType: ext4
在这个例子中,我们可以重复创建和销毁 pod,同一个持久存储会被提供给新的 pod,无论容器位于哪个节点上。但是,这个例子还是无法提供动态存储,因为我们在创建 pod 之前必须先创建好 EBS 卷。为了从 Kubernetes 获得动态存储的支持,我们需要另外两个重要的概念。第一个是 storageClass,Kubernetes 允许我们创建一个 storageClass 资源来收集一个持久化存储供应者的信息。然后将其和 persistentVolumeClaim,一个允许我们从 storageClass 动态请求持久化存储的资源结合起来。Kubernetes 会帮我们向选择的 storageClass 发起请求。这里还是以 AWS EBS 为例:
kind: StorageClass
apiVersion: storage.k8s.io/v1
metadata:
name: file-store
provisioner: kubernetes.io/aws-ebs
parameters:
type: io1
zones: us-east-1d, us-east-1c
iopsPerGB: “10”
—
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
name: web-static-files
spec:
resources:
requests:
storage: 8Gi
storageClassName: file-store
—
apiVersion: v1
kind: Pod
metadata:
name: webserver
spec:
containers:
– image: nginx
name: nginx
volumeMounts:
– mountPath: /usr/share/nginx/html
name: web-files
volumes:
– name: web-files
persistentVolumeClaim:
claimName: web-static-files
如你所见,我们还在使用 volume 关键字来制定 pod 所需要的持久化存储,但是我们使用了额外的 PersistentVolumeClaim 声明来请求 Kubernenetes 替我们发起请求。总体上,集群管理员会为每一个集群部署一个 StorageClass 代表可用的底层存储。然后应用开发者会在第一次需要持久存储时指定 PersistentVolumeClaim。之后根据应用程序升级的需要部署和更换 pod,不会丢失持久存储中的数据。
Docker Swarm
Docker Swarm 利用我们在单节点 Docker 卷上看到的核心卷管理功能, 从而支持能够为任何节点上的容器提供存储:
version: “3”
services:
webserver:
image: nginx
volumes:
– web-files:/usr/share/nginx/html
volumes:
web-files:
driver: storageos
driver-opts:
size: 20
storageos.feature.replicas: 2
当我们使用 docker 栈部署时,Docker Swarm 会创建 web-files 卷,仿佛它并不存在。这个卷会被保留,及时我们删除了 docker 栈。
总的来说,我们可以看到 Kubernetes 和 Docker 都满足了云原生存储的要求。他们允许容器声明依赖的存储,并且动态的管理存储从而使其在应用需要时可见。无论容器在集群的哪个机器上运行,他们都能够提供持久存储。