服务治理咋这么难?我想得换个治法了。

引出问题
在服务开发的时候,我们一般将功能开放为接口。而功能的驱动来源于上游,但上游需要按照下游的接口定义进行通信才能驱动,这样接口就将两个系统耦合在了一起。因为接口是面向功能的,我们称这种模式为功能驱动。这在系统规模较小时问题不大,但现在是(微)服务化的年代,我们需要大量的接口用于服务间的通信,这时耦合问题就变得比较突出了。
现举例说明,假设我们要做一个金豆系统,我们常规的做法是将增、删、改等功能开放给外系统并由它们进行调用,对接的系统非常多,如购物、评价、活动等,每个系统都按照自己的规则对金豆系统进行控制。因为规则限定在各自的业务系统内,所以不存在复用及打架的问题。但在运行了一段时间后,业务部门打算引入会员制,针对不同的业务依据不同的会员级别定义不同的奖励系数。于是问题出来了,会员级别该在哪里处理?开发团队商量了一下,给出两个方案并做了评估:
方案1:改造金豆系统,提供V2版的增、删、改接口,每个接口增加对会员规则的处理。接入方换个地址就好,不需要参数变更,但因为需要重新开发接口且相关的对接项目都要改造,开发周期较长。
方案2:金豆接口不动,由各个对接系统各自处理会员规则。好处是周期短,可由各个团队分头实施,缺点是是存在功能重复。
这个例子也许不具有代表性,这里也不评价这两个方案的好坏,这里的重点不是金豆项目本身,而是这个例子所折射出来的扩展性设计问题。那么扩展性问题与开头提到的耦合又有啥关系?因为扩展性不好的设计会使接口过多,这个多有可能是业务职能分配不合理造成的重复性的多,有可能是版本迭代造成的重复性多,这会放大系统间的耦合问题。也就是说扩展性如果做的比较好,那么接口的数量和稳定性都会得到很好的控制。
问题分析
其实这个问题要比想象的还要复杂,假设在金豆系统开发之前我们就知道有会员等级这一需求,我们也往往很难抉择系统的边界,因为我们会有很多的维度去划分这个边界,最终大多是一个权衡的结果。如果要以金豆的复用性为考量依据则不应该将规则纳入到金豆系统里,因为规则多且易变。但如果以控制的集中性为依据则恰好相反。虽然市面上有很多的设计模式,但每个设计模式都有特定的应用场景,但现实世界往往是多个场景的综合,所以这非常考验设计人员的能力,素养以及经验等。
那么我们有没有办法使多个场景各自独立呢?如果可以那么问题将极大的简化。但很可惜功能驱动模式是不支持的,因为系统间存在着硬相关性。下游需要实现这个功能接口,而上游需要控制这个功能接口。注意这里没有用“调用”一词而是用的“控制”,因为调用是技术层面的内容,而这里讲的是业务,功能是面向业务的,业务当然需要控制。由此可见控制和实现必然要耦合在一起,否则不能行使功能。这种耦合必然成为场景独立的绊脚石。
于是,当我们的系统开始庞大起来的时候,这种耦合便成了程序员的噩梦,尤其是基础服务!因为想控制它的系统实在是太多了,每当我们对基础服务进行调整的时候,你会发现异常的困难,我在京东主导订单账历时半年多才搞定,这个是最主要的原因。这也是微服务化建设道路中一般都会遇到的普遍性问题。
虽然大家都在强调服务治理,但我感觉目前市面上的解决方案大都在外围打圈圈,都是在自动化,可视化上做文章,并没有看到有关减少复杂度的理论或产品面世。它们基本上没有触及服务化的实质性矛盾:服务单一职责与服务间关系复杂之间的矛盾。而服务间关系的复杂性的根本原因在于服务间业务控制的复杂性。
既然问题这么普遍那是不是我们的方法论出了问题?当前所有的系统基本上都是基于面向对象技术构建的,我们的类、库、服务都是这一思想的产物,可以说没有面向对象就没有现在软件产业的蓬勃发展,所以在基本面上我们是不能怀疑面向对象的价值的,虽然有人已经指出了面向对象的一些问题(见参考1),我想还不足以动摇面向对象的支撑地位,尤其在单个系统内部设计上。
然而时代在变迁,系统由原先的单体已经演变为现在的分布式服务网格,如果把所有服务当做一个整体来看,每个服务就是一个对象,用于对外提供可复用的功能,这完全符合面向对象的思想。不过我们的场景已经发生了很大的变化,服务之间的调用已经把强力的控制降格为公平的协作,具体表现为:控制者和实现者各自拥有独立的进程,这便为功能变更造成了很大的障碍,这会导致控制的复杂性、脆弱性、延迟性等一系列问题。其次控制者和实现者很可能由不同的团队来主导,团队之间的地位相对来讲是平等的,这会为接口变更制造困难,所以控制的效果将大打折扣。
控制要求对资源进行强有力的占有,最好的体现形式是内聚而不是耦合,毕竟内部问题解决起来要比外部轻松的多。然而现有的微服务体系大多是由松散的多个团队来协作完成任务的,我们需要克服大量的沟通问题,系统规模越大困难越大,而且这个问题要比进程独立更难以解决,是矛盾的主要体现。所以面向对象在(微)服务场景下显得力不从心了!。
解决的方法
 
既然服务间的控制这么难做,我们是否可以“舍弃”控制呢?
 
