微服务的未来,乍一看文章题目有点大,但实际上,从“道”的角度,而不是从具体“术”的角度来看,这个题目并不是一个说不清的大问题,甚至可以用一句话概括,那就是“微服务的未来,就是尽可能把自己做的跟单体服务一样”(为什么这么说?后面会解释),基于这句话出发,本文会阐述微服务现在该有的样子和未来可能的样子,以及未来微服务会如何结合云计算DevOps,形成最终跟单体服务一样的效果。
重点:本文所谓的单体服务是指系统的所有功能都是自己独立完成的,不需要调用其他应用服务,即完全高内聚的服务。——那些把代码都写在同一个程序里,但是需要同一个程序部署多台,相互之间调用的单体软件,不算本文讨论范围(有些系统真是这么干的,虽然笔者不太明白他们为什么要这样)。
本文脉络:
1、为什么会有微服务?
2、微服务的几种形式
3、微服务该有的形式(类比单体服务)
4、微服务的稳定性问题(分布式带来的烦恼,其实并不是新烦恼)
5、微服务的迭代问题(只有封装,才有未来,回归微服务本质)
6、微服务的终极未来(云操作系统+云语言)
7、对现实的意义(不走弯路,站在巨人的肩膀上)
注:本文会多次运用类比思考,这里强调一下,类比是帮助理解分析问题的一种思维方式,类比的两样东西并不会在细节上完全一致,它们只是等价关系,而不是相等关系,所以请读者不要过多纠结于这一点上。
1、为什么会有微服务?
关于为什么会有微服务,笔者之前写过一篇文章阐述,大家可以去看下,《Xin He:微服务之殇:也许会因人而生、因AI而逝?——从生命3.0角度看微服务架构》,所以这里仅概括该文的主要观点,来解释为什么会有微服务。
观点一:本质原因,是因为人类个体和群体的智力局限所致(人脑工作记忆不够用,即人脑内存不够)。
观点二:为解决内存问题,利用微服务化来进行功能解耦,让程序员更专注单一问题,降低人脑内存压力。
观点三:为实现解耦,依据两个基本理论,一是设计模式,二是康威定律,前者是程序之间的解耦,后者是人类组织间的解耦。
基于上述三个观点,基本可以明白,为什么随着软件复杂度加大,微服务会必然出现,这块不多说。
2、微服务的几种形式
这里的形式是指微服务间的调用形式,是微服务设计中最需要考虑的问题。如何设计调用形式,直接影响了后面提到的软件稳定性和迭代问题。
基本接口形式:
形式一:API(Application Programming Interface,应用程序接口)
形式二:RPC(Remote Procedure Call,远程过程调用)
几种基本调用形式:(这里用A、B表示两个微服务,→←表示请求和请求方向,忽略长短连接、同步异步请求方式)
外部调用触发请求【sever系统】
形式一:→ A → LB(外部负载均衡) → B
形式二:→ A(内部负载均衡) → B
形式三:→ A → MQ(消息中间件) ← B
无外部调用,自主触发请求【任务系统】
形式一':A → LB → B
形式二':A →B
形式三':A → MQ ← B
以上几种调用形式,其实都可以跟单体服务的内部调用逻辑做类比,类比如下:
经过类比,我们做微服务设计的时候,就可以参考前面笔者说的“把微服务做的尽可能跟单体服务一样”这个角度,来选择方案了,不至于迷茫纠结。例如对于系统内部我们应该尽可能选用RPC方式相互调用,而对于系统外部,才选用API方式调用等等,具体略过,下一节细说。
3、微服务该有的形式
微服务该有的形式,这个问题看似不好回答,但如果我们能够回到本文最开始说的那句话,“把微服务做的尽可能跟单体服务一样”,从这个角度来思考,那么问题就简单多了,只要参考现在单体服务在往什么形式上发展,我们也就知道微服务该往什么方向走,该是有什么样的形式了。那我们来看看,现在单体服务的开发,在往哪个方向走。
方向一:从阻塞io向非阻塞io过渡。现在一些较新的程序语言,已经自带非阻塞io的实现能力,例如Golang的go程和Python3的asyncio机制,老的语言也已经有比较通用的非阻塞io框架,例如Java基于nio的Netty,所以很多之前为了避免io阻塞而应用的编程方式,已经基本被淘汰,现在调用接口,只要使用了非阻塞io框架,调用方式就跟阻塞模型调用一样,非常简单,大大降低了开发人员的心智成本。
方向二:线程池维护被屏蔽掉。程序框架自己维护线程池,减少了开发者的心智成本,例如Golang自身维护线程池,Java的Netty框架也是。
以上两个方向,现在在微服务中也有体现,例如Golang用的gRPC,Java里的Dubbo,都实现了上面的能力,从而让微服务间的调用,就好像在单体服务中调用一样。
总结:对于微服务该有的形式,结合前一节所说,除了系统入口端对外提供API,内部应该全部使用RPC,并且采用类似gRPC和Dubbo的服务发现机制和非阻塞框架。另外,除非业务需要,笔者不建议随意使用LB或者MQ作为调用中间件,因为实际上,这两个组件的使用都是因为微服务出现早期,微服务生态不够发达所致,对于现在的微服务生态来讲,新系统完全没必要再用这两套调用方案。在下一节稳定性问题上,笔者还会再细说,为什么最好别用MQ。
4、微服务的稳定性问题
注:这里所谓的稳定性,是指微服务之间的稳定性,而不是指单个微服务自身的稳定性。
笔者自己总结,稳定性大致包括三方面,一是服务间的调用稳定性,二是数据稳定性,三是设备稳定性。其中,调用稳定性即下游服务调用失败,该如何处理的问题(可以类比单体服务的程序健壮性问题),数据稳定性可以认为是微服务间的数据事务ACID类问题(即常说的分布式事务问题),设备稳定性就是微服务设备自身和网络的稳定性问题(这里强调下,设备宕机和服务调用失败是不同的,前者已经不工作,后者还可以继续进行问题上报和补救工作)。
对于前两种稳定性问题,单体服务的时候,其实也都存在,那我们是否可以借鉴呢?当然可以。
一、关于调用稳定性,单体服务的处理方式,就是提高程序的健壮性,最主要的手段就是提前预判或者捕获异常,然后进行相应的处理。有几种处理方式:
先说最简单无为的处理方式,就是打出堆栈,写入Error日志,程序返回错误,人工处理。
类比一下,对于微服务间调用失败,我们有没有也像单体服务打出堆栈一样,打出整个上游调用链呢?如果答案是没有,说明我们的微服务,对标单体服务,连最基本的异常处理机制都还没有做到,相关人需要重视了。
再说高阶一点的处理方式,就是不只会记录异常,还会针对异常做特殊的处理。处理方式无外乎如下两种:
1、回滚。为了数据稳定性,需要用到,后面数据稳定性再说。
2、重试(或针对性修改后重试)。有些异常可以重试,例如调用下游IO异常,可能只是网络抖动原因,直接重试会成功;还有一些异常是已知可能会发生的,被捕获后,改一些参数,再重做就能成功,其实就是单体服务的一种work around。
微服务与之类比,我们是否有相应的回滚机制?是否有重试机制?我想大部分的答案可能是没有,当然,有一些相对重要的业务,大家应该会在业务代码中写入回滚或者重试机制,但是这些都是业务代码,非平台或框架代码,缺少复用性规范性,只能算是症状解,但并不是根本解。
其实从全局来看,这种回滚和重试机制,应该是微服务框架与运维体系共同来保证的。笔者前一篇文章《Xin He:DevOps你该长成啥样?——从公司发展角度看DevOps演进》里面阐述的全自动化DevOps闭环图中的反向故障处理部分,就可以与微服务框架结合,来做这个事情。仔细想下,把单体服务的回滚和重试,放大到微服务场景下,不就是反向运维动作嘛,用运维平台来做,也顺利成章,这个理念,也是未来AIOps的基础,后文会再细说。
这里说一下上文为什么提到除非业务需要,尽可能不用MQ,因为MQ本身属于发布订阅模型,它的存在其实割裂了一个正向的调用链,让中间形成了一个黑洞,让某些东西不可知了。例如消息发送出去了,下游是否执行,我们是不知道的,只有MQ知道,这就凭空增加了一层耦合关系(其实MQ也不知道消息是否被正确执行,因为在它看来,消息只是被消费走了,但并不意味着执行成功,这也是为什么RocketMQ需要有一个ack的机制,来确认被消费的消息一定被下游成功执行,这样确实解决了消息执行情况不可知的问题,但是心好累)。当然,有人说MQ除了解耦的作用,还有缓冲请求的作用,这个地方我认同,但是现在的RPC框架,大多都有缓冲区机制,其实MQ的缓冲作用可以被替代,并且如果是使用Kubernetes平台,或者具备动态扩缩容能力的应用平台,对于请求缓冲,也不是一个非常强烈的需求。至于其他一些辅助功能,就更不是必须使用MQ的理由了,所以对于利用MQ来解耦微服务,笔者是持否定态度的(当然,历史遗留原因除外,笔者公司就是一例,还听过相关人抱怨,如果不是历史遗留,当初也不会用MQ,是的,技术债有时候就是这样,所以我们要尽最大可能重视初始设计,以免欠债)。
二、关于数据稳定性,相对复杂一些,大致问题有如下几种:
1、微服务间数据不一致(分布式事务中最主要的问题)。
其实这个在单体服务中,就是一个已经存在的问题,比如在前面那个类比图中的两个方法A和方法B,分别包含写入数据库的动作,大家是会把这两个写入动作放到一个大事务中写入?(这就类似于分布式事务中两阶段提交的方式了,效率很低),还是分成两个事务,但是一旦方法B的事务出错,就坚持重试,直到把B的事务做完,做不完就向上抛异常告知情况?(分布式事务中最大努力完成的解决方案),还是会把A的事务回滚?(分布式事务中的saga机制,回滚事务),或者两者都做?(尽力完成,最后完不成就回滚)。我想当大家知道自己在单体服务的情况下,会选用哪种方式的时候,也就知道微服务化后该选哪种了。——其实不管选择哪一种,最终都是性能、一致性、程序复杂度、人工运维程度以及软件迭代能力之间的妥协,不同的公司,公司中不同的人,都会从不同的视角做出不同的选择,但作为技术人,要有技术人心里的一杆秤,我们应该尽可能想的稍微超前一点,符合未来方向一点,这样才符合技术人本身的使命,这也是笔者会写这篇文章的初衷。
2、微服务间数据错乱。
这个主要是数据的abb'a'乱序问题,即a、b两个请求按a先b后的顺序发给了微服务A,A也按这个顺序落了数据库,同时去调用下游服务B(我们把发到下游服务B的请求叫做a'和b'),但是B在接收a'和b'两个请求的时候,却是先执行了b'后执行了a',大部分情况下,不会影响最终结果,但是如果数据设计存在问题,就可能存在先做的操作a覆盖后做的操作b的问题(描述的有点多,其实本质上就是数据库事务ACID中的I问题,隔离性问题)[注1],那么这种情况下,怎么解?有两个解法:
一是如果所有的数据库操作都是增量操作,不是绝对值操作,就不需要考虑规避这个问题。
(当然,每次更新数据必须采用select for update方式,锁死数据后再更新数据)。
二是对于绝对值类操作,必须有版本控制机制。
其实对应于金融类业务应用,覆盖类操作相对较少,一般只有修正数据的时候才会直接覆盖,但是对于运维类平台,这种动作却较多,比如要将运维平台数据库中的某些信息同步到配置中心etcd中去[注2],举个例子,为了做流量调度(或无感上线),运维平台需要将nginx中的upstream业务应用ip,从数据库mysql同步到配置中心etcd上去,这个同步过程就有乱序的风险,只要同一个nginx下面有应用并发上线,那么就有错乱可能,如果没有一个有效的版本控制,保证配置中心上面的信息,总是跟数据库中的最新版本一致,那这个运维平台的稳定性,就无从谈起了(之前文章[4]提到过的借贷宝的运维平台Zodiac就实现了这个版本一致性控制,回头找机会再细说)。
注1:数据错乱这个问题不是微服务的专利,单体服务也是一样,平时喜欢select数据,再传递,修改,然后commit的同学都需要注意,如果没有用select
for update,确保数据的隔离性,整个过程就是可能存在数据错乱的问题,没有压力的情况下,一切安好,一旦出现并发,会死的很惨。
注2:从一个地方拿数据,然后到另一个地方执行数据,是非常常见的一种操作,但是也是非常需要注意的一种操作。——例如对云平台中的虚拟VR下发ip网络规则,对nginx下发upstream配置,如果没有有效控制版本,一旦出现并发,就会存在配置错乱问题。
3、增量操作请求的幂等性问题(任务可重做的前提)。
对于单体服务,最好的方式就是在数据库落盘的时候,通过数据库的数据唯一性限制来判断是否为重复请求,只不过单体服务是通过捕获数据库唯一性异常,来返回已完成或者顺序执行后面的动作,从而保证任务可重做的能力,那么微服务进行类比的话,就需要针对下游调用的重发机制,来保证整个调用链路的可重作。
那么对于数据稳定性,微服务具体要怎么做呢?简单来说,就是每一个本地事务的落盘动作都要保证幂等性,并且这个本地事务,都该包括两个周边动作,一个是对上游的回滚动作,一个是对下游的可重做动作,这些都是微服务异常框架和运维平台来共同协作完成,后面说。
三、最后说设备稳定性。
设备宕机问题,可以跟调用失败问题类比,区别就是设备宕机后,程序无法自主修复了(类似前面提到的work around机制),服务陷入一个不可知状态,这种情况下,重视机制就派上用场了,而且因为不是程序bug,不用更新代码后重试,直接重试发到别的设备上再执行服务即可,成功概率极高。所以服务设备非单点,服务可重做,是保证设备稳定性最重要的两个点。当然,前提是你需要知道该重做哪些请求(这个跟RocketMQ的事务消息理念一样,其实RocketMQ的事务消息,本质上也就是解决了一旦发送消息端宕机,可以告知其他发送端回滚,或者重新执行哪些消息这么一件事情。解决宕机后的不可知问题),这个问题也好解决,就是每个微服务设备在接收请求的时候,都记录下自己接收的请求id,如果自己真的挂了,把失败的请求调用链,重新执行一下就可以了(具体从哪个位置重新执行,需要业务和运维平台共同商定维护,重新执行的起点,可以不一定是最开始,中间某个点即可)。
有一个问题,重做是否需要让用户感知。笔者觉得,这个看解决的时间长短,如果可以较快解决,就不必让用户感知,如果时间相对长,就需要页面端告知用户,请求失败,后台在重新执行,然后成功后,提示用户。
总结一下,对于微服务稳定性最重要的关键点,就是:
幂等
可重试
可回滚
如果每个微服务能够做好这三点,那么再附加外围的微服务框架和运维平台,一个具有极高健壮性的微服务体系就会在开发和使用上跟单体服务一样可靠和稳定。其实想想看,这三个点就是对应单体服务编程里的try...catch...finally,try...本地事务+调用下游...catch...自身重试,或回滚,或下游重试...finally...失败告知上游回滚,或成功返回。只要我们按照这个经典的异常处理机制,去思考问题,相信服务的健壮性,是可以有保证的。
微服务的稳定性要素
当然,话说回来,如果我们做单体服务设计的时候,对健壮性就不够重视,那么进行微服务设计,健壮性也必然会存在问题的,所以还需要扎实基本功,多从软件设计本质上进行思考。
5、微服务的迭代问题
迭代问题,笔者认为,只有封装才有未来,我们必须对微服务的这些异常处理操作,进行封装,才能将异常处理代码与业务代码进行解耦,只有解耦才能降低开发人员的开发心智成本。对于业务开发,我们最多需要他们考虑如何执行事务,事务失败后如何重试和回滚,但是不能让他们考虑这几件事情如何连动,如何被执行,这个必须被封装掉,就像try...catch...finally...这个过程,程序员并不需要关心它是怎么被程序实现的,他们只要能用好它就可以了。——对于微服务try...catch...finally...这个过程的实现,前文提到了,笔者认为最好用微服务框架+运维平台来共同完成。
代码要进行封装,部署也要回归单个微服务仅作为整体系统中一个服务单元的本质地位。即一个微服务占用多大的内存资源,应该是与其实际使用情况相关联的,应该是随用随取的,理论上微服务系统中的一个微服务不该使用独占固定资源(类比单体程序中的类或方法[注])。但是现在来看,如果将微服务部署到占用固定资源的虚机上,显然无法实现这一点,如果想实现这一点,必须部署到Kubernetes(以下称K8s)这样的容器平台才行,做到全资源域内存共享,这样可以大大节省资源,方便调度,也回归了微服务的本质。
注:微服务本身可以类比成单体服务中的类,微服务中的RPC接口,可以类比成类方法。
如果再深入一步迭代的话,对于RPC调用,其实上游就是想把下游当作一个方法来调,那我们为什么还需要运行一个RPC server呢?我们为什么要维护一个这样的server框架呢?让云来做不可以吗?答案当然是可以的。所以这个时候你想到了什么?如果你了解云计算的前沿技术,你应该能想到,那就是serverless。当然,现在的serverless和笔者说的这个还不太一样,但是如果云帮我们维护了一套微服务框架,那么我们在上面写的微服务,完全可以是serverless的。现在云厂商还没有做这个方向,应该是微服务框架还没有实现标准化的原因,一旦标准化以后,做成类似serverless使用方式的微服务,是必然可以实现的。——关于这个话题放到下一节说。
6、微服务的终极未来
直接说观点,笔者认为微服务的终极未来就是,就是构建在云资源(云CPU+云内存+云硬盘+云网络...)+ 云操作系统(云平台)+ 云语言(Serverless)+ AIOps运维平台之上的应用服务。
关于云资源:笔者不用再解释,现在云计算已经做到了。
关于云操作系统:相对来说还没有做到很好,但是已经有了它该有的样子,以K8s为代表的应用编排容器平台,已经基本实现了云应用与云资源的解耦,这是操作系统最核心最重要的能力,即实现软件与硬件的解耦,现在看来,K8s已经基本实现,相当于云操作系统的内核,好比Linux内核一样,有了这个东西,更多的操作系统能力,围绕这个开发,以后都好说。
关于云语言,serverless:现在还处于百家争鸣的局面,不像云操作系统内核K8s,已经基本成为事实标准。这里笔者要说一下,为什么现在大家都在研究serverless,不光是因为serverless具备即时使用资源的能力,可以节省用户费用,同时也因为serverless才真正符合微服务该有的样子。微服务产生的目的就是为了解决程序员的心智成本问题,而serverless则是实现这个目的的集大成者。对于一个程序开发人员,使用了serverless以后,就不需要再考虑任何其他问题,只需要考虑具体方法的具体实现,然后放入上一节提到的标准微服务框架里就好了(也就是所谓的云语言)。引用几张别人的图,大家可以尝试理解一下笔者的意思。
SCF(Serverless Cloud Function),无服务云函数
注:以上两图出处《Serverless架构开发与SCF部署实践》[5],可以看出现在微服务已经是往serverless这个方向走,当然,很多人还是在强调serverless可以减少费用,部署灵活等等,但我个人觉得,这些点很好,但都不是最本质的,最本质的还是因为它让微服务中的单个服务回归到了它作为一个整体服务中一个功能节点的本质,就好像程序中的一个类、一个方法一样的这个事实本质,这大大降低了程序开发者的心智成本,也降低了资源使用,符合微服务的终极趋势。
所以severless这个技术趋势,也验证了笔者最开始的说法,微服务的未来就是尽可能把自己做的跟单体服务一样。
关于AIOps运维平台:重点完成类似单体服务下的异常处理流程(try...catch...finally),平台保证的是异常处理机制的正确运行。而不同类型的异常要如何处理,就是业务开发者自己根据业务需求写进云语言里去的,也即到底是重试还是回滚,这些具体的选择和执行方式。
为方便理解,如下图表达
这个未来会成立的前提逻辑,是IT内部产业分工这个必然趋势是成立的。现在看来已经势不可挡,毕竟传统产业在几十年前就已经开始了这种分工,而IT产业的分工,应该是在近十几年开始的,本文不做过多阐述,大家只要明白产业升级就会带来产业标准化,产业标准化就会带来产业分工,以现在IT产业的升级速度,未来10-20年,将是IT产业分工的黄金期,所以,笔者所谓这个微服务的终极未来,很可能也会在较快的时间内时间,真的不好说。
最后再说一下,为什么我在文章最开头,会说微服务的未来,就是“把微服务做的尽可能跟单体服务一样”,原因很简单,因为太阳底下没有新鲜事,有的只是把过去的事情又重新做了一边而已[6]。不相信的话,我们把时间拉久一点,看50年前的软件演进,从最开始零散的打孔编程,到大型汇编编程,到玛格丽特·汉密尔顿(Margaret Hamilton)提出程序健壮性的重要性,挽救了美国的登月计划(1969年),到现在2019年,正好50年过去了,软件设计已经从原始走到了先进,从单体软件走到了微服务软件集群,但是,对于软件服务的实现(正向实现机制),软件健壮性的保障(反向异常处理机制),两个方面的本质逻辑是没有变的。
所以我说,现在大家通过DevOps(AIOps)对于微服务的改进,跟50年前,玛格丽特·汉密尔顿对于汇编编程健壮性的改进,是没有区别的,都是在践行软件自身这个东西的本质。当我们想清楚了软件这个东西本质是什么,不管它具体会用到什么形式,是汇编也好,是其他高级语言也好,是单体服务也好,是微服务也好,它最终的本质,就是要实现语言到现实的对接,敲几行代码,就能在现实世界实现一个必然的结果,这是多么神奇的一件事情,也是软件存在的本质,只要这个本质不变,并且还是用人的思维来书写语言,那么不管软件本身的实现过程会变成什么样子,它都会需要一个正向的实现过程,和一个反向的健壮性过程,只不过随着软件复杂程度的提升,我们也把他们分别封装起来了而已。当然,除非最后编写软件的已经不再是人,而是超级智能,笔者前面的文章《Xin He:微服务之殇:也许会因人而生、因AI而逝?——从生命3.0角度看微服务架构》提过,也许只有到这个时候,软件会被重新定义,微服务可能也会消失,但那可能是很久以后的事情了,不谈。——扯远了,不知道读者是否理解了笔者的意思,但笔者语言能力有限,也就只能表达到这个地步了,在此略过。
7、对现实的意义
本质上,思考未来走向,对现实的意义,只有一个,那就是尽可能的不走弯路,做符合迭代方向的选择。当然,也不能走的太快了,太快会成为先烈,但也不能太慢了,太慢就会被淘汰。如何把握好这个度,只有真正看懂了方向,才能把握好。笔者个人评估,现在的时代如前所说,已经处于云操作系统内核相对成型的时代,所以再做微服务设计的时候,尽可能基于这个内核来做,至少不会落后于时代,如下图。
8、全文总结
全文重点讲了笔者认为微服务现在该有的样子和未来可能的样子,以及理由,由于篇幅有限,部分细节没有展开,但是也基本讲清楚了需要关注的点和问题,算是指出了思考的线头,也表达了笔者对于微服务软件架构设计领域的一层封装理解。全文更适合参与顶层设计的架构师和管理者品读,也适合有上进心、想要看懂软件架构全貌的程序员同学读一下,应该会有一些收获。
最后说一下,笔者一直定位自己是一个技术思考者,全文也或多或少讲了一些笔者思考的过程,但是关于技术思考,到底是一个什么东西,跟其他的思考方式有什么区别?个人觉得,应该从“道”和“术”两个角度来说。对于“道”中的技术思考,更像技术哲学,更具备普适性,所谓“万变不离其宗”;而对于“术”中的技术思考,则是各有各的门道,细微之处见真章,这就要求我们技术人必须“知其然知其所以然”,深入其中。——本文就属于更多偏于“道”而略于“术”的文章,侧重思考、整理和封装。
总结来看,“万变不离其宗”和“知其然知其所以然”,这两句话是对于技术的“道”和“术”最好的阐述。对于技术人,思考任何问题,都不能忘了这两句。
全文结。