PART 01
业务背景
eBay为卖家提供了多种营销工具,专属优惠(Seller Initiated Offers,以下简称SIO)是卖家使用最广泛的工具之一。卖家可以在卖家中心、MyEbay或者Native APP上向对该商品的感兴趣买家(Interested Buyers)发送SIO。
多数卖家存在以下两个痛点:
卖家需要每隔几天都进行一次SIO操作才能触达到感兴趣的买家。
卖家有很多商品,对每一个商品都进行一次SIO操作显然过于繁琐。
针对以上痛点,我们的方案是支持批量发送SIO和自动发送SIO(Bulk SIO & Automation)。具体到AutoSIO,卖家只需在发送SIO的时候勾选“自动发送优惠(Automatically send offers)”,设置好优惠额度和细节,系统将会定期自动给感兴趣买家发送SIO。
所以Bulk SIO & Automation on Native项目的业务需求重点在于移动端Native APP对批量发送SIO和自动发送SIO进行赋能。
此外,eBay还支持商家优惠券(Coded COUPON,以下简称COUPON)。卖家可以在发送SIO的时候加上COUPON,在SIO之外鼓励买家去购买商家其他商品,从而提升营销效率。
综上,业务需求可以归纳为:
支持卖家在Android/IOS Native APP上批量勾选商品,向这些商品的感兴趣买家发送SIO;
支持卖家勾选AutoSIO,系统会向这些商品的感兴趣买家自动发SIO;
支持卖家通过发送COUPON进行长尾营销,从而系统会在SIO过期或者被接收后再向这些感兴趣买家发送COUPON;
将卖家的个体偏好持久化,且根据这些偏好进行相应的预填充。
图1: 卖家端用户体验(DWeb)
图2: 买家端用户体验(DWeb)
PART 02
SIO既有架构
图3: SIO既有架构
Experience Service是eBay自研的后端BFF模式(Backends For Frontends Pattern)技术方案。SIO现有架构遵循该方案。现有Experience Service无法支持业务需求。基于业务特点,我们要实现Native APP上的批量发送和自动发送主要有如下挑战。
Domain Service下游服务依赖复杂。SIO需要调用很多的domain service去获取商品信息、卖家偏好、商品配置、感兴趣的买家列表、买家资料等信息。依赖的服务数量多,链路复杂。
服务响应时间要求高。卖家在Native APP上可以一次勾选很多商品,每个商品可能有很多感兴趣的买家,加上下游服务依赖复杂,且发送SIO是同步操作,这对服务响应时间提出了很高的要求。
数据实时性要求高。由于业务特点,过期的数据如商品价格、商品库存、用户交易历史等有可能对卖家造成损失。
数据可靠性及服务可观测性。服务链路中任何一个环节的数据错误都可能对卖家造成损失。基于此,任何原因导致的异常数据,比如异常的折扣都需要被及时观测到,这样才能更快的进行排查降低损失。
渐进式发布和实验。由于业务特点,发布过程需要渐进式发布且支持AB实验。
PART 03
基于Federated GraphQL的架构选型
前文中提到,现有Experience Service并不能支持Bulk SIO & Automation on Native的业务需求。业界经验表明,直接让IOS和Andorid APPs去调用Domain Service的REST API是不合理的,大量重复冗余的服务集成且移动端开发人员需要对大量的下游依赖进行复杂的服务编排(orchestration)。此外这些REST API的返回会带来大量冗余字段。
所以,我们需要BFF这一层,去聚合出Android/IOS APPs实际需要的数据,从而对手机端开发人员屏蔽掉下游依赖的复杂性,让他们可以一个请求获取所有需要的数据。
01 / Experience Service方案
作为eBay内部目前使用最广泛的BFF解决方案,Experience Service将UI展示单元抽象成Module,然后在Experience Service这一层去调用所依赖的Domain Service服务,将Domain Service返回进行清洗和转换,拼接出展示单元Module所需要的元素,并对复杂的服务调用存在依赖的情况进行调用顺序编排,针对需要并发调用的服务进行并发调用。为了提升工程效率降低重复逻辑,Experience Service会承担不同客户端共同的Tracking、服务编排(Orchestration)、实验、L10N(国际化)、数据转换(Data Massaging)工作,避免不同客户端都得重复实现这些逻辑。
图4: Experience Service
Experience Service有效的解决了不同客户端都需要去进行复杂的Domain Service调用和编排,接收大量冗余字段,重复实现较多共有逻辑的问题。针对不同页面共享相同展示单元的情况,Module Provider和Module Fragment可以实现module的复用。
然而根据其架构特点,不同的业务领域都得开发和维护自己的Experience Service,在展示单元不一致的情况下对于数据转换,服务编排的复用能力大打折扣。并且,由于Experience Service是eBay内部解决方案,其学习曲线相对高,开发和维护过程中也很难从社区搜索解决方案。
02 / 基于GraphQL的方案
GraphQL是Meta发明并于2015年开源的一种查询语言。彼时移动端网络和移动设备性能比现在差不少,传统REST API overfetching获取过多冗余数据的问题对移动端用户体验造成了很大的影响。比如有时候客户端只需要展示商品名字和价格,基于REST的item service却会返回大量字段。GraphQL的出现有效的解决了这个问题,客户端可以指定需要哪些字段,服务端只返回所需字段。
GraphQL的操作类型主要包括:
Query: 查询
Mutation: 包括数据的创建、 更新、删除
Subscription: Subscription会让客户端和服务端建立双向连接,它是事件驱动的,当服务端监听到具体事件的时候会将具体结果发送给客户端
GraphQL服务端需要定义严格的schema,这样客户端开发人员可以根据schema选择自己想要查询的字段,设定query、mutation和subscription的细节。目前对于后端开发人员,spqr是spring应用常用的GraphQL框架,DGS是spring-boot应用常见的GraphQL框架,它们都是基于graphql-java构建,其最大的差别是一个是code-first,一个是schema-first。时至今日,GraphQL已经在业界广泛使用和落地,除了Meta之外,Netflix、Uber、Airbnb、美团、携程等企业都有成功使用经验。
GraphQL Monolith方案
传统REST API存在overfetching的问题,Experience Service存在复用性差的问题。基于此,最直观的方案就是创建一个GraphQL大单体(Monolith)应用,让这个大单体去集成复杂的下游Domain Service,完成服务调用的编排和查询结果的拼接。这样,任何客户端,任何使用场景都可以根据自己的需求去拼接出自己的query、mutation或者subscription。从而有效的解决了复用性差的问题。
图5: GraphQL Monolith
然而,开发并维护这个GraphQL大单体应用也是有很多局限性的。比如,如果这个大单体面向所有业务,维护这个GraphQL大单体的开发人员需要掌握所有业务的领域知识,大单体GraphQL Schema的设计和维护将会变得非常困难。如果这个大单体面向部分业务领域,GraphQL大单体跨业务领域调用Domain Service,或者客户端跨业务领域调用GraphQL Monolith都会显著增加开发维护成本。
Federated GraphQL方案
针对GraphQL Monolith的痛点,业内提出了Federated GraphQL这种新的模式,增加了Federated GraphQL这一个Gateway。将GraphQL Schema拆分成supergraph和subgraph。
Subgraph开发人员会定义自己领域Subgraph Schema,然后在统一的Schema Registry上进行注册,比如user团队定义user subgraph,item团队定义item subgraph,store团队定义store subgraph。这些所有的subgraph会注册在Schema Registry里并拼接成统一的SuperGraph。
当请求到达Federated GraphQL,Gateway根据schema情况完成以下工作:
找到请求所对应的字段都来自哪些subgraph,确定哪些subgraph有能力来resolve请求对应的字段。
生成查询计划(query plan),包括subgraph之间的依赖关系,根据有无依赖关系再去串行/并行进行subgraph服务调用。
拼接subgraph返回结果。
当subgraph收到来自Federated GraphQL Gateway请求的时候,会根据具体的业务逻辑触发data fetcher,data loader等模块,再去调用下游REST API或者查询数据库完成业务逻辑。
这一切对客户端开发人员都是无感的。客户端开发人员只需和Supergraph进行交互即可。Subgraph中定义的schema也因此做到了可组合,可复用。
图6: Federated GraphQL
这一套方案依赖于企业内对Federated GraphQL的schema有效的治理,以及后端domain service和GraphQL框架的有效集成。好在,eBay架构团队对Federated GraphQL提供了完善的schema管控,Gateway维护以及技术支持,eBay框架团队基于开源的DGS和spqr开发了组件,和eBay技术栈特别是logging、tracing、auth等进行了有效的集成,由于spqr欠缺集成Federation的能力,eBay内部组件也基于spqr额外构建了Federation能力。这为我们基于Federated GraphQL的方案奠定了技术基础。
03 / 架构选型
经过比较,我们的架构选型是基于Federated GraphQL的架构。其中:
User Subgraph已经存在
Item Subgraph已经存在,SIO业务需要扩展其Item Schema
需要开发新的SIO Subgraph和Seller Promotion Subgraph。这两个subgraph会和依赖的REST API及数据库集成,完成具体的query和mutation。
图7: Bulk SIO & Automation on Native架构
PART 04
工程实践
01 / Schema设计
关注点分离
如下图所示,商品的标题、图片、价格这些信息现有Item Subgraph都有提供。这些我们都不需要重复劳动去重新定义一遍。我们需要的只是去扩展Item数据模型,增加SIO相关信息,比如有多少个感兴趣买家,有效的折扣区间是什么。
对于我们扩展现有Schema的需求,Federated GraphQL提供了entity对象类型(object type),使得在不同的subgraph中resolve同一数据类型的不同字段成为现实。
图8: 预览SIO
如图所示,商品的图片,标题,当前价格都是商品的基本信息,Item Subgraph可以提供。
而其他的SIO相关扩展信息,比如感兴趣买家数量(number of interested buyers),有效SIO折扣区间等,这些都是SIO领域的信息,由SIO提供。Schema中使用entity的时候会强制要求定义好key,这样不同的subgraph中查询到的不同的字段可以有效的组合到一起。
所以,我们在SIO Subgraph中扩展了Item数据模型,提供了SIO相关字段,当来自客户端的商品查询请求中包括了SIO相关字段时,Federated GraphQL根据注册好的schema信息,会调用SIO Subgraph来查询这些字段,而商品标题等基本信息仍然由Item Subgraph提供。
通过这样的设计,从客户端的角度,item成为了一个大宽表,客户端无需关注大宽表下具体字段怎么实现怎么转发的,只需编写出当前场景需要查询的字段。从服务端的角度,完全做到了关注点分离(Separation of concern),item团队无需关注SIO相关的扩展字段,SIO团队也无需去重复定义商品基本字段,亦无需专门集成item service去获取商品基本字段。
02 / GraphQL框架集成
eBay目前后端Java,Kotlin等应用主要分两种类型:
基于Spring-boot,内部称为RaptorIO
基于Spring,内部称为Raptor
前文中曾提及,对于RaptorIO项目,其GraphQL框架主要基于Netflix开源的DGS,它的特点是schema-first,DGS的codegen module会根据schema生成相应的POJO。这样schema的编程实现可以更加高效,更多的专注于query、mutation、subscription对应的具体业务逻辑。
而Raptor项目是基于spqr框架,其特点是code-first,其设计初衷是无需重复编写schema,只需在POJO code的基础上加上GraphQLQuery、GraphQLType、GraphQLNonNull、 GraphQLInputField等注解,便可生成其schema。
项目中COUPON相关的数据包括coupon eligibility,coupon list都来自Seller Promotion Subgraph。该应用是一个Raptor应用,所以是基于spqr框架。其他SIO的query和mutation都由RaptorIO应用处理。
图9: 创建SIO
工程实践中经过对比,我们发现spqr相对于DGS有更繁琐的学习曲线。主要体现在,社区活跃度相对低,通过代码实现POJO来生成schema这个阶段验证成本高。为此,我们采用的策略是更早的引入单元测试来验证具体的schema细节,可以在开发阶段更快的验证生成的schema,更早的发现不符合预期的schema。
接下来便是GraphQL当中的其他模块,比如data fetcher,data loader等。框架层会基于反射调用相应的方法,在这些方法里和具体业务细节集成便可。DGS和spqr的共同点在于都是基于graphql-java构建,data loader都是基于graphql-java中的java-dataloader提供的能力,所以两个框架和底层业务逻辑的集成是类似的。
03 / 性能优化
响应时间是项目中的难点。原因在于:
单次操作可能包括很多个商品;
每个商品可能有很多感兴趣的买家;
业务细节依赖很多的下游服务,由于各种限制这些下游服务有的并未提供批量查询或者批量操作的接口。
基于这些特性,我们采用了以下策略来优化性能:
对于商品批量查询的场景,使用Batch Data Loader来解决GraphQL中著名的N+1问题;
对于冷启动问题,编写特定的warmup逻辑,在每次项目发布后触发warmup逻辑来保证GraphQL相关组件已经提前预热;
对于那些可以异步的下游服务调用异步处理,比如卖家个性化偏好的持久化;
针对包括大量商品的mutation操作,划分好任务的粒度。这些任务主要包括,给商品的感兴趣买家发送SIO,为商品配置AutoSIO。如果单个任务商品数量多,耗时久,会造成这个任务拖累整个请求的响应时间,如果划分任务数量太多,可能造成部分线程长时间处于等候状态。实践中我们根据产线上观测到的性能数据,选择了较为合理的单次任务商品数量。使得即使包含大量商品的mutation操作其响应时间也符合要求。
04 / 可观测性增强
SIO在卖家中使用广泛且对数据实时性准确性都高度敏感,所以我们需要通过加强可观测性,提升监控,报警和日志处理,做好异常状态下的兜底和快速响应。
我们主要关注:
整体请求数量
响应时间
SIO发送数量和成功率
配置AutoSIO请求数量和成功率
来自不同设备和APP版本的请求数量
折扣分布
商品类别分布
自动发送SIO数量
为了避免侵入业务逻辑提升开发效率,我们多数指标都通过Prometheus采集。
对于Prometheus不能实现的指标,我们借助Kafka、ES、Kibana方案来实现监控和报警。借助于ES的强大聚合能力创建细化指标,这些细化指标出现异常的时候也会触发alert从而开发人员可以在第一时间介入排查。
此外,对于重要场景,比如AutoSIO,我们也依赖于ES来实现日志聚合。
图10: 可观测性提升
PART 04
项目复盘
01 / 研发效能提升
相对于过去基于Experience Service的架构,基于Federated GraphQL的架构实现了研发效能上的提升。
具体体现在以下方面。
高效的前后端集成
现有模式将前后端集成变得更加的标准化,只需确定好Subgraph schema,后端开发人员便快速往mock service里提交mock response。在这个阶段Federated GraphQL Gateway会将来自客户端的请求转发到mock service获取mock response,客户端开发人员便可基于这些mock response进行客户端的开发。
等到后端开发人员完成了具体subgraph的开发测试工作后,便可将具体的subgraph接入Federated GraphQL。这样Federated GraphQL便会将请求转发到真实的后端服务。整个替换过程前端开发人员完全是无感的。
而原有模式则需要Experience Service,Domain Service,Native APPs三方较多的口头约定甚至大量的拉通对齐会议。
图11: 高效的前后端集成
降低架构复杂度
新架构下Federated GraphQL这一层无需处理业务细节,对于业务团队只需将subgraph schema接入Schema Registry加入supergraph就好。省掉了Experience Service大量的开发和维护工作。
02 / 短板和折衷
软件工程领域多数时候有优势的方案同时也会有折衷(tradeoff)。基于Federated GraphQL的方案提升了研发效能,降低了维护成本,然而和Experience Service对比却并非全无弊端,由于相对于各个业务领域独立开发维护的Experience Service,Federated GraphQL轻量级很多,但对于以下场景它也引入了新的挑战点。
PART 05
总结
以上是SIO业务基于Federated GraphQL开发Bulk SIO & Automation on Native项目的工程实践。项目实践中我们通过基于Federated GraphQL实现了项目的高效研发,在研发效能上实现了很大的提升。同时对它的短板也实现了折衷方案。项目总体实施成功,为eBay业务增长贡献了价值。
Reference
Experience Service: https://innovation.ebayinc.com/tech/engineering/experience-services-ebays-solution-to-multi-screen-application-development/
BFF Pattern: https://microservices.io/patterns/apigateway.html
Module Provider: https://innovation.ebayinc.com/tech/engineering/sharing-modules-across-experience-services-and-multi-screen-applications/
spqr: https://github.com/leangen/graphql-spqr
graphql-java: https://www.graphql-java.com/
dgs: https://netflix.github.io/dgs/
https://netflixtechblog.com/how-netflix-scales-its-api-with-graphql-federation-part-1-ae3557c187e2
https://netflixtechblog.com/how-netflix-scales-its-api-with-graphql-federation-part-2-bbe71aaec44a