编者按:本文来自微信公众号“InfoQ”(ID:infoqchina),作者陈康贤,阿里基础平台技术部技术专家,著有《大型分布式网站架构设计与实践》一书,编辑小智;36氪经授权发布。

软件架构在不断发展扩大、平台化,与此同时,业务多样性、复杂化也伴随相生。在这个过程中不可避免地出现诸多矛盾与挑战。如何在软件平台化和业务多样性两者间取舍,是一个引人思考、亟待解决的问题。

写在前面

最近做的几个项目有一些感触,借这篇文章来做一下总结,也希望以一个软件设计者的思考角度,就软件平台化和业务多样性这两者之间的博弈和取舍关系,做一些深入的探讨,并为大家展现这其中的困难与挑战。

一种架构解决多种问题的美好愿望

人们总有一些美好的愿望,比如发明永动机,亦或者是移民到其他星球,这些愿望推动着整个社会在不断进步,对于工程人员来说也是。总有那么一小戳优秀工程师,希望用一种架构或者方案,同时解决很多通用的问题,节约成本,提升效率, 让设计人员能够不至于疲于奔命, 解放生产力来完成更多有创新有挑战的工作。这种解决问题的思路,其实早在工业化时代的制造行业,就得到了很好的运用。

1978 年瑞典决定研制一种在 20 世纪初供军方使用的步兵战车,也就是现在的CV-90 战车,并在此基础上发展自行高炮、装甲人员输送车、装甲指挥车、自行迫击炮等多种变型车,形成 CV90 履带式装甲战车族。作为一种通用化的履带式装甲底盘,瑞典的 CV-90 将平台的通用性发挥到了极致。当然,这只是表象,而真正驱动设计人员这么做的,是成本以及效率。

如果分别研制装甲车、坦克、迫击炮的底盘,可能要花费好几倍的时间,这中间包括设计、 制造、 测试、 定型、批量生产各个步骤,除了时间之外,由于车辆底盘完全不同,那么生产线所需要的模具,零件势必也不同,这一切都需要重新建立,这将会增加很多额外的成本来组织新的生产线。这还没完,等这些不同的装甲车底盘制造出来之后,他们还需要后期维护和保养、零备件、保养工具、人员培训种种开销,由此可见,通用的底盘和个性化的底盘成本差异之大。

而为了达到不同的功能也就是所谓的个性化,底盘上允许个性的炮塔,炮塔可以是滑膛炮,也可以是双管迫击炮,还可以是遥控机枪等等。这样,既满足了炮塔对于功能的个性化的需求,又通过装甲底盘的通用化,极大的节约了成本。

cv-90 履带式底盘所衍生出来的各种型号(图片来源于网络)

另一个经典的案例是美国的 F-35 战斗机, 21 世纪初美国空军需要一款高性能的多用途战斗机, 而美国海军则需要一款能够在航母上起降的飞机。 因为航母滑跑距离短, 但是可以使用弹射器弹射起飞, 并且因为甲板的尺寸有限要求飞机能够短距离降落,而对美国的海军陆战队来说,需求又不太一样。因为美国的海军陆战队没有航母(航母属于海军,海军陆战队只有两栖攻击舰),而两栖攻击舰的甲板非常狭窄, 因此需要一款能够垂直起降的飞机。 

三个军种的需求都是替换上一代战斗机,对潜在的对手形成空中优势,但问题是,随着战斗机的技术含量越来越高, 成本也节节攀升, 美国的国防预算并不能支撑为三个军种同时研制和装备三种不同的战斗机。

很显然,不同的战斗机生产线,装配所需要的夹具,制造零部件所需要的模具,机器生产所编写的软件程序等等,都需要大量的资金投入,这还不算前期的研发费用,要节约成本,只能够尽可能的通用化。因此,在 F-35原型的机体上衍生 F-35A、F-35B、F-35C 三种型号,其中 A 给空军,B 给海军陆战队,C 给海军。

但是他们都使用同一种机体平台,绝大部分零部件都能够通用,A 型实现大航程,B 型实现垂直起降,C 型实现弹射起飞,同时满足三军的需求。 这样不仅仅是研发成本、 生产成本极大的降低, 后期维护成本也降低很多,当然,这也极大的考验了设计师的智慧以及项目经理的管理能力。

