存储 频道

利用Kubernetes实现容器的持久化存储

  【IT168 技术】可以说,容器化彻底改变了我们对应用程序开发的思考方式,它带来很多好处:开发和生产之间的一致环境、使用共享的资源但容器之间相互隔离、云环境之间的可移植性、快速部署……等等不胜枚举。容器所固有的短暂性是它之所以伟大的核心原因:不可变的、相同的容器,可以在一瞬间快速启动。但容器的短暂性也有不利的一面:缺乏持久化存储。

  引入Kubernetes

  持久状态(Persistent state)通常数量较大且难以移动,这一概念与容器这种快速、轻量级且易于随时部署到任何地方的概念有很大的不同。正是由于这个原因,容器规范有意将持久状态排除在外,转而选择存储插件,将管理持久状态的责任转移给另一方。

  开源容器编排工具Kubernetes已经开始着手解决这个问题。在这篇文章中,笔者将向您介绍Kubernetes中的几个组件,它们有助于解决在容器环境中持久化状态的问题。

  有状态性


  管理持久状态的最大问题是决定它应该存留在何处。在决定持久化存储应该放在何处时,有三种选择,每种方法各有其优点:

  ·持久化存储保存在容器中。如果数据是可复制的,而且不是关键数据,那么这中方法是非常有效的,但是当容器重新启动时,您将丢失这些数据。

  ·持久化存储保存在主机节点上。这种方法绕过了容器的短暂性问题,但是您可能会因为主机节点的易受攻击性而遇到类似的问题。

  ·持久化存储保存在远程存储中。这消除了容器和主机存储的不可靠性,但是需要仔细考虑如何提供/管理远程存储。

  什么时候需要考虑状态?

  应用程序有两个需要持久状态的关键特性:1、需要在应用程序中断和重启之前持久保存数据;2、需要跨相同的中断和重启,来管理应用程序状态。此类应用程序的典型例子有数据库及其副本、某种日志记录应用程序,或者需要远程存储的分布式应用程序。

  但是,此类应用程序对持久性的需求并不是相同程度的,因为对于不同的应用程序,其关键程度显然是不同的。由于这个原因,笔者在设计有状态的应用程序时,常常会问自己几个问题:

  ·我们要管理多少数据?

  ·从最新的快照开始就可以吗?还是需要绝对最新的可用数据?

  ·从快照重新启动是否花费了太长时间,或者对这个应用程序而言已经足够了?

  ·数据的复制有多容易?

  ·这些数据对任务有多重要?能否在容器或主机终止时“存活”,还是需要远程存储?

  ·这个应用程序中的不同Pods是可互换的吗?

  存储解决方案

  许多应用程序要求数据能够跨容器和主机重启实现持久化,这就需要远程存储。幸运的是,Kubernetes已经意识到这种需求,并提供了一种Pod与远程存储交互的方式:卷(Volumes)。

  Kubernetes卷

  Kubernetes卷提供了一种与远程(或本地)存储交互的方法。可以将这些卷视为挂载的存储,这些存储将持续存留到封闭Pod的生存期。卷比在该Pod中spin up/down的任何容器的寿命都长,这为容器的短暂性提供了一个很好的解决方案。下面是一个利用卷的Pod定义示例。

  apiVersion: v1

  kind: Pod

  metadata:

        name: test-pod

  spec:

        containers:

              — name: test-container

                   image: nginx

                   volumeMounts:

                          — mountPath: /data

                               name: testvolume

        volumes:

              — name: testvolume

                   # This AWS EBS Volume must already exist.

                   awsElasticBlockStore:

                   volumeID: <volume-id>

                   fsType: ext4

  正如我们从上面Pod定义中看到的,spec下的volumes部分指定了卷的名称和已经创建的存储的ID(在本例中是EBS卷)。要使用此卷,容器定义必须在spec下的containers volumeMounts字段中指定要挂载的卷。

  在利用卷时要记住的一些关键点:

  ·Kubernetes提供许多类型的卷,一个Pod可以同时使用任意数量的卷。

  ·卷仅能持续与封闭的Pod一样长的时间。当Pod停止存在时,卷也将停止。

  ·持久存储的供应不是由卷或Pod本身处理的,卷后的持久存储需要以其他方式提供。

  虽然卷解决了容器化应用程序的一个巨大问题,但某些应用程序要求附加卷的生存期超过Pod的生存期。对于这个用例,持久卷和持久卷声明将非常有用。

  Kubernetes持久卷和持久卷声明

  Kubernetes持久卷和持久卷声明提供了一种方法,以便从存储的使用方式中提取关于如何提供存储的细节。持久卷(PV,Persistent Volume)是一个集群中由管理员提供的可用持久存储,它们作为集群资源存在,就像节点一样,它们的生命周期独立于任何单独的Pod。持久卷声明(PVC,Persistent Volume Claim)是用户对存储的请求,与Pod消耗内存和CPU等节点资源的方式类似,PVC也消耗存储等PV资源。

  PV的生命周期由四个阶段组成:供应(provisioning)、绑定(binding)、使用(using)和回收(reclaiming)。

  供应——PV的供应可以通过两种方式完成:静态或动态。

  ·静态供应需要集群管理员手动创建大量要使用的PV。

  ·动态供应可以在PVC请求PV时发生,而不需要集群管理员进行任何手动干预。

  ·动态供应需要以存储类(Storage Classes)的形式进行一些预先供应(我们稍后将对此进行讨论)。

  绑定——创建PVC时,它具有特定的存储空间和特定的访问模式。当有一个匹配的PV可用时,无论PVC需要多长时间,它都将只与请求的PVC绑定。如果匹配的PV不存在,则PVC将无限期地保持松散状态。在动态供应PV的情况下,控制循环会始终把PV绑定到请求的PVC。否则,PVC至少会得到它们要求的存储空间,但是卷可能会比要求更多。

  使用——一旦PVC认领了PV,它就可以作为一个安装体在封闭的Pod中使用。用户可以为附加卷指定特定的模式(例如ReadWriteOnce、ReadOnlymany等)以及其他挂载的存储选项。只要用户需要,就可以使用安装好的PV。

  回收——当一个用户完成了对存储的使用后,他需要决定如何处理正在释放的PV。在决定回收策略时,有三个选项:保留(retain)、删除(delete)和循环利用(recycle)。

  ·保留PV只需要释放PV,而不需要修改或删除任何包含的数据,并允许相同的PVC在稍后手动回收此PV。

  ·删除PV将完全删除PV以及底层存储资源。

  ·循环利用PV将从存储资源中删除数据,并使PV可用于任何其他PVC的请求。

  下面是一个持久卷(使用静态供应)的示例,以及持久卷声明定义。

  apiVersion: v1

  kind: PersistentVolume

  metadata:

        name: mypv

  spec:

        storageClassName: mysc

        capacity:

            storage: 8Gi

        accessModes:

            — ReadWriteOnce

        persistentVolumeReclaimPolicy: Recycle

        awsElasticBlockStore:

          volumeID: <volume-id> # This AWS EBS Volume must already exist.

