这次介绍的主要内容包括5个点:
1、首先,我们需要看一下就是去年9月我刚到蘑菇街时,我所看到的下单系统。当时这个系统所面临的一个问题是什么?面对这些问题我们应该做如何的应对方案。
2、其次,我们为什么要做系统拆分,整个服务化过程是怎么做的。我们抽取的数据模型、服务梳理、系统架构是如何做的。
3、下单系统独立过程中,对蘑菇街快速的业务发展,如何进行对应的支撑。如何实现下单真正的平台化,快速支持业务和需求。
4、独立化前后,我们在日常的问题排查、性能优化中所做的变化。
5、总结,新阶段下单平台的能力。
在做下单服务化之前,蘑菇街整体还是以PHP为主的。随着蘑菇街每年业务成倍数的发展,系统面临的问题越来越严重,挑战越来越大。主要体现在三个方面:
系统上:这个时候的蘑菇街应用称为主站。主站是一个单体的应用。蘑菇街所有的业务场景代码基本都在这个PHP应用中,大小2G+的应用。这个时候,几百号人同时修改着这个应用,每周周一到周四下午是发布时间点。我记得,每次发布的时候,各个应用模块的人,都把代码提交到主干,然后上百人修改的发布就这么浩浩荡荡的往上面发布了。
这个时候,最常出现的问题就是:A:某个同学发布把另外⼀一个同学的代码带上线了,因为他们在同时修改同一个文件。B:发布过程中,在测试机时(线上beta环境)发现了问题,这个时候,所有人的代码都回滚回来,重新发布。在这个时候,各个应用模块之间的耦合严重,稳定性也相当差。很容易出现线上故障。由于这样复杂的相互依赖关系,导致整体性能很低,无法满足大促需求。同时,出现线上问题时,排查起来相当低效。
业务上:没有一个抽象的业务模型,各个业务都是对应的业务PD提需求过来,然后开发评估工作量和可行性,然后对应这个业务流程就开始开发。没有把所有业务流程好好的梳理出来(其实也努力过,梳理过程中,也没有对应的下单能力支撑,最终以失败告终)。另外,业务的边界很不清楚,比如,资金的同学需要对某种业务做红包或者支付减免,这个时候,就需要我们对这个业务做对应的判断,然后新增加一个字段传递给资金,告诉他们这个业务类型。扩展上,也是一样的问题。
总结种种因数,我们得到一个结论:拆。把下单系统从主站中拆出来,做为一个独立的java服务化应用。
刚刚分析了,拆分是势在必行。那么我们要做应用独立,要服务化,遇到的第一个问题就是:现有服务依赖问题。这么多的依赖,这么多的入口:PC、H5、App、快抢、秒杀等,不同入口不同的业务,他们对各个服务的依赖也不一样,处理逻辑也不一样。另外,第二个问题就是,下单过程中涉及到的一个分布式事务问题。
这里是我在入职之初,删老代码过程中,整理出来的一个原有下单系统内部流程图。其中每个框(无论大小,大框代表核心节点,小框代表各种⼩小业务节点)代表一个功能或者业务节点。基本一个节点就是对应的一个服务依赖。
整个下单过程中的依赖有几十个,分属于各种业务,也包含了强弱依赖。
那么,对于这样一个流程,问题来了:我们怎么知道哪个业务走了哪个节点。对于每个节点对外的服务依赖是强依赖还是弱依赖,大促时我们应该如何降级。另外还有个很严重的问题,就是由于经历了多代码农的辛勤耕耘,相互之间代码理解不一致,加上没有通用的数据模型规范,有一些业务节点,分散在四处。
举个例子:校验这个节点,在入口的地方,在商品读取,拆单,价格计算,运费计算都有。并且,在新来业务时,如做预售,又要新增一个全局的util方法,供给各个接口使用。这样,校验节点不断累积重复。
总结原因,在服务化之初,做的第一项重要的任务就是把所有的服务进行梳理,下单过程中所有的功能节点进行抽取。合并相同的功能节点,拆分负责的综合节点。然后将下单过程中所有可能涉及的功能节点进行固化(对应提炼了功能节点新增标准和扩展标准)。对于同一类型的服务依赖,定义为一个功能节点。比如,计价节点:包括了促销优惠计价,普通优惠计价,多阶段计价等不同的实现。
如上图所示,下单的功能节点和对应的依赖抽象出来之后。就可以很轻松的总结出,下单过程中其实主要包括了三个要素:下单的静态数据(下单流程中所需要的所有数据),下单功能节点(抽象出来的对应节点)以及对应的订单数据模型(落入对应的DB数据,持久化)。
另外,抽象出来了这些功能节之后,我们对于强弱服务的依赖治理也变得相对简单很多。每个节点对应一个对应的功能,每个节点的具体实现,有对应实现该功能的服务。
比如,对于校验模块,我们可能有黑名单校验,有限购检验两个实现;我们在大促时,可以直接对黑名单校验进行对应开关推送,降级该服务以保障整个下单流程的稳定。
对于其他的服务同理,如果对应依赖的服务出现故障或者系统不稳,我们可以切换对应的节点开关,⾛走其他的备用链路。
刚刚在总结功能节点时,我们提炼出来了三大要素。针对这三大要素之后,我们在实现过程中发现,其实这样的功能节点实现时,就是一个模板+一个策略模式的选择器。
就 实现了整个下单流程。整体流程如上图1所示:用户请求进入时,首先根据下单的入参,进行对应的外部信息查询,拼装好对应的下单数据模型(下单流程上下文, 见图2中:orderSDTO);拼装好数据之后,开始执⾏行整个模板对应的功能节点,获取到最终的订单模型;最后将订单模型写入DB生成订单。
我 们可以看到,每个功能节点实现方式,以及执行过程完全一致,如图3中的举例计价功能节点的选择实现过程。这些功能节点都是:通过一定的 condition,然后选择对应的一个或者多个provider,然后顺序执行(invoke)。同时,在退款,在购物车等其他应用中,都有类似的一种 场景需求;所以,对应这样的功能节点实现以及选择过程,抽象出来了一个基础框架:SPI框架。这个框架做了以下几件事情:
1:抽象出了对应的SpiBase类银SpiProvider类,所以具体的功能节点实现继承者两个基础的类。框架将所有的实现类实现集中管理到ProviderFactory中。
2:各个节点针对不同的业务场景实现,通过xml配置对应的condition。不同的业务场景,执行不同的功能节点实现。
3:对功能节点提供互斥(只能选择一个实现类执行)、组合(选用对个实现类执行)的选择器功能(SpiSelector)。
通过下单的功能节点管理过程抽象获取到的SPI框架,同时也建立了对应的SPI标准,也是对应的功能节点管理的一个标准:
SPI粒度:对应功能节点的抽取,与依赖管理的功能节点一一映射的关系。
SPI互斥和组合:每个功能点执行的时候,不是说简单的只能执行一个,也不是说顺序执行等。而是可以建立对应的互斥组合关系。比如:校验,可以多个实现都执行;计算价格,只能选择⼀一种计算价格方式(优惠计价/多阶段价格/其他系统自定义价格/等)
SPI与共建:由于有了SPI,对于某些业务方需求,可以开权限让业务方自己实现对应的某个SPI节点的特殊业务实现。举例:快抢业务,下单过程对于超时功能节点的需求,不是通用节点的30分钟或者24小时;于是让快抢业务方对于这个SPI新写了一个provider(15分钟),执行的condition为:当快抢业务时,该SPI选择新写的provider。这样业务方自己写代码,我们负责review,然后发布上线,实现平台共建。
SPI隔离:不同的SPI实现隔离标准,目前是通过condition条件隔离。
在服务化过程中,遇到的第二个问题是之前的分布式事务实现方式。
PHP时代,采用的是手动catch,然后人工回滚的方式。比如:减库存时,失败了。这个时候手动catch住,然后调用回库存和解锁券的接口。这种手动处理的方式会带来比较多的问题:首先,如果回库存接口或者回券接口异常,会导致券和库存不准问题。其次,如果回券接口卡了,会影响这个下单rt。
在服务化过程中,不再使用这样的分布式事务处理方式。通过消息来结构整个回滚过程,流程如上图所示。 如果在锁券或者减库存时,出现了异常,这个时候异步发出废单消息,库存和促销应用接对应的废单消息,即可以回滚对应的优惠券和库存。(库存和优惠券内部做幂等)
在服务化过程中,除了系统方面的问题外,还有很多业务支撑相关的问题。
首先,没有业务模型:所有的对各个垂直业务进行详细的梳理,每个垂直业务的特点、流程、下单过程的要求、依赖等。对于部分业务,如快抢、秒杀等都是单开接口,单独处理对应的业务流程。导致同时维护多个下单流程接口。
扩展性不好:下单内部,没有抽取一个可扩展的业务模型和数据模型,遇到新的业务流程时,⽐比如:新来一个预售,可能需要从下单入口开始,对应的PC、H5、App、秒杀、快抢等多个接口同时维护对应的校验和相关的处理。订单表也没有合适的字段来标记这个类型的订单,导致后续的订单列表,退款,物流等各个系统处理复杂。
业务边界:如试用有抽奖、魔豆商城也有抽奖,还有很多地方都有抽奖流程面对临时需求:只能通过if else,然后在整体的大的php数组中添加一个额外的字段,来标记这个临时需求,下单内部的上下文也不断增大。开发人员之间,也相互不是很
理解这么多的字段具体的含义。
为了支撑各类业务的发展,在服务化过程中,对所有的业务流程进行了抽象和整理。总结出来了20+种下单业务流程类型,如上图所示。每种业务流程,在下单内部映射为对应的流程标记,SPI以这些标记作为condition条件,执行不同的功能节点以及走对应的后续下单流程。
如上所述,我们需要根据不同的业务,把不同的外部流程转换为内部的标记,然后通过标记执行不同的功能节点实现。对于整个流程,对于外部入参,整理出来了六个维度的识别入口。
1:channel:渠道信息,不同的渠道,如快抢(channel_fastbuy)、秒杀(channel_seckkill),传不同的channel,通过标记转化规则转化为内部的entranceTag。
2:flowType:不同的业务流程类型,传不同的流程,如0元订单(flowType=ZERO)。通过标记转化规则转化为内部的itemProcessTag。
3:店铺:根据店铺信息获取对应的卖家信息,映射为对应的sellerTag。
4:用户、会员:通过用户接口,获取对应的买家信息,通过标记转化规则转化为内部的buyerTag。
5:商品:通过商品接口,获取对应的商品类型信息,通过标记转化规则转化为内部的itemTypeTag。
内部,根据6个Tag,决定对应的流程类型(processType),这也决定了下单内部的功能节点执行condition。
整合上面对业务流程的总结,我们整理出来了真个交易业务流程的一个规则(如上图所示):
根据业务的描述—-》对应的交易流程——》交易的内部环节——》下单内部选择哪些功能节点——》写⼊入不同的订单数据
结合上面对交易流程的总结,我们确定了新的下单流程的一个内部分层结构。
外部请求过来时,下单根据对应的外部数据源,经过标记隐射层,转换获取到对应的下单内部的六个标记和流程。然后进入到下单内部服务层,根据Tag执行SPI,写入对应的订单信息。
在这样的一个业务流程抽象标准之下,我们对实际的业务也进行了对应的PHP到新java应用的迁移。具体情况如上图所示。
图2中,绿色的部分为对原有通用节点有改动,蓝色部分为标示该业务对此节点新增的provider实现。
举例:在快抢这个业务接入的时候,我们对订单校验(添加只能一次购买一个商品校验),extra数据(添加快抢的活动Id,计入订单表的extra字段)两个节点的通用实现进行了扩充,对订单超时(超时时间修改为半小时)和减库存节点(新增减活动库存实现)进行了新增实现。
我们对于整个下单系统的服务治理、业务支撑、系统耦合等等问题都进行了对应分析和处理。改造完成之后对于我们具体有什么效果,或者说好处。那么可以从线上问题分析这一点直接看出来
1:线上问题排查:
php时代,耦合比较严重、职责不清:问题排查效率低(一般通过打日志跟踪)
服务化之后:入口收拢、逻辑内聚,只有一个入口接口,规范错误码,所有的服务节点和实现都有对应的错误码,大部分错误通过错误码一眼就能看到问题。例如:上右图
中所示,测试妹子在测试环境测试时,看到确认下单接口返回的错误码FAIL_BIZ_CALC_PROMOTION_INVOKE_PROMOTION_INIT_EXCEPTION 直接就去找促销的同学了,一眼就定位出来是促销异常。
同样,在对系统优化时,相对于服务化之前,也有了很多改观。
1:首先,服务化过程去除了很多冗余的依赖,这样使得下单的整体链路短了很多。
2:通过流程标记+SPI的方式,让不同业务能跳过不必要的功能节点,减少了对其他服务的压力。如:非海淘业务,跳过对关税税率节点的依赖。
3:新的系统下,做服务优化时,也变得简单易行:每个服务都是一个功能节点内部。整个下单系能优化的过程,其实主要是对下单各节点依赖的服务的优化过程。通过压测,压出各个节点的拐点,然后配合相应的服务提供方,优化接口。如缓存问题,多线程问题等。
4:对于下单系统内部的优化,也有很多成熟可行的方案。如:上图中,是最近,通过Tprofiler对下单应用的踩点分析结果。该结果中显示,在下单内部的一个util方法JSON转化过程中,平均耗时18ms,对于这样的实现,就可以进行对应的优化。
经过对服务依赖的治理,对业务流程的抽象,对整个下单流程的进行的独立和服务化之后,形成了现有的下单平台。
整个下单系统也从最初的综合主站应用(左图)—-》现有的下单分层架构(右图)
Q&A
提问一:蘑菇街这套下单系统发展了几年,为什么有这么严重的技术债务。或者说虽然历经过淘宝洗礼的黄裳等也必须经历再一次的洗礼?(黄裳->皇上,即蘑菇街创始人之一岳旭强。)
回答:其实不仅蘑菇街,很多初创公司,为了满足业务的发展,都会选择快速支持业务的PHP技术架构。这样在早期的业务快速扩展中,能够实现快速的迭代。这个也是为什么会出现到15年,蘑菇街业务高速发展时,技术上面会面临的一些挑战。
问题二:对于新搭建电商的创业团队,业务也是拓荒。老师在设计原则上能不能总结一些建议?
回答二:
首先,其实蘑菇街的业务发展,经历了6年的时间。在做服务化时,如我上面的介绍也说到,其实已经有一个大致的问题参考点在了。我们觉得主站比较冗杂,所以做拆分。我们觉得业务没有梳理好,所以,我们梳理了业务模型和流程等。
其次,对于设计过程中,除了技术架构的简单、分层的明确以外,业务参照上面,其实还是会考虑到未来至少2年的业务发展情况;同时还会参照其他电商公司的一些业务情况,进行分析。
问题三:新下单平台架构,DSL这一层 是用什么实现的,和普通的web服务有什么区别
问题三:其实就是我们内部的业务规则。就是我们目前对于业务接入时,有个业务规则映射过程,这个是抽象出来的一个规则引擎。目前,是通过配置Groovy脚本来实现的。与普通Web服务的区别在于,普通的Web服务只是针对具体的业务请求和提供对于的数据服务接口,而DSL这一层只是一个业务规则的层,不直接对外暴露服务的。
分享者简介
朱伟,来自蘑菇街电商基础平台,花名:逆天。本、硕均就读于于华中科技⼤大 学(俗称:关山⼝口男子职业技术学院)。毕业后进入阿里(花名:猪尾),主要做运费、物流相关的工作,15年9月进入蘑菇街后,主要负责下单、去支付相关 ⼯工作。
本文来自中生代技术交流群 微信公众号:freshmanTechnology