生产环境中 Docker 的持久化存储模式

1,893 阅读17分钟
原文链接: mp.weixin.qq.com

查看图片

一般看法认为容器对于无状态的应用程序是很好的,但是不适合有持久化数据的有状态应用。如果这是真的,这并不是因为技术不到位,而是因为管理持久化数据和有状态应用程序的模式并不总是为人们所熟知。你面临的挑战很多不是关于持久化状态的,而是如此操作不会影响敏捷性和自动化,而这些恰恰是我们第一时间喜欢容器化的因素。

我看到了所有类型的应用程序都可以很容易地自动化地部署的未来,其中的伸缩性——即使是对于有着持久化状态的集群系统——也都可以通过按下一个按钮来搞定,甚至备份和恢复也都是自动化的,下面让我展示给你如何操作。

“状态……”你继续使用这个词……

大多数应用程序,即使是那些我们称之为“无状态”的,仍然会保持某种状态。这种状态可以简单如应用程序的执行流程,或者重要如我的Pokemon的CP(或者是我的银行账号)。“无状态”和“有状态”并没有太多的将应用程序描述为组成了状态和恢复成本的数据价值。

随机数生成器是无状态的,因为每次运行它时,我们预料会有一个不同的值。我们可以很容易地将其Docker化,如果实例发生故障了,我们可以在别处重新提供,而不必担心因为现在运行在一台新的主机上而会改变其运行行为。

然而我的银行账号不是无状态的,如果不得不在一台新的服务器上重新提供银行账号应用,那么最好拥有在第一台服务器上曾经有过的相同数据,否则很多用户会生气,我们都希望对我们有利的银行差错,即使是一些人会生气的。

然而有状态性并不是严格依赖于应用代表的数据类型的。考虑一个像Memcached的内存缓存。Memcached经常被用来承载非常有价值的数据,包括银行数据,但我们不认为它是有状态的,因为我们构建的应用可以以某种方式恢复Memcached的状态,这么做时会有性能的消耗,但是没有数据丢失。

应用程序的状态和有状态性是与状态的恢复成本密切相关的,而这个成本取决于表示状态的数据性质和应用程序的架构。

“持久化……”你继续使用这个词……

这开始容易,在我们的笔记本电脑上,内存中的数据不是持久化的,但是硬盘(或者SSD,美其名曰SSDs)中的数据是持久化的,直到我们删除它(或者磁盘驱动器损坏)。但在云计算中,我们任何时候都在构建有弹性的应用程序,这个简单的持久化概念变得更加复杂了。

越来越多的,我们希望服务是可清理的。有时我们称之为不可变的基础设施,但我们的目标是,我们可以在任何地方的任何服务器运行我们的应用程序。并且,如果该服务器损坏了,我们可以把它扔掉——清理它——然后转到另一个。这在公有云上是必需的,因为我们只是租用服务器,或者其中的一部分,这个对于弹性应用是必需的,因为我们必须按照可以从托管基础设施失效中生存下来的方式来构建这些应用程序。

但是,服务的可清理性是与我们从笔记本电脑上学到的有关对于数据的持久化的认知是不兼容的。如果我们没有获得任何可以持久化数据的硬盘或者SSD,我们如何用持久化数据构建有状态的应用呢?

一些云服务和少数供应商引入了一个昂贵的基于持久化数据的折中解决方案:移动数据到一组不同的服务,并且通过某种网络连回到应用服务器。那个网络可能是光纤通道或者基于TCP/IP协议的网络,但是这是一个网络,并且增加了数据和需要数据的应用之间的距离。随着距离的增加,错误率和延迟也会增加,而吞吐量和最大性能会下降……这些增加的错误率包括将应用从数据分离的网络分区。

