解耦的艺术—大型互联网业务系统的插件化改造 | 高可用架构

此文是根据金自翔在【QCON高可用架构群】中的分享内容整理而成,转发请注明出处。

金自翔,百度资深研发工程师。在百度负责商业产品的后端架构,包括基础架构和业务架构。本文主要是根据讲师多年经验,针对业界经常碰到的问题,经过分析与实践提出的一种解决方案。

在大型系统中,“插件化”是对若干类似的子系统进行深度集成的一种方法,“插件化”的特点是能清晰的划分子系统间的边界,从实现上明确的区分系统中“变”与“不变”的部分。

在大的业务系统重构和改造的过程中,使用“插件化”不但能整合底层数据流,还能同时整合上层的流程和交互,在大规模跨团队集成中提供开发者的灵活性、用户体验的一致性和底层系统的安全性。

“插件化”要解决的问题如下图所示

0?wx_fmt=jpeg

图中的差异在不同案例中可能表现为代码思路的差异,用户体验的差异或者是性能的差异。

无论哪种差异,根本问题在于用自然语言描述的规范文档不够形式化,留下了太多不必要的自由发挥的空间。

“插件化”的目的就是尽量减少这些空间的存在。下面结合一个具体案例说下如何具体实施插件化。

这个案例是个源码过千万行的业务系统,整个开发团队规模比较大。该系统集成了很多业务产品,用了很多年,用户很多,需求也一直很频繁,是个核心的业务系统。

讲师观察过业界一些大的系统,经过大的重构拆分成了若干支持系统和业务系统后,拆分出来的系统分别由(组织架构上)独立的团队负责,这样有效减少了团队内沟通的成本。

但正如上图所示,拆分后不可避免的遇到了以下两个问题

1、文档

人员更替,排期压力,都会造成原有设计意图不能得到贯彻。想靠文档规范和约束,文档和代码同步的代价也很大。

尤其是敏捷开发模式下,大量想法没有落实到文档上,初始设计可能不错,但在实施过程中会越走越偏,留下大量不良的耦合与既有实现,维护成本越来越高。以前大团队中通过大规模review能在很大程度上避免这个问题,拆分后跨团队review实施难度很大,基本起不到什么作用。

2、碎片化

技术架构和业务架构的拆分是同时进行的。整个系统架构拆分后,研发和产品团队也拆分组成了垂直的团队,迭代速度快了很多。

但是垂直化不可避免的带来了碎片化的问题。在垂直体系中,每个产品的流程设计、交互设计有着极大的自主权。产品间的差异越来越大,业务流程和用户体验都开始碎片化。

碎片化造成用户的学习和使用成本提高,每个团队都认为自己的设计是最适合自己产品的, 但用户的反馈则是“为啥你们的产品长的都不一样呢?用起来好累。” 然后有些用户就不愿意用这样的系统了。

碎片化的问题越来越严重,开始影响到了整个业务目标的实现。也发现在一些系统中,出现最后老板拍板,把解决碎片化放到最高优先级来解决。

消除碎片化最容易想到的方案是在各产品上再抽一层,作一个新系统(后面称其为U系统),把容易碎片化的流程,交互等工作放到这里统一处理,用户只接触U系统,不再接触各个产品,自然也就没有碎片化的问题。

可是细想想,这方案无非是把问题搬了个地方。因为U系统的需求还是从各产品来的,本来拆完后各产品能自行实现需求(并行),现在只能把需求提到U系统等排期(串行),U系统很可能成为瓶颈,妨碍业务需求的快速响应。

这个方案能保证统一,但丧失了好不容易获得的灵活性,是产品团队不能接受的。

最终解决矛盾的方法就是开始提到的插件化

插件化类似软件设计中的“依赖注入”,将整个系统分成 “引擎”和“插件”两部分。用“引擎”保证统一,用“插件”提供灵活性。

具体实现时,U系统是一套“引擎”(可类比为spring-core这样的框架),自身没有逻辑,只是提供了一组插件规范。

引擎可以解析插件,运行插件中指定的逻辑。

“插件”由接入的产品提供(可类比为spring的配置文件),插件中指定了产品需要的各种逻辑,可随时更改。当“插件”注入到“引擎”后,就完成了新产品接入U系统的工作。

U系统的架构图如下

0?wx_fmt=jpeg

