在 Docker 上运行微服务

  • 杨爽

2015 年 6 月 19 日

话题:ThoughtWorks云计算语言 & 开发架构

在过去的两年里,微服务架构已经成了非常热门的名词,它出现在很多论坛、视频、演讲中。作为一种更灵活、可靠、开放的架构,其应用实践也越来越多。近日,来自七牛云存储、DaoCloud、京东、ThoughtWorks 的架构师们分别就微服务架构实践、容器技术带来的技术价值、微服务面临的挑战和解决目标等方面的经验做了交流分享。

微服务大探险

软件架构是在考虑业务、技术能力、团队、可维护性、安全性、可靠性及可持续性等多重因素下对软件内部进行的划分。通过划分,让软件内部不同的部分之间能够相互独立,又能够相互协作,同时给用户提供最终的价值。但什么是微服务架构呢?ThoughtWorks 首席咨询师王磊通过一个互联网门户案例为大家解释了微服务架构的概念,以及它如何影响传统的软件架构设计。

一年前,该门户每签一个 10 万的合同所耗费的成本是 3.5 天。他们当时的 CRM 结构是典型的三层架构,整个应用程序由一个 40 万行的代码库组成,后端有一个主动的数据库。虽然使用三层架构的成本比较小,但随着代码和功能的增加,代码库不断膨胀,修改代码存在的风险很大,整个维护成本也变得越来越高。

每当开发人员提交代码后,所需的数据集成和构建需要 50 分钟,意味着每天 8 小时工作时间最多能有 9 次代码提交。但为了系统的稳定性,持续集成过程中要尽量避免提交代码,因此,整个团队的交付能力受到了限制。此外,从准备部署包到上线需要3天,3天后才能让用户真正用到部署包,才能实现价值。而如果增加新人,要开发新的环境,包括测试和产品环境,培养周期会很长。针对以上难题,必须制定如何在团队中对系统进行改造从而满足业务需求的策略。

  • 将现有的系统保护起来,把所有开发新功能的优先级都降下来,只对系统做最紧急的修改,其他和部门进行协商,让团队保持新的精力和时间在重要的业务上。
  • 功能剥离。通过定义新服务,在前端用一些代码的机制让用户逐渐访问新服务,可以达到从原有系统抽出小功能,让客户访问小功能。
  • 数据解耦。对于庞大的系统,因为无法很快将所有系统换掉,所以为了保证系统仍然可用,要启用数据同步机制,让服务里的数据同步到原有数据库。
  • 渐进替换。通过不断地运行以上策略,将原有系统的复杂功能抽离出来用新的方式来做。

目前,每签一个 10 万的合同所耗费的成本由 3.5 天变为 1 天,持续集成构建从 50 钟降低到 18 分钟,团队成员从 10 人降到 7 人,部署周期由 3 天降到 2 小时。

对于每个应用程序,可能有一组小的服务组成,每个服务运行在自己的进程中,服务与服务之间通过轻量级的机制进行交互。那么,如何使用微服务做系统改造呢?

  • 为每个服务建立独立的环境,包括基础设施、持续集成环境、运维、监控、日志聚合、报警。
  • 不断演进的微服务开发模板,发现问题及时修改,让模板更高效。
  • 轻量级的通信协议。
  • 消费者的契约测试,解决随着服务增多带来集成测试效率低的问题。
  • 基础设施自管理,帮助管理自己需要的资源。

Container为微服务带来了什么?

七牛云 CTO 韩拓从服务端的架构和运维两个方面介绍了 Container 和微服务带来的价值。他认为 Container 是基于内核的空间。一个操作系统的内核主要管理资源,把服务器交给操作系统的内核,它把内存、CPU 和硬盘等资源管理起来。Container 进一步做隔离,这个隔离以进程为单位,让一个进程只能看到这个网卡收发的数据,但是看不到其他的网卡。

Container 有以下几个名字空间。

  • 网络的空间,它隔离了和网络相关的资源,如服务器上的网卡、IP 地址、服务表等,之后这个进程在某个网络的空间内运行就看不到其他空间相关的网络资源。
  • 文件系统,这个名字空间把这类资源也进行了隔离。一个进程运行时看到的根目录可能不是操作系统原生的根目录,看到的块设备也不是原来的块设备。
  • PID,每运行一个进程都有一个 PID,现在内核里的名字空间,PID 的资源也被隔离起来。

Container 对系统调用做了权限的控制。例如一个进程启动的时候限制它的权限,让很多系统调用做不了。Container 的作用包括镜像管理和运行实例的管理,还有输入输出的管理。

