微服务:分解应用程序从而实现更好的部署特性及可伸缩性
本文描述了越来越受欢迎的微服务架构模式(Microservice architecture pattern)。微服务背后的大创意是将大型的、复杂的、长期的应用程序架构为随时进化的紧密结合的一组服务。术语微服务强烈建议服务应当是微小的。
社区中甚至提倡构建10-100个LOC服务。然而,拥有微小的服务是可取的,但其不应该是主要目的。你应该旨在将你的系统分解为服务,从而解决下面讨论的开发及部署问题。一些服务确实应当是微小的,其它的则有可能是相当大的。
微服务架构的本质并不是一个新事物。分布式系统的概念是非常古老的。微服务架构也类似于SOA。
在本文中,你将学习使用微服务架构的动机以及与更传统的架构-单块架构(monolithic architecture)的比较。我们讨论了微服务的优点和缺点。你将学习如何通过微服务架构来解决一些关键的技术挑战,包括服务间通讯和分布式数据管理。微服务甚至被称为轻量级的或细粒度的SOA。确实,某种意义上说微服务是非商业化的不能感知WS*和ESB包的SOA。尽管微服务并不是新鲜的玩意,但是仍值得讨论,因为它与传统的SOA是不同的,更重要的是,它解决了许多组织当前遭受的很多问题。
(有时是邪恶的)单块架构
开发web程序的最早期时间,最被广泛使用的企业程序架构是将程序的服务器端组件打包为单个单元。很多企业Java应用程序由单个WAR或EAR文件组成。其它语言(比如Ruby,甚至C++)编写的应用程序也大抵如此。
让我们想象一下,例如你在构建一个在线商店,从客户那里获取订单,验证清单及可用的信用卡,然后运送。你构建的程序与图1所示会非常相似。
图 1单块架构
该应用程序由好几个组件组成。包括了存储前端UI,其实现了用户接口,和服务一起管理产品分类,处理订单和管理客户的账户。这些服务共享一个由多个实体组成的领域模型,实体包括产品,定点和客户等。
尽管该程序拥有一个逻辑清晰的模型设计,但仍是一个单块架构。例如,如果你是使用Java,则该应用程序将由一个单独的WAR文件组成,并且运行在一个web容器中(比如Tomcat)。该程序的Rails版本可能会有一个具有一定层级结构的目录组成,部署也使用该目录,比如使用Phusion
Passenger部署在Apache/Nginx,或者使用JRuby部署在Tomcat。
这种所谓的单块架构有一定的优点。单块架构的应用程序非常容易开发,因为IDE及其它开发工具都适合开发单个应用程序。这些程序也很容易被测试,你只需启动一个程序即可。单块架构的应用程序也很容易部署,因为你只需复制开发单元(一个文件或目录)到一个运行者相应服务容器的机器即可。
相对而言该方式更适用于小程序。然而,单块架构在复杂的程序中很难驾驭。一个庞大的单块程序对于开发者来说很难理解和维护。它对频繁改动的开发过程来说也是一种阻碍。为了对某个程序组件做修改,你不得不构建和部署整个程序,这相当复杂,风险极大,也比较耗时,需要很多开发者共同协作,还需要较长的测试周期。
单块架构也使得试用和采用新的技术变得困难。例如,尝试一个新的基础设施框架而不重写整个程序是非常困难的,风险又大又不现实。因此,你经常被项目开始时你做的技术选型阻塞。换句话说,单块架构对于支持大型的,周期长的应用程序并不具备伸缩性。
将应用程序分解为服务
幸运的是,有其它的具有可伸缩性的架构风格。《The Art of Scalability》一书中描述了真实有用的三维伸缩性模型:伸缩性立方体,如图2所示。
图2 伸缩性立方体
在该模型中,通过一个负载均衡来运行应用程序的多个完全一样的副本的方式来实现应用程序伸缩性,这种方式称为X轴伸缩性。这是一种很好的方式来提高应用程序的容量和可用度。
当使用Z轴伸缩性,每个服务器运行代码的一个完全相同的副本。在该方面,它与X轴伸缩性很相似。最大的不同是每个服务器只负责数据的一个子集。该系统的一些组件负责将每个请求路由给适当的服务器。一个常见的路由规则是把请求的一个属性作为被访问的实体的主键,比如分区。另一个常见的路由规则是客户类型。例如,应用程序可以向付费用户提供比免费用户更高的SLA,实现方式是将付费用户的请求路由到具有更高容量的一组服务器上。
Z轴伸缩性与X轴伸缩性类似,提高了应用程序的容量和可用度。然而,没有任何一个方式能够解决不断增加的开发工作和程序复杂度的问题。解决这些问题需要Y轴伸缩性。
伸缩性的第三个维度是针对功能性分解的Y轴伸缩性。Y轴伸缩性与Z轴伸缩性分解事情的方式相似但有不同。在应用程序层级,Y轴伸缩性将单块应用程序分割为一组服务。每个服务实现了一组相关的功能特性,例如订单管理,客户管理等。
决定如何将系统分割为一组服务更像是一门艺术,但是可借助于一些策略。一种方式是通过动词或使用情况分割服务。例如,接下来你会看到被分割的在线商店有一个结账UI服务,其实现了结账用例的UI。
另一个分割方式是通过名词或资源分割系统。这种服务负责处理给定的实体/资源的所有操作。例如,稍后你将看到为什么在线商店拥有目录服务是有道理的,其管理产品的目录。
理想情况下,每个服务只有一小组职责。Bob Martin(大叔)讨论了使用单一职责原则设计类。SRP定义了类的职责为有且只有一个理由被改变。将SRP应用到服务设计中也是有道理的。
另一个有助于服务设计的类似设计是Unix工具的设计。Unix提供了大量的工具,比如grep,cat和find。每个工具只做一件事,效果往往非常好,并且可以使用shell脚本组合多个工具以执行复杂的任务。在Unix工具中对服务建模并创建单一功能服务很有道理。
强调分解的目标不只是为了拥有微小的(例如,一些主张有10-100 LOC)服务。相反,目标是解决之前讨论过的实际问题和单块架构的局限性。一些服务应当是微小的,但是其它服务可能更大些。
如果应用Y轴来分解示例程序,我们得到的架构如图3所示。
图3 微服务架构
分解后的程序由各种各样的前台服务和多个后台服务组成,这些前台服务实现了用户接口不同部分。前台服务包括目录UI和结账UI。目录UI实现了产品搜索和浏览,结账UI实现了购物车和结账流程。后台服务包含了在文章开始时相同的逻辑服务。我们将该应用程序的每个主要的逻辑组件转换为了独立的服务。让我们看看这样做的后果。
微服务架构的优点和缺点
该架构有一些优点。首先,每个微服务相对较小。开发者很容易理解该代码。少量的代码不会拖慢IDE,使得开发者更加高效。并且,每个服务比一个大型的单块程序启动速度要快的多,这又一次使得开发者更加高效,加快部署过程。
其次,每个服务的部署与其它服务是独立的。如果某程序员只对一个服务负责,并且想要对该服务部署一个改动,只需修改f本地服务而无需其他程序员的协作。程序员部署修改很简单。微服务使得持续部署更加可行。
第三,每个服务可通过X轴复制和Z轴分割独立于其它服务进行扩展。此外,每个服务可被部署到最适合该服务的资源要求的硬件上。这与使用单块架构的情况完全不同,单块架构中的组件的资源要求是不同的,例如是CPU密集型的还是内存密集型的,但是你又必须一起部署。
微服务架构使得开发过程更具扩展性。你可以使用多个小型(例如,两个披萨饼)的团队进行开发。每个团队只负责对单个服务或一组相关的服务的开发和部署。每个团队可独立于其它的团队来开发,部署和扩展他们的服务。
微服务架构也提升了错误隔离。例如,一个服务中的内存泄露只影响该服务。其它服务将会继续正常的处理请求。对比而言,一个单块架构的具有错误行为的组件会使整个系统崩溃。
最后但不是最重要的一点,微服务架构消除了技术栈任何长期的承诺。原则上来说,当开发一个新的服务时,开发者可以选择任何适合于当前服务的语言和框架。当然,许多组织团体限制这些选择也有一定道理,但是关键点在于你不受限于过去的决定。
此外,由于服务是微小的,使用其它语言和技术重写服务也变得更加实用。这也意味着如果尝试新技术失败,你只需丢掉这些工作而无需给整个项目带来风险。这与使用
单块架构是完全不同的,这里你最初的技术选择会严格限制未来使用不同的语言和框架的能力。
缺点
当然,没有任何一项技术是银弹,微服务也有一些重大的缺点和问题。首先,开发者必须面对创建一个分布式系统的额外的复杂性。开发者必须实现一个进程间通讯机制。不用分布式事务实现跨服务的用例是困难的。IDE和其它的开发工具关注于创建单块架构的应用程序,并不对开发分布式应用程序提供显式的支持。编写引用了多个服务的自动化测试颇具挑战性。而你使用单块架构则无需处理这些问题。
微服务架构也引入了重大的操作复杂度。有很多容易变动的部分(不同类型的服务的多个实例)需要在产品环境中管理。要成功实现这点你需要高级别的自动化,无论是自己编写的代码还是类似于PaaS的技术(例如Netfix
Asgard)和相关的组件,或者一个现成的PaaS(例如Pivotal Cloud Foundry)。
而且,跨多个服务开发功能要求多个开发团队间小心翼翼的协作。你需要创建一个展示计划,该计划基于服务间依赖情况而制定服务部署顺序。这与使用单块架构的情形非常不同,你只需使用原子操作即可部署更新多个组件。
使用微服务架构的另一个挑战是在应用程序的那个周期点决定使用该架构。当开发应用程序的第一个版本时,你通常不会遇到该架构能够解决的问题。此外,使用复杂的分布式架构会拖慢开发速度。
这可能在项目刚开始时陷入左右为难的情况,最大的挑战经常是如何伴随着应用程序快速演化业务模型。使用Y轴分割可能会导致快速迭代更加困难。然而,当挑战变为如何提高可伸缩性时你需要使用功能性分解,但是纠缠不清的依赖使得将单块应用程序分解为一组服务变得困难。
正因为如此,不能轻易着手采用微服务架构。然而,对于需要高伸缩性的应用程序,比如面向消费者的web程序或SaaS程序,采用微服务架构通常是正确选择。一些出名的网站,比如eBay,Amazon.com,Groupon和Gilt都已经把单块架构进化为微服务架构。
现在我们已经知道微服务架构的关键设计的优点和缺点,现在开始了解程序间和程序与客户端的通讯机制。
微服务架构中的通讯机制
微服务架构中,应用程序和客户端通讯的模式,以及应用程序组件间的通讯机制与单块应用程序是不同的。首先来看应用程序的客户端与微服务是如何交互的。接下来我们将查看应用程序内部的通讯机制。
API网关模式
在单块架构中,应用程序的客户端,比如web浏览器和原生应用程序,发送HTTP请求通过一个负载均衡到N个完全一样的应用程序实例的其中一个。但在微服务架构中,单块程序被服务集合替代。结果,我们需要回答的关键问题是客户端应该与什么交互?
一个应用程序客户端,比如原生的移动应用程序,可以向单个服务发送RESTful
HTTP请求,如图4所示。
图4 直接调用服务
表面上来看这很有吸引力。然而,在单个服务的API和客户需要的数据之间可能会有一个显著的错误匹配粒度。例如,显示一个网页可能潜在需要调用大数量的服务。例如Amazon.com,描述了一些页面如何需要100+的服务调用。即使在高速的网络连接下,更不用说低带宽,高延迟的移动网络,如此多的请求会非常低效且导致低劣的用户体验。
更好的方式是客户端对每个页面发出少量的请求,甚至少至一个在互联网前端服务器被称为API网关,如图5所示。
图5 API网关
API网关位于应用程序的客户端与微服务之间。它提供了专为客户端定制的API。API网关为移动客户端提供了粗粒度的API,为桌面客户端提供了细粒度的API,因为客户端使用高性能的网络。在本例中,桌面客户端发送多个请求来获取一个产品信息,而移动客户端只发送单个请求。
API网关处理接收的请求,将这些请求通过高性能的局域网(LAN)转发给一定数量的微服务。例如,Netfix描述了每个请求如何平均分给6个后台服务。在本例中,从桌面客户端发送来的细粒度的请求只是被简单的代理给对应的服务,而从移动客户端发来的粗粒度的请求处理的方式是组合调用多个服务的结果。
API网关不仅可以优化客户端和应用程序间的通讯,也能隐藏微服务的细节。这使得微服务的进化不会影响客户端。例如,两个微服务可能会被合并。另一个微服务则可能被分割为两个或更多的服务。API网关唯一需要的做的是更新或反映这些修改。客户端完全不受影响。
现在已经知道了API网关是如何调解应用程序和其客户端的,现在看看如果实现微服务间的通讯。
服务间通讯机制
使用微服务架构的另一个不同之处是应用程序的组件之间交互方式的不同。单块应用程序中,组件间调用是通过常规的方法调用实现的。但是微服务架构中,不同的服务运行于不同的进程。结果,服务间必须使用一个进程间的的通讯(IPC)机制来交互。
同步HTTP
在微服务架构中有两个主要的方式实现进程间通讯。一种选项是基于同步HTTP的机制,比如REST或SOAP。这是简单和熟悉的IPC机制。它是防火墙友好的,所以可以穿透网络,而且实现通讯的请求/回复风格也比较容易。HTTP的低层不支持其它的通讯模式,比如发布-订阅模式。
另一个限制是客户端和服务器端必须保持同时在线,通常这不能随时保证,因为分布式系统很容易出现部分故障。而且,HTTP客户端需要知道服务器的主机地址和端口。听起来很简单,但整个并不简单,特别是在使用自动扩展的云部署中,这些服务实例是短暂的。应用程序需要使用一种服务发现机制(service
discovery mechanism)。一些程序使用一个服务注册器,比如Apache ZooKeeper或Netflix
Eureka。其它的程序中,服务必须注册到负载均衡器中,比如在Amazon VPC的一个内部的ELB。
异步消息机制
同步HTTP的一个替代方案是使用异步的基于消息的机制,比如基于AMQP的消息中间件。这种方式有一些优点。它解耦了消息生产者和消息消费者。消息中间件将缓存消息直到消费者能够处理它们。生产者完全不知道消费者的存在。生产者简单地与消息中间件交互,并且不需要使用服务发现机制。基于消息的通讯也支持多种通讯模式,比如单向请求和发布-订阅。使用消息的一个缺点是需要一个消息中间件,这是系统容易变动的另一部分,这会增加系统复杂度。另一个缺点是请求/回复风格的通讯不是天作之合。
两种方式各有优劣。应用程序可能混合使用这两种方式。例如,接下来的部分将会讨论在分段的架构中如何解决数据管理问题,你将看到如何同时使用HTTP和消息机制。
分散数据管理
将应用程序分解为服务的结果是数据库也被分割了。为了保证解耦,每个服务要有自己的数据库(模式)。此外,不同的服务可以使用不同的数据库,这被称为多语言的持久架构。例如,需要ACID事务的服务可能使用关系型数据库,而操作社交网络的服务可能使用图形数据库。分割数据库是必要的,但有一个新问题要解决:如何处理需要访问多个服务拥有的数据的请求。先来看如何处理读请求,再看如何处理更新请求。
处理读请求
例如,考虑在在线商店中每个客户有信用额度。当客户试图添加订单时,系统必须验证所有未结账单的总价不会超出信用额度。在整体应用程序中实现这种业务逻辑不难。但是如果客户是由客户服务管理,而其它部分由订单服务管理的情况下,在系统中实现登记更困难。订单服务必须通过某种方式访问由客户服务维护的信用额度信息。
一个解决方案是订单服务通过一个RPC调用向客户服务获取信用额度。这种方式很容易实现,而且保证了订单服务始终拿到的是最新的信用额度。缺点是它降低了可用性,因为客户服务必须时刻运行来订货。由于额外的RPC调用也增加了响应时间。
另一种方式是订单服务保存信用额度的一份副本。这消除了向客户服务发请求的需要,从而提高了可用性,减少了响应时间。然而,这意味着我们必须实现一种机制:当客户服务中的信用额度被修改时,来更新信用额度在订单服务中的副本。
处理更新请求
保持订单服务中信用额度一直是最新的问题是一个常见的问题的示例。该问题是如何处理更新被多个服务拥有的数据的请求。
分布式事务
当然,有个解决方案是使用分布式事务。例如,当更新客户的信用额度时,客户服务调用一个分布式的事务来更新本身的信用额度以及被订单服务维护的对应的信用额度。使用分布式事务也保证了数据的始终一致性。使用分布式事务的缺点是减少了系统可用性,因为所有参与者都必须可用,以保证事务能够提交。此外,分布式事务已经失宠,现代的软件栈(例如REST,NoSQL数据库等)通常已不支持分布式事务。
事件驱动的异步更新
另一种方式是使用事件驱动的异步复制。服务通过发布事件来宣布一些数据被修改。其它服务订阅这些事件来更新各自的数据。例如,当客户服务更新了一个客户的信用额度时,它发布了一个CustomerCreditLimitUpdatedEvent,其包含了客户id和新的信用额度值。订单服务订阅了这些事件并更新自身的信用额度副本。该事件流显示在图6中。
使用事件复制信用额度
本方式的主要优点是事件的生产者和消费者是解耦的。这不仅简化了开发,并且与分布式事务相比它提高了可用性。如果消费者无法处理事件,消息中间件会将消息保存在队列中直到消费者可以处理。该方式的主要缺陷是以一致性换可用性。应用程序的编写方式要能容忍最终一致性数据。开发者也需要实现修正事务来执行逻辑回滚。尽管有此缺陷,但仍不失为许多程序中的最佳方式。
重构单块架构
不幸的是我们不能总是工作于新品牌的绿色项目。如果你在负责一个大型的可怕的单块程序的项目中,那是个好机会。每天你都会处理在文章开头描述过的那些问题。好消息是有很多你可以使用的技术来分解你的单块应用程序为一组服务。
首先停止让问题更糟。不要继续通过向单块应用程序添加代码的方式来实现新功能。你应当采用某种方式来将新功能实现为独立的服务,正如图7所示。这可能并不容易。你可能会编写凌乱的,复杂的胶水代码来向单块应用程序集成服务。但这是打散单块程序的第一步。
图7 抽取服务
其次,识别单块程序的组件并转换为紧密结合的独立服务。从组件抽取的好的候选者是不断改变的组件,或有资源需求冲突的组件,比如大型的内存缓存或CPU密集型操作。表示层也是另一个好的候选者。然后你可以将该组件转换为服务并编写胶水代码来与程序的其它部分集成。再一次,这可能很痛苦,但是它使你可以增量迁移到微服务架构。
总结
单块架构模式是构建企业级应用程序常用的模式。对于小的应用程序它很适用:开发,测试和部署小型的单块程序相对简单。但是,对于大型的复杂的应用程序,单块架构会阻碍开发和部署。如果你经常长期的锁定你的初始技术选择,则会使得持续交付变得困难。对于大型的应用程序,更适合适用微服务架构,其将应用程序分解为一组服务。
微服务架构有很多优点。例如,单个服务更容易理解,可以独立于其它服务来开发和部署。也更容易使用新的语言和技术,因为你可以一次只对一个服务尝试新技术。微服务架构也有一些显著的缺点。特别是对那些更复杂,拥有更多变化部分的应用程序。你需要高级别的自动化,比如PaaS,来高效的使用微服务。你也需要在开发微服务时处理一些复杂的分布式数据管理问题。尽管有这些缺点,微服务架构还是更适用于大型的复杂的应用程序,因为可以快速演化,特别是针对SaaS风格的应用程序。
有多种多样的策略来增量地将单块应用程序演化为微服务架构。开发者需要将新的功能实现为服务并编写胶水代码来将该服务与单块应用程序集成。也可以反复识别可从单块程序中抽取组件并转换为服务。演化并不容易,但总比开发和维护一个难驾驭的单块应用程序要好。 |