临时性存储是容器的一个很大的买点。“根据一个镜像启动容器,随意变更,然后停止变更重启一个容器。你看,一个全新的文件系统又诞生了。”
在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中,容器存活于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卷上看到的核心卷管理功能, 从而支持能够为任何节点上的容器提供存储:
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都满足了云原生存储的要求。他们允许容器声明依赖的存储,并且动态的管理存储从而使其在应用需要时可见。无论容器在集群的哪个机器上运行,他们都能够提供持久存储。