那么,Container 对服务端架构有什么影响呢?韩拓认为不管做什么架构,很重要的工作是模块化,去定义这个模块的边界,怎么工作、怎么测试及在生产环境如何部署。因此,从组件的角度看微服务化主要有以下三点。

  • 组件划分的方式,Container 以功能为单位来划分组件的边界。
  • 组件物理边界,以前的边界有静态或动态的库,模块间的边界通常是函数调用。而微服务组件的物理边界是网络,这些组件都是独立的、可编译的进程(即每个单独的服务实例),这些服务实例之间通过网络来沟通。
  • 组件的依赖方式,以前是在编译期考虑怎么才能把可执行程序编译出来,只要编译出来,就能肯定依赖关系肯定被解决了。当微服务化之后,依赖方式的处理被延后了,延后到运行的时候,因此错误被延后了,组件间的依赖方式变复杂了。Container 中组件间的依赖可通过渲染文件和环境变量等实现。

关于 Container 对运维的影响,主要是告别 DevOps。作为 DevOps 实践者,韩拓认为,DevOps 的核心是试图寻找一个合理的开发和运维的边界。有很多方法论寻找这个边界,DevOps 是其中一种,主要将运维平台化,做监控平台、日志平台等。

在持续集成方面,以前的任何一行代码做了更变都要把整个业务重新做一遍,需要很长时间。微服务化之后,每个模块独立编译、独立打包、独立测试,其边界也很明确,代码的变更影响的是一个组件和服务,单独进行编译和测试的动作会很快。

部署和升级方面变得简单,可以独立地部署和升级。

资源规划方面,按峰值申请资源会存在资源浪费。微服务化后,可以按模块去扩容。对资源的规划更灵活和合理。尤其再结合 Container,因为 Container 以进程为单位,它对资源的使用是竞争的,不需要提前规划。

网络方面,Container 一般通过端口映射的方式做网络。

监控方面,业务的运行状态对运维来讲更透明,小的服务可以单独监控,一些问题出来之后可以做报警。但监控变多了,需要监控系统更智能。

最后在高可用方面,传统的架构会做成透明的。现在,高可用由外部的框架完成,例如在做服务间依赖时就可以做高可用,不仅屏蔽了服务在哪里,服务了多少人,而且还可以把服务是死是活的细节屏蔽掉。

Micro Service,Continuous Delivery and Docker

DaoCloud 联合创始人 & 研发副总裁郭峰认为容器技术解决了原来 PaaS 技术不能解决的问题。于是他给大家展示了微服务带来的挑战,即如果将一个应用微服务化,那么很容易导致一台机器上跑很多程序。因为服务纠缠在一起,所以微服务需要运用自包含。DaoCloud 的容器技术是把最古老的几个技术融合在一起,用了一个很方便的脚本化的工具,让大家都可以用起来。它提供了标准化的镜像,不仅方便地用容器,而且可以方便地发布容器。

容器和虚拟机是什么关系呢?虚拟机有物理环境、有操作系统,但容器下面是 server,上面跑应用程序和库,这样的架构使得容器的性能比虚拟机好很多。首先没有虚拟机的虚拟化过程,直接把跑内核的进程跑到 OS 上。别人想发一个微服务,可以利用在镜像上面加一层,即使有很多镜像,有时多个镜像会一起依赖,会省资源和容量。那么,为什么容器是轻量级的?因为它只有应用和应用的依赖所打成的包,如果应用做了一点点修改,不需要完全打一个,在原有的应用上打一个补丁,把修改记下来即可。

上图是 Docker 的流程图, Dockerfile 这个镜像可以运行在本机。在上面运行容器,等容器开发好并且测试通过了再把这个容器 push 到镜像仓库上。任何一个人都可以从镜像仓库上下载,下载后,Docker 运行一套命令让行为一致。重要的是,Docker 官方维护了 DockerHA,所以 Docker 允许以镜像的方式发布 Container,发布的 Container 像未来的 VM,这个 VM 很轻量,大家可以把它 pull 下来,不管用什么物理硬件资源,都可以让 Docker 运行起来,而且运行结果和其他没有差别。Docker 是如何做到自包含的呢? Docker 的自包含是它容器的本身包括了 OS 的版本,以及依赖的环境和客户,所有的都描述成 Dockerfile。Docker 可以帮助两类人——开发人员和运维人员,它跨越了开发和运维间的那道鸿沟。