F-35 战斗机家族(图片来源于网络)

实际上,CV-90 履带式底盘和 F-35 机体就是平台,而不同的型号就是差异性,不仅仅是飞机坦克, 通过平台复用来节约成本的规律, 在软件工程领域同样适用。软件的架构设计与开发同样也会受到研发成本、 测试成本、 维护成本等诸多成本因素的制约。这里通过一个非常典型的复杂业务 case—电商交易系统,尝试着去剖析软件工程领域的通用化平台化的原始述求, 以及这中间所面临的各种纠结与困境。

电商交易系统的痛点与难点

一开始的交易,链路十分的清晰和简单,买家选择商品,填写购买数量,写好收货地址之后,便可以创建订单,然后进行支付。随着卖家的发货以及买家的确认收货,一次实物交易的正向履行就完成了。当然,也会存在有逆向的流程,商品与描述不符,买家不满意,需要能够退款。

而在早期,整个电商可能是同一个系统,交易是其中的一个功能,其他功能还包括会员、商品、卖家、类目、导购等等, 这些业务代码可能都耦合在同一个应用中, 互相之间通过本地调用来实现交互。这便是第一代的平台,而这个平台的域是整个电商。

臃肿的巨无霸系统

分布式与SOA 架构

随着业务的发展、功能的扩充、系统的迭代,整个应用变得越来越臃肿,内部的模块很难在其他新构建的垂直应用中复用, 所有的基础服务都需要重新搭建,成本很高,而这大部分工作属于重复建设。对于程序员这样的高智商群体来说,自然十分鄙视这种重复的低水平工作,因此,有人提出用分布式以及 SOA 的思路来解决这个问题。

服务化之后的系统架构

SOA 架构将原本互相耦合的系统功能抽取出来,拆分成一个个独立的小系统,比如会员、商品、店铺、类目、交易等等,每个小团队负责一个小系统,而这些小系统是可复用的,一定程度上避免了重复建设,随之衍生的服务治理、监控等等的一整套分布式的运维和管理系统,为分布式系统的稳定运行保驾护航。

另外,一些相对来说非常独立又非常基础的问题,被抽取出来成为中间件。而这里面所提到的一个个独立的小系统,就是域的进一步细化,这里称之为第二代平台。集中式架构到分布式架构的演变历程,网上相关的介绍文章已经汗牛充栋,这里就不详细介绍,之所以改变的原因,是由于第一代平台已经无法继续很好的支持业务的多样性。

演进式发展的问题

问题也在演进式的发展,原来不是主要矛盾的矛盾,现在演变成为主要矛盾。这个矛盾是什么呢?举个例子,当初设计的电商交易流程, 可能只能够支持标准的实物类交易模型,如果需求没有发生变化,这套系统可以很好运行,但这显然是不可能的。

电商的商品和交易类型实际上比超市货架要复杂的多,随着品类的扩充,新的交易类型不断的衍生出来。除了实物交易,还会有话费充值、游戏点卡充值这样需要输入充值账号的虚拟场景的交易,还有旅游酒店这样需要指定消费日期的交易,还有家具家电这种需要上门安装的交易,还有拍卖、还有众筹、还有彩票、还有教学课程,还有理财产品等等,甚至还有个人服务,比如上门打扫卫生、指甲美容、水电维修等等,业务类型五花八门,种类繁多。

本质上来讲,这些都是交易,整个过程就是买家付款给卖家,并享受所购买的商品或者服务的过程,而交易的订单,就是买家与卖家所达成的契约。因此,至少在优惠及价格的计算,加入购物车合并付款,已买到的商品查看,已卖出的订单列表,售后维权等等这些流程大部分都是一致的。

但是他们的区别也是非常大的,实物交易需要收货地址,需要物流,而虚拟类的商品比如网游点卡、话费充值,这些商品都不需要收货地址, 也不需要物流,但是他们需要充值的账号或者手机号码,需要对接充值商家。