持久卷

  apiVersion: v1

  kind: PersistentVolumeClaim

  metadata:

        name: mypvc

  spec:

        storageClassName: mysc

        accessModes:

          — ReadWriteOnce

        resources:

           requests:

              storage: 8Gi

  持久卷声明

  持久卷定义指定存储资源的容量,以及一些其他特定于卷的属性,如回收策略和访问模式。可以使用spec下的storageClassName将PV分类为特定的存储类,PVC可以利用它来指定要声明的特定存储类。上面的持久卷声明定义指定了它试图声明的持久卷的属性,其中一些是存储容量和访问模式。PVC可以通过指定spec下的storageClassName字段请求特定的PV。特定类的PV只能绑定到请求该类的PVC,没有指定类的PV只能绑定到没有请求特定类的PVC。选择器还可以用于指定要声明的PV的特定类型,有关这方面的更多文档可以在这里(https://kubernetes.io/docs/concepts/storage/persistent-volumes/#selector)找到。

  下面是利用持久卷声明来请求存储的Pod定义示例:

  apiVersion: v1

  kind: Pod

  metadata:

        name: test-pod

  spec:

        containers:

              — name: test-container

                   image: nginx

                   volumeMounts:

              — mountPath: /data

                   name: myvolume

        volumes:

              — name: myvolume

                   persistentVolumeClaim:

                       claimName: mypvc

  当将这个Pod定义与前面使用卷的定义进行比较时,我们可以看到它们几乎是相同的。持久卷声明不是直接与存储资源交互,而是用于从Pod抽象存储细节。

  关于持久卷和持久卷声明的一些关键结论:

  *持久卷的生命周期独立于Pod的生命周期。

  *持久卷声明将存储供应的细节从Pod的存储消耗中抽象出来。

  *与卷类似,持久卷和持久卷声明不直接处理存储资源的供应。

  Kubernetes存储类和持久卷声明

  Kubernetes存储类和持久卷声明提供了一种在请求时动态提供存储资源的方法,从而消除了集群管理员过度提供/手动提供存储资源以满足需求的必要性。存储类允许集群管理员描述其提供的存储“类”,并在动态创建存储资源和持久卷时利用这些“类”作为模板。可以根据特定的应用程序需求(如所需的服务质量级别和备份策略)定义不同的存储类。

  存储类定义围绕三个特定区域:

  ·回收(Reclaim )策略

  ·供应程序(Provisioner)

  ·参数(Parameter)

  Reclaim ——如果持久卷是由存储类创建的,那么只有Retain或Delete作为回收策略可用,而手动创建的、由存储类管理的持久卷在创建时将保留其分配的回收策略。

  Provisioner——存储类提供者负责决定在提供PV时需要使用哪个卷插件(例如AWS EBS的AWSElasticBlockStore或Portworx volume的PortworxVolume)。Provisioner字段不仅限于内部可用的Provisioner类型列表,任何遵循明确定义的规范的独立外部供应程序都可以用来创建新的持久卷类型。

  Parameter——定义存储类的最后、也可以说最重要的一部分是参数部分。不同的提供程序可以使用不同的参数,这些参数用于描述特定“类”存储的规范。

  下面是持久卷声明和存储类定义:

  apiVersion: v1

  kind: StorageClass

  metadata:

        name: myscz

  provisioner: kubernetes.io/aws-ebs

  parameters:

        type: io1

        iopsPerGB: “10”

        fsType: ext4

  持久卷声明

  apiVersion: v1

  kind: PersistentVolumeClaim

  metadata:

        name: mypvc

  spec:

        storageClassName: mysc

        accessModes:

            — ReadWriteOnce

        resources:

        requests:

                storage: 8Gi

  存储类

  如果我们将PVC定义与上面静态供应用例中使用的定义进行比较,可以看到它们是相同的。

  这是因为存储“供应”和存储“消费”之间存在明显的分离。与静态创建的存储类相比,使用存储类创建的持久卷的消耗有一些巨大的优势,最大的优点之一是能够操作仅在资源创建时可用的存储资源值。这意味着我们可以准确地提供用户请求的存储量,而无需集群管理员进行任何手动干预。由于存储类需要由集群管理员提前定义,因此它们仍然会控制哪些类型的存储对最终用户可用,同时抽象出所有供应逻辑。

  存储类和持久卷声明的要点:

  *存储类和持久卷声明允许最终用户使用存储资源的动态供应,从而消除集群管理员所需的任何手动干预。

  *存储类抽象了存储供应的细节,而依赖于指定的供应程序来处理供应逻辑。

  应用程序状态

  当我们考虑状态时,持久存储是至关重要的。我的数据在哪里?当我的应用程序故障时,它是如何做到持久化的?而某些应用程序本身也需要状态管理,不仅仅是持久化数据。这在利用多个不可互换Pod的应用程序中最容易看到(例如,主数据库Pod及其某些分布式应用程序如Zookeeper或Elasticsearch的副本)。诸如此类的应用程序要求能够在任何重新调度期间为每个Pod hat分配惟一标识符。Kubernetes通过使用StatefulSet提供了这种功能。

  Kubernetes StatefulSets

  Kubernetes StatefulSet提供类似于ReplicaSets 和Deployments的功能,但是具有稳定的重新调度。对于需要稳定标识符和有序部署、伸缩和删除的应用程序来说,这种差异非常重要。StatefulSet有几种不同的特性,可以帮助提供这些必要的功能。

  惟一网络标识符——StatefulSet中的每个Pod都从StatefulSet的名称和Pod的序号派生其主机名。这个Pod的标识是粘滞的,不管这个Pod被调度到哪个节点,也不管它被重新调度了多少次。这种功能对于会形成不可互换的Pod逻辑“组”的应用程序特别有用,这类应用程序典型例子是分布式系统中的数据库副本和代理。识别单个Pod的能力是StatefulSet的优势的核心。

  有序的部署、伸缩和删除——StatefulSet中的Pod标识符不仅是惟一的,而且是有序的。StatefulSet中的Pod是按顺序创建的,在转移到下一个Pod之前,等待中的上一个Pod处于健康状态。这种行为也扩展到了Pod的伸缩和删除,在Pod的所有前身都处于健康状态之前,任何Pod都不能进行更新或扩展。类似地,在Pod终止之前,必须关闭所有后续的Pod。这些功能允许对StatefulSet进行稳定的、可预测的更改。

  下面是StatefulSet定义的一个示例:

  apiVersion: v1

  kind: StatefulSet

  metadata:

        name: web

  spec:

        selector:

              matchLabels:

                    app: nginx # has to match .spec.template.metadata.labels

        replicas: 3

        template:

              metadata:

                    labels:

                          app: nginx # has to match .spec.selector.matchLabels

              spec:

              terminationGracePeriodSeconds: 10

              containers:

              — name: nginx

                    image: nginx

                    ports:

                    — containerPort: 80

                          name: web

                    volumeMounts:

                    — name: www

                          mountPath: /usr/share/nginx/html

        volumeClaimTemplates:

              — metadata:

                    name: www

              spec:

                    storageClassName: mysc

                    resources:

                          requests:

                                storage: 1Gi

  如上所示,StatefulSet的名称在metada下的name中指定,在创建封闭的Pod时将使用该名称。这个StatefulSets定义将产生名为web-0、web-1和web-2的三个Pod。

  这个特定的StatefulSet通过spec下的volumeClaimTemplates字段利用Pvc,以便将持久卷附加到每个Pod。

  StatefulSet的关键要点:

  * StatefulSet将其封闭的pod命名为唯一的,允许存在需要不可互换pod的应用程序

  *以有序的方式处理StatefulSet的部署、扩展和删除

  虽然StatefulSet提供了部署和管理不可互换Pod的能力,但仍然存在一个问题:我如何找到和使用它们。这就是Headless Service发挥作用的地方。

  Kubernetes Headless服务

  有时我们的应用程序不希望或不需要负载平衡或单个服务IP,诸如此类的应用程序(主数据库和副本数据库、分布式应用程序中的代理等)需要一种将流量路由到支持服务的各个分离Pod的方法。具有唯一网络标识符的Headless服务和Pod (例如使用statefulset创建的那些标识符)可以在此用例中一起使用。能够直接路由到单个Pod将大量的性能重新交到开发人员手中,从处理服务发现到直接路由到主数据库Pod。

  下面是一个Headless服务的例子:

  apiVersion: v1

  kind: Service

  metadata:

        name: nginx-svc

  spec:

        clusterIP: None

        selector:

        app: nginx

        ports:

        — name: http

              protocol: TCP

              port: 80

              targetPort: 30001

        — name: https

              protocol: TCP

              port: 443

              targetPort: 30002

  使该规范真正“Headless”的属性是设置.spec下的clusterIP为None。这个特殊的示例使用spec下的selector字段,以指定应该如何配置DNS。在这个例子中,所有匹配app: nginx选择器的Pods将创建一条A记录,直接指向支持服务的Pod。关于DNS如何为Headless服务自动配置的更多信息,可以在这里(https://kubernetes.io/docs/concepts/services-networking/service/#headless-services)找到。这个特殊的规范将创建端点nginx-svc-0、nginx-svc-1、nginx-svc-2,它们将分别直接路由到名为web-0、web-1和web-2的Pod。

  Headless服务的要点:

  *无头服务允许直接路由到特定的豆荚

  *使应用程序开发人员能够以他们认为合适的方式处理服务发现

  结论

  Kubernetes使有状态应用程序开发在容器世界中成为现实,特别是在管理应用程序状态和持久数据时。持久卷和持久卷声明建立在卷的基础之上,以支持持久数据存储,从而支持在一个主要是短暂性的环境中保持数据持久性。存储类进一步扩展了这一思想,允许按需提供存储资源。StatefulSet提供Pod惟一性和粘滞标识,为每个Pod提供有状态标识,这些标识在Pod中断和重启期间持续存在。Headless服务可以与StatefulSet一起使用,为应用程序开发人员提供根据应用程序需求来利用Pod的独特性的能力。

  本文介绍了Kubernetes中有状态应用程序所需的基本元素。随着Kubernetes的不断发展,围绕有状态应用程序的功能将继续出现。对于有状态应用程序开发人员和集群管理员来说,了解这些基本元素是非常重要的。

  原文作者:Nick Groszewski 来源:Medium

  原文链接:https://medium.com/capital-one-tech/conquering-statefulness-on-kubernetes-26336d5f4f17

0
相关文章