把数据放在一组不同的服务器上并通过网络连接,不会使数据更加持久化,但是确实使得更加可移植了。不幸的是,最需要持久化数据的系统往往是那些最受不好和不可靠的远程存储影响的系统。大多数数据库供应商,举例来说都会警告用户使用远程存储时性能表现不佳,当工作良好时也是如此。远程存储的故障模式是相当惊人的(例子1、2,以及一个发人警醒的故事),比如提高解决方案的成本和风险,尝试在基础设施层面使得持久化数据可移植。

当然,最后这并没有真正解决问题,并消除宠物,它只是把宠物移动到一组不同的服务器,我们在性能、可靠性和金钱方面付出了很高代价。

所有这一切都将在哪里?

让我们赞同持久化是从可移植性分离出来的,一个应用程序的有状态性(以及服务的可清理性)是与重建状态的成本相关的。

考虑:在服务器上持久化数据可以将其变成宠物……


而使得持久化数据可移植的基础设施解决方案是一个从来没有在当中一起出现的维恩图,迫使我们从三个中选择两个:速度、可靠性、可移植性。


但是,一些应用处理状态和持久性比别的更好。考虑有自聚集特性的数据库像Couchbase。Couchbase在应用层面来管理数据的可移植性。

当我将一个新的Couchbase实例加入集群,它就会自动复制并重新切分数据到新的实例。事实上,针对上一代的数据库必需的那些运维操作现在可以完全自动化了。失效也是如此:只要我们不失去太多的Couchbase实例太快,集群中剩余的实例就可以可靠地持久化那些数据和状态。Couchbase甚至支持在应用层面跨数据中心复制。这些相结合,可以在基础设施层面完全消除远程存储的任何需要。