又或者是,对于机票酒店这一类的交易来说,他们需要预约出行或者入住时间,需要对接票务系统出票,或者是对于生活服务这种场景,线上选择或者购买了服务,需要到线下去消费,比如美容美甲、洗衣、婚纱摄影,或者是预约上门服务,比如家政服务、数码家电维修。林林总总,这些都是差异化的需求。系统的变化永远赶不上业务的变化,那怎么办呢?编程语言提供了这种灵活性,if…else 语句。

If…else 真是个伟大的发明,面对层出不穷的需求,只需要来个 if…else 就能够解决问题。但是慢慢的,你就会发现,if…else 越来越多,整个团队,包括这个系统最初的设计者,都没有办法完全说清楚这个系统到底具备哪些能力,有哪些功能,所有的东西都沉淀在代码的 if…else 里。 

随着时间的推移, 部门有人离职、有人转岗,原先的系统维护者不再维护这个系统,业务开始交接,而业务的节奏本身是非常快的,然后又经年累月,很多信息都是口口相传,这在互联网公司也很常见。

记得多年前流行一句话「源码面前,了无秘密”」,这句话用来鼓励新人确实没错,但是,经过几个团队好几年甚至是十几年的轮换,系统已经变得庞大无比,新来的人要把这些 if…else 都搞清楚,估计也得个一年半载吧。

这段时间里面,新的需求还是会源源不断的过来,以前留下的业务逻辑和规则,随着市场环境的改变, 也会产生一些新的变化,但是,这一切对于用户来说,都是透明的,原先的功能不能不可用,还要更好用,还得帮助用户解决新的问题,对于接手的开发者来说,所面临的压力并不仅仅是维护,还有创新。

当然,你会觉得奇怪,为何所有人都前赴后继的将业务绑到同一条船上呢,自己再开发一套不就好了么?这是个好问题,很多人也是这么干的,不过,你可能忘记了 SOA,忘记了在交易系统的周边,还有一揽子关联系统和团队,上下游密切配合,来完成招商、导购、下单、买家、卖家、商品、类目等等一系列动作,在一家大型电商企业里, 这些系统也是经年累月的发展, 并且积累了丰富的业务逻辑和规则。

所以,作为架构师的你,想单独搞一套,不是不可以,首先成本将会成为横亘在你面前的一座大山, 你需要将这些周边的配套系统一一都建起来,想想得投入多少人力、多少时间,即便老板给你足够的资源,说不定等你开发完,商业环境已经发生翻天覆地的变化。大公司就是这样变慢的,如果不解决这些问题,是很难追赶轻装上阵的创新企业的,历史上已经太多这样的案例了。

是时候进行系统重构了

人穷则思变,既然遇到瓶颈,那就需要用新的思路对现有系统进行重构。重构最基本的目的,还是要解决平台化和个性化之间的差异所导致的矛盾。要解决这个矛盾,就需要对系统进行重新抽象。抽象的过程需要说明白一个问题,就是这个系统要解决哪些域的问题。

因为随着业务的不断发展,原先定义的域已经被极大的泛化和扩展,解决这些域的问题又分别要用到哪些能力,而这些能力,哪些能够进行扩展,哪些不能扩展, 如何进行扩展,另外一个问题是,不同的业务,该以什么样的流程组织这些能力以及进行扩展,而系统又该如何理解这些能力。

交易系统要解决一个什么样的问题呢,简单来说就是交易的履行,包括正向以及逆向流程,但是,它已经被泛化到要支持所有的行业。订单是这中间的一条主线,它是一张契约,所有的履行环节,包括下单、付款、发货、确认收货等等关键节点,都是以订单作为凭证,来进行状态流转,那么,这张契约包含了什么信息呢?

  • 首先,应该包含所购买的商品,即所购买的实体。

  • 然后,价格和购买数量,契约的另外两个核心要素,而价格实际上又会关联到另外两个概念,那就是优惠和资产。

  • 优惠,就是卖家为了吸引用户所设置的一系列活动,比如满多少金额可以减免一部分金额,能否使用优惠券等等。

  • 资产,用户的资产可能包括下单可能用到积分、红包,以及用户账户内的资金等等。

  • 收货地址,购买的商品该被邮寄到什么地方以及邮寄给谁,当然,虚拟类的商品是没有收货地址的。

  • 买家和卖家,谁买了这个商品,这个商品是由谁出售的。

