进入2016年以后,容器技术早已经从最初的牛逼满天飞到了脚踏实地的大规模铺开。很多企业都已经在实际项目中或深或浅的使用着容器技术,享受到新技术带来的简洁和高效。作为国内最早研究和使用Docker技术的企业,ThoughtWorks在2013年底就在实际项目中将Docker用于测试环境的资源复用,并在之后的许多项目中逐渐总结出许多有用的实践和经验。在这篇文章里,我将聊聊Docker在我经历过项目中的一些比较有代表性的运用场景。
现实中的容器技术运用方式非常广泛而灵活,时常让人觉得脑洞大开,概括来说是『可小可大,可远可近』。下面用四个案例来阐示容器在非特定领域里的运用场景。
容器之小:小而美的容器DevOps架构栈
图1基于容器和DevOps理念的运维架构
这张架构图来自于一个规模不到20人的小型产品团队,团队的结构十分精巧,由两名开发人员兼任主要的运维工作。这两位开发人员,花了几周时间通过Ansible陆陆续续搭建起了这套由上百个服务器节点组成的集群,并由团队所有开发人员共同维护。整体套集群系统高度自动化,使得团队的每个人都能够十分快速而安全的完成业务功能的部署、获取线上业务的运行状况、以及对出现问题的故障点进行快速的日志错误定位。
麻雀虽小,五脏俱全。这套技术方案包含了集群管理、网络管理、服务发现、日志管理、性能监控等许多方面的设计,从架构的角度上看,已经俨然是一个小型私有PaaS平台。
Swarm作为集群的管理工具,具有与Docker原生命令良好的一致性,在学习曲线比较缓和,在DevOps文化比较好的团队中很容易让开发人员快速上手。在这个架构方案中使用了Consul作为集群元数据存储的方案,Swarm的主、从节点信息以及Docker的跨节点网络划分的信息都存放在这里。Consul除了作为集群信息的存储,还可以用于应用服务的配置存储和服务发现,以及作为内网的DNS服务使用。不过出于安全性和可维护性的考虑,应该为应用服务单独搭建独立的Consul节点,与存储集群配置的Consul分开,防止由于数据干扰和意外修改引起大规模系统故障。
使用Swarm的另一个潜在好处是它能够充分利用Docker内置的跨节点网络功能,这套基于VxLAN的SDN实现十分简洁易用,通信效率也很不错。
容器集群的的性能监控和日志管理是使得这个小团队得以驾驭比团队人数更多的服务节点的关键要素,任凭运行的服务在机器漫漫海洋中随意穿行,这两件工具就是开发人员的罗盘和风向标,在关键时候为线上故障的定位争取宝贵时间,并能从中迅速找到每个服务当前运行的节点,从而采取必要的应急措施。cAdvisor+Influxdb+Grafana是一套为容器集群性能监控设计的开源解决方案,利用cAdvisor对容器信息的良好监控能力,Influxdb对时间序列数据的快速检索能力,以及Grafana的强大图表展示能力,形成性能数据的实时查看和历史回溯,并反馈到开发和运营的状态报表,形成完整闭环。不过,这个开源组合的缺陷在于缺乏现成的事件告警组件,在Influxdata公司的Telegraf项目逐渐成熟后,可以考虑使用它替代cAdvisor的功能,然后集成Kapacitor作为告警模块,提前预知服务的不正常状态。日志管理方面,这套系统使用了当下最主流的容器日志开源工具组合Fluentd+EslasticSearch+Kibana,在《程序员》2016年6月刊『容器的性能监控和日志管理』一文中已经对这个组合进行过比较深入的探讨。
这是Docker集群化实践中运用得比较出色的一个案例,特别是对中小型产品团队,会有不少可借鉴和启发之处。在不用增加额外运维人员的情况下,这套系统可以比较轻松的扩容至几百上千的规模。然而,这个架构本身并没有考虑譬如多数据中心、租户隔离、访问授权、资源配额等复杂情景,它主要的设计初衷在于解决集群易用性的问题。试想在过去使用虚拟机管理服务的时代,让只有几个人的团队去维护上千个计算节点上运行的需要各种不同环境和配置的服务,这简直是不可完成的任务,然而通过容器化的部署、DevOps思维的团队、加上适当的集群辅助工具,他们做到了。
容器之大:大型任务集群的容器化调度
图2基于容器的多数据中心任务平台架构
并不是所有的团队都愿意从头构建自己的整套运维架构和基础设施环境。在许多企业里,服务的运维管理是有专门的组织负责的。这些组织可能叫做平台部门、运维部门、或者环境支持部门,不论称呼如何,这些组织以及部门通常都需要管理数量相当庞大的计算资源。这些资源可能是跨机房,跨城市,甚至是分布在欧洲、美洲、非洲并且相互无法直接通信的数据中心里。他们所需要调度的作业数量和种类也远远超过一个自运维产品团队所需要考虑的规模。
为这样的组织设计基于容器的任务调度平台需要对企业的需求和特定业务领域有充分的了解,越是大型的基础设施集群,所需要应对的风险和不确定也越大,设计一个面面俱到的通用大型集群也越困难。因此针对具体业务场景做出一定的取舍是不得已、但又是必要的。例如为了获得较高的响应速度而将集群划分为多个互不重叠的调度区域,因而限制了每个区域的容量;为了避免内网数据网络风暴而将节点数据分层处理并逐级减少数据汇总的维度,因而增加监控管理复杂度;或者为了增加系统规模而采用高度聚合而不适合多数据中心的方案。这些方案往往不需要具备普适性,而是会针对特定企业和业务场景进行恰到好处的修剪和优化。
上面图中展示的是一个企业PaaS服务平台的结构,架构基于Kubernetes集群,需要应用在多个异地数据中心,并在统一的部署系统上对服务进行管理。由于单Kubernetes集群容量有限,这个方案实际上根据地域划分和租户的规模构建了多个几十到上千节点不等的子集群,集群直接互不重合,属于同一个任务组的服务只会在特定的某个集群内进行部署和调度,其实就是将集群和租户进行了绑定。在所有集群之上,通过自研的一个任务分发服务作为所有调度任务的入口,在这里处理服务的依赖关系、所属区域、以及其他元数据信息,然后调用Kubernetes的API完成任务的部署和调度,并通过额外的组件处理网络、存储等资源的配置。
在图中省略了系统采用的其他自研模块,值得一提的是这个系统的性能数据管理使用了开源的Promethus软件。Promethus是SoundCloud公司维护的一款包含信息采集、处理、分析、展示和告警的性能监控整体解决方案,它提供了比较灵活的多数据中心级联能力和集中式的配置管理功能,因此特别适合规模较大的计算集群。不同于前一案例中Influxdb方案每个数据采集节点发数据给存储数据的中心节点的方式,Promethus的性能数据采集是由中心服务器主动向所有节点定时轮询的方式拉取的,因此所有与数据采集相关的配置全部在中心服务器上进行修改即可。而节点的数量和IP地址变动则通过服务发现机制来告知中心服务器,这大大简化了修改数据收集参数的流程。
这个案例是一个比较典型的大规模容器集群,在大型容器集群方面许多企业都有着自己的实践沉淀。其中有两个比较明显的特点是从业务场景制定架构和系统中包含许多自研的组件,因此在借鉴的时候更需要广泛的收集信息,避免盲目照搬。
容器之远:基于容器的持续集成实践
图3基于容器的持续交付流水线示意
接下来,让我们用广角镜头来审视一下软件发布的生命周期。通过持续交付的流水线,我们能够清晰的定义出软件从代码提交到上线发布之前所需要经过的每个环节,协助开发者发现工作流程中存在的瓶颈,并促使团队提升端到端的自动化程度,缩短独立功能上线的周期。
那么容器在其中能扮演什么样的角色呢?首先是资源的隔离,为了确保每一次编译和测试的独立性,软件应该在干净的环境中分别进行构建、打包、并运行测试用例,而容器是非常合适用来提供这种虚拟环境的轻量级工具。其次是一致的软件打包方式,Docker的封装意味着不论运行的服务是用Java、Python、PHP还是Scala、Golang,平台可以用几乎相同的方式去完成部署,而不用考虑安装服务所需的环境,这些都在软件开发的时候就已经准备好了。***是成熟的调度平台。基于容器有许多现成的任务调度框架,也正是由于前两个角色,容器使得任务的分发变得容易,由于应用不需要依赖主机的配置,这就让任务的灵活调度成为可能。
基于容器的持续交付流水线和普通交付流水线很相似,包含构建、打包、测试、部署等环节。同时这其中也有许多技巧和专用于容器的优化手段。这个案例中我们选取其中两个比较具有启发性的来说。
***个例子是关于容器构建的优化。容器的构建通常都是由某个基础镜像开始,通过Dockerfile的描述自动化逐步执行,直至完成预期的状态。几乎所有项目的Dockerfile都不会每次从一个原始的Ubuntu或者CentOS的镜像做为基础,从头构建整个运行环境,因为那样会使得每次构建花费非常长的时间。制作用于继承的公共基础镜像是早已世人皆知的镜像构建提速优化的方法,这样可以让费时而又不常改变的步骤固定下来,每次构建时候就只需要基于这个镜像再进行增量修改就可以了。但这种方法其实也有潜在问题,那就是当我们需要升级基础镜像的时候,不得不重新构建所有基于它制作的所有服务镜像。
这个问题被称为『脆弱的基础镜像』,该问题的应对策略有很多。例如简单的延迟子级镜像的升级时间,直到每个子镜像下次重新构建发布时自然会获得更新。又例如比较激进的方式,通过流水线建立镜像的依赖关系,在父级镜像一旦更新时,自动触发所有子级镜像的自动重建,这种方式要慎重采用,因为它很可能会导致同时产生大量的镜像构建任务,对网络和磁盘造成严重的压力。那么,有没有在一种办法既能获得具有时效性的更新,又不会产生短时间内的构建风暴呢?其实对于一些场景是可以有取巧方法的,通过Docker的外挂存储能力,将经常可能变化的内容做成单独的镜像,然后利用Docker的『–volume-from』参数在服务启动时覆盖掉运行容器的特定目录。典型的场景就是用于编译其他服务的容器,这些容器中一般都会有一些编译服务时所需的时依赖库,这些依赖库随着项目所需依赖的变化也要跟着变,像Maven的~/.m2/repository目录,Node的全局node_module目录等就很适合这样管理。当这个目录下面的内容需要更新时,只需重新构建提供目录内容的一个镜像,而不会产生镜像构建的链式反应,服务下次启动时候就会获得新的依赖库目录了。
第二个例子是流水线中的测试环节。进行自动化测试的时候,容器的优势发挥尤其明显。对于外部服务的依赖,比如与数据库相关的测试,由于测试过程需要反复运行,过去时候,如果测试运行完没有正确的清理留下的数据,特别容易影响后续测试的运行结果。容器恰恰是提供这种即用即弃基础设施***的方式,完全可以在测试脚本中先启动一个全新的MySQL服务,然后测试完就销毁,保证了每次测试的独立性。关于这方面的应用在下个案例中再介绍更多细节。
类似的技巧还有很多。持续交付流水线是最能体现容器在软件领域带来各方面改进的大观园。许多现成的工具可以***化的避免手工操作对流程的干扰,让软件发布开上高速公路。
容器之近:容器在自动化测试平台的运用
图4基于容器的自动化测试平台架构
***这个案例是一个针对软件自动化测试环节的容器化基础设施设计。它是软件持续交付流水线上的一个重要环节,让我们带上长焦镜,近距离审视容器在软件测试场景中能解决怎样的问题。
容器快速启动、快速销毁的特性与软件测试时所需的每次干净独立的临时运行环境十分匹配。使得在这方面容器可以大有作为。特别是在集成测试和功能性测试的阶段,被测系统的运行往往会需要涉及多个要独立运行的子组件或子模块。还有外部模块的依赖,如果进行的是界面相关的测试用例,往往还会用到Selenium和浏览器的组件。而运行数据库相关的测试则会需要MySQL、Mongodb等组件。手工为每个测试用例准备并维护这些环节依赖是十分让人抓狂的事情。过去做这类测试时候为了解决依赖问题,通常做法是额外部署一套专用于测试依赖的环境,所有模块测试需要别的模块时都统一指向这套测试环境作为目标。由于过于频繁的升级这个依赖环境可能会打断正在运行的测试用例,因此只能对它进行定期的更新,这种无形中限制了的时效性和可靠性。
特别是一些比较重要并且耗时较短的回归测试和冒烟测试用例,理想情况下应该在每次代码提交后都全量的更新并执行,以便***时间发现一些潜在的功能缺陷。但为每次提交创建一套测试环境不论是手工操作还是过去基于虚拟机的自动化方式都过于繁琐。
案例中的测试平台正是意图通过容器和简单的依赖描述,来解决测试环境管理的问题。它基于所有被测组件和所依赖的组件都使用Docker镜像来提供的前提之上,将所有组件抽象成一致的模型写成描述文件,描述文件的主要内容就是整个测试环境所需的镜像和启动顺序。
示意图中的『运行调度器模块』是接入持续交付流水线的调用入口,可以采用譬如Jenkins的形式插件实现,它用来创建和保存特定测试用例所需的环境描述文件内容。在流水线触发该测试环节时,这个模块调用『测试执行器模块』,将描述模型用特定的结构体传递给后者,后者解析这个数据模型,转化为接近Kubernetes服务模型的形式,然后在『服务依赖管理模块』的协助下,通过Kubernetes创建临时的Namespace,并依次创建每个服务。当测试环境就绪后,『测试执行器模块』就开始执行测试用例,***又通过『服务依赖管理模块』通知Kubernetes销毁整套环境。
整个过程对于平台的用户而言,仅仅是增加了一个测试环境描述的内容,写在持续交付流水线测试步骤的定义(例如Jenkins插件配置)里。而这套系统内部颇为复杂的执行过程,能够有效的利用整个集群的资源,恰到好处的为测试的过程提供支持。
总结
这四个案例由浅入深、由远及近的展现了容器在现代软件和基础设施设计中举足轻重的作用。有些技术会直接改变人们的生活,而另一些技术则会改变技术本身以及技术的发展方向,容器技术属于后者。
随着容器运用的普及,当下的主流媒体对容器周边技术的关注还在持续升温。不仅是《程序员》推出了本期的容器技术专刊,在***一期的ThoughtWorks公开刊物《技术雷达【1】》中,容器和Docker相关的关键词同样占据了大量版面。在越来越多的技术领域里,无论是移动设备、物联网、大数据,都能看到容器技术各种形式的延伸,作为现实容器运用的一道缩影,此文可作为窥斑见豹、抛砖引玉之用。
参考链接:
【1】https://assets.thoughtworks.com/assets/technology-radar-apr-2016-cn.pdf