Docker实战:一部失败史

背景信息。我们是一家小公司,有几百台服务器。在核心部分,我们运行一套财务系统,每天处理的资金大约数百万美元(每年几十亿美元)。

可以说,我们的预期要求比一般公司要高,我们对于生产环境相当重视。

总的来说,如果你没有在生产环境下大规模使用Docker,及/或如果没有长期使用Docker,未遇到所有这些问题很“正常”。

我想要指出的是,这些问题和变通方法是一年多点的时间里面出现的,但用一篇只需10分钟看完的文章加以概述。

无论怎样,过去发生的一切都已经是过去的了。最重要的部分是路线图。这就是你运行Docker(或改而使用自动扩展组)所需要知道的方面。

引言

我生平第一次遇到Docker还是在2015年初的时候。我们试用Docker是想一探究竟,看看它能不能给我们带来好处。那时候还无法在后台运行容器,也没有任何命令可以查看什么进程在运行、调试或者通过ssh进入到容器。那次尝试很快宣告结束,Docker毫无用处,它更像是一个测试版原型,而不是正式版本。

快进到2016年。新工作、新公司、围绕Docker的炒作都在疯狂增多。我们的开发人员把Docker部署到生产项目,结果被卡住了。从好的方面来看,运行(run)命令总算管用了,我们可以开启、停止和查看容器。容器还算实用。

截至本文截稿时,我们在生产环境中有12个容器化的应用程序,散布在31个基于AWS的主机上,每个主机1个Docker容器(注意:下文介绍了原因)。

下面叙述了我们使用Docker的历程,这个过程充满着危险和意想不到的转折。

Docker在生产环境中的问题

Docker的问题:破坏性的变化和回归

我们运行了所有这些版本(或试着运行):
1.6 => 1.7 => 1.8 => 1.9 => 1.10 => 1.11 => 1.12

每个新版本都带来了破坏性的变化。今年初,我们从docker 1.6开始入手,运行单单一个应用程序。

3个月后我们更新了版本,因为需要一个只有后期版本才有的修正版。1.6分支已经被遗弃。

版本1.7和版本1.8无法运行。于是我们升级到了版本1.9,结果两周后发现它存在一个严重漏洞,于是我们又升级到了版本1.10。

Docker版本之间存在各种各样的细微的回归。它不断带来意想不到的破坏。

我们不得不调试的最棘手的回归与网络有关。Docker完全抽象了主机网络部分。这牵涉端口重新定向、DNS技巧和虚拟网络,可谓一团糟。

另外,去年Docker从官方的Debian软件库中删除,后来软件包由docker.io更名为docker-engine。这一变更之前的说明文档和资源过时了。

Docker问题:无法清理旧的镜像

Docker中最受期待也是最缺乏的功能就是清理旧镜像的命令(早于X天或X天不用的镜像)。考虑到镜像经常更新,每个镜像占用的空间超过1GB,存储空间是个重大问题。

清理空间的唯一办法就是运行这个命令,最好是每天按计划运行:
docker images -q -a | xargs –no-run-if-empty docker rmi

它列出所有镜像,然后清除。运行中的容器目前使用的镜像则无法清除(给出错误信息)。很麻烦,但还算管用。

Docker之旅始于一段清理脚本。这是每家企业组织都要经历的一种初始化仪式。

可以在互联网上找到许多尝试的方法,但没有一个很有效。没有API可以列出标有日期的镜像,有时虽有,但是在6个月内就被废弃了。一个常见的策略就是,从镜像文件读取日期属性,并调用“docker rmi”,但是命名变化后,这一招失灵了。另一个策略就是读取日期属性,直接删除文件,但要是没做好,这会引起损坏,除了Docker自己,别人根本做不好这项工作。

Docker问题:内核支持(或者缺少内核支持)

内核、发行版、Docker和文件系统之间的交互方面有着没完没了的问题。

我们在生产环境中使用带backports的Debian稳定版。我们开始在2015年11月发布的Debian Jessie 3.16.7-ckt20-1上运行。这个版本存在一个重大的关键漏洞,导致主机莫名其妙地崩溃(平均每几个小时就崩溃一次)。

Linux 3.x:不稳定的存储驱动程序

