尽管“调用远程方法”与“调用本地方法”只有两字之差,但若要兼顾简单、透明、性能、正确、鲁棒、一致等特点,两者的复杂度就完全不可同日而语了。光是“远程”二字带来的网络环境下的新问题,譬如,远程的服务在哪里(服务发现),有多少个(负载均衡),网络出现分区、超时或者服务出错了怎么办(熔断、隔离、降级),方法的参数与返回结果如何表示(序列化协议),信息如何传输(传输协议),服务权限如何管理(认证、授权),如何保证通信安全(网络安全层),如何令调用不同机器的服务返回相同的结果(分布式数据一致性)等一系列问题。全都需要设计者耗费大量精力。

因为将一个系统拆分到不同的机器中运行,为解决这样做带来的服务发现、跟踪、通信、容错、隔离、配置、传输、数据一致性和编码复杂度等方面的问题所付出的代价已远远超过了分布式所取得的收益。

原始分布式时代的教训
某个功能能够进行分布式,并不意味着它就应该进行分布式,强行追求透明的分布式操作,只会自寻苦果。

——Kyle Brown,IBM Fellow,Beyond Buzzwords:A Brief History of Microservices Patterns,2016

摆在计算机科学面前有两条通往更大规模软件系统的道路:一条是尽快提升单机的处理能力,以避免分布式带来的种种问题;另一条是找到更完美的、解决如何构建分布式系统的解决方案。

 

对于小型系统,单台机器就足以支撑其良好运行的系统,不仅易于开发、测试、部署,且由于系统中各个功能、模块、方法的调用过程都是进程内调用,不会发生进程间通信(Inter-Process Communication,IPC[1]),因此连运行效率也是最高的,所以此时的单体架构完全不应该被贴上“反派角色”的标签,反倒是那些爱赶技术潮流却不顾需求现状的微服务吹捧者更像是个反派。单体系统的不足,必须在软件的性能需求超过了单机、软件的开发人员规模明显超过了“2 Pizza Team”[2]范畴的前提下才有讨论的价值,因此,本书后续讨论中所说的单体,均特指“大型的单体系统”。

单体系统的真正缺陷不在如何拆分,而在拆分之后的自治与隔离能力上。由于所有代码都运行在同一个进程内,所有模块、方法的调用都无须考虑网络分区、对象复制这些麻烦的事和性能损失,但在获得进程内调用的简单、高效等好处的同时,也意味着如果任何一部分代码出现缺陷,过度消耗了进程空间内的资源,所造成的影响也是全局性的、难以隔离的。譬如内存泄漏、线程爆炸、阻塞、死循环等问题,都将会影响整个程序,而不仅仅是影响某一个功能、模块本身的正常运作。如果出现问题的是某些更高层次的公共资源,譬如端口号或者数据库连接池泄漏,还将会影响整台机器甚至集群中其他单体副本的正常工作。

由于隔离能力的缺失,单体除了难以阻断错误传播、不便于动态更新程序以外,还面临难以技术异构的困难,每个模块的代码通常都需要使用一样的程序语言,乃至一样的编程框架去开发。单体系统的技术栈异构并非一定做不到,譬如JNI就可以让Java混用C或C++实现,但这通常是迫不得已的,并不是优雅的选择。

 

OA在21世纪最初的十年里曾盛行一时,有IBM等一众行业巨头厂商为其呐喊冲锋,吸引了不少软件开发商,尤其是企业级软件开发商,但最终还是偃旗息鼓,沉寂了下去。在后面的2.1节中,笔者会提到SOAP协议被逐渐边缘化的本质原因:过于严格的规范定义带来过度的复杂性,而构建在SOAP基础之上的ESB、BPM、SCA、SDO等诸多上层建筑,进一步加剧了这种复杂性。开发信息系统毕竟不是作八股文章,过于精密的流程和理论需要懂得复杂概念的专业人员才能够驾驭。