这个设计的好处是, U系统聚焦在作好引擎上就行,大部分提给U系统的需求可以作成配置,而配置的更改可以交给接入产品来完成。这样U系统和接入的产品彼此解耦,有足够的灵活性。至于碎片化的问题,则通过引擎中“用户交互”和“业务流程”两层来解决。

以下详细介绍实现

整个U系统由服务、插件、引擎和通信四大块组成。

其中服务由U系统指定接口,业务产品提供具体实现以供调用。目前业务系统基本都是java实现,RPC框架支持U系统直接发布一个jar包,指定每个接入产品必须提供的一组接口(比如提供基础信息,进行数据校验,提供报表数据等)。每个产品首先要实现这些服务,然后才能接入U系统。

实现服务后,每个产品会提供一个插件(XML)文件,这个文件包含所有定制化的信息,包括

产品使用的流程

产品提供的服务(地址和接口)

交互界面的DSL(控件)

数据处理(展示、格式化、转化)

报表(从何处取,如何处理)

在U系统上传这个插件文件后,系统中就多了一个产品,用户也能看到并使用这个产品。引擎保证了所有插件是可以热插拔的,所以业务变更时只需上传新的插件即可,不用重启和上线,非常方便。

下面说一下引擎,引擎是整个系统最核心的模块,插件的的管理、解析和运行都在这里完成。

引擎模块是独立开发的,并没有采用OSGI的规范实现,而是实现了一套自定义的规范,这是出于以下几点考虑:

1.OSGI更多定位在服务的注册、发现、隔离和调用,而U系统中远程RPC已有成熟的框架完成服务发现和治理的工作。至于引擎自身的代码是可控的,采用进程内调用即可,不需要隔离,进程内的依赖管理用Spring就足够了。

2.OSGI基本没有交互界面相关的功能,而解析插件提供的用DSL处理交互界面和逻辑是引擎很重要的功能。

3.OSGI 的发布采用Bundle方式,可以比较灵活的指定Import和Export功能。但其实并不需要这么灵活,写死引擎提供的接口可以降低使用成本。另外Bundle的构建和发布也没有直接上传XML来的简单。

引擎分层如下:

0?wx_fmt=jpeg

最底层是基础服务。像账户、资金、报表这些功能其实很依赖于外部系统,需要将这些外部系统封装成内部的基础服务供上层调用。这里的难点主要是外部系统经常不稳定甚至出错。所以这层设计时要考虑很多容错的方案。

比如:

1) 异步化,所有的同步接口异步调用,针对某些错误自动重试。

2)自动超时,监控所有未在一定时间内得到处理的请求

3)对账,下游系统支持的情况下对每个请求引入uuid,定时对账

4)数据修复自动化,可恢复的错误尽量不要引入人工干预。

除此之外,为了稳定起见,引擎的基础服务层会提供自己的服务降级/快速失败功能,以适配没有通过RPC框架提供类似功能的外部系统。

基础服务之上是业务流程

U系统不允许具体产品自定义业务流程。而是提供若干标准流程由产品在插件中指定。

考虑到流程会变动,内部实现上使用了工作流引擎,这样流程变更时的工作量会小很多。虽然技术上可行,但短期内不会把工作流引擎开发给业务产品定制。

统一流程这事儿业务价值很大,因为终于有办法用技术手段防止流程碎片化了。业务产品再没办法偷偷地改流程了,毕竟流程图都在U系统,交互也都在U系统的server上进行,业务方想改也改不了。

业务流程之上是自定义逻辑层

接入产品是有很多自定义功能,其中相当一部分用xml很难表示,所以允许在配置文件中直接提交代码,引擎动态编译并执行这些代码。

在实践中,允许用代码实现的功能大致可以有

1)交互界面初始值的获取

2)用户输入的转换、处理

3)系统间交互数据的转换

4)展示界面数据的格式化

允许提交代码,很重要的一点就是要处理好安全性,避免提交代码中有错波及到系统的其它部分.

这里可以做一个沙盒,让所有自定义代码都在沙盒中运行,沙盒可以保证

1.不同的沙盒互相不能感知彼此的存在。

2.插件中的代码无法修改沙盒以外(引擎)中的状态。

在实际的设计中,引擎制定的接口可以在配置文件中用不同的语言实现,只需要用<java> <scala> <python>等标签区分实现是哪种语言即可。

当然支持不同语言会对沙盒机制提出更高的要求,可以采用基于JVM的沙盒,通过区分classloader来实现隔离,也可以考虑引入docker,把沙盒作成一个本机的虚拟环境,这样隔离效果会更好。

