在之前的博文中,我讲解了Linux容器技术的相关实现,比如如何使用Docker来建立流线型的开发和测试体验。因为可以实现跨不同类型基础设施的兼容(比如,在AWS上,容器也可以如实体服务器上一样轻松的运行),容器让代码的部署异常便捷。在实际工作中,测试和开发环境的细微不同很可能会导致应用程序的部署失败;因此在这种情况下,对于开发和测试工作,容器技术可以让开发者豁免很多预想之外的工作和相互推脱。
在本篇文章中,我们将讨论是什么特性让容器技术如此适应开发和测试工作,同样适用于在AWS平台上构建一个基于微服务的架构。对于Web应用程序来说,微服务架构可以让应用程序的代码库更加敏捷,并且容易管理。下面,我们将介绍这个架构为何可以大幅提升开发者生产效率的原因,并了解它能够快速迭代和扩充一个代码库的原理。对于快速发展中的创业公司来说,微服务架构可以让开发团队在研发过程中更加的敏捷和灵活。
Web开发简史
首先,我们先简洁地回顾下20年内基于Web开发的历史,它可以让我们知悉微服务架构为什么可以在Web开发领域如此的盛行,同时也顺便了解这个架构可以解决的问题。
在Web应用程序开发的早期,应用程序通常使用Common Gateway Interface(CGI)建立,这个接口为网络服务器提供了处理浏览器发来的HTTP请求时执行脚本(通常情况下用Perl编写)的能力。CGI的扩展性非常很好,因为它需要为每个输入请求都建立一个Perl进程。为了解决这个问题,那个时代的网络服务器通常都会添加模块化的支持。Apache,现下最为流行的网络服务器之一,增加了“mod_perl”让Perl代码可以在内部运行,这样一来,CGI脚本就可以在更少的时间内执行。
即使对比传统的CGI类似mod_perl这些技术有了很大的提升,但仍然存在问题。也就是说,负责视图层(比如,在HTML页面上执行一个动态模块)的代码通常会被混入应用程序逻辑代码中。这就意味着,完成一个简单的任务,比如在HTML列表中增加一列,或者在form中增加一个元素,通常需要修改一个低等级的应用程序代码。因此,Web程序开发技术下一个阶段中衍生了“server
pages”,它允许在HTML嵌入执行代码。这样一来,应用程序逻辑代码与视图代码被很好的分离。在Java开发领域,一个被称为“Model
2”的设计模式得以快速演变,在这里,应用程序代码放到Java servlets中,数据则通过Java Beans进行,视图层逻辑则使用了Java
server pages,详见下图:
图1:Model 2设计模型
随后,在Java领域,“Model 2”模式在很短的时间就演化成了“Model-View-Controller(MVC)”框架,比如Apache
Struts。而在其他领域,Ruby on Rails则非常盛行。在MVC模式中,控制器类会定义方法,通过“route”类映射成URL模式被调用。
控制器方法会利用“model”类封核心应用程序实体的业务逻辑和数据。最后,每个控制方法都会渲染一个“view”用于显示,并修改相应模式类的方法。在这种模式下,业务、应用程序、视图逻辑被很好的分离,如图2:
图2:MVC设计模型
REST协议的盛行
就在MVC被广泛接受并成为网络开发途径的同时,进程间通信(IPC)也开始利用上了基于文本的序列化格式,比如XML和JSON。而在类似SOAP这些协议实现跨HTTP
IPC的不久后,网络开发已不再限制于给浏览器建立交付内容的应用程序,为其他程序执行操作和交付数据的网络服务也逐渐走上历史的舞台。这种基于服务的架构拥有非常强大的功能,因为它消除了代码库共享的依赖性,从而开发者可以更进一步的解耦应用程序组件。而SOAP协议和相关的WS-*标准也变得越来越复杂,并更加依赖于应用程序服务器的实现,至此开发者开始投身更为轻量级的REST协议。同时,随着移动设备的剧增,Web
UX development逐渐走向AJAX和JavaScript框架,应用程序开发者开始广泛使用REST在客户端设备和网络服务器之间做数据传输。
后来人们发现,MVC框架同样非常适合开发REST端点。REST面向资源的特性被很好的映射成了控制器和模型理念,如图3所示:
图3:MVC的REST端点
Monolithic架构
因此,曾今由模型、视图层、控制器组成,主要用于给应用程序交付HTML内容的MVC应用程序发生了本质上的变化——它们不仅可以支撑传统的HTML,也可以通过REST端点来支撑JSON。应用程序被部署为一个单一的文件(比如Java)或者同一个目录下的文件合集(比如Rails)。然而不容忽视的是,所有应用程序代码都运行在相同的进程中。因此在缩放过程中,开发者需要将应用程序代码的多个副本部署到多个所需的服务器上。下图解析了Monolithic架构:
图片4:Monolithic架构
在Monolithic架构中存在着很多的问题。首先,随着应用程序的功能和服务越来越多,代码将变得越来越复杂。对于新的开发者来说,这一点非常头疼。新型集成开发环境在加载、编译整个应用程序代码时也可能存在问题,同时这个过程的耗时也可能非常长。此外,因为所有程序代码都运行在服务器上的相同进程中,导致应用程序某个组成的扩展也非常难。如果某个服务是内存密集型的,而另一个是CPU密集型的,那么服务器必须有足够的内存和CPU来满足每个服务的需求。因此,鉴于每个服务器都使用非常高的CPU和内存,基础设施的整体花费可能会非常高,特别是在纵向扩展策略下。最后非常微妙的是,应用程序的组成通常也会映射到研发团队的结构上。UX工程师负责UI组件的建立,中间层开发者通常负责建立服务器端点,而数据库工程师和DBA们则负责数据访问组件和数据库。如果某个UX工程师期望给增加一些显示,他往往需要来自中间层和数据库工程师的配合。就像水一样,人们通常期望以最少的阻力执行,每个工程师也都期望为其负责的应用程序嵌入尽可能多的逻辑。鉴于这些问题,随着时间的推移,代码将越来越难以管理。
微服务架构
微服务架构的发明就是用来解决这些问题。定义在Monolithic架构应用程序中的服务将拆分成独立的服务,它们在不同的主机上进行独立的部署。
图片5:微服务架构
每个微服务都对应了一个独立的业务功能,也只定义了该功必须的一些操作。这听起来比较类似面向服务架构(SOA),事实上,微服务架构和面向服务的架构确实有很多共同的特性。两个架构都使用服务的模式组织代码,两种架构在不同的服务间都建立了非常明确的边界。然而,面向服务的架构起源于Monolithic应用程序交互的需求,通常彼此都会提供一个API(基于SOAP)。在面向服务架构中,集成重度依赖于中间件,特别在企业服务总线(EBS)中。微服务架构通常会利用一个消息总线,但是无论任何情况在消息层都不会存在逻辑——它纯粹的被用于服务之间的交互。这与ESB有着非常显著的差别,ESB包含了大量逻辑——用于消息路由、模式验证、消息翻译和业务规则。因此,对比传统的面向服务架构,微服务架构往往更为简单,不会包含用于定义服务间接口的同级别控制和规范化数据建模。通过使用微服务,开发将非常快速,服务的衍变也只需匹配业务的需求。
微服务架构的另一个核心优势就是服务可以基于资源的需求进行独立扩展。取代运行包含大量CPU和内存的大服务器,微服务可以被部署在更小的主机上,这些主机只需要满足其部署服务的需求。同时,开发者可以根据业务的需求选择开发语言,比如:一个图像处理服务可以使用类似C++这样的高性能语言实现,一个执行数学或者静态操作的服务可以使用Python实现,对资源进行增删查改的基础操作则往往通过Ruby进行。在微服务中,开发者并不需要考虑Monolithic架构中使用的“一刀切”模型——比如只使用MVC框架和单一的编程语言。
然而,不容忽视的是,微服务同样存在一些劣势。因为服务通常部署在多个主机上,很难持续跟踪指定服务究竟运行在某台主机上。同时,因为微服务架构使用的主机容量往往小于Monolithic架构,随着微服务架构不停的横向扩展,主机数量将以一个非常恐怖的速度增长。在AWS环境中,微服务架构中独立服务需要的资源往往会小于最小的EC2实例类型。从而造成了超量配置并浪费开销。此外,如果服务使用不同的编程语言将开发,这就意味着每个服务的部署都需要完全不同的库和框架,从而服务的部署非常复杂。
容器的用武之地
Linux容器技术的使用可以很大程度上缓解微服务架构所带来的问题。Linux容器技术使用了类似cnames和namespaces这样的内核接口,它允许不同容器共享相同的内核,同时容器之间还进行了完全的隔离。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镜像所建立的容器。容器执行缓解隔离了主机上运行的不同容器,因此不存在使用不同语言、库和框架容器所造成的冲突问题。
同时值得高兴的是,近期发布的Amazon EC2 Container Service(Amazon ECS)可以帮你搞定所有这些工作。使用Amazon
ECS,你可以定义一个被称为“cluster”的计算资源池,一个cluster由一个或以上的EC2实例组成。Amazon
ECS负责管理集群中所有基于容器的应用程序,提供 telemetry和logging,并管理集群的容量优化,进行高效的任务调度。Amazon
ECS提供了一个“任务内容(task definition)”的理念,它可以定义组成一个应用程序的一组容器。task
definition中的每个容器都指定了该容器所需的资源,而Amazon ECS将基于集群中的可用资源来调度这个任务的执行。
微服务可以非常便捷地被定义为一个任务,它可以由两个容器组成——一个负责运行服务终端代码,另一个负责运行数据库。Amazon
ECS可以管理这些容器之间的依赖性,同时也可以跨集群进行资源平衡。同时,Amazon ECS还可以无缝的访问多个AWS重点服务,比如Elastic
Load Balancing、Amazon EBS、Elastic Network Interface和Auto
Scaling。通过Amazon ECS,使用 Amazon EC2部署应用程序的所有基本特征都对基于容器的应用程序可用。
此外,类似Amazon ECS 这样的容器解决方案还可以简化“service discovery(服务搜寻)”这样的实现。因为微服务往往会跨多个主机部署,并根据负载进行缩放,service
discovery更有利于服务之间的定位。在最简单的情况下,可以使用负载均衡器来进行,但是在更为复杂的环境中,一个真正的分布式配置服务非常有必要,比如Apache
Zookeeper。使用Amazon ECS API,与类似Zookeeper这样的第三方工具整合将非常容易。配置了Zookeeper的容器可以被添加到一个task
definition中,并可以通过Amazon ECS在集群中的Amazon EC2调度执行。
从许多方面来看,使用容器技术实施微服务架构转变都与过去20年Web开发的衍变非常类似。许多这些衍变都是为了更好的利用计算资源,以及更方便的维护越来越复杂的Web应用程序。如我们所见,使用Linux容器技术来实现微服务架构完全匹配了这两个需求。在本文中,我们简单地接触了使用Amazon
ECS来定义一个微服务架构,但是容器在分布式系统中的使用已经远超过了微服务。在分布式系统中,越来越多的容器成为了first
class citizens,而在未来的报告中,我将讨论为什么 Amazon ECS对管理给予容器的计算是至关重要的。
|