当然, 还有很多很多, 并且, 不同的交易类型, 所需要填充的数据还不一样。

订单的创建是这中间最重要的一环,是整个流程的起始节点,包括了非常多的重要信息的置入。订单的创建可以划分为多个子域,比如优惠、 库存、 价格、 支付、交付等等,每个域都需要使用到一些能力,来完成这个域的功能。举例来说,库存域会涉及到查询库存的能力,会涉及到减库存的能力,并且,这些能力要是可以自定义或者是可扩展的。

就拿减库存来说,可以是下单减库存,也可以是付款减库存,一部分业务要求下单减库存,另外一部分业务,可能需要付款减库存,能否实现差异化, 如何来实现不同业务间的差异化, 成为了新的交易系统架构中,最为关键的一环。

不同的业务,有可能会对同一个能力进行扩展,哪个扩展该执行,哪个扩展不该执行,甚至是谁先谁后,而同一个业务,可能需要对多个扩展点进行扩展,那么一个流程中,哪些能力使用扩展点,哪些能力使用默认,如果这些信息使用 if…else 来管理,无疑又进入了死胡同,因此肯定是需要有一个统一的元数据管理中心来进行管理。

域通过域服务来暴露自己的能力,举例来说,订单创建的过程中,可能会要用到优惠域、库存域、价格域、交付域等等这些子域的能力,就需要调用到这些域的服务,那么,哪个域先调用,哪个域后调用,调用哪些服务,不同的业务又不太相同,这就需要提供流程定义的可扩展性,让每一个业务都能够自定义流程。

经过这样的抽丝剥茧以及对业务流程的抽象分析和实施,第三代平台诞生了,一个看似完美的可扩展的系统,所有问题都解决了,平台从各个角度来看,都是可扩展的,而具体的业务方来实施功能扩展,平台化和业务多样性二者兼顾,那么事实真如此的 easy 么?

事情没有想的那么简单

实际上远没有那么简单, 程序员都会热衷于设计复杂的系统,来体现架构和设计能力,就像很多设计师喜欢雄伟宏大的建筑一样。但是,系统变得复杂的代价就是,理解和熟悉成本成指数级别提升。特别是在刚刚开始的阶段,基础设施还不太完善,很有可能的结果是,做业务的工程师不懂系统的架构设计,做系统架构的工程师没法完整的理解业务的内涵。

导致的问题是,接入和扩展的成本非常高,怨声载道,而系统为了兼容更多的业务类型,又多了很多 if…else,又或者是为了避免可能的风险或者减少回归测试的工作量,同一段代码多处 copy。

要保持扩展的可持续性,开闭原则非常非常重要。「对扩展开放,对修改封闭」,这句话说起来容易,但真真正正做到则非常非常的困难。总会有各种各样的原因来诱导你打破最初系统架构设计之初所设计的约束规范,如何合理的做出抉择,并且让这些变更符合开闭原则,考验着每一个架构师的智慧。

大型系统中要想让架构理念得到很好的诠释以及执行,让代码按到统一的架构风格有序编排,除了制定条条框框的约束和规范之外,还得要从架构层面让这些约束被完整的执行,这些工作需要一套可以扩展的框架来完成。随着代码的规模的扩大,如何让这些代码对于后面接手的人来讲是可以理解的,也非常的重要。程序员并不喜欢写文档,在时间不是那么充裕的情况下就更是如此,因此,如果能够在架构设计阶段就进行考虑将会大有帮助。

另外,业务与业务之间,业务与平台之间,代码与部署是否能够隔离。如果不能很好的隔离,那么就会存在一个误区,做业务的人不了解系统整体的架构,做系统的人无法熟悉所有业务。

这样的结果是,对原有的功能进行的扩展会变得非常谨慎,细微的变更都需要大量的回归,甚至会发现,重复的代码被反复拷贝,这将会极大的增加后期的维护成本。不同的团队在同一个代码分支上开发、部署、测试,效率会变得十分的低下。代码冲突的解决,环境的不稳定,问题的调试,bug 的定位,都会变得异常艰难,大量的时间将浪费在协调上。

世界上的很多大型工程在设计和建造的过程中,都会受到“尺度效应”的支配。