信息系统经历了巨石、烟囱、插件、事件、SOA等架构模式,应用受架构复杂度的牵绊却越来越大,已经距离“透明”二字越来越远了,这是否算不自觉间忘掉了当年的初心呢?接下来我们所谈论的微服务时代,似乎正是带着这样的自省式的问句而开启的。这一阶段的微服务是作为SOA的一种轻量化的补救方案而被提出的。时至今日,在英文版的维基百科上,仍然将微服务定义为SOA的一种变体,所以微服务在最初阶段与SOA、Web Service这些概念有所牵扯也完全可以理解,但现在来看,维基百科对微服务的定义已经颇有些过时了。

·围绕业务能力构建(Organized around Business Capability)。

这里再次强调了康威定律的重要性,有怎样结构、规模、能力的团队,就会产生对应结构、规模、能力的产品。如果本应该归属同一个产品内的功能被划分在不同团队中,必然会产生大量的跨团队沟通协作,而跨越团队边界无论在管理、沟通、工作安排上都有更高昂的成本,因此高效的团队自然会针对其进行改进,当团队、产品磨合稳定之后,团队与产品就会拥有一致的结构。
·分散治理(Decentralized Governance)。

这里是指服务对应的开发团队有直接对服务运行质量负责的责任,也有不受外界干预地掌控服务各个方面的权力,譬如选择与其他服务异构的技术来实现自己的服务。微服务更加强调的是在确实需要技术异构时,应能够有选择“不统一”的权利,譬如不应该强迫Node.js去开发报表页面,要做人工智能训练模型时可以选择Python,等等。
·通过服务来实现独立自治的组件(Componentization via Service)。

之所以强调通过“服务”(Service)而不是“类库”(Library)来构建组件,是因为类库在编译期静态链接到程序中,通过本地调用来提供功能,而服务是进程外组件,通过远程调用来提供功能。前文我们也已经分析过,尽管远程服务有更高昂的调用成本,但这是为组件带来自治与隔离能力的必要代价。

·产品化思维(Product not Project)。

避免把软件研发视作要去完成某种功能,而是视作一种持续改进、提升的过程。以前在单体架构下,程序的规模决定了无法让全部成员都关注完整的产品,如开发、运维、支持等不同职责的成员只关注自己的工作,但在微服务下,要求开发团队中每个人都具有产品化思维,关心整个产品的全部方面是具有可行性的。
·数据去中心化(Decentralized Data Management)。

微服务明确提倡数据应该按领域分散管理、更新、维护、存储。在单体服务中,一个系统的各个功能模块通常会使用同一个数据库。诚然,中心化的存储天生就更容易避免一致性问题,但是,同一个数据实体在不同服务的视角里,它的抽象形态往往是不同的。譬如,Bookstore应用中的书本,在销售领域中关注的是价格,在仓储领域中关注的是库存数量,在商品展示领域中关注的是书的介绍信息,如果使用中心化存储,所有领域都必须修改和映射到同一个实体之中,这很可能使不同服务相互影响而丧失独立性。尽管在分布式中处理好一致性问题也相当困难,很多时候都没办法使用传统的事务处理来保证,但是两害相权取其轻,即使有一些必要的代价,但仍是值得使用的。
·强终端弱管道(Smart Endpoint and Dumb Pipe)。

弱管道(Dumb Pipe)几乎是直接反对SOAP和ESB的通信机制。ESB可以处理消息的编码加工、业务规则转换等;BPM可以集中编排企业业务服务;SOAP有几十个WS-*协议族在处理事务、一致性、认证授权等一系列工作,这些构建在通信管道上的功能也许对某个系统中的某一部分服务是有必要的,但对于另外更多的服务则是强加进来的负担。如果服务需要上面的额外通信能力,就应该在服务自己的Endpoint上解决,而不是在通信管道上一揽子处理。微服务提倡使用类似于经典UNIX过滤器那样简单直接的通信方式,所以RESTful风格的通信在微服务中会是更合适的选择。

·容错性设计(Design for Failure)。

不再虚幻地追求服务永远稳定,而是接受服务总会出错的现实,要求在微服务的设计中,能够有自动的机制对其依赖的服务进行快速故障检测,在持续出错的时候进行隔离,在服务恢复的时候重新联通。所以“断路器”这类设施,对实际生产环境中的微服务来说并不是可选的外围组件,而是一个必需的支撑点,如果没有容错性设计,系统很容易被一两个服务崩溃所带来的雪崩效应淹没。可靠系统完全可能由会出错的服务组成,这是微服务最大的价值所在,也是本书前言中所说的“凤凰架构”的含义。
·演进式设计(Evolutionary Design)。