Docker有各种存储驱动程序(https://docs.docker.com/engine/userguide/storagedriver/selectadriver/)。唯一得到广泛支持的驱动程序就是AUFS。

AUFS驱动程序不稳定。它存在几个关键漏洞,会引发内核崩溃(kernel panic)、导致数据损坏。

它至少在所有“linux-3.16.x”内核上都出过问题。至于原因,我们毫无头绪。

我们非常密切地关注Debian和内核更新版。Debian在常规发布周期外还发布特别的补丁。2016年3月左右,AUFS有了一个重大的修正版。我们原以为它是真正的修正版,到头来却发现并不是。之后,虽然内核崩溃不常发生了(每周发生,而不是每天发生),但是这个问题依然存在,很碍眼。

今年夏天重大更新版中出现了一次回归,又带来了之前的严重问题。它开始逐台终止持续集成(CI)服务器,平均每2小时就终止一台服务器。有人迅速发布了应急补丁,以修复这个回归。

2016年发布了AUFS的多个修正版。一些严重问题得到了修复,但是还有更多的问题依然存在。AUFS至少在所有“linux-3.16.x”内核上不稳定。

Debian稳定版在内核3.16上卡住了。它不稳定。除了换成Debian测试版(可以使用内核4),没有什么其他法子。
Ubuntu LTS运行内核3.19。无法保证这个最新的更新版修复这个问题。更换我们的主操作系统会带来太大的破坏,但我们实在无计可施了,于是有一段时间考虑这一招。
RHEL/CentOS-6在内核2.x上,RHEL/CentoS-7在内核3.10上(Red Hat后来开发了许多backports)。

Linux 4.x:内核正式叫停对Docker的支持

众所周知,AUFS有没完没了的问题,它被开发人员视作累赘。作为一个由来已久的目标,AUFS文件系统在内核版本4中终于被丢弃了。

没有非官方补丁支持它,没有可选模块,没有任何backport,什么都没有。AUFS完全不见了。

那么,没有了AUFS,Docker如何运行?它无法运行。

于是,Docker的人员编写了一种新的文件系统,名为overlay。

“OverlayFS是一种现代的统一文件系统,类似AUFS。相比AUFS,OverlayFS的设计更简单,并且自版本3.18以来就在主线Linux内核中,可能速度更快。”— Docker Overlay文件系统驱动程序

请注意:它并不向后移植到现有的发行版中。Docker从来不关注向后兼容性。

补充说明:Overlay这个名称既指支持它的内核模块(由Linux维护人员开发),又指使用它的Docker存储驱动程序(这是Docker的一部分,由Docker开发)。它们是两个不同的组件(历史和开发人员可能会有重叠)。问题似乎主要与Docker存储驱动程序有关,而不是与文件系统本身有关。

Overlay大失败

文件系统驱动程序是一个复杂的软件,它需要很高级别的可靠性。老读者会记得Linux从ext3迁移到ext4。编写要花时间,调试要花更多时间,将其作为流行发行版的默认文件系统来发布要花的时间就更长了。

一年内搞出一种新的文件系统是不可能完成的任务。考虑到这项任务派给了Docker,这实际上很可笑,因为Docker的版本一向不稳定,时常有灾难性的破坏性变化,我们绝不希望这种情况出现在文件系统中。

长话短说。这方面进展不大顺利。你在网上搜一下还是能找到可怕故事。

在最初发布1年内,Overlay的开发工作就被叫停了。

然后出现了Overlay2。

“overlay2驱动程序旨在克服overlay的局限性,但是只与Linux内核4.0[或以后版本]和Docker 1.12兼容”— Overlay vs Overlay2存储驱动程序。

1年内搞出一种新的文件系统仍是不可能完成的任务。Docker刚试过,但失败了。不过它又在尝试!我们会看看几年后结果怎样。

眼下,它在我们运行的任何系统上都得不到支持。我们无法使用它,甚至无法测试它。

汲取的教训:正如你从Overlay以及后来的Overlay2中可以看到,没有backport,没有补丁,没有向后兼容性。Docker一味向前迈进,结果破坏了系统。如果你想要采用Docker, 也得向前迈进,密切关注来自Docker的版本、内核、发行版、文件系统和一些依赖项。

补充内容:全球性的Docker故障

2016年6月2日,大概上午9点(伦敦时间)。新的软件库密钥被发到了Docker公共软件库。

直接后果是,只要在配置了故障软件库的系统上运行“apt-get update”(或同等命令),就会失败,出错信息是“错误https://apt.dockerproject.org/哈希值总和不匹配”

这个问题波及全球。它影响了配置Docker软件库的全球所有系统。它在所有的Debian和ubuntu版本上得到了证实,与操作系统和Docker版本无关。

全球依赖Docker安装/更新或系统安装/更新的所有持续集成管道都受到了破坏。不可能在现有系统上运行系统更新或升级。不可能创建一个新的系统、将Docker安装在上面。

一段时间后,我们拿到了Docker员工发来的更新版:“发布更新版;我在内部提出了这个问题,但需要修复这个问题的人在旧金山(时差与伦敦相差8小时),所以还没有到位。”

我个人在内部向开发人员宣布了这一点。如今,没有Docker持续集成,我们无法创建新的系统,也无法更新依赖docker的现有系统。我们的希望全寄托在旧金山的那个家伙,他目前在呼呼大睡。

在佛罗里达州的Docker人员大概在下午3点(伦敦时间)发布了更新版。他醒着,发现了这个问题后编写修正版。

密钥和软件包随后重新发布。

大概下午5点(伦敦时间),我们试图核实修正版。

由于Docker,出现了7个小时的全球性故障。那次故障之后留下的就是GitHub issue上的几则消息(https://github.com/docker/docker/issues/23203)。没有事后分析。尽管这次故障带来了灾难,却几乎没有什么科技新闻或媒体报道。

Docker注册中心

Docker注册中心负责存储和提供Docker镜像。

自动化持续集成构建 ===> (成功后)将镜像发送到===> Docker注册中心
部署命令<=== 获取镜像,来自<=== Docker注册中心

有一个公共注册中心是由Docker运营的。作为一家企业组织,我们也运行自己的内部Docker注册中心。它是在Docker主机上的Docker里面运行的Docker镜像。Docker注册中心是最常使用的Docker镜像。

Docker注册中心有三个版本。客户机可以从下面任何注册中心独立获取:

  • 注册中心v1(https://github.com/docker/docker-registry),现已被废弃和丢弃
  • 注册中心v2(https://github.com/docker/distribution),用Go完全重写,2015年4月首次发布
  • 可信注册中心(https://www.docker.com/products/docker-datacenter),这是在文档到处提到的一种(收费?)服务,不确信它是什么,干脆忽视它。

Docker注册中心问题:丢弃和淘汰

Docker注册中心v2是完全重写的。注册中心v2一发布,注册中心v1被停用了。
就为了让Docker可以正常运行,我们不得不再次安装新的版本。它们改变了配置、URL、路径和端点。

迁移到注册中心v2的过程并非很顺畅。我们不得不修复安装的系统、构建的版本和部署脚本。

汲取的教训:别信任任何Docker工具或API。它们被不断丢弃和淘汰。

注册中心v2的目的之一就是,带来更好的API。它在此(https://docs.docker.com/registry/spec/api/)有记载,我们不记得9个月前有这个说明文档。

Docker注册中心问题:无法清理镜像

不可能从Docker注册中心清理镜像。也没有垃圾回收机制,文档提到了一个,但并不真实。(镜像确实有压缩和重复数据删除,但那是另一回事)。

注册中心一直在变得庞大。我们的注册中心每周就会增大50GB。

我们不可能有存储量无限制的服务器。我们的注册中心有几次用完了空间,导致构建管道一团糟,然后我们把镜像存储到了S3。

汲取的教训:使用S3来存储镜像(S3在默认情况下得到支持)。

我们总共执行了3次手动清理。无论在什么情况下,我们都要停止注册中心,删除所有存储的内容,开启一个新的注册中心容器(幸好,我们可以借助自己的持续集成,重新构建最新的Docker镜像)。

汲取的教训:从Docker注册中心存储系统手动删除任何文件或文件夹会造成损坏。

时至今日,不可能从Docker注册中心删除镜像。也没有API。(v2的目的之一就是有一个更好的API。任务失败)。

Docker问题:发布周期

Docker发布周期是Docker生态系统中唯一不变的:

1.丢弃任何存在的东西

2.制作新的内容并发布

3.忽视现有的用户和向后兼容性

发布周期适用但并不局限于:docker版本、功能特性、文件系统、docker注册中心和所有API……

从Docker的过往历史来判断,我们可以大致推测:Docker开发的任何东西其半存留期只有大概1年,这意味着现在存在的一半东西一年后被丢弃和淘汰。通常会有替代版本,但是与它所取代的对象并不完全兼容,是否可以在同一生态系统上运行是个未知数。

“我们开发软件不是为了供人们使用,而是由于我们喜欢开发新的东西。”— 未来的Docker墓志铭

目前Docker在我们企业组织的现状

方兴未艾的Web服务和微服务

Docker最初是因Web应用程序而出现的。那时候,它是开发人员包装和部署应用程序的一种简易方法。他们试过Docker后迅速采用了它。后来,Docker扩散到了一些微服务,我们开始采用微服务架构。

Web应用程序和微服务很相似。它们是无状态应用程序,可以不假思索地启动、停止、终止和重启。所有棘手的方面交给了外部系统(数据库和后端系统)。

Docker的采用始于次要的新服务。最初,一切在开发、测试和生产环境下运行顺畅。由于更多的Web服务和Web应用程序采用容器化,内核崩溃慢慢开始出现。到了后来,稳定性问题变得更突出、影响更大。

一年内发布了几个补丁和回归。一段时间来,我们在Docker方面一直在想变通方法。它令人痛苦,但似乎没有打消人们采用Docker的热情。在企业组织内部,支持和需求仍呈增长势头。

注意:没有一次故障曾经影响任何客户或资金。我们在遏制Docker方面相当成功。

被禁止用于核心系统

我们在运行的几个关键应用程序是用Erlang编写的,由“核心”团队的几个人来管理。

他们试图在Docker中运行几个应用程序,但行不通。由于一些原因,Erlang应用程序和docker无法兼容。

那是很久前搞的,我们记不起所有细节了。Erlang对于系统/网络应如何运行有特定的想法,预计的负载是每秒数千个请求。 任何不稳定性或不兼容性都会导致显眼的故障。(我们现在确信,尝试过程中使用的版本存在多个重大的不稳定性问题)。

尝试发出了危险信号。Docker没有准备好用于任何关键系统。这是个正确的信号。后来的崩溃和问题证实了这点。

我们只用Erlang来编写关键应用程序。比如说,核心团队的人员负责一套本月处理了96544800美元的交易的支付系统。该系统包括几个应用程序和数据库,它们都归核心团队负责。

Docker是危险的累赘,可能会让数百万美元岌岌可危。它被禁止用于所有核心系统。

被禁止用于数据库

Docker旨在是无状态的。容器没有永久性磁盘存储,发生的一切都是短暂的,容器停止后,不复存在。容器并不是用来存储数据。实际上,设计它们的初衷是不存储数据。谁要试图违背这个理念,注定会招惹灾难。

此外,Docker通过抽象机制将进程和文件锁起来,它们接触不到,就好像不存在似的。如果出了什么岔子,它阻止执行各种恢复。

长话短说。按照设计,Docker不可在生产环境下运行数据库。

情况比这还要糟糕。还记得Docker存在的内核崩溃这个老大难吗?

崩溃会摧毁数据库,并影响与之连接的所有系统。它可能是个异常错误,在频繁使用情况下更常被触发。数据库是终极的输入/输出密集型负载,保证会引发内核崩溃。另外,另一个错误会破坏docker挂载(毁灭所有数据),可能还会破坏系统的文件系统(如果在同一个磁盘上)。

恶梦般的场景:主机崩溃,磁盘受到损坏,在此过程中摧毁了主机系统和所有数据。

结论:Docker不可在生产环境下运行任何数据库,万万不行。

偶尔会有人过来问:“为何我们不把这些数据库放入到docker中?”我们会告诉经历的几个难忘的可怕故事,到目前为止,没人再问第二遍。

个人观点

Docker日益受到追捧,得到了一些狂热的支持。Docker炒作不再是一种技术累赘,它还演变成了一个社会问题。

眼下,其外围受到了控制,局限于一些无状态Web应用程序和微服务。如果是不重要的东西,可以容器化,哪怕每天崩溃一次,我也不在乎。

到目前为止,想使用docker用于重要东西的所有人在快速讨论后打消了念头。我最担心的是,有一天,docker发烧友听不进道理,一意孤行。

恶梦般的场景:未来的会计集群改造,目前保管2300万美元的客户资金。已经有个人认真地问架构师“为什么你不把这些数据库放入到docker?”架构师脸上是什么样的神情难以言表。

我的责任就是保护客户及其钱财。

让Docker在生产环境下存活下来

密切关注版本和变更日志

密切跟踪内核、操作系统、发行版、Docker以及其他一切的版本和变更日志。寻找错误,寄希望补丁,认真阅读一切。
ansible ‘*’ -m shell -a “uname -a”

任由docker崩溃

任由docker崩溃。这不言而喻。
我们偶尔看一下哪些服务器已完蛋,强行重启它们。

每个服务都有3个实例

高可用性需要每个服务至少有2个实例,那样实例故障后才会存活下来。

使用docker用于任何不太重要的系统时,我们应该有3个实例。Docker一直在死亡,我们需要误差幅度,以支持同一服务的连续2次崩溃。

大多数时候,崩溃的是持续集成或测试实例。(它们运行大量密集的测试,问题来得尤其突出)。我们有许多这种实例。有时候,一个下午会出现3个实例连续崩溃的情况。

别将数据放入到Docker

存储数据的服务无法容器化。

Docker旨在不存储数据。别违反这一点,那样只会招惹灾难。

之外,目前有些问题在终止服务台,可能摧毁数据,所以这确实是一大禁忌。

别在Docker里面运行任何重要的东西

Docker会崩溃。Docker会摧毁它触及的一切内容。

它局限于可能崩溃,但不引起停运的应用程序。这意味着大多数无状态应用程序,它们完全可以在其他某个地方重启。

把docker放入到自动扩展组

Docker应用程序应该在自动扩展组中运行。(注意:我们还没有完全到那一步)。

无论实例何时崩溃,它都在5分钟内自动更换。不需要手动操作。自我愈合。

未来路线图

Docker

Docker方面不可能克服的挑战就是,提出一种切实可行的组合:内核+发行版+ docker版本+ 文件系统。

眼下,我们并不知道任何运行稳定的组合。我们在积极寻找这样一种组合,不断测试新的系统和补丁。

目的:找到一种可运行docker的稳定的生态系统。

开发一款优秀、稳定的软件需要5年,而Docker v1.0问世才28个月,它没有时间来成熟。

硬件更新周期是3年,发行版的发布周期是18-36个月。Docker在之前的周期并不存在,所以系统无法考虑与它的兼容性。导致情形更糟糕的是,它依赖许多高级的系统内部组件,它们比较新,也没有时间来成熟,无法进入到发行版。

5年后它可能是一款还不错的软件,敬请拭目以待。

目的:等待情况有所改善。在此期间尽量不要破产。

使用自动扩展组

Docker局限于无状态应用程序。如果某个应用程序可以包装成Docker镜像,它可以包装成AMI。如果某个应用程序可以在Docker中运行,它可以在自动扩展组中运行。

大多数人忽视了,可是Docker在AWS上毫无用处,它实际上后退了一步。

首先,容器的意义在于通过在同一个大大的主机上运行许多容器,以此节省资源(不妨暂且忽略当前的docker漏洞,这个漏洞导致主机及在其上面运行的所有容器崩溃,迫使我们只好每个主机运行1个容器,以确保可靠性)。

因此,容器在云提供商上毫无用处。始终有一个大小合适的实例。就为应用程序创建一个拥有适当内存/处理器的容器(AWS上的最基本实例是t2.nano,512MB和处理器的5%资源,每月仅需5美元)。

其次,容器周围有一个完整的编排系统,可以自动管理创建/停止/开启/滚动更新/金丝雀版本发布/蓝屏部署,容器才会显现其最大好处。目前不存在可以做到这一点的编排系统。(这时候,Nomad/Mesos/Kubernetes最终会应运而生,但它们的现状不够好)。

AWS有自动扩展组来管理实例的编排和生命周期。这款工具与Docker生态系统完全毫无关系,但是它可以获得更好的结果,没有任何缺点和混乱。

为每个服务创建一个自动扩展组,并为每个构建AMI(提醒:使用Packer来构建AMI)。如果在AWS上进行操作,人们已经熟悉管理AMI和实例,没有太多的东西要学习,也没有陷阱。因而获得的部署是完全自动化。采用自动扩展组的安装系统比Docker生态系统领先3年。

目的:将docker服务放入到自动扩展组,以便能够自动处理故障。

CoreOS

补充说明:Docker和CoreOS由不同的公司开发。

如果只给Docker一次机会,它需要和依赖许多新的高级系统内部组件。一款典型的发行版无法在主要版本之外升级系统内部组件,即便它想这么做。

Docker有必要拥有一款特殊用途的操作系统,并且有适当的更新周期。这也许是拥有能够运行Docker的内核和操作系统这对组合的唯一途径。

目的:试用CoreOS生态系统,评估稳定性。

总的来说,把用来运行容器的服务器(基于CoreOS)与正常服务器(基于Debian)分离开来是切实可行的。容器没必要了解(或在乎)它们在运行什么操作系统。

麻烦在于管理新的操作系统家族(安装、配置、升级、用户帐户、日志和监控)。不知道我们会怎么做,或者这可能需要多大的工作量。

目的:随意部署CoreOS。

Kubernetes

未来的重大突破之一就是,能够管理从它们最终运行的机器抽象出来的大批容器,能够自动启动/停止/滚动更新和调整容量。

Docker的问题在于,它并不处理这任何任务。它就是一个哑的容器系统。它有容器的缺点,却没有好处。

目前市面上没有一种优秀的、久经考验的、生产环境就绪的编排系统。

  • Mesos并不是面向Docker的
  • Docker Swarm不可靠
  • Nomad只有最基本的功能特性
  • Kubernetes是新的试验性项目

Kubernetes是唯一旨在解决容器方面棘手问题的项目。它得到了其他任何项目所没有的资源的支持,即谷歌在大规模运行容器方面有着丰富的经验,谷歌拥有大量的资源可供支配,而且知道如何编写切实可行的软件。

眼下,Kubernetes是一个年轻的试验性项目,缺少说明文档。进入壁垒令人痛苦,远谈不上尽善尽美。不过,它取得了一定的成效,已经造福于一小批人。
从长远来看,Kubernetes是未来所在。它是重大突破(或者准确地说,它是容器在基础设施管理领域掀起重大革命所需要的最后一块拼图)。

问题不是是否采用Kubernetes,问题是何时采用它?

目的:请密切关注Kubernetes。

注意:Kubernetes需要docker才能运行。它会受到所有docker问题的影响(比如说,别在除CoreOS之外的任何系统上试用Kubernetes)。

谷歌云:谷歌容器引擎

正如前面所说,没有已知稳定的操作系统+ 内核+ 发行版+ docker版本这对组合,因而没有稳定的生态系统可以用来运行Kubernetes。这是个问题。

可能有一种变通方法:谷歌容器引擎(Google Container Engine,https://cloud.google.com/container-engine/)。它是一种托管的Kubernetes(和Docker)即服务,是谷歌云的一部分。

谷歌得解决Docker问题来提供目前提供的服务,别无选择。顺便说一下,也许只有谷歌能够为Docker找到稳定的生态系统,修复错误,并将这作为一种云托管服务来销售。我们可能会有一个共同的目的。

谷歌已经在提供这项服务,所以这意味着它已经在解决Docker方面的问题。因此,要让容器在生产环境下切实运行起来,最简单的办法可能就是使用谷歌容器引擎。

目的:改用谷歌云,从未被AWS锁定的子公司开始入手。忽视路线图的其余部分,因为那些无关紧要。

谷歌容器引擎:这是证明谷歌云是未来、AWS过时的另一个原因(除此之外,平均而言,实例要便宜33%,网络速度和IOPS要高出3倍。)

K8S中文社区微信公众号
分享到:更多 ()