如果“舍弃”服务间的控制,我们将面对功能如何驱动的问题。驱动方式无非两种:主动和被动。我们现在的服务大多是被动的,它不能自主完成任务,需要上游发出针对自己的指令才可以工作。主动方式的也有,如基于消息消费的服务,这类服务可以依据别人的数据自行启动处理程序。显然被动驱动是无法舍弃控制的,所以我们只能寄希望于自主控制。
要想自主控制,现行的接口的工作方式是不可以的,现在的接口名称是业务功能的标识符,实际上等同于指令控制编码,要想“舍弃”控制,就得舍弃这一部分。既然接口没有必要对功能进行标记,我们只需要一个纯通信意义上的接口就好了。但我们的数据要做下特殊处理,否则我们无法识别不同的数据,更别说驱动功能,这个下面会讲到,这里先不展开。
当然我们不能真的把“控制”给丢掉,我们只是希望它不要成为管理上的负担。所以我们得为他安排一个去处。这里有两个地方,第一个地方我们可以把控制放到一个独立的类似于消息系统的中间平台上,由它来进行集中控制;另一个地方是将控制转移到下游,把控制由耦合转变为内聚,让它从调用关系上消失。
我们先说平台思路,由于我们把控制从接口中剔除,平台和各个服务之间只能交换数据,所以所有的控制逻辑都要在平台内实现。而这显得非常的不现实,毕竟控制是非常复杂且个性的。然而有人确实做了这方面的工作,如各种工作流引擎。调研了一下,发现这些引擎最终都是功能的选择,而我们的功能标记已经去除了,所以根本无法满足我们的要求。
这里有一个例外:Nature 项目。Nature 虽然也是一个流引擎,但 Nature 是面向数据的而不是面向对象的。Nature 用数据与数据之间的上下游关系来代替显式的控制,也就是说 Nature 把控制真的“干掉”了,这正好与业务系统无业务意义的纯技术接口相吻合,那控制从哪里来呢?答案就是数据间的上下游关系,只不过这是一种隐式控制,这样 Nature 只通过数据就可以将不同的业务系统串起来,从而简化了业务系统的开发。 有关 Nature 项目请请见参考2。
也许你还发现了另一个例外:消息系统。其实就金豆和订单这个事情来讲完全可以由消息系统来解决,业务系统与消息系统的接口也是不区分业务意义的纯技术接口,那么消息系统与 Nature 系统的区别是什么呢?答案是:消息系统是完全没有控制,因为消息系统是不能定义数据之间的关系的,所以消息系统不能对整个流程进行控制,要想控制还的借助于外部的业务系统。就服务治理来讲消息系统可以降低系统间的耦合,但遗憾的是它没有流程控制能力。
如果你觉得 Nature 在认知上过于颠覆,那么我们可以采取另一个方法,就是将控制放到下游系统里去,使之成为内聚的一部分。那么接下来的问题是如何去定义我们的数据,以及如何去触发下游系统的不同功能,答案就是事件!上游只需告诉下游我发生了什么,不需要告诉你该怎么做
继续以下单奖励金豆为例进行说明,我们在下单时定义一个下单事件,事件内容为订单内容,其形式如下:
{
      "eventType": "order.paid",
      "eventID": "订单号,如123456",
      "eventPara": "订单内容的JSON串"
}
然后我们将这个数据传递给金豆的统一接口,在金豆系统内部,我们依据不同的 eventType 值进行自主控制。只要“我发生了什么”信息是全面的,下游是完全有能力自行完成任务,从而也能起到降低耦合的作用,只不过这个耦合没有 Nature 来的彻底。因为即便金豆系统提供了一个去功能化的接口,但这个接口的url必定要耦合“金豆”这个业务系统而不是其他业务系统,也就是说使用此种方式我们没有办法来彻底解决业务系统耦合的问题。其带来的最大的问题就是流程硬编码,而 Nature 则不存在这个问题。
控制内聚化除了这个遗憾外,还是有一个让人困惑的问题:eventType 的值以及 eventPara 中的内容应该由哪个业务来定义?由订单业务来定义可以说的通,因为大家都依赖于订单。但是由金豆业务来定义也可以说的通,因为这毕竟是金豆相关的业务,所以统一由金豆业务来定义也说的过去。这又是一个权衡的问题,这个权衡不一定是技术合理性为依据的,所以这里就不讨论了,但如果使用 Nature 的方式,则不需这个权衡了,因为对数据定义的“控制”必须都要在 Nature 上进行。
其它
事件机制给了下游自主控制的能力,下游接口的稳定性独立迭代的灵活性以及功能的可扩展性都可以得到大幅度的提升。除此之外事件还有一个天然的特性:业务溯源能力。对下游来讲来自上游的事件类型和事件ID必须作为下游数据的一部分来存储,数据来源于哪里一目了然。这一点在我们的常规功能性接口上往往是需要特意设计才能具有此功能,或者借助笨重的第三方工具或平台来实现。
事件的溯源能力还有另一个好处,因为事件是外部传入的,我们需要唯一标记一个事件,这会为业务的幂等处理打下天然的基础,我们无需进行额外的防重入设计。
还有一个好处,因为我们移除了接口的功能标签,使得接口得到统一,原先需要做切面处理的现在可以不用了,这可以降低相关技术的难度,提升系统的可维护性。但这也同时是一个坏处,一些功能性的信息在 url 上就无法体现了,这会对一些监控或管理工具的使用造成影响。
参考
参考1:面向对象编程—价值万亿美元的灾难
参考2:Nature 项目

 

posted on 2021-03-07 11:41  李学斌  阅读(457)  评论(2编辑  收藏  举报