回 顾
在本系列文章的上一篇中,我们讲到了PV,PVC,Storage Class以及Provisioner
简单回顾一下:
PV在最一开始是设计成了一个需要管理员预先分配的存储块。引入Storage Class和Provisioner之后,用户可以动态地供应PV。
PVC是对PV的请求,当和Storage Class一起使用时,它将触发与相匹配PV的动态供应。
PV和PVC总是一一对应的。
Provisioner是给用户提供PV的插件。它可以把管理员从为持久化创建工作负载的繁重角色中解脱出来。
Storage Class是PV的分类器。相同的Storage Class中的PV可以共享一些属性。在大多数情况下,Storage Class和Provisioner一起使用时,可以把它当作具有预定义属性的Provisioner。因此,当用户请求它时,它能够用这些预定义的属性动态地提供PV。
不过上述这些只是在Kubernetes中使用持久化存储的其中一种方法而已
Volume
在前一篇文章中,我们提到Kubernetes中还有一个卷(Volume)的概念。为了把Volume和持久卷(Persistent Volume)区分开,大家有时会称它为In-line Volume或者Ephemeral Volume。
这里我们引用Volume的定义:
Kubernetes Volume…有一个显式的生命周期——这和包含它的pod的生命周期相同。因此,Volume的生命周期比在pod中运行的任何容器都长,并且在容器重启的时候会保存数据。当然,当Pod终止时,Volume也将终止。更重要的是,Kubernetes支持多种类型的Volume,一个pod中也可以同时使用任何数量的Volume。
在其核心部分,Volume只是一个目录,可能其中包含了一些数据,这些数据可由pod中的容器访问。这些目录是如何产生的、支持它的介质、以及它的内容都是由所使用的特定volume的类型决定的。在其核心部分,Volume只是一个目录,可能其中包含了一些数据,这些数据可由pod中的容器访问。这些目录是如何产生的、支持它的介质、以及它的内容都是由所使用的特定volume的类型决定的。
Volume一个重要属性是,它与所属的pod具有相同的生命周期。如果pod消失了,它也会消失。这与Persistent Volume不同,因为Persistent Volume将继续存在于系统中,直到用户删除它。Volume还可以在同一个pod中的容器间共享数据,不过这不是主要的用例,因为通常情况下用户只会在每个pod中使用一个容器。
因此,这更可以把Volume看作是pod的属性而不是一个独立的对象。正如它的定义所说,Volume表示pod中的目录,而Volume的类型定义了目录中的内容。例如,Config Map Volume类型将会在Volume目录中从API服务器创建配置文件;PVC Volume类型将从目录中相应的PV里挂在文件系统等等。实际上,Volume几乎是在pod中本地使用存储的唯一方法。
Volume、Persistent Volume和持久卷声明(Persistent Volume Claim)之间很容易弄混淆。假设有一个数据流,它是这样PV->PVC->Volume。PV包含了真实数据,绑定到PVC上,最终变成pod中的Volume。
然而,除了PVC,Volume还可以由Kubernetes直接支持的各种类型的存储库支持,从这个意义上来说,Volume的定义也挺令人困惑的。
我们需要知道的事,我们已经有了Persistent Volume,它支持不同类型的存储解决方案。我们还有Provisioner,它支持类似(并不完全相同)的解决方案。而且我们还有不同类型的Volume。
那么,它们到底有什么不同呢?如何在它们之间选择?
持久化数据的多种方式
以AWS EBS为例。让我们来细数Kubernetes中的持久化数据方式吧。
Volume方式
awsElasticBlockStore是一个Volume类型。
你可以创建一个Pod,定义一个awsElasticBlockStore类型的volume,设置好volumeID,接着使用pod中存在的EBS volume。
该EBS volume在直接和Volume使用前必须已经存在。
PV方式
AWSElasticBlockStore还是一个PV类型。
所以你可以创建一个PV,用它来表示EBS volume(假设你有这样的权限),然后创建一个和它绑定的PVC卷。最后,令PVC作为volume,然后就可以在pod中使用它了。
和Volume方法类似,EBS volume在创建PV之前就必须存在。
Provisioner方式
kubernetes.io/aws-ebs是一个Kubernetes中用于EBS的内置Provisioner。
你可以用Provisioner kubernetes.io/aws-ebs来创建一个Storage Class,通过Storage Class创建PVC。Kubernetes会自动为你创建相对应的PV。接下来指定PVC为volume就可以在pod中使用了。
在本用例中,你不需要在使用使用之前创建EBS,EBS Provisioner会为你创建的。
第三方方式
上面列出的都是Kubernetes内置选项,如果你不太满意的话,其实还有一些使用Flexvolume driver格式的第三方EBS实现,它们可以帮助你和Kubernetes连接起来。
如果Flexvolume不适合你,还可以使用具备同样功能的CSI drivers(为什么这么说?稍后会对此进行详细介绍)
VolumeClaimTemplate方式
如果你在使用StatefulSet,那么恭喜你!你现在有额外多了一种使用工作负载中EBS的方式——VolumeClaimTemple。
VolumeClaimTemple是StatefulSet规范属性,它为StatefulSet所创建的Pod提供了创建匹配PV和PVC的方式。这些PVC将通过Storage Class创建,这样当StatefulSet扩展时就可以自动创建它们。当StatefulSet缩小时,多余的PV/PVCs会保留在系统中。因此,当StatefulSet再一次扩展时,它们会再次作用于Kubernetes创建的新pods中。稍后我们会详细讲StatefulSet。
举个例子说明,假设你用replica 3创建了一个名为www的StatefulSet,并用它创建了名为data的VolumeClaimTemplate。Kubernetes会创建3个pods,分别起名www-0、www-1、www-2。Kubernetes还会创建PVC,其中www-data-0用于pod www-0,www-data-1给www-1,www-data-2给www-2。如果你把StatefulSet扩展到5,Kubernetes就会分别创建www-3、www-data-3、www-4、www-data-4。如果接着将StatefulSet降为1,www-1到www-4全都会删除,而www-data-1到www-data-4会保留在系统中。因此当你决定再次扩展到5的时候,pod www-1到www-4又回被创建出来,而PVC www-data-1仍然会服务于Pod www-1,www-data-2对应www-2,以此类推。这是因为StatefulSet中pod的身份在是stable的。使用StatefulSet时,名称和关系都是可以预测的。
VolumeClaimTemple对于像EBS和Longhorn这样的块存储解决方案非常重要。因为这些解决方案本质上是ReadWriteOnce,你不能在Pod之间共享它们。如果你有不止一个运行了持久化数据的pod,那么就无法顺利地进行部署。因此,VolumeClaimTemplate的出现为块存储解决方案提供了一种水平扩展Kubernetes工作负载的方式。
如何在Volume、Persistent Volume和Provisioner之间做出选择
正如你所看到的,现在有了内置的Volume类型、PV类型、Provisioner类型、以及使用Flexvolume和/或CSI的外部插件。让人比较头大的是,它们之间提供的功能基本相同,不过也有略微的区别。
我认为,至少应该有一个准则来确定如何在它们之间选择。
但是我并没有找到。
所以我翻遍了代码和文档,画出了下面的比较表格,以及对我来说最有意义的准则,从Volume、Persistent Volume和Provisioner几个方面进行对比。
这里我只涉及到Kubernetes中in-tree所支持的,除此之外一些官方的out-of-tree的Provisioners:
https://github.com/kubernetes...
可以看到,Volume、Persistent Volume以及Provisioner在一些细微的地方还是不一样的。
- Volume支持大部分的volume插件。
A.它是连接PVC和pod的唯一方法
B.它也是唯一一个支持Config Map、Secret、Downward API以及Projected的。这些所有都与Kubernetes API服务器密切相关。
C.它还是唯一一个支持EmptyDir的,EmptyDir可以自动给pod分配和清理临时volume。(注:早在2015年,Clayton Coleman就提出了一个关于支持EmptyDir的问题。这对于需要持久化储存但只有本地卷可用的工作负载,这非常有用。可是这一观点并没有得到太多的关注。没有scheduler的支持,这一目标在当时很难做到。而现在,在2018年,Kubernetes v1.11版本的Local Volume已经加入scheduler和PV的节点亲和支持(node affinity support),但是仍然没有EmptyDir PV。而且Local Volume特性并不是我所期望的那样,因为它并不具备在节点上使用新目录创建新卷的能力。因此,我编写了Local Path Provisioner,它利用scheduler和PV节点亲和更改,为工作负载提供动态的Host Path type PV。)
- PV支持的插件是Provisioner支持的超集,因为Provisioner需要在工作负载使用它之前创建PV。但是,还有一些PV支持而Provisioner不支持的插件,比如Local Volume(正在进行修改中)。
- 还有两种类型Volume是不支持的。他们是两个最新的特性:CSI和Local Volume,现在还有一些正在进行的工作,会在之后把它们用于Volume。
在Volume、Persistent Volume和Provisioner之间选择的准则
那么用户到底应该选择哪种方式呢?
在我看来,用户们应该坚持一个原则:
在条件允许的情况下,选择Provisioner而不是Persistent Volume,接着再是Volume。
详细来说:
- 对于Config Map、Downward API、Secret或者Projected,请使用Volume,因为PV不支持它们。
- 对于EmptyDir,直接使用Volume,或者使用Host Path来代替。
- 对于Host Path,通常是直接使用Volume,因为它绑定到一个特定的节点,并且节点之间它是同构的。
a. 如果你想用异构的Host Path Volume,它在Kubernetes v1.11版之后才能使用,因为之前缺少对PV的节点亲和知识,使用v1.11+版本,你可以使用我的Local Path Provisioner创建带有节点亲和的Host Path PV:
https://github.com/rancher/lo...。
- 对于其他的情况,除非你需要和现有的卷挂钩(这种情况下你应该使用PV),否则就使用Provisioner代替。有些Provisioner并不是内置的选项,但是你应该能在此链接(https://github.com/kubernetes...)或者供应商的官方仓库中找到它们。
这个准则背后的原理很简单。在Kubernetes内部进行操作时,对象(PV)比属性(Volume)更容易管理,而且和手动创建PV相比,自动创建PV容易得多(Provisioner)。
不过这里有一个例外:如果你喜欢在Kubernetes外面进行存储,那么最好使用Volume,尽管使用这种方式需要用到另一组API进行创建/删除。此外,由于缺少VolumeClaimTemplate,会失去使用StatefulSet自动伸缩的能力。我不认为这是多数Kubernetes用户会选择的方式。
为什么做同样的事会有这么多选项?
当我开始研究Kubernetes存储时,首先想到的就是这个问题。由于缺乏一致性和直观性,Kubernetes存储看起来就像是事后才想到的。于是我试图研究这些设计决策背后的历史缘由,可是在2016之前都毫无收获。
最后,我倾向于相信这些是由于一些早期的设计造成的,这可能是为获取供应商支持的迫切需求,导致安排给Volume比原本更多的责任。在我看来,所有复制了PV的内置volume插件都不应该存在。
在研究历史的过程中,我发现在2016初发布的Kubernetes v1.2中,dynamic provisioning就已经成为了alpha特性。它需要两个发布版周期变成beta,在两个周期实现稳定,这都是非常合理的。
SIG Storage(它推动了Kubernetes存储开发)还进行了大量的工作,使用Provisioner和CSI将Volume插件从tree中移出来。我认为这是朝着更加一致、更加精简的系统迈出了一大步。
可另一方面,我也不认为这一大堆Volume类型会消失。这像是和硅谷非官方的格言唱反调:快速行动,打破常规。有时候,快速迭代的项目所遗留下来的设计,修改它们实在是太难了。我们只能和它们共处,在它们身边小心工作,不要用错误的方式调用它们。
下一步
本系列的下一节中,我们将讨论扩展Kubernetes存储系统的机制,即Flexvolume和CSI。一个小小的提示:你可能注意到了,我并不是Flexvolume的粉丝,而且这不是存储子系统的问题。