微服务作为架构风格几乎成为云时代企业级应用的事实标准,构成微服务的技术元素本身却并非革命性。跨平台的分布式通信框架、地址无关的服务注册与发现、智能路由与编排等技术早已在CORBA、SOA时代实现了一遍又一遍,我们不禁好奇,微服务有什么不同?本文是对企业分布式应用的一次回顾,与前微服务时代相比,我们究竟在哪些领域吸取了教训,哪些方面持续搞砸。
我们在重新界定抽象边界上取得了进展…
架构的关键在于构造合理的封装抽象。良好的抽象构造如进程,由操作系统接管CPU调度、内存地址空间分配和I/O,程序员的心智从此解放,得以聚焦在业务逻辑上。糟糕的抽象往往引向万丈深渊,大量精力被浪费在抽象泄露带来的问题上。
在分布式系统中我们关注组件、组件间的通信以及伴随的工程实践,微服务在企业应用的上下文中就技术约束和业务价值间达成了更好的平衡点。
RPC?不,是API!
让我们从组件间的通信开始,最初人们认为这只是需要被解决的技术要素。
(图片来自:https://upload.wikimedia.org/wikipedia/en/thumb/f/f0/Orb.svg/802px-Orb.svg.png)
关于如何实现跨平台的分布式通信,30年前诞生的CORBA架构在今天来看仍然非常漂亮:通过定义IDL/ORB/API我们可以将内存对象任意分布于网络中。只要共享IDL,对象可以由C++/Java等不同的语言实现,其互相调用就像本地方法一样简单。然而实践经验告诉我们,分布式系统总是会出现本地调用不会发生的各种问题:网络的开销、传输的延迟、消息的超时和丢包、远端系统的崩溃……物理世界的技术约束是无法被忽略的,我们没有办法把分布式调用抽象成简单的本地方法。因此Martin Fowler在他的< 企业应用架构模式>里提出了著名分布式对象第一定律:“不要分布式你的对象”。相反,你应该把尽可能多的操作置于进程之内,通过replicate整个应用的方式来实现系统的scale。
由分析师们发起的SOA运动从另一个角度看待这个问题,Web Service应该是对企业资产和业务能力的封装。我们开始站在更高的维度,远过程调用不再只是技术意义上的集成。WSDL不仅是通信调用的接口,更是服务间的契约;UDDI不仅是服务描述、发现、集成的中心,更是企业业务与服务的黄页。WS-*在厂商的裹挟下发展成包罗万象,却也没几个人能掌握。开发者们抱怨花了太多时间写冗余的XML制定所谓的规范,WSDL生成的客户端也将不同服务耦合在一起。是否有更加轻量敏捷的方式,让我们快点开始写第一行生产代码?
于是我们看到REST的兴起。起初是作为反叛,用更加轻量级的方式(http+json)使用Web。然后我们发现"企业级"应用并非需要ESB这样昂贵的专有中间件,由"消费级"技术组成的万维网是世界上最大规模的分布式网络,我们应该向其学习如何构建健壮、可演化的系统。Roy Fielding那篇论文所提出的无状态、可缓存等特征已经深入人心,而狭义上的REST API(基于资源的URI、HTTP动词和状态码的标准接口)也成为API设计的最佳实践。
既然API和网站一样都是基于通用Web技术,API是否可以像网站一样作为产品提供呢(APIs as product)?于是越来越多的企业开始将自己的业务能力封装成API,提供给消费者,随之而来的是更弹性的商业应用和更灵活的计费方式。很多组织也着手构建自己的API市场,把内部IT能力整合、复用,并为孵化外部产品做准备。API已经成为商业价值主张的一部分。
我们从聚焦实现细节的rpc出发,来到了更具价值导向的REST API。即使构建内部系统,以消费者驱动的方式,也总是能帮助我们设计出更加松耦合和易于演进的API。
技术组件?不,是业务服务!
编程语言中的组件构造(如Java中的jar, C#中的dll)是软件架构师们封装可复用单元的最常用武器。组件作为理论上的最小部署单元,在工程实践中却并不容易独立变更。一般应用程序需要讲多个组件打包成一个部署单元(如war包),链接在内存地址中进行调用。对单个组件的热更新往往对组件间耦合和对象状态管理有很高的要求,重新部署整个应用一般是默认选项。以进程为边界构建可独立部署的服务成为架构师的另一项选择。
早期的服务只是单纯的技术构件,大多数组织从纯粹的技术实现角度考虑服务的划分。SOA的推动者们指出企业的信息资产应该被复用,信息孤岛应该被打通。通过将不同的服务编排组合,我们应该能够实现IT对业务更加灵活的支撑。
(图片来自:0SOA in practice, Nicolai Josuttism, 2009)
SOA的服务建模一般采用业务流程驱动的方式。一个典型的SOA设计是由业务分析师自顶向下地对企业现有业务流程进行分析,通过BPM引擎对流程进行建模,向下分解成组合服务,并进一步拆分成数据访问服务(很多可怜的SOA实现中数据的访问被拆分成不同的读服务和写服务)。然而这带来的问题是,服务跟服务间的耦合非常严重。当我的业务发生了变化,可能会需要修改很多不同的服务,涉及到多个团队的沟通和协调。在运行时层面,服务器间的通信非常频繁,用户在界面上的一次点击按钮,对应的后台多层服务间的级联通信。这给系统性能和稳定性也带来了巨大的挑战。SOA式的服务建模从分析型思维出发,却往往低估了分布式系统和跨团队协调的复杂度,导致服务拆分粒度过细。
微服务的名字常常让人误解,但实施正确的微服务粒度可能并不"微"。Martin Fowler与James Lewis在开创微服务定义的一文中已经指出微服务应该围绕完整的业务能力。今天我们在做微服务设计时,常常利用领域驱动设计中的Bounded Context来进行服务边界的划分。假设你的库存管理是一个独立的业务子域,针对库存的维护和操作应该被放到通过一个上下文和微服务中,由一个团队进行开发维护。多数业务变更都发生在上下文内部,不涉及跨团队协调。单个codebase内的重构和部署让发布更加容易。维护库存所需要的信息查询的调用多发生在进程内,更好的性能,同时无需处理额外的一致性问题。
微服务的另一个特点在于Product over Project,这需要不同于传统投资组合的预算管理与团队组建。传统的项目制将预算分配在相对短期的服务开发过程中,项目团队关注的是如何将业务范围(scope)实现,开发结束后服务转交运维团队进行维护,项目团队则被解散进行其他项目的开发。将微服务作为产品运营则需要建立业务结果导向的稳定产品团队。服务的设计不只聚焦于当下需求,更需要考虑价值定位和产品愿景。工程团队则需要思考如何用有限成本支撑非线性的业务接入增长。
(图片来自:Enterprise Architecture as Strategy, Ross et al, 2006
如今我们对服务的定义已经超越了技术组件,领先的组织已经在尝试将design thinking, business operating model应用到微服务设计中。
解耦服务就足够了吗?我们需要去中心化一切!
即使有了设计合理的服务于API,我们仍然需要与之匹配的工程实践才能将其顺利实施。
今天仍有很多企业使用集中式的应用服务器部署应用:开发团队将软件包构建出来,再统一安装到应用服务器中。对应用团队来说,这往往意味着漫长的反馈周期和痛苦的自动化。我们很早就推荐用Jetty这样内嵌式的应用容器部署软件,启动更快,测试环境更接近生产。one Tomcat per VM的部署方式虽然运行时开销较大,却是前容器时代隔离性最好的服务部署模式。Docker将这个实践更进一步,除了更轻量级的隔离,我们第一次可以将软件和所依赖的环境本身打包成版本化的artifact,彻底统一开发和生产环境。容器技术的成熟让我们可以将部署去中心化,开发团队可以独立部署一个服务。
数据库耦合是影响服务独立变更的另一重要因素。相比代码构成的应用软件,数据库schema更加难以变动。因为难以测试、难以兼顾性能优化和耦合的发布周期等因素,服务间以数据库集成成为臭名昭著的反模式。服务间的集成应该依赖封装好的显示接口,而不是数据库这种实现细节。我们应该在兼顾数据一致性的情况下,为每个微服务分配独立的db schema甚至db instance。如果说十年前数据几乎等同于关系数据库。如今数 据则可能呈现出各种形态:键值、文档、时间序列、图…我们完全可以采用更加合适的技术,以去中心化的方式进行微服务的数据治理。
即使将这一切都解耦,如果将交给一个集中的团队去实施,很有可能最终还是得到一个耦合的架构。这就是是著名的康威定律。康威定律告诉我们“设计系统的架构受制于产生这些设计的组织的沟通结构”。但同样我们可以将康威定律反转应用:如果你想达成一个目标架构,则必须对团队结构进行调整,使之和目标架构对齐。相比单体系统,微服务在运行时监控和运维所带来的挑战更大。"you build it, you run it"的DevOps文化成为必须。监控运维不再是Ops部门的事情,产品团队必须对微服务的整个生命周期负责。授权的去中心化自治团队是实施微服务的必要条件。
我们干得还不错,但也在持续搞砸一些事情…
我们在很多方向的确取得了进展。但即使在微服务时代,很多问题仍然在轮回发生着,似乎我们总是无法吸取历史的教训。让我们看一看那些挥之不去的反模式阴云。
一个例子是开发者对强类型RPC代码生成的依恋。尽管历史经验已经证明同步的rpc无法为分布式通信提供足够好的封装,伪装成本地方法调用的客户端往往鼓励程序员做出糟糕的接口设计:细粒度的频繁调用、缺少缓存和容错处理。IDL生成客户端也会导致服务间耦合,每次变更接口都需要升级数个相关服务。如果用可演进的REST API(如HATEOS)和tolerant reader模式,则可以优雅地解决这个问题。然而新一代的开发者们还是经常“重新”发现rpc的这些能力并陷入依赖——更快的序列化反序列化、类型安全和来自IDE的智能提示、通过spec反向生成代码…分布式计算先驱Vinoski不禁感叹“开发人员的便利性是否真的胜过正确性,可扩展性,性能,关注点分离,可扩展性和意外复杂性?”
另一个挥之不去的阴影是ESB。ESB在将异构的应用wire在一起有着关键的作用。然而当越来越多的职责被加入:数据报文的裁剪转换、难以测试和版本控制的编排(orchection)逻辑、服务发现智能路由监控治理分布式事务等All in One的solution将ESB变成了一个可怕的单点梦魇。所以微服务发出了“智能终端哑管道”的呐喊:我们只是需要一个不那么智能的代理处理可靠消息传输,将灵活的逻辑交给服务本身去编配(choreography)吧。
于是在典型的微服务架构里,负载均衡、服务注册发现、分布式追踪等组件以Unix way的方式各司其职。然而在利益诱惑和特性竞争压力之下,很多厂商不断将更多的功能放进他们的中间件,其中为代表的Overambitious API gateways俨然要重新实现占据中心的ESB。如果API gateway只是处理鉴权、限流等横切层逻辑没有问题,如果API gateway开始处理数据转换和业务逻辑编排,你应该提高警惕!
尽管行业在不断发展,但很多时候人们仍然沿用旧的思维,用新的技术去一遍遍重新实现这些旧的反模式。
如何更进一步
你总是可以在技术雷达里追踪微服务的state of art,如今这个领域的前沿方向是什么,Service Mesh, Chaos Engineering, 还是Observability as Code?然而历史告诉我们,新的技术在解决一些问题的同时,也可能会产生新的问题。更糟糕的是,我们永远无法记住历史,用新的工具更高效地重现旧日问题。
Technologies come and go, Principles stay forever。好在那些架构和实践背后的原则是经久不变的。从操作系统到移动应用都会需要高内聚低耦合的架构,任何软件开发都需要版本控制、自动化构建等实践。谨记这些核心原则、谨记软件被创造出来是为了解决有价值的问题,可以帮我们更好的借鉴历史的经验,理解和采纳新的技术。
文/ThougtWorks刘尚奇
本文首发于刘尚奇个人网站:https://6up7.com/a-retrospective-for-enterprise-distributed-application/
更多精彩洞见请关注微信公众号:ThougtWorks洞见