仅仅自包含就做微服务是不够的,微服务要求把之前大系统的问题分解为很多简单的小问题,然后将每个简单的小问题用最合适的技术实现。但这导致现在有很多小应用,原来靠人工可以解决的事现在解决不了,比如做 CI(持续集成)/CD(持续交付)。微服务化以后,会导致一个企业的应用 CI 很难做。

此外,他还向大家介绍了 Docker 的三件法宝。第一个是 Docker Compose,如果应用不是单应用可以用同一方法描述应用和应用的依赖,然后上线。第二个是 Volumes,可以共享文件。最后一个是环境变量,不管是微服务和微服务之间的依赖以及微服务运行时的依赖的环境变量也好,还是端口也好,都描述清楚,定义好后给运维人员。这是可复制的迭加过程,不仅把代码和运行做自包含变成一个镜像,而且镜像用运行的方式提供出来。运维人员不需要关心应用内部的逻辑,只需要关心模块的镜像以及如何将镜像运行起来,这样可以大大地减少运维成本。

Docker的应用与实践

来自京东的资深架构师季锡强分享了 Docker 的应用与实践。主要讲了 Docker 的系统整体架构及其技术实现、镜像存储部分、Container 启动流程,还介绍了制作镜像命令的原理以及 volume 挂载的原理。

他为大家介绍了 Docker 的组件,Docker 真正的实现主要是几个组件组成,第一层是镜像存储的模块,实现有很多种,比如 device mapper、aufs 以及 btrfs。第二层是 exec driver,主要是 libcontainer 及 lxc。第三层是 network driver,主要是主机以及网络命名空间,而 Namespace 支持 ipc、mnt、pid、net、uts 及 user。组件 CGroup 主要做资源控制,涉及的方面有 CPU、IO,IO 可以不依赖于 Namespace 单独使用,和操作文件一样改一下值就可以。CGroup 的限制主要是通过子系统来实现,相应的模块有 IO、CPU 等。而组件 Device Mapper 的实现有 DM - thin provision。

上图是启动容器的流程。启动的命令请求首先会把设备 mount 到指定的挂载点上访问。启动 go monitor 会带着参数启动里面的命令,接下来会用 Dockerinit 做初始化,最终命令会进行系统调用,再 exec 执行。停止容器则比较粗暴,相当于 Docker 根据 PID 直接将容器中的进程杀掉,再做一些资源的释放,这样就完成了停止容器的流程。Docker 启动的时候,如果 Docker 是意外死掉的(人为或者程序的破坏),那么之前的程序不会消失,这时候 Docker 会把所有的容器 kill 掉,会把之前容器创建的操作信息保存在 Docker 的目录下。Docker 将所有的容器都 kill 掉后,资源会被释放,这个时候就会触发一个永久性的 bug。因为当容器重启的时候,会检查 mount count,这个初始化的值是 0,而 Docker 死掉后重启则会发现 mount count 不为 0,所以会导致 mount 失败,导致出错;这个时候我们想启动容器的话,关键的动作是把设备释放并重新挂载到 mount 下。因此,告诫大家,如果 Docker 异常死掉了,要及时清理资源。

关于制作镜像的原理,你可以建立一个镜像生成一个容器,这个镜像可以分到某一个文件目录下,可以做一些直接的操作。首先 mount 上去,镜像原来的版本也会 mount 上去,遍历所有的文件看两者之间的状态是否有变化,然后打包,产生 diff。这时要创建新的容器,因为已经获得了 diff,接下来要创建一个新的镜像。创建新的镜像之前要拿到 Container 的基本镜像,这时要把这个镜像 mount,就可以看到所有的文件。最后的操作是解压一个文件到一个新的 snapshot,就相当于有一个新的镜像了。

最后,他对 Docker 做了一些总结,Docker 主要用到 CGroup 的一部分子系统;Docker 启动和停止之间有一个大 Bug 需要及时关注;commit 要打包、解压文件,机器磁盘很高的时候 commit 会相当耗时。

注:「开发者最佳实践日」是由七牛云存储发起并联合各方小伙伴为开发者举办的系列技术沙龙,关注开发者在实际应用中可能遇到的技术问题。


感谢魏星对本文的审校。

给 InfoQ 中文站投稿或者参与内容翻译工作,请邮件至editors@cn.infoq.com。也欢迎大家通过新浪微博(@InfoQ@丁晓昀),微信(微信号:InfoQChina)关注我们,并与我们的编辑和其他读者朋友交流(欢迎加入 InfoQ 读者交流群)。

ThoughtWorks云计算语言 & 开发架构