本文中,我们将探讨容器的一些特性,在减轻开发/测试工作量的同时,也使得它们非常适合在AWS中构建一个微服务构架。对于Web应用程序来说,微服务架构可以让应用程序的代码库更加敏捷,并且容易管理。下面,我们将介绍这个架构为何可以大幅提升开发者生产效率的原因,并了解它能够快速迭代和扩充一个代码库的原理。对于快速发展中的创业公司来说,微服务架构可以让开发团队在研发过程中更加的敏捷和灵活。
Web开发简史
首先,我们快速回顾一下过去20年的Web开发历史,这有助于解释为什么微服务架构变得如此流行,以及这一架构解决的问题。
在Web应用开发的最初阶段,应用都是使用通用网关接口(CGI)创建的。这个接口为Web服务器提供了一种在处理浏览器HTTP请求时执行脚本(通常是使用Perl编写)的方法。由于对脚本资源的每个请求都需要启动一个Perl进程处理,CGI架构无法很好地扩展。为了解决这个问题,当时流行的Web服务器添加了模块支持。Apache,如今***的Web服务器之一,添加了“mod_perl”模块,在服务器内部运行Perl代码,这使得CGI脚本能更快执行。
尽管像mod_perl这样的技术让传统CGI获得了巨大提升,问题依然存在。负责构建视图的代码(比如解析页面HTML的动态部分)与应用的业务逻辑混杂在一起。这意味着要完成一个简单的任务,比如在HTML表格中添加一列或在表单中添加一个新元素,都需要修改底层应用代码。因此,Web编程技术的下一轮革新成果“server pages”出现了,它是允许在HTML里嵌入可执行代码的模板框架。可以更清晰地将应用逻辑与视图逻辑分开。在Java编程世界,一种名为“Model 2”的设计模式迅速出现,如图1所示,它将应用代码放入Java servlet、数据放入叫Java Bean的类中、视图逻辑放入JSP:
“Model 2”很快演变成了如今广泛使用的Model-View-Controller(MVC)设计模式。很多早期的MVC框架都是以Java为基础的(如Apache Struts),不过其它框架如Ruby On Rails也在快速普及。在MVC设计模式中,“controller”类定义了映射到“route”类URL的方法。controller方法使用封装了核心应用实体业务逻辑和数据的“model”类。***,每个controller方法渲染一个用于显示和编辑相应model类数据的“view”。如图2所示,这个模式将业务、应用和视图逻辑清晰地分离开了:
REST协议崛起
在MVC作为Web开发的事实选择被快速采纳的同时,进程间通讯(IPC)演进为使用以文本为基础的系列化格式,如XML和JSON。像SOAP这样的协议允许通过HTTP进行IPC,很快Web开发人员不再只是创建为浏览器提供内容的Web应用,还有为其它程序执行任务并交付数据的Web服务。这种基于服务的架构拥有非常强大的功能,因为它消除了对共享代码库的依赖,允许应用开发人员更进一步解耦应用组件。SOAP协议和相关的WS-*标准很快变得越来越复杂,并且严重依赖于应用服务器的具体实现,因此开发人员迁移到更轻量的REST协议上。随着移动设备的广泛使用,Web UX开发转移到了AJAX和JavaScript框架上,应用开发人员开始扩展REST的功能用于在客户端设备与Web服务器间传输数据。
事实上,MVC框架也非常适合开发REST端。如图3所示,REST面向资源的特性被很好的映射成了控制器和模型理念,如图3所示:
一体化架构(Monolithic Architecure)
MVC应用由模型、视图和控制器组成,主要为应用提供可变的HTML内容,包括传统的HTML和REST端的JSON。很多这类应用使用了一体化架构。应用以一个单一文件(如Java)或放置在同一目录下的一组文件(如Rails)的形式进行部署。所有应用代码运行在同一进程中。扩展时要求将完全相同的应用代码部署到多台服务器上。图4是一体化架构的表示:
一体化架构存在一些问题。首先,在添加功能和服务到应用中时,代码库会变得非常复杂。对于新的开发人员是个巨大挑战。如今的IDE甚至在加载整个应用代码时会发生问题,而且编译与构建时间非常久。因为所有的应用代码都运行在服务器的同一个进程中,要扩展应用的其中一部分(就算可能)非常困难。假如有一个服务是内存密集型的,另一个是CPU密集型的,服务器就必须提供足够的内存和CPU来处理每个服务的基本负载。如果每台服务器都需要大量CPU和内存,花费不小,如果使用负载均衡进行应用的横向扩展,则费用将更高。***,更微妙的是,随着时间推移,工程团队架构常常开始呈现为应用架构。UX工程师被派去构建UI组件,中间层开发人员构建服务端,数据库工程师和DBA处理数据访问组件和数据库。如果一个UX工程师想要添加一个数据到界面上,就必须与中间层和数据库工程师协商。像水一样,人类倾向于选择阻力最小的路径,这意味着每个工程组都会在他们控制的应用部分嵌入尽可能多的逻辑。时间一久,注定会产生难以维护的代码。
#p#
微服务架构
微服务架构设计用于解决这个问题。在一体化架构中定义的服务被分解成一个个单独的服务,并在不同主机上分开部署。
每个微服务针对一个特定的业务功能,并且只定义该业务功能所需的操作。这听起来像是以服务为导向的架构(SOA),实际上,微服务架构与SOA有一些共通的特性。两个架构都将代码组织成服务,二者都定义了清晰边界来区隔服务。然而,SOA源于将提供API(通常以SOAP为基础)的一体化应用与其他应用整合在一起的需求。在SOA中,整合严重依赖于中间件,特别是企业服务总线(ESB)。微服务架构经常使用消息总线,不过在消息层完全没有逻辑,它简单的被用来在服务间传送信息。这与ESB存在巨大差别,后者包含了大量消息路由、schema验证、消息转换和业务规则的逻辑。由此可见,微服务架构相比传统SOA累赘更少,且在定义服务间接口时不要求相同级别的治理和规范的数据模型。使用微服务,开发速度快并且服务可根据业务需要演进。
微服务架构的另一个关键优势在于,服务可以根据其资源需求进行单独扩展。与运行配备了大量CPU和内存的大型服务器不同,微服务可以部署在仅包含该服务需要的资源的较小主机上。此外,每个服务可以使用最适合其操作执行的语言来实现。图像处理服务可以使用高性能语言如C++实现。执行数学或统计操作的服务可以使用Python实现。执行基本的资源CRUD操作的服务可以用Ruby实现。微服务架构不要求一体化架构的“一刀切”模型,后者通常使用单一的MVC架构和单一的编程语言。
不过,微服务也有一些缺点。因为服务分布在多台主机上,要了解哪台主机运行了某些服务可能会很困难。另外,尽管每台主机可能不像运行一体化架构的主机那么强大,在微服务架构横向扩展时,主机的数量将比一体化架构增加得更快。在AWS环境中,即便最小的EC2实例类型,微服务可能都不需要其全部资源,结果就是超量供给及成本上升。如果服务使用不同编程语言实现,意味着每个服务的部署都需要一个完全不同的库与框架集合,使得部署到服务器非常复杂。
容器是解决之道
Linux容器可以减轻微服务架构面临的这些挑战。Linux容器使用了内核接口如别名(cname)和命名空间(namespace),允许多个容器共享同一个内核,同时运行时又与其他容器完全隔离。Docker执行环境使用了一个叫libcontainer的模块,用于规范这些接口。Docker还提供了一个与GitHub类似的名叫DockerHub的容器镜像仓库,便于共享和分发容器。
同一台主机上运行的多个容器间的隔离性使得部署用不同语言和框架开发的微服务代码时非常简单。使用Docker,我们可以创建一个Dockerfile描述所有该服务使用的语言、框架和依赖库。比如,以下Dockerfile可以用来定义一个微服务的Docker镜像,在其中使用了Ruby和Sinatra框架:
- FROM ubuntu:14.04
- MAINTAINER John Doe <jdoe@example.com>
- RUN apt-get update && apt-get install -y curl wget default-jre git
- RUN adduser --home /home/sinatra --disabled-password --gecos '' sinatra
- RUN adduser sinatra sudo
- RUN echo '%sudo ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers
- USER sinatra
- RUN curl -sSL https://get.rvm.io | bash -s stable
- RUN /bin/bash -l -c "source /home/sinatra/.rvm/scripts/rvm"
- RUN /bin/bash -l -c "rvm install 2.1.2"
- RUN /bin/bash -l -c "gem install sinatra"
- RUN /bin/bash -l -c "gem install thin"
使用该镜像创建的容器可以简单地放置到一台运行了另一个容器的主机上,即便这个容器创建自一个使用了Java和DropWizard框架的Docker镜像。容器执行环境将运行在该主机上的容器彼此隔离开,因此容器使用的语言、库和框架依赖不会与其它容器相冲突。
容器的可移植性让微服务的部署变得微不足道。要对运行在指定主机上的服务进行升级,只需要简单地停止运行中的容器,然后启动以***版服务代码的Docker镜像创建的新的容器。该主机上的所有其它容器都不会受此变化影响。
容器还有助于提高主机资源利用率。如果指定的服务无法使用Amazon EC2实例的所有资源,可以启动其它服务容器来利用这些空闲资源。当然,如果是手工在容器中部署服务、管理哪些服务运行在哪些主机上以及监控所有运行容器的主机的利用率,事情将迅速变得难以掌控。
最近发布的Amazon EC2容器服务(Amazon ECS)就是用于解决这个问题的。使用Amazon ECS,定义一个名叫“集群(cluster)”的计算资源池。一个集群由一个或多个Amazon EC2实例组成。Amazon ECS管理了运行在集群里的所有容器应用的状态,提供自动监测和日志,管理集群的利用率,完成工作的高效调度。Amazon ECS提供了一个叫“任务定义(task definition)”的组件,用于定义一组包含应用的容器。任务定义里的每个容器指明其所需资源,Amazon ECS将根据集群里的可用资源调度任务执行。
微服务可以简单的定义成一个任务,并可由两个容器组成——一个运行服务端代码,另一个是数据库。Amazon ECS管理着容器间的依赖,以及集群里的资源均衡。Amazon ECS还提供了对弹性负载均衡(Elastic Load Balancing)、Amazon EBS、弹性网络接口(Elastic Network Interface)及自动扩展(Auto Scaling)等重要的AWS服务的无缝访问。使用Amazon ECS,容器应用也可以使用所有这些部署应用到EC2的关键功能。
像Amazon ECS这样的容器管理方案还简化了“服务发现(service discovery)”的实现。因为微服务经常要跨多主机部署,并常常需要根据负载进行伸缩,要让一个服务知道如何定位另一个服务,服务发现就是必需的。最简单的方式,就是使用负载均衡器。不过很多情况下,还是有必要使用一个真正的分布式配置服务,比如Apache Zookeeper。Amazon ECS API可以整合像Zookeeper这样的第三方工具。也能使用Amazon ECS管理Zookeeper集群。可以使用任务定义将组成Zookeeper集群的容器分在一组,并通过Amazon ECS服务调度到集群的Amazon EC2主机上执行。
从很多方面而言,使用容器实现微服务架构是一项不同于过去20年Web开发观察到的进化。这种进化受到了如何更好的利用计算资源和维护日益复杂的web应用的需求驱动。如我们所见,通过Linux容器使用微服务架构解决了这两项需求。我们简要说明了如何将微服务定义成Amazon ECS的一项任务,但是在分布式系统中使用容器远远不仅限于微服务。渐渐地,容器已经成了所有分布式系统的“一等公民”,未来的文章中我们将探讨类似Amazon ECS这样的工具如何在管理以容器为基础的计算资源中发挥重要作用。