所谓的“尺度效应”是指当尺寸放大之后,构件的承力面积与线性尺寸成平方增加,而构件的重量却随尺寸成立方放大,因此,尺寸加大到一定程度,结构中的应力将急剧随尺寸加大而提高。「尺度效应」

尺度效应在分布式架构下的大型的软件工程领域同样适用,随着业务域的扩展以及工程规模的扩大,代码的规模会越来越大,管理和维护成本越来越高。在 all in one 的集中式应用架构时代,通过 SOA 的架构升级解决了代码管理与团队合作间的难题,那么在深度 SOA 的分布式时代,如何来解决巨无霸应用的工程维护难题呢, 将业务拆分为更小更细的微服务?

如今在微服务 docker 化的思潮之下,发布一个服务的成本越来越低,系统的独立性越来越强,但系统间的远程调用会越来越多,所需要的机器规模越来越大,机器成本不是线性而是指数级别增长。

这对于本来基数就很大的集群来说,成本非常之高,并且系统的性能随之也会急剧下降,对用户体验来说几乎已经到了无法接受的程度,保障数据的一致性也越来越困难, 这些难题都让微服务看起来像一条走不通的路。

那么,就没有其他办法了么,其实也不是,借用容器的概念,或许能给这一问题的解决带来了曙光。基础域的能力作为底层容器下沉,业务的流程和功能基于基础域容器来扩展,业务与平台代码完全分离,由不同的团队维护和升级,并且容器和业务升级完全独立,从而避免多个团队多个业务同时合并分支、解决冲突、测试并修改 bug、发布上线的工程难题,服务的调用还是本地调用,避免网络开销所带来的成本增长和性能损失。

容器化部署的构图

新的架构是否能够很好的解决当下的痛点, 可能要运行过很长时间之后才能够验证,但是,要完成这样的改造,可能会面临一些当下的难题。企业的存在都是为了追逐利润,而重构是不会带来任何利润的,因此,能否争取到足够的资源来投入,这便到了考验企业决策者智慧和远见的时候了。

另外,重构老系统的同时,新的业务也不能停止, 有人将这个过程比喻成给一架在天上飞行的喷气式飞机换发动机,这个比喻再贴切不过了:既要对现有的功能进行优化和更新,同时要将老的业务迁移到新的架构体系中,与此同时,还需要开发新功能。

对老系统的更新需要及时的同步到新的系统,这本身是一个渐进又漫长的过程,显然,这个过程中,双倍的开发工作会导致效率的降低,并且,新的系统是否能够完全兼容老的业务,也需要不断的试错和磨合,这个过程是痛苦的。

也许终于有一天,新老系统的过渡完成了,原先的主要矛盾解决了。但是,整个系统的架构,代码的整洁度,可能并不如预期那样,还是会有很多的不完美和瑕疵。另外,还有一个可能是,新架构引入的同时,可能也引入了新的问题,比如性能和吞吐能力的下降,平台容器版本碎片化,容器与业务的难以分离,更多的分布式事务,更多的限制等等。

写在最后

最终,还是回到原点:到底是先解决业务需求系统演进式发展,还是先设计平台来套业务,实际上平台就是预先定义型架构的一个典型案例。平台并不是原罪,但是,开发一个好的平台,难点在哪呢?关键还是看能否更好更简单的解决当下的问题和需求。

预先定义式的架构,由于前期的输入不足,后期可能会难以适应需求的变化,而演进式的架构,由于架构的不确定性,随着系统的规模增大,后期会比较难以维护。不要指望当下的设计的系统,能够满足未来不可知的需求,那既然未来的需求不确定, 那就不为未来设计了么?

当然也不是,我们要为可以预见的未来设计,为未来预留扩展点,除此之外,还要尽量去保障扩展点与当前系统是解耦的,否则等于是封闭的。在系统的更新换代的过程中,技术洞察力是非常重要的,通过技术的更新换代也许能够根本性的改变你的做事方式。 最后想说的是,平台化和业务多样性的矛盾实际上一直都是存在,只不过在某个阶段可能暂时达到了平衡,所以不要期待有一劳永逸的解决方案。

大型软件架构的平台化 VS 业务多样性,如何取舍?

发表评论

电子邮件地址不会被公开。 必填项已用*标注