容错性设计承认服务会出错,演进式设计则承认服务会被报废淘汰。一个设计良好的服务,应该是能够报废的,而不是期望得到长存永生。假如系统中出现不可更改、无可替代的服务,这并不能说明这个服务多么优秀、多么重要,反而是一种系统设计上脆弱的表现,微服务所追求的自治、隔离,也是反对这种脆弱性的表现。
·基础设施自动化(Infrastructure Automation)。

基础设施自动化,如CI/CD的长足发展,显著减少了构建、发布、运维工作的复杂性。由于微服务架构下运维对象数量是单体架构运维对象数量的数量级倍,使用微服务的团队更加依赖于基础设施的自动化,人工是很难支撑成百上千乃至上万级别的服务的。

 

对于服务的注册发现、跟踪治理、负载均衡、故障隔离、认证授权、伸缩扩展、传输通信、事务处理等问题,微服务中将不再有统一的解决方案。即使只讨论Java范围内会使用到的微服务,

仅一个服务间远程调用问题,可以列入解决方案的候选清单的就有RMI(Sun/Oracle)、Thrift(Facebook)、Dubbo(阿里巴巴)、gRPC(Google)、Motan2(新浪)、Finagle(Twitter)、brpc(百度)、Arvo(Hadoop)、JSON-RPC、REST,等等;

仅一个服务发现问题,可以选择的就有Eureka(Netflix)、Consul(HashiCorp)、Nacos(阿里巴巴)、ZooKeeper(Apache)、etcd(CoreOS)、CoreDNS(CNCF),等等。其他领域也与此类似。

一个简单服务,并不见得会同时面临分布式中的所有问题,也就没有必要背上SOA那百宝袋般沉重的技术包袱。需要解决什么问题,就引入什么工具;团队熟悉什么技术,就使用什么框架。

作为一个普通的服务开发者,作为一个“螺丝钉”式的程序员,微服务架构是友善的。可是,微服务对架构者却是满满的“恶意”,对架构能力的要求已提升到史无前例的程度。

 

后微服务时代

而在微服务时代,人们之所以选择在软件的代码层面而不是硬件的基础设施层面去解决这些分布式问题,很大程度上是因为由硬件构成的基础设施跟不上由软件构成的应用服务的灵活性的无奈之举。软件可以只使用键盘命令就拆分出不同的服务,只通过拷贝、启动就能够实现伸缩扩容服务,硬件难道就不可以通过键盘命令变出相应的应用服务器、负载均衡器、DNS服务器、网络链路这些设施吗?

但是,被业界广泛认可、普遍采用的通过虚拟化基础设施去解决分布式架构问题的开端,应该要从2017年Kubernetes取得容器战争的胜利开始算起。在这一年,Kubernetes的最大竞争者Docker Swarm的母公司Docker,终于在10月被迫宣布Docker要同时支持Swarm与Kubernetes两套容器管理系统,也即在事实上承认了Kubernetes的统治地位。这场已经持续了三年时间,以Docker Swarm、Apache Mesos与Kubernetes为主要竞争者的“容器编排战争”终于有了明确的结果。

Kubernetes中提供的基础设施层面的解决方案与传统Spring Cloud中提供的应用层面的解决方案的对比,尽管因为各自出发点不同,解决问题的方法和效果都有所差异,但这无疑是提供了一条全新的、前途更加广阔的解题思路。

 Kubernetes成为容器战争胜利者标志着后微服务时代的开启,但Kubernetes仍然没能完美解决全部的分布式问题——“不完美”的意思是,仅从功能上看,单纯的Kubernetes反而不如之前的Spring Cloud方案。这是因为有一些问题处于应用系统与基础设施的边缘,使得很难完全在基础设施层面中精细化地处理。