至于为什么提交源代码而不是提交编译好的jar?这主要是因为:

1.要支持脚本语言,提交代码的设计更通用。

2.提交代码可以在更新插件时热编译,发现依赖问题,及时反馈;jar包只能在运行期抛异常,反馈置后。

3.通过本机缓存可以避免多次编译,作到相同的运行效率。

如何处理用户交互

交互界面是影响用户体验最大的部分,之前一些产品类似功能的交互界面差别很大,用户会很反感。

但这块完全由引擎作也不合适,因为界面是业务产品最易变的部分,引擎都包下来后续维护成本太高。

推荐的作法是提供控件库,把选择界面元素的自由提供给配置文件,但界面的渲染和逻辑由引擎统一负责。

具体来说,插件中可以像HTML一样指定文本框,下拉列表等控件,自由地构造界面,但不能自定义css和js,而是由平台统一渲染和校验。

假如插件配置中有这么一行

<date key="testDate" title="测试Date" required="true" defaultOffsetDays="2" description="活动开始时间" />

U系统的界面上会渲染出一个日期选择控件,控件的排版和样式由引擎决定,插件不能指定。

引擎会做一些基础校验(比如这里需要非空),更高级的有业务含义的校验规则由引擎把用户输入传给产品异步进行。

引擎的控件库包含20多种通用控件,包括<lable><text><date><checkbox>等交互控件,并通过<row>这样的控件提供简单的排版功能,基本已经可以满足需要。

如果有特殊的交互需求,也是由引擎开发特殊控件而不是由产品提供,这样在提供灵活性的同时最大程度的保证了前端交互的一致性。

从结果来看,因为只提供给产品“满足需求的最小自由”,整个系统交互的用户体验还是很统一的。

如何处理数据


为保证数据的一致性,避免用户看到的数据和其它途径不同。U系统对于产品提供的数据尽量不落地,而是实时调服务去取。

和前面提到的基础服务一样,这里要考虑到产品服务不可用的情况,所以在界面和流程设计以及实现中都要很小心,避免一个服务不可用影响到其它产品功能的正常使用。

出于性能考虑,统计报表的数据是少数的在引擎落地的冗余数据。

报表数据通常不可修改,所以一致性问题不大。

当然极端情况下还可能会存在数据的不一致,因此可以提供一个通知机制来作数据订正,但这不是一个正常的功能,并没有在插件中提供出来,只能算是一个后门。

这也算是业务系统实际落地时没法作的那么“纯粹”的一个例子。

U系统上线后,基本同时达到了设计要求

1.开发高效。引擎和插件解耦后,接入产品可以自助修改服务和开发插件,因为引擎能够快速反馈,发现错误的速度比以前更快,整体开发工作量也比以前更小。同时引擎自身没有业务逻辑,工作少了很多,基本上一到两个人就能完成维护工作。

2.体验统一。以前所有前端要统一的地方都是出个规范文档,希望大家照此执行,但总是渐行渐远。现在这些地方都统一了,很自然的约束住了产品的RD和PM,起到了控制作用。

3.结构清晰。在这套架构下,遇到变更时, RD会很自然的在脑海中将其区分为引擎要作的工作和插件要作的工作。大家的思路和认知统一,跨组的学习代价就小,整个组织架构也能更容易变更。

总结

讨论应用架构时,通常的服务化,包括现在很热的微服务更多地聚焦在后端服务的集成上。

今天分享的插件化在此基础上进了一步,不但要集成后端服务,同时要集成前端用户能感知到的流程和交互界面。

这个项目的实践表明,这种进一步的集成是可行的,是能兼顾灵活性和一致性的。

插件化的集成方案也有其局限性

比如追求个性化的用户产品,UE和PM的话语权会大很多,在这种场景下实施插件化,流程和UI的部分可能会被千奇百怪的业务需求搞的复杂很多。

但通常的业务系统,一致性往往比特立独行更重要,这种场景下插件化的解决方案还是比较合适的。

想进一步讨论插件化解决方案?或想同群专家进一步高可用架构,可回复arch申请进群。

本文策划 秋翾@百姓网, 内容由Carson@永利宝编辑与发布,其他多位志愿者对本文亦有贡献。读者可以通过搜索“ArchNotes”或长按下面图片,关注“高可用架构”公众号,查看更多架构方面内容,获取通往架构师之路的宝贵经验。转载请注明来自“高可用架构(ArchNotes)”公众号。

0?wx_fmt=jpeg

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值