购物车面临的挑战:
1)新业务:随着业务形态的丰富,购物车在不断支持各种新业务,依赖的外部接口也随之增加;
2)下沉:一些前端调用的接口下沉到购物车中台;
3)前置:结算流程很多业务前置到购物车中,如优惠券、京豆;
4)扩容:为改善用户体验购物车可容纳的商品数量在不断增长;
这些导致购物车依赖的 RPC 接口数量及分页调用次数都在不断增加。购物车作为交易流程开端,本身流量较大,在业务复杂化的背景下,如何提高性能保证用户体验,成为购物车面临的较大挑战。
通过增加服务器资源虽然能在一定程度上解决问题,但会带来较大的成本开销,也与工匠精神相悖。能否通过技术手段提升性能呢?通过分析,异步化改造成为解决这一问题的有效手段。
1)不同 RPC 并行
购物车依赖接口达几十个,各接口间存在复杂依赖关系。必须先梳理各接口间依赖,识别哪些可以并行。然后将原有代码拆分为两部分:RPC 异步请求和结果处理,按照依赖关系,让 RPC 最大限度并行执行,减少在结果处理阶段异步响应等待时间,从而达到提升性能的目的。
2)批量接口多分页并行
购物车依赖接口多为批量接口,且单次调用有数据量限制,需将数据拆分为多个分页调用。那么多个分页间也可以并行,改造中封装了异步分页工具,使业务层对分页逻辑无感知,异步工具自动将超过接口上限的数据拆分为多个分页并行调用,提升单接口响应速度。
3)底层采用 JSF 异步调用
异步调用基于京东 RPC 框架 JSF,推荐使用 1.7.5 以后版本,支持 CompletableFuture。
异步化改造的总体方案并不复杂,但是在实际落地过程中,遇到了很多细节问题:
1)异常重试需精细化
同步调用时,如果超时会重新调用。改为异步后重试会失效,因为在调用时一般不会报错,需要在结果处理阶段获取异步响应超时后,再进行重试。
另外,多分页并行时,当某一页请求超时后,应该只重试出错的分页。底层对分页调用进行了封装,上层业务代码在获取数据时无法感知是哪一页超时,所以必须在异步调用时将现场信息保存在包装类中,一起返回给业务层,在 Get 数据超时后,单独重试出错的分页。
发生异常时,并不是所有情况都需要重试,当遇到限流等异常时,不能进行重试。底层工具需要自动过滤限流异常,当然也支持自定义规则。
2)异步 RPC 监控更复杂
底层 RPC 耗时监控需要拆分为两部分,在分页调用时记为开始时间,在异步结果到达后,记为结束时间。如果调用异常或 Get 超时,需要标记本次调用失败。对于重试同样需要记录调用耗时,且正常调用与重试调用需分开记录。
除了需要监控 RPC 耗时外,还需要监控结果处理阶段 Get 等待时长,这个时间才是真正对应用性能有影响的时间。由于底层是分页调用,所以业务调用次数和底层 RPC 调用次数并不相同。
3)分页异步结果不能合并,否则无法获取异常 Provider 信息
底层异步调用结果,必须通过包装类原样返回给上层,除了上边提到的需要单分页重试外,另一个原因是必须保留异步结果,在分页超时后才能输出超时的 Provider 信息。这是由于 Provider 信息依赖 JSF 框架的 JSFCompletableFuture,如果在底层合并结果,会导致信息丢失。
4)每页超时时间需单独控制
分页调用过程如上图所示,在结果处理时,每页 Get 超时时间需要单独控制,因为获取结果是顺序进行,获取后边的分页时,前边分页等待的时间也应计算在内,以保证整个获取结果的时间不超过单个分页的最大超时时间。计算公式如下:
超时=RPC 超时时间 > (当前时间 - 异步调用开始时间) ? RPC 超时时间 – (当前时间 - 异步调用开始时间) : 0
5)分页均衡
为避免最后一页数据过少造成数据倾斜,需要将请求数据均分到每一页,以最大限度提高整个请求的性能。
改造完成后购物车核心接口耗时减少 30%,保证用户体验,节省大量服务器资源。后续增加新的 RPC 接口时,只要处在调用拓扑的非关键路径上,对购物车性能没有太大影响。另外,容量增加时除少数不能分页调用的接口外,对性能影响已经比较小。
作者:京东零售 王利辉 梁奉龙
内容来源:京东云开发者社区