作为一个实际问题,容器中的Couchbase容器(https://www.joyent.com/blog/couchbase-in-docker-containers)就相关的基础设施和调度器而言实际上是无状态的。只要调度器和管理程序保持足够的运行实例,Couchbase的自动重新分片和复制功能将会照料剩余的部分。

我们可以使用的模式

不同的应用程序提供了不同级别的自动化操作,然而不是每一个应用程序都是可以构建为自动地管理代表它的状态的所有的各种数据的。我们能做些什么,放弃或重写这些应用程序?


第一个问题是我们谈论的是什么样的有状态数据?

一些最常见的有状态数据是与配置相关的,那些配置包括密钥和其他机密材料,是经常被写入到磁盘的各种文件中的。当提供了实例时,这些数据是(或者是应该)很容易被恢复的,因为如果我们不能自动化地配置这些实例,那么我们是不能自动提供和伸缩的。

UGC显示了另外一个挑战,它们可以是文本、视频、我的Pokemon的CP或者银行交易信息。每种类型的数据都值得单独考虑。

配置

通常情况下,最具动态的配置详细信息是那些将服务X连接到服务Y的。其中一个例子就是将我的应用连接到数据库,这个过程被称为服务发现,但大多数应用程序/服务只是简单地把它与其它配置详细信息一道作为其中的一项配置。为了使应用程序具备伸缩性和弹性,有必要在运行时更新这个配置,也就是说,当我们添加或删除一个服务的实例时,我们必须能够更新连接到该服务的所有其他服务。举例来说:如果我的数据库处于满负载,我需要添加更多的实例来提高性能,所有需要连接到该数据库的组件都需要知道新的实例。否则,新实例将被闲置着,什么都不做来提高应用程序的性能。如果数据库的一个实例故障了,我们不得不经历相同的过程:如果我们不更新数据库客户端,我们会得到很多失败的请求。

好消息是,这种动态配置是很容易的,我们有一个关于如何操作的教程(https://www.joyent.com/blog/applications-on-autopilot)。

其他配置细节可以包括性能调优的参数以及其它细节,还有关于这些细节是否属于应用代码仓库(这样可以进行版本化和追踪)或者在动态存储中这样可以不需要重建和重新部署就可以交换数据的等等坦率真诚的意见交换。一个折中的方案是使用类似git2consul的工具来自动复制仓库内容到配置存储中。到底如何处理其它的配置数据取决于具体的应用程序,但一般可以如下所示:

  • 保存配置数据,也许是Hashicorp的Consul中的模板,或者是另一个强一致性的分布式键/值存储

  • 运行时收集配置详细信息

  • 如果应用期望存在磁盘上,使用实例内存储

机密材料

对我们的应用程序来说,机密材料一般只是配置的细节,但对我们来说,它们更敏感。Hashicorp’s Vault对于安全存储机密是一个通用的解决方案,但CI/CD的解决方案往往还包括了机密材料管理功能(可交付性是其中之一)。

  • 在Vault或者另外一个安全机密材料管理服务总保存机密材料

  • 运行时收集机密材料

  • 如果应用期望存在磁盘上,使用实例内存储

数据库

数据库通常是一个典型的有状态服务难以Docker化的例子,不幸的是,也有很多不好的建议以及如何这样做的噱头。

Docker化数据库是与在云上实现数据库同样的有相当多的问题的,并且这是与构建一个弹性的实现了自动化的数据库有着相同的问题的,许多数据库操作自动化的问题就是文件系统上对象的重要性。我们以自动化MySQL和PostgreSQL的复制为例,需要自动复制文件系统的内容到新的实例。Postgres和MySQL有文档解释如何操作,但他们没有提供可以使得工作以一致和可靠的方式(或者根本就是)进行的工具。更新的数据库往往会自动化这些任务,正如上述Couchbase的例子。

然而,这个自动化是可以被实现的,并且演示了MySQL如何自动驾驶的模式(https://www.joyent.com/blog/dbaas-simplicity-no-lock-in)。这自动化实现了典型的MySQL生命周期感知。作为数据库运维人员,当我们启动了一个新的数据库实例时,我们知道我们必须做的事情:我们是否应该加入现有的数据库集群或者启动一个新的?如果加入现有的集群,我们需要引导副本。数据库生命周期的开始现在使用自动驾驶模式来自动化了,正如在整个数据库的生命周期中都是备份的。如果现有的主服务失效了(结束了生命周期),在一个可配置的超时之后该实现还要(可选地)自动化地选举新的主服务。

数据/状态在多个层面持久化:

  1. 主从复制过程可以跨多个数据库实例复制状态以及任何现存的实例,并用于引导一个新实例

  2. 主服务实例的状态会被定期备份到一个对象存储区,这保证了一定程度的数据生存能力,即使整个运行集群丢失了

结果是,这个MySQL实现现在是无状态的,并且调度器不需要做任何数据编排,我们不需要不可靠或缓慢的远程存储。这些细节可以具体到MySQL的实现,但是通常是如下的:

  • 使用实例内存储

  • 将数据分布在多个实例

  • 通过添加新的对端副本和删除旧的对端副本来升级数据库(按需为主服务实例添加副本)

  • 使用数据库特定的解决方案来备份

共享数据

共享数据为应用程序提供了一个特例缓慢接近遗留分类,也就是说,这些应用程序可能会使用很长一段时间。一个很常见的例子是WordPress,一款几乎无处不在的博客软件。WordPress使用MySQL来存储文本内容和它的大部分配置,但用户上传的图片和其他的BLOBs都存储在文件系统里。

有WordPress插件使用对象存储,但它不是默认的,并有相当数量的插件与这种方式是不兼容的,所以,为了扩展WordPress到多个实例而不需要担心不兼容,我们需要一个共享的文件系统。如果没有共享的文件系统,每个实例都将有一个独立的(不一致的/分区的)供用户上传文件的部分。

我们的WordPress自动驾驶模式实现使用NFS作为共享文件系统只是这个原因,但一般还有如下的:

  • 使用Manta对象或者其他对象存储,如果你可以的话

  • 使用一个共享文件系统(NFS)作为最后的手段

要避免的事情

敏锐的读者可能会奇怪为什么我没有建议比如从Docker宿主机映射的卷或者使用Docker卷驱动器?总而言之,我建议避免那样操作,尤其是任何那种只是简单地将持久化问题从容器移动到底层主机的方式。这可能使它可以忽略容器中数据和状态管理方面的挑战,但它并没有解决我们应用程序的问题。我们仍然需要找到一种方法来管理数据,而在容器外部实现,这意味着我们这么做将享受不到存在与容器内部的各种工具和状态感知的益处。更糟糕的是,它把那些容器主机变成了宠物,并限制了我们的应用程序只能到特定的主机上运行。在容器外部工作解决这个问题需要更多的复杂性,调度器内特定于应用程序的编排以及更大的基础设施管理团队和行为使其可以工作。最重要的是,从他们的应用的运维细节分离开发人员是导致“在我的机器上工作”的原因(查看Sci-Fi DevOps获得更多信息)。

上面讨论的远程存储的问题,也适用于通过Docker卷驱动器访问远程存储。大多数远程存储问题也适用于分布式存储,只是增加了在一致性、分区、读取旧数据、写入丢失以及不可调和的变化等方面的挑战。分布式存储的承诺是具有本地存储的性能和方便的共享文件系统,但它是一个特别棘手的问题,到目前为止所有的解决方案都已经证明了这是丑陋的失效模式,并带来了可靠性和一致性方面的严重问题。

再次的,您的应用需求会有所不同,但它通常是最好避免如下所示的:

  • 从宿主机映射-v卷

  • —volumes-from

  • 大多数的Docker卷驱动器

  • 分布式文件系统

每一个应用都可以是无状态的

任何应用程序假定一个文件系统可以无状态化,途径是容器运行时自动备份文件系统内容,应用程序启动时恢复文件系统内容。上面的MySQL的例子正是如此,尽管它使用应用程序级别的复制作为主要的在大量的实例间分发状态的机制,该机制考虑到了性能和生存能力。

我们可以为不想要数据库的应用程序使用相同的方法。想象在一个容器中运行Gerrit代码同行评审:通过在hook被触发的任何时刻备份文件到一个对象存储(或排队备份),我们可以得到一个可靠的应用状态的副本(Postgres数据库要分别备份和复制)。如果我们设计容器来恢复在启动时备份的内容,我们可以使应用程序非常持久和有弹性,而不需要任何基础设施的支持。

并且,通过触发基于应用程序活动的备份,在有意义的变化时我们可以确保正在备份文件系统,而不用管频率快或者慢。

我们要做的是让Gerrit对于调度器无状态,这并不意味着Gerrit不会有停机时间,但我们可以轻松更换Gerrit或者当我们需要最小的停机时间时,干脆将它移到新的服务器上。这是因为这个假设的Gerrit实现可以自动从我们的备份恢复状态到我们离开备份的那个时刻。我们已经使它成为足够的无状态了,使得调度器不必做任何复杂的编排,并且不需要任何神奇的基础设施。相反,我们已经用可靠的服务于我们的应用很多年的工具和系统来完成此项工作了。

这是从为了伸缩性和可用性使得应用程序跨多个实例共享一致性状态而分离出来的。共享状态通常需要应用程序的支持。具体地对于Gerrit来说,团队已经决定基于停机成本的假设不加入集群 /分片状态功能。相反地,上述的MySQL和Couchbase的例子演示了应用程序如何设计为可以共享跨多个实例的状态,并且对于调度器仍然以无状态的方式来运行。

选择您的应用程序,挑战我

不认为有可能使任何应用程序无状态吗?尝试着给DevOps@joyent.com发送有关你的问题的电子邮件,一定要包括链接到你的问题相关的代码库和文档,这样我可以查看细节。我会在博客上解答你的问题。



活动推荐

【CNUTCon全球容器技术大会】微服务、持续集成、容器云、大数据、电商、传统行业、创业公司等12个专题,Docker、Kubernetes、Netflix、Mesos、CoreOS、阿里巴巴、京东等公司的核心技术负责人现场独家揭秘,容器化和微服务化,从这里开始,8折报名中,详情请点击阅读原文链接。

查看图片