举个例子,微服务A调用了微服务B的两个服务,称为B1和B2,假设B1表现正常但B2出现了持续的500错,那在达到一定阈值之后就应该对B2进行熔断,以避免产生雪崩效应。如果仅在基础设施层面来处理,这会遇到一个两难问题,切断A到B的网络通路会影响B1的正常调用,不切断则会持续受B2的错误影响。


以上问题在通过Spring Cloud这类应用代码实现的微服务中并不难处理,既然是使用程序代码来解决问题,只要合乎逻辑,想要实现什么功能,只受限于开发人员的想象力与技术能力,但基础设施是针对整个容器来管理的,粒度相对粗犷,只能到容器层面,对单个远程服务则很难有效管控。类似的,在服务的监控、认证、授权、安全、负载均衡等方面都有可能面临细化管理的需求,譬如服务调用时的负载均衡,往往需要根据流量特征,调整负载均衡的层次、算法等,而DNS虽然能实现一定程度的负载均衡,但通常并不能满足这些额外的需求。

为了解决这一类问题,虚拟化的基础设施很快完成了第二次进化,引入了今天被称为“服务网格”(Service Mesh)的“边车代理模式”(Sidecar Proxy),如图1-5所示。所谓“边车”是一种带垮斗的三轮摩托车,笔者小时候还算常见,现在基本就只在影视剧中才会看到了。在虚拟化场景中的边车指的是由系统自动在服务容器(通常是指Kubernetes的Pod)中注入一个通信代理服务器,相当于那个挎斗,以类似网络安全里中间人攻击的方式进行流量劫持,在应用毫无感知的情况下,悄然接管应用所有对外通信。这个代理除了实现正常的服务间通信外(称为数据平面通信),还接收来自控制器的指令(称为控制平面通信),根据控制平面中的配置,对数据平面通信的内容进行分析处理,以实现熔断、认证、度量、监控、负载均衡等各种附加功能。通过边车代理模式,便实现了既不需要在应用层面加入额外的处理代码,也提供了几乎不亚于程序代码的精细管理能力。

 

无服务时代

对软件研发而言,不去做分布式无疑是最简单的,如果单台服务器的性能可以是无限的,那架构演进的结果肯定会与今天有很大差别,分布式也好,容器化也好,微服务也好,恐怕都未必会如期出现,最起码一定不是今天这个样子。绝对意义上的无限性能必然是不存在的,但在云计算落地已有十余年的今天,相对意义的无限性能已经成为现实。

无服务的愿景是让开发者只需要纯粹地关注业务:不需要考虑技术组件,后端的技术组件是现成的,可以直接取用,没有采购、版权和选型的烦恼;不需要考虑如何部署,部署过程完全托管到云端,由云端自动完成;不需要考虑算力,有整个数据中心支撑,算力可以认为是无限的;不需要操心运维,维护系统持续平稳运行是云计算服务商的责任而不再是开发者的责任。在UC Berkeley的论文中,把无服务架构下开发者不再关心这些技术层面的细节,类比成当年软件开发从汇编语言踏进高级语言的发展过程,开发者可以不去关注寄存器、信号、中断等与机器底层相关的细节,从而令生产力得到极大解放。

·后端设施是指数据库、消息队列、日志、存储等这类用于支撑业务逻辑运行,但本身无业务含义的技术组件,这些后端设施都运行在云中,在无服务中将它们称为“后端即服务”(Backend as a Service,BaaS)。
·函数是指业务逻辑代码,这里函数的概念与粒度都已经很接近于程序编码角度的函数了,其区别是无服务中的函数运行在云端,不必考虑算力问题,也不必考虑容量规划。

 

但另一方面,对于那些信息管理系统、网络游戏等应用,或者说对于具有业务逻辑复杂、依赖服务端状态、响应速度要求较高、需要长链接等特征的应用,至少目前是相对不那么适合的。这是因为无服务天生“无限算力”的假设决定了它必须要按使用量(函数运算的

时间和占用的内存)计费以控制消耗的算力的规模,因而函数不会一直以活动状态常驻服务器,请求到了才会开始运行,这就导致了函数不便依赖服务端状态,也导致了函数会有冷启动时间,响应的性能可能不太好。目前无服务的冷启动过程大概是在数十到百毫秒级别,对于Java这类启动性能差的应用,甚至是接近秒的级别。