微服务这个词已经在软件行业变的非常热门,不了解微服务已经不好意思说自己是IT行业的从业人员,笔者学习和践行微服务也有一段时间,看了很多人的文章和参与了很多讨论,每个人的理解和认识都存在差异,最近一次在机场等待的时机重读了Martin Fowler(微服务之父,他1999年的著作《重构》在编码界也是非常火爆,影响了很多从业者)的文章,收货满满,再结合最近几年的实践经验,对微服务的认识有了新的提高,对微服务的理解也更加深入。
正值新型冠状病毒肆虐,国务院延长了春节假期,可以抽时间总结分享出来,给需要的朋友。有条件的同学建议阅读https://www.martinfowler.com/articles/microservices.html的英文原文,这里是原汁原味的微服务理念介绍和经验分享,相信读者更能获益。
1.微服务的定义
在开始讨论之前,首先明确微服务是一种架构风格,一种开发方法。用来开发一个软件套件中的单个应用的方法;这种方法具有共同的特点,即——这单个的应用运行在独立的进程内,和其他应用采用轻量级的通信方式进行通信(通常是HTTP),这些单个应用程序围绕业务功能构建,独立的开发和完全自动化的部署;这些应用之间有最少限度的集中管理,可以采用不同的语言开发,不同的存储技术。
为了更好的理解微服务架构,先认识一下微服务流行之前的架构,这种架构我们称之为单体架构风格Monolithic,这种架构构建的应用程序整体作为一个单元,通常包含三部分即客户端界面程序、服务端应用程序和数据库。客户端用户界面包含HTML,CSS,Javascript等运行在浏览器的程序;数据库包含数据库的表,视图和存储过程等用以存储数据;服务端应用程序包含处理HTTP请求,处理业务逻辑和数据访问等,这个服务端的应用程序就是个单体应用,系统的任何改变都将牵涉到重新构建和部署服务端的一个新版本。
这种单体应用程序是最自然的构建系统的方式,所有请求的处理都到单体应用程序的进程内,采用应用程序开发语言的基本特性把它拆分成不同的包、类和函数中从而实现模块化,可以在开发者的便携机上开发和测试单体应用。可以采用部署流水线把测试后的应用程序部署到生产环境中,可以增加更多负载均衡器后面的实例数量来水平的扩展服务能力。
单体应用程序可以做的很好,但人们也越来越发现问题,尤其是把应用程序部署到云上以后。变更被捆绑在一起,单体应用程序中一个小的改变都需要整个程序重新部署,时间长了保持模块化的架构也会比较困难,本应影响一个模块的变更也很难做到仅影响单个模块。单个模块的伸缩需要整个程序来伸缩,这也导致需要更多的资源。这些问题就导致了人们自然而然地想到了微服务架构,把程序构建成一系列服务,每个服务可以独立部署和伸缩,每个服务也定义了清晰的边界,不同的服务可以采用不同的语言编写,不同的团队来维护。
下图来之martin fowler的官方网站,不同颜色和形状的色块代表了不同的功能,单体架构把所有的功能放在一个程序中,微服务将每个功能放入一个一个微服务中,单体架构需要整个程序部署多份以达到伸缩的目的,而单体应用可以根据功能的使用情况再伸缩。
正是单体架构的这些问题,才使得微服务风格的架构得以发展,微服务除了服务是可独立部署、可独立扩展的之外,每个服务都提供一个固定的模块边界。甚至允许不同的服务用不同的的语言开发,由不同的团队管理。下面文章探讨一下微服务所具有的共同特征。
2.微服务架构的特征
微服务之父也无法给出微服务架构风格的一个正式定义,但他尝试去描述该架构的一些共性。就这些共性来说,并非所有的微服务都具有这些共性,但我们期望大多数微服务都具备大多数特性。
2.1 通过服务的方式组件化
在软件开发行业,长期以来都渴望通过插拔、物理世界堆积木的方式实现组件化,最近这几十年各种语言通过库的方式已经有很大的进步,一些语言也一定程度地实现了可插拔的模块化库。在讨论组件化的时候,不可避免的回到一个问题,什么是组件化,它的定义是什么?我们对组件化的定义是可以独立替换和升级的软件包。
微服务架构也会使用到现代编程语言提供的各种类库,但微服务实现组件化的方式是把业务拆分成不同的服务来实现的。我们把库定义为链接到程序并使用内存函数调用的组件,而服务是一种进程外的组件,它通过WEB服务请求或RPC(远程过程调用)机制来通信。
我们使用微服务做为组件化的原因是微服务可以独立的部署,如果你的应用程序在一个进程内包含了多个库作为组件,这些组件中的任何一个变化都需要整个应用程序重新部署。如果是以微服务的方式实现组件化,那么仅对实现变化的这些微服务重新部署就可以了,不需要整个应用重新部署。当然这也不是绝对的,一些变更将会导致服务接口变化,这样就导致多个服务发生变化,但一个好的微服务架构的目的是通过高内聚和按接口契约演进机制来最小化变更。可见微服务和库的很大一个区别就是是否可以单独部署。
用微服务来实现组件化的一个另一个好处是,微服务之间有清晰的服务接口定义;大多数语言没有没有提供定义发布接口的能力,只能通过文档和原则来约定,这就导致调用方很容易浸入到被调用的内部或者具体实现上去,从而造成了过度耦合(例如JAVA的JAR包,我们提倡面向接口编程,但调用方也可以直接构造实现类);微服务的接口定义方式很好的解决了这一问题,调用方只能通过远程调用的方式访问服务提供者,不可能侵入到被调用方的内部。 微服务的这种调用方式也有副作用,远程的调用比进程内通信代价要高很多,因此微服务的调用方式接口粒度要比进程内调用粒度粗,也即一个请求尽可能多的获取到所需的信息,而不是像进程内调用那样,需要的时候获取一小部分信息。
2.2 围绕业务能力划分微服务
当想要把大型应用程序拆分开时,通常聚焦在技术层面,导致划分出UI团队、服务侧团队、数据库团队的划分。当团队按这些技术线路划分时,即使是简单的更改也会导致跨团队的协调。按照这个划分,有新需求UI团队会把逻辑放到UI层,服务器团队会放到服务器层。这就导致逻辑无处不在,这是康威法则在起作用的一个例子。
有什么样的组织结构,就会有什么样的软件架构--- Melvyn Conway, 1967
而微服务的方式划分是不同的,围绕业务能力划分成不同的微服务,一个组织部门内部包含1到多个微服务,这些微服务里面包含了全面的用来实现这些微服务技术栈的人员,包含UI、服务器和数据库存储人员。这样的一个团队是跨技术栈团队,这个团队的成员能完成微服务的前后台开发、数据库开发及项目管理。
如下图所示,左边的图是微服务团队,团队里面包含了微服务所需的技术人才,有UI、服务器和数据库等,而围绕技术能力划分的团队,将团队划分为UI团队、服务器团队和数据库团队。
这里提到的微服务团队,很自然的问题是微服务团队应该由多大,这里实际上并没有一个清晰的定义,但根据业界实践,亚马逊建议最大不超过2个披萨,也即1个团队2个披萨能够喂饱,也意味着这个团队大小不超过12个人,也有的团队6个微服务用6个人来维护,也即1个人1个微服务,这样的粒度无疑太细了,我们不建议太细,但再大也不建议超过12个人。
2.3 做产品而不是做项目
产品和项目的最大区别在于项目是一次性的,目标是交付软件,完成后的软件被交接给维护组织,然后它的开发团队就去干别的事情了,或者说压根解散了,而产品是长期演进的,是不断迭代演化的。微服务更倾向于避免项目的模式,而是以产品的模式来运作,一个团队负责产品的整个生命周期。
亚马逊的理念 “you build, you run it” ,即谁开发谁维护,开发团队负责软件的整个产品周期。这使开发者经常接触他们的软件在生产环境如何工作,并增加与他们用户的联系,因为他们必须承担至少部分的支持工作。
这样的“产品”理念,是与业务功能的联动绑定在一起的。它不会将软件看作是一个待开发的功能集合,而是认为这是一个持续迭代的、发展和演进的产品,即软件如何能助其客户来持续增进业务功能。
2.4 智能端点和哑管道
在传统的SOA架构下,在不同的服务之间通信时,采用的方法是在其通信机制上增加了很多智能化,增加了不少业务逻辑处理。一个典型的例子就是ESB(Enterprise Service Bus), ESB 往往包含很多高级的功能,比如消息路由、编排、消息转化、和业务规则处理。
而微服务的则往往采用另一种替代方法,智能的端点和哑管道,也即微服务在通信管道上不处理业务逻辑,仅作为通信方式存在,业务逻辑在通信双方的微服务上处理。构建成微服务的目的是要做到高内聚和低耦合,每个微服务拥有自己的领域逻辑,而且很像是UNIX的过滤器那样工作,接受一个输入请求,对其应用业务逻辑,并产生一个响应。这些微服务之间通信,更倾向于采用简单的RESTFUL风格协议而不是复杂的协议(如基于SOAP的web service,或者BPEL)。
微服务和十几年前的SOA架构看起来非常相似,很多理念也很类似,但本质上有很大差异,SOA架构通常都有一个庞大、复杂的ESB总线,各个单体应用之间通过ESB来交换数据,ESB也承担了很多业务逻辑转换和处理的工作,但在微服务概念里面,没有ESB,有的只是轻量级的消息通信机制。
微服务建议采用2种方式来通信,一个是HTTP请求-响应式的API,第二个者轻量级的消息中间件通信。第一种HTTP方式,正是构建万维网使用的协议,通常使用的资源很容易被缓存起来。第二种方式是通过轻量级消息中间件来通信,比较常见的是RabbitMQ, ZeroMQ, Kafka等,这类消息中间件仅提供一个可靠的异步通信机制,不提供额外的功能,这也就是哑管道,而智能部分在端点侧,也就是通信双方微服务。
在单体应用中,组件之间通过内存中进程内的方法调用或者函数调用完成,把单体应用拆分为微服务架构的最大问题就是通信方式的变化。简单的把进程内的方法调用转换为RPC调用是十分错误的,因为在进程内调用开销非常小,获取的信息的粒度非常小,可以非常频繁的通信,但转换为微服务之间的调用之后,就要切回为更大粒度的通信了。 例如:单体应用里面订单信息和顾客信息在一个应用内,订单需要姓名,则调用订单模块的getName接口获取,需要地址信息再调用getAddressInfo接口获取,但拆分成微服务之后,就不能这么频繁的调用,顾客信息应该提供一个粗粒度的接口给订单管理,一次性提供顾客相关的姓名、地址信息。
2.5 去中心化治理
中心化治理带来的结果是趋向于标准化到单一的技术、工具、方法和流程,经验显示这种方法限制太多了,不是所有的问题都是钉子,也不是所有的解决办法都是锤子,多样性的世界和多样性的业务更趋向于不同的事情采用不同的工具,虽然单体应用也可以这么做,但这在单体应用中并不常见。
在将单体应用拆分成微服务时,可以根据业务特点和需要选择合适的技术,报表页面就用个简单的Node.js,当然可以,实时处理的需要用C++,当然也可以,想采用特定类型的数据库来提高读取性能,当然也没有问题。这里仅仅是说可以这么做,但不代表应该这么做。拆分成微服务后,不限制每个微服务采用的技术,但作为整个团队的一部分,技术还是应该尽可能的统一。
采用微服务的方式不是说就没有标准要遵守了,微服务的标准不是定义一堆预先选定的技术,而是微服务开发风格。大家共同的标准就是遵从微服务风格。每个微服务团队更倾向于采用对开发有利的工具,这些工具可以给其他团队用,当在解决问题时,也是优先采用已有的工具,可能来自于公司内部的开源,也可能来自于互联网开源。Netflix就是采用此哲学的很好的例子, 他们共享有用的,完整测试的代码,把这些代码作为库共享出来,鼓励其他开发者用这些库来解决问题,当然也鼓励开发者采用其他的方式来更好的解决。
对微服务社区而言,大家不愿意消耗太多的时间去做服务契约的管理,这不是说不重视服务契约,相反,非常重视服务的契约,有不同的方法来探索和尝试管理服务契约。如Tolerant Reader和消费者驱动型契约模式经常应用到微服务。这些模式有助于服务契约独立进化。采用消费者驱动服务契约的方式不但可以给服务团队增加信心,也能快速获得微服务是否工作正常反馈的良好方式。有的团队,先定义服务契约,之后再基于契约自动生成编码骨架,开发团队负责实现这些服务,不少团队采用这种方法,效果不错。
可能去中心化的最高境界就是亚马逊提倡的“谁开发,谁维护”的理念,开发团队负责软件的方方面面,包含安装和运维,这种模式确实很难达到,因为这会造成组织结构的重大调整,一些岗位的消失,开发团队的压力变大,这必然会带来阻力,但越来越多的公司正在这么做,或者走在这么做的路上,典型的就是亚马逊和 NETFLIX; 这种模式给开发团队的压力(晚上3点中被叫起来处理问题),才会促进开发团队注重代码的质量。这种模式和传统的开发团队开发、运维团队运维想去甚远。
2.6去中心化的数据管理
去中心化的数据管理通过几个方面表现出来:
(1)最抽象的层次是系统之间的概念模型不同,这会在大企业的系统集成带来问题,销售眼中的客户和技术支持眼中的客户是不同的,一些销售眼中看到的属性,在技术支持眼中可能完全没有,即使出现也可能具有不同的属性,或者(更糟糕的)属性相同,但语义却有细微的差别。 这个问题在单体应用之间很常见,在应用内部也时常发生,特别是应用被划分为多个模块的时候,一个有用的方法是采用领域驱动模型DDD,DDD是把一个复杂的领域划分为多个有边界的上下文,然后在这些有界上下文上做映射,这个方法对单体应用和微服务都有用,只是DDD划分的边界和微服务更为贴合。
(2)除了上述的概念模型去中心化,数据存储也去中心化,如下图所示,单体应用倾向于采用单一的数据存储技术,比如Oracle来存储数据,而微服务让每个微服务自己管理自己的数据,因此各个微服务采用的数据存储技术也可以相差很大,各自管理各自的数据,采用多数据库来管理各自的数据。
去中心化的数据管理对微服务的数据更新有较大影响,在单体应用中,跨越多个资源的数据更新用数据库的事务就可以保证一致性,但在微服务领域,由于每个微服务都单独管理自己的数据库,数据库采用的管理软件,存储方式都不一样,所以这是一个很大的难点,分布式事务实现起来难度很大,带来了很多的额外的复杂度,因此微服务架构强调使用没有事务的微服务之间的协调机制,这种方式明确的提出一致性只能是最终一致性来保证,可能产生的问题只能通过补偿机制来完成。
这种去中心带来的不一致问题会给开发团队带来很大的挑战,但这也是最符合实际的业务实质。在实际的业务处理中为了快速应对需求也会有一些不一致,往往通过逆向的流程来处理。只要处理这种错误的代价小于强一致性带来的业务损失,这种取舍就是值得的。
2.7 基础设施自动化
基础设施自动化在过去几年有了长足的发展,云计算的发展简化了构建、部署和维护微服务的复杂度。很多产品和系统通过持续集成和持续交付构建,采用这些方法的团队极大的利用了基础设施自动化,下面这张图展示了构建流水线的过程。
单体应用能够用持续集成和持续交付流程快速的部署到生产环境,只要单体应用完成了这种自动化构建,部署多个微服务也不会更复杂,无非是单体应用是单个少量的应用,但微服务是多个应用,1个和多个之间没有太大的区别。
2.8 失效设计
采用微服务这种设计风格,应用本身就应该被设计成能容忍失效,任何服务调用都有可能因为目标服务不可用而失效,调用方必须能够尽可能优雅的处理,从这一点上来说,这相比单体应用引入了额外的复杂性。所以微服务团队要考虑微服务的失效,如何影响到用户体验。Netflix 通过自生产环境里面注入微服务、数据中心故障来测试应用的韧性和应用的监控能力。这种一周繁忙工作结束后在生产环境的自动化测试足以让很多的开发团队不寒而栗,需要很强大的勇气和技术实力作为保障。
当然这不是说单体架构就不能做这种类型的失效设计,只是不像微服务架构风格这么常见。因为微服务可能在任何时候失效,所以快速的发现这些失效的微服务,并恢复它们(如果可能)至关重要。微服务架构在服务的监控上需要花费更多的精力,需要监控架构指标(如数据库每分钟处理多少请求)和业务指标(每分钟接收到多少订单),这种指标层面的监控可以给开发团队告警,从而让开发团队去跟进和调查。这种微服务的监控,在单体应用的设计中可以而且也应该被监控记录下来,但单体应用往往在单个进程里面,因此某个单体应用中的组件,连接不上、失效的价值就没有微服务这么大了。
现在越来越多的更高级的监控工具和日志工具已经被开发出来,很多是开源使用的,在这些监控工具下,可以在管理面板上看到各个微服务的运行状态、吞吐量和业务指标,熔断状态、延迟等我们关注的一系列指标。
2.9 演进式设计
微服务的实践者,通常具有演进式的设计思路,把服务拆分作为一种工具来控制变更,而不是降低变更的速度。控制变更不是说减少变更,用正确的工具和态度,不但可以频繁、快速的,而且可以良好的控制变更。
如论什么时候,开始拆分系统的时候,都面临如何划分的问题,这里一个简单的原则即可了独立替换、可独立升级原则,这也就说我们可以重写整个微服务而不用影响微服务之间的协作。事实上有的团队更进一步,认为服务从长远来看更倾向于走向消亡而不是长期演进。
英国卫报网站是一个单体应用向微服务演进很好的例子,单体应用还是网站的核心部分,但新增加的功能和特性就采用微服务调用单体API的方式实现,这种方式对一些天生就是临时性的服务特别有用,比如处理某个体育时间的专题页,这种一次性的专题页,可以通过快速开发的方式,采用快速开发语言开发出一些服务出来,在事件结束后再废弃掉。这种方式在财经机构的服务里面也很常见,快速的开发出服务,几个月以后再废弃掉。
这里强调可替换性,是更通用的模块化设计的一个特殊场景,我们总应该让变更同一个时间发生在一个模块内部,不希望是跨越多个模块的,如果你发现两个服务总是同时一起改动,那么这就是这两个微服务应该合并的信号。把组件组合成一个服务可以形成更粗粒度的版本发布计划,单体应用需要整个应用全部的构建和发布一遍,但微服务架构风格下,仅需要构建和替换修改的这个微服务,这可以简化和加速发布过程,缺点是不得不考虑单个服务升级对服务的调用方的影响,传统的做法是采用版本的概念来管理,但在微服务架构风格下,版本仅作为最后不得已的手段,我们在设计微服务时,应该尽可能的考虑能容忍被调用方的变化。
2.10 微服务是未来的主流趋势吗
微服务是未来的主流趋势吗,这个问题没有人能给出确定的答案,文章本身也是聚焦在阐述微服务的主要特点以及和单体应用的不同,就连微服务之父也不能确定的说微服务就是未来的主流趋势,他也承认需要更多的时间来观察,尽管当前微服务带给我们的积极的正向的影响要多于负面的影响,但可以肯定的是微服务是当前重要的一种架构风格,值得我们去深刻考虑和投资,有一些先驱已经践行这种风格,并且应用的非常成功,国外的比如亚马逊、Netflix、英国卫报、英国政府等,国内的阿里巴巴、腾讯、网易、以及笔者供职的华为也都大量的采用了微服务这种架构风格,还有很多的这样的企业正在是使用这种风格的架构来构建自己的企业IT系统。