微服务架构我们没有一个明确的定义,但简单来说微服务架构是: 采用一组服务的方式来构建一个应用,服务独立部署在不同的进程中,不同服务通过一些轻量级交互机制来通信,例如 RPC、HTTP 等,服务可独立扩展伸缩,每个服务定义了明确的边界,不同的服务甚至可以采用不同的编程语言来实现,由独立的团队来维护。
传统实现组件的方式是通过库(library),传统组件是和应用一起运行在进程中,组件的局部变化意味着整个应用的重新部署。 通过服务来实现组件,意味着将应用拆散为一系列的服务运行在不同的进程中,那么单一服务的局部变化只需重新部署对应的服务进程。 另外将服务作为组件可以更明确的定义出组件的边界。
康威定律(Conway’s law)指出:
organizations which design systems … are constrained to produce designs which are copies of the communication structures of these organizations.
任何设计系统的组织,最终产生的设计等同于组织之内、之间的沟通结构。
传统开发方式中,我们将工程师按技能专长分层为前端层、中间层、数据层,前端对应的角色为UI、页面构建师等,中间层对应的角色为服务端业务开发工程师,数据层对应着DBA等角色。 事实上传统应用设计架构的分层结构正反应了不同角色的沟通结构。 而微服务架构的开发模式不同于传统方式,它将应用按业务能力来划分为不同的服务,每个服务都要求在对应业务领域的全栈(从前端到后端)软件实现,从界面到数据存储到外部沟通协作等等。 因此团队的组织是跨功能的,包含实现业务所需的全面的技能。 近年兴起的全栈工程师正是因为架构和开发模式的转变而出现,当然具备全栈的工程师其实很少,但将不同领域的工程师组织为一个全栈的团队就容易的多。
传统的应用开发都是基于项目模式的,开发团队根据一堆功能列表开发出一个软件应用并交付给客户后,该软件应用就进入维护模式,由另一个维护团队负责,开发团队的职责结束。 而微服务架构的倡导者提议避免采用这种项目模式,更倾向于让开发团队负责整个产品的全部生命周期。Amazon 对此提出了一个观点:
You buidl it, you run it.
开发团队对软件在生产环境的运行负全部责任,让服务的开发者与服务的使用者(客户)形成每天的交流反馈,来自直接客户端的反馈有助于开发者提升服务的质量。
微服务架构抛弃了 ESB 过度复杂的业务规则编排、消息路由等。 服务作为智能终端,所有的业务智能逻辑在服务内部处理,而服务间的通信尽可能的轻量化,不添加任何额外的业务规则。
传统应用中倾向采用统一的技术平台或产品来解决所有问题。 不是每个问题都是钉子,也不是每个解决方案都是一个锤子。 问题有其具体性,解决方案也应有其针对性。 用最适合的技术方案去解决具体的问题,在大一统的传统应用中其实很难做到,而微服务的架构意味着,你可以针对不同的业务服务特征选择不同的技术平台或产品,有针对性的解决具体的业务问题。
单一进程的传统应用被拆分为一系列的多进程服务后,意味着开发、调试、测试、集成、监控和发布的复杂度都会相应增大。 必须要有合适的自动化基础设施来支持微服务架构模式,否则开发、运维成本将大大增加。
正因为将服务独立在不同的进程中后,引入了额外的失败因素。 任何时刻对服务的调用都可能因为服务方不可用导致失败,这就要求服务的消费方需要优雅的处理此类错误。 这其实是相对传统应用开发方式的一个缺点,不过随着一些开源服务化框架的出现,对业务开发人员而言适当的屏蔽了类似的错误处理,不过开发人员依然需要知道对服务的调用是完全不同于进程内的方法或函数调用的。
一旦采用了微服务架构模式,那么在服务需要变更时我们要特别小心,服务提供者的变更可能引发服务消费者的兼容性破坏,时刻谨记保持服务契约(接口)的兼容性。 对于解耦服务消费方和服务提供方,伯斯塔尔法则(Postel’s law)特别适用:
Be conservative in what you send, be liberal in what you accept.
发送时要保守,接收时要开放。
按照伯斯塔尔法则的思想来设计实现服务调用时,发送的数据要更保守,意味着最小化的传送必要的信息,接收时更开放意味着要最大限度的容忍信息的兼容性。 多余的信息不认识可以忽略,而不应该拒绝或抛出错误。
假设你正准备开发一款与Uber和Hailo竞争的出租车调度软件,经过初步会议和需求分析,你可能会手动或者使用基于Rails、Spring Boot、Play或者Maven的生成器开始这个新项目,它的六边形架构是模块化的应用核心是业务逻辑,由定义服务、域对象和事件的模块完成。围绕着核心的是与外界打交道的适配器。适配器包括数据库访问组件、生产和处理消息的消息组件,以及提供API或者UI访问支持的web模块等。
尽管也是模块化逻辑,但是最终它还是会打包并部署为单体式应用。具体的格式依赖于应用语言和框架。例如,许多Java应用会被打包为WAR格式,部署在Tomcat或者Jetty上,而另外一些Java应用会被打包成自包含的JAR格式,同样,Rails和Node.js会被打包成层级目录。
这种应用开发风格很常见,因为IDE和其它工具都擅长开发一个简单应用,这类应用也很易于调试,只需要简单运行此应用,用Selenium链接UI就可以完成端到端测试。单体式应用也易于部署,只需要把打包应用拷贝到服务器端,通过在负载均衡器后端运行多个拷贝就可以轻松实现应用扩展。在早期这类应用运行的很好。
不幸的是,这种简单方法却有很大的局限性。一个简单的应用会随着时间推移逐渐变大。在每次的sprint中,开发团队都会面对新“故事”,然后开发许多新代码。几年后,这个小而简单的应用会变成了一个巨大的怪物。
一旦你的应用变成一个又大又复杂的怪物,那开发团队肯定很痛苦。敏捷开发和部署举步维艰,其中最主要问题就是这个应用太复杂,以至于任何单个开发者都不可能搞懂它。因此,修正bug和正确的添加新功能变的非常困难,并且很耗时。另外,团队士气也会走下坡路。如果代码难于理解,就不可能被正确的修改。最终会走向巨大的、不可理解的泥潭。
单体式应用也会降低开发速度。应用越大,启动时间会越长。比如,最近的一个调查表明,有时候应用的启动时间居然超过了12分钟。我还听说某些应用需要40分钟启动时间。如果开发者需要经常重启应用,那么大部分时间就要在等待中渡过,生产效率受到极大影响。
另外,复杂而巨大的单体式应用也不利于持续性开发。今天,SaaS应用常态就是每天会改变很多次,而这对于单体式应用模式非常困难。另外,这种变化带来的影响并没有很好的被理解,所以不得不做很多手工测试。那么接下来,持续部署也会很艰难。
单体式应用在不同模块发生资源冲突时,扩展将会非常困难。比如,一个模块完成一个CPU敏感逻辑,应该部署在AWS EC2 Compute Optimized instances,而另外一个内存数据库模块更合适于EC2 Memory-optimized instances。然而,由于这些模块部署在一起,因此不得不在硬件选择上做一个妥协。
单体式应用另外一个问题是可靠性。因为所有模块都运行在一个进程中,任何一个模块中的一个bug,比如内存泄露,将会有可能弄垮整个进程。除此之外,因为所有应用实例都是唯一的,这个bug将会影响到整个应用的可靠性。
最后,单体式应用使得采用新架构和语言非常困难。比如,设想你有两百万行采用XYZ框架写的代码。如果想改成ABC框架,无论是时间还是成本都是非常昂贵的,即使ABC框架更好。因此,这是一个无法逾越的鸿沟。你不得不在最初选择面前低头。
许多公司,比如Amazon、eBay和NetFlix,通过采用微处理结构模式解决了上述问题。其思路不是开发一个巨大的单体式的应用,而是将应用分解为小的、互相连接的微服务。
一个微服务一般完成某个特定的功能,比如下单管理、客户管理等等。每一个微服务都是微型六角形应用,都有自己的业务逻辑和适配器。一些微服务还会发布API给其它微服务和应用客户端使用。其它微服务完成一个Web UI,运行时,每一个实例可能是一个云VM或者是Docker容器。
比如,一个系统可能分解,让每一个应用功能区都使用微服务完成,另外,Web应用会被拆分成一系列简单的Web应用(比如一个对乘客,一个对出租车驾驶员)。这样的拆分对于不同用户、设备和特殊应用场景部署都更容易。
每一个后台服务开放一个REST API,许多服务本身也采用了其它服务提供的API。比如,驾驶员管理使用了告知驾驶员一个潜在需求的通知服务。UI服务激活其它服务来更新Web页面。所有服务都是采用异步的,基于消息的通讯。微服务内部机制将会在后续系列中讨论。
一些REST API也对乘客和驾驶员采用的移动应用开放。这些应用并不直接访问后台服务,而是通过API Gateway来传递中间消息。API Gateway负责负载均衡、缓存、访问控制、API 计费监控等等任务,可以通过NGINX方便实现,后续文章将会介绍到API Gateway。
微服务架构模式在上图中对应于代表可扩展Scale Cube的Y轴,这是一个在《The Art of Scalability》书中描述过的三维扩展模型。另外两个可扩展轴,X轴由负载均衡器后端运行的多个应用副本组成,Z轴是将需求路由到相关服务。
应用基本可以用以上三个维度来表示,Y轴代表将应用分解为微服务。运行时,X轴代表运行多个隐藏在负载均衡器之后的实例,提供吞吐能力。一些应用可能还是用Z轴将服务分区。下面的图演示行程管理服务如何部署在运行于AWS EC2上的Docker上。
运行时,行程管理服务由多个服务实例构成。每一个服务实例都是一个Docker容器。为了保证高可用,这些容器一般都运行在多个云VM上。服务实例前是一层诸如NGINX的负载均衡器,他们负责在各个实例间分发请求。负载均衡器也同时处理其它请求,例如缓存、权限控制、API统计和监控。
这种微服务架构模式深刻影响了应用和数据库之间的关系,不像传统多个服务共享一个数据库,微服务架构每个服务都有自己的数据库。另外,这种思路也影响到了企业级数据模式。同时,这种模式意味着多份数据,但是,如果你想获得微服务带来的好处,每个服务独有一个数据库是必须的,因为这种架构需要这种松耦合。
每种服务都有自己的数据库,另外,每种服务可以用更适合自己的数据库类型,也被称作多语言一致性架构。比如,驾驶员管理(发现哪个驾驶员更靠近乘客),必须使用支持地理信息查询的数据库。
表面上看来,微服务架构模式有点像SOA,他们都由多个服务构成。但是,可以从另外一个角度看此问题,微服务架构模式是一个不包含Web服务(WS-)和ESB服务的SOA。微服务应用乐于采用简单轻量级协议,比如REST,而不是WS-,在微服务内部避免使用ESB以及ESB类似功能。微服务架构模式也拒绝使用canonical schema等SOA概念。
SOA与微服务应用了很多相同的原则,只是在组织中的应用层次不同。SOA专注于对“大型服务”进行编排操作,但这些大型服务也可以通过对一系列微服务进行组合而实现。服务的大小并不是一种定义微服务的好方法。
表面上看来,微服务架构模式有点像SOA,他们都由多个服务构成。从另外一个角度看此,微服务架构模式是一个不包含Web服务(WS-)和ESB服务的SOA。
SOA的提出是在企业计算领域,就是要将紧耦合的系统,划分为面向业务的,粗粒度,松耦合,无状态的服务。服务发布出来供其他服务调用,一组互相依赖的服务就构成了SOA架构下的系统。
基于这些基础的服务,可以将业务过程用类似BPEL流程的方式编排起来,而BPEL反映的是业务处理的过程,这些过程对于业务人员更为直观,调整也比hardcode的代码更容易。当然企业还需要对服务治理,比如服务注册库,监控管理等。
我们知道企业计算领域,如果不是交易系统的话,并发量都不是很大的,所以大多数情况下,一台服务器就容纳将许许多多的服务,这些服务采用统一的基础设施,可能都运行在一个应用服务器的进程中。虽然说是面向服务了,但还是单一的系统。
而微服务架构大体是从互联网企业兴起的,由于大规模用户,对分布式系统的要求很高,如果像企业计算那样的系统,伸缩就需要多个容纳续续多多的服务的系统实例,前面通过负载均衡使得多个系统成为一个集群。
但这是很不方便的,互联网企业迭代的周期很短,一周可能发布一个版本,甚至可能每天一个版本,而不同的子系统的发布周期是不一样的。
而且,不同的子系统也不像原来企业计算那样采用集中式的存储,使用昂贵的Oracle存储整个系统的数据,二是使用MongoDB,HBase,Cassandra等NOSQL数据库和Redis,memcache等分布式缓存。
那么就倾向采用以子系统为分割,不同的子系统采用自己的架构,那么各个服务运行自己的Web容器中,当需要增加计算能力的时候,只需要增加这个子系统或服务的实例就好了,当升级的时候,可以不影响别的子系统。这种组织方式大体上就被称作微服务架构。
微服务与SOA相比,更强调分布式系统的特性,比如横向伸缩性,服务发现,负载均衡,故障转移,高可用。互联网开发对服务治理提出了更多的要求,比如多版本,比如灰度升级,比如服务降级,比如分布式跟踪,这些都是在SOA实践中重视不够的。
微服务架构模式有很多好处。
首先,通过分解巨大单体式应用为多个服务方法解决了复杂性问题。在功能不变的情况下,应用被分解为多个可管理的分支或服务。每个服务都有一个用RPC-或者消息驱动API定义清楚的边界。微服务架构模式给采用单体式编码方式很难实现的功能提供了模块化的解决方案,由此,单个服务很容易开发、理解和维护。
第二,这种架构使得每个服务都可以有专门开发团队来开发。开发者可以自由选择开发技术,提供API服务。当然,许多公司试图避免混乱,只提供某些技术选择。然后,这种自由意味着开发者不需要被迫使用某项目开始时采用的过时技术,他们可以选择现在的技术。甚至于,因为服务都是相对简单,即使用现在技术重写以前代码也不是很困难的事情。
第三,微服务架构模式是每个微服务独立的部署。开发者不再需要协调其它服务部署对本服务的影响。这种改变可以加快部署速度。UI团队可以采用AB测试,快速的部署变化。微服务架构模式使得持续化部署成为可能。
最后,微服务架构模式使得每个服务独立扩展。你可以根据每个服务的规模来部署满足需求的规模。甚至于,你可以使用更适合于服务资源需求的硬件。比如,你可以在EC2 Compute Optimized instances上部署CPU敏感的服务,而在EC2 memory-optimized instances上部署内存数据库。
像任何其它科技一样,微服务架构也有不足。其中一个跟他的名字类似,『微服务』强调了服务大小,实际上,有一些开发者鼓吹建立稍微大一些的,10-100 LOC服务组。尽管小服务更乐于被采用,但是不要忘了这只是终端的选择而不是最终的目的。微服务的目的是有效的拆分应用,实现敏捷开发和部署。
另外一个主要的不足是,微服务应用是分布式系统,由此会带来固有的复杂性。开发者需要在RPC或者消息传递之间选择并完成进程间通讯机制。更甚于,他们必须写代码来处理消息传递中速度过慢或者不可用等局部失效问题。当然这并不是什么难事,但相对于单体式应用中通过语言层级的方法或者进程调用,微服务下这种技术显得更复杂一些。
另外一个关于微服务的挑战来自于分区的数据库架构。商业交易中同时给多个业务分主体更新消息很普遍。这种交易对于单体式应用来说很容易,因为只有一个数据库。在微服务架构应用中,需要更新不同服务所使用的不同的数据库。使用分布式交易并不一定是好的选择,不仅仅是因为CAP理论,还因为今天高扩展性的NoSQL数据库和消息传递中间件并不支持这一需求。最终你不得不使用一个最终一致性的方法,从而对开发者提出了更高的要求和挑战。
测试一个基于微服务架构的应用也是很复杂的任务。比如,采用流行的Spring Boot架构,对一个单体式web应用,测试它的REST API,是很容易的事情。反过来,同样的服务测试需要启动和它有关的所有服务(至少需要这些服务的stubs)。再重申一次,不能低估了采用微服务架构带来的复杂性。
另外一个挑战在于,微服务架构模式应用的改变将会波及多个服务。比如,假设你在完成一个案例,需要修改服务A、B、C,而A依赖B,B依赖C。在单体式应用中,你只需要改变相关模块,整合变化,部署就好了。对比之下,微服务架构模式就需要考虑相关改变对不同服务的影响。比如,你需要更新服务C,然后是B,最后才是A,幸运的是,许多改变一般只影响一个服务,而需要协调多服务的改变很少。
部署一个微服务应用也很复杂,一个分布式应用只需要简单在复杂均衡器后面部署各自的服务器就好了。每个应用实例是需要配置诸如数据库和消息中间件等基础服务。相对比,一个微服务应用一般由大批服务构成。例如,根据Adrian Cockcroft,Hailo有160个不同服务构成,NetFlix有大约600个服务。每个服务都有多个实例。这就造成许多需要配置、部署、扩展和监控的部分,除此之外,你还需要完成一个服务发现机制,以用来发现与它通讯服务的地址(包括服务器地址和端口)。传统的解决问题办法不能用于解决这么复杂的问题。接续而来,成功部署一个微服务应用需要开发者有足够的控制部署方法,并高度自动化。
一种自动化方法是使用PaaS服务,例如Cloud Foundry。PaaS给开发者提供一个部署和管理微服务的简单方法,它把所有这些问题都打包内置解决了。同时,配置PaaS的系统和网络专家可以采用最佳实践和策略来简化这些问题。另外一个自动部署微服务应用的方法是开发对于你来说最基础的PaaS系统。一个典型的开始点是使用一个集群化方案,比如配合Docker使用Mesos或者Kubernetes。
“微”服务并不一定微。服务的具体规模可谓多种多样。其中规模最大的成果源自Amazon公司旗下的“两块披萨”团队(即整个团队只需两块披萨即可填饱肚子),这意味着其总人数在十位左右。而规模较小的团队则由六人组成,负责支持六项服务。
采取此类组织方式的企业实例,其各职能团队共同负责构建并运营每款产品,而每款产品则被拆分为一系列独立的服务——且各服务间通过一套消息收发总线实现通信。
大型整体应用程序亦可以始终围绕业务功能实际模块化,由大型团队构建的单一整体应用程序根据自身业务线进行设计与划分。然而在这类情况下,最大的问题在于整体应用程序在组织当中需要考虑太多背景信息。如果其整体范畴当中包含太多模块边界,那么团队中的单一成员将很难通过短期记忆对其进行管理。
这种模块化业务线的维护工作还要求相关人员具备极高的专业技能水平。相比之下,服务组件能够令拆分方式更为明确,从而大大简化团队边界的设定与认知。
大部分应用程序开发工作都会遵循项目模式:其目标在于交付软件方案中的特定部分,并拥有直观的完成指标。在软件开发工作完成后,其会被传递至运维部门,这时负责构建该软件的团队也将即刻解散。
微服务的支持者们则认为这种模式并不可取——他们的主张是相关团队应该伴随产品走过整个生命周期。这方面最典型的例子应该是Amazon公司提出的“谁构建,谁运行”原则,其中开发团队需要对生产环境下的软件成果承担全部责任。这就要求开发人员在日常工作中全程关注其软件的生产运行情况,同时掌握来自用户的反馈意见,意味着他们需要在一定程度上为用户提供技术支持服务。
产品的定位应始终与业务功能相协调。相较于以往将软件视为一整套已经完成的功能集的心态,微服务架构要求我们全程与之保持关联,并思考该软件能够如何协助用户加强业务功能。
当然,我们完全可以将同样的思路引入整体应用程序当中,不过大量小型服务集合能够显著简化服务开发人员与及用户之间的个人联系。
在跨越不同进程构建通信结构时,我们发现很多产品及方案会直接把智能化机制塞进通信机制本体当中。这方面的典型实例就是企业服务总线(简称ESB),ESB产品当中通常包含复杂度极高的消息跌幅、编排、转换以及业务规则应用等机制。
微服务社区则倾向于使用另一种实现方式:智能化端点与傻瓜式流程。采用微服务架构的应用程序旨在尽可能实现解耦化与关联性——它们各自拥有自己的域逻辑,而且在经典Unix场景下的运作方式更像是过滤器机制——接收请求、应用合适的逻辑并生成响应。这一切都通过简单的REST类协议实现编排,而非经由WS-Choreography或者BPEL等复杂协议以及中央编排工具实现。
目前最常用的两类协议为配合源API的HTTP请求-响应与轻量化消息收发协议。对于前者,最简练而准确的说明是:
立足于Web,而非居于Web背后。
微服务团队采用的正是万维网(在很大程度上亦包括Unix在内)所遵循的原则与协议。一般来讲,其使用的资源能够为开发人员或者运维人员轻松实现缓存处理。
第二类作法则是立足于轻量化消息总线实现消息收发。这类基础设施选项通常具备傻瓜式特性(这种傻瓜特性体现在实现操作上,即只需匹配消息路由机制,再无其它)——以RabbitMQ或者ZeroMQ为代表的简单实现方案仅仅需要提供一套可靠的异步结构,而服务的全部智能化元素仍然存在于端点当中并负责消息的生成与消费。
在整体应用程序当中,各组件在进程内执行并通过方法调用或者函数调用的方式实现彼此通信。将整体应用程序转化为微服务形式的最大难题在于改变这种通信模式。由内存内方法调用指向PC通信机制的简单转换往往无法良好起效。相反,大家需要利用粗粒度方式取代原本的细粒度通信机制。
服务围绕业务能力建模,服务应该清晰地反应业务能力。
采用微服务架构模式后,开发和运行的协作模式都会发生变化。
按微服务的组织方式,不同人或小团队负责一个或一组微服务,服务之间可能存在相互调用关系,所以在服务之间也完全采用了像面向外部开放的契约化开发模式。
每一个服务都提供了一份契约文档,发布到公开的内部 wiki,方便服务干系人可自由获取查看。契约文档要求至少对服务的几个基本方面作出说明,如下:
-API,具体接口的 API 接入技术说明。
- 能力,服务能力的描述。
- 契约,提供这些能力所约定的一些限制条件说明。
- 版本,支持的最新和历史的版本说明。
使用契约文档来减少多余且可能反复重复的口头沟通,降低协作成本。
采用微服务后一个业务功能的调用会涉及多个服务间的协同工作,由于服务间都是跨进城的调用通信,一个业务功能的完成涉及的服务调用链条可能较长,这就涉及到服务间需遵循一些规则来确保协作的可靠性和可用性。我们采用的原则是:长链条的内部服务之间的调用异步化。若一个调用链条中的个别服务变慢或阻塞可能导致整个链条产生雪崩效应,采用异步化来规避调用阻塞等待导致的雪崩情形。
而微服务的测试,服务开发和运营人员专注于做好服务实现层面的单元测试和服务契约层面的接口测试。而面向业务功能的端到端测试,更多是依赖自动化脚本完成。而为了维护好这些自动化测试脚本,也需要保持服务接口和契约的兼容性和稳定性,这些自动化测试脚本也属于服务的消费方之一。
借助于虚拟化或容器等隔离技术,每个服务感觉都是独享资源,不必考虑额外的资源使用冲突。
大量松耦合的微服务通过相互协作来完成业务功能的流程处理,在这样一个复杂的生产环境中,出现异常或错误是很难迅速定位的。这就需要一套成体系的监控基础设施,在我们的实践中借助了公司统一的监控基础设施,对监控进行了分层,顶层的监控站在用户视角,底层的监控站在系统视角,形成更完善的反馈链路。
构建复杂的应用真的是非常困难。单体式的架构更适合轻量级的简单应用。如果你用它来开发复杂应用,那真的会很糟糕。微服务架构模式可以用来构建复杂应用,当然,这种架构模型也有自己的缺点和挑战。
如果这篇文章对您有所帮助,或者有所启发的话,求一键三连:点赞、评论、收藏➕关注,您的支持是我坚持写作最大的动力。