真实需求中的接口性能优化-技术与产品方案的取舍

需求背景

在商品详情页面有一个banner位,最初产品形态为静态图片,为了增加吸引力,支持在banner上展示优惠券和商品,券信息和商品信息需要经过算法推荐

技术背景

投放系统

  1. banner需要运营同学在投放B端进行配置,然后存储到Mysql等关系型数据库中

  2. 投放系统等C端会通过定时任务将B端配置刷新到本地缓存+redis的分布式缓存中

商品详情场景服务

  1. 商品详情场景服务复杂提供接口返回渲染商品详情所需要的数据(价格、套餐信息、门店信息,以及banner等等)

  2. 商品详情具备很多的下游,需要进行RPC调用,多个下游一般是并行的进行调用,投放服务是商品详情场景服务的一个下游。

商品详情的接口响应时间是非常重要的,毕竟影响到用户的下单交易链路,如果商品详情渲染越慢,越会导致用户流失减少交易成交

如何实现投放商品和券的新需求

一个很朴素的想法如上,我们负责的投放系统C端可以并行的调用券投放服务和商品推荐服务,并且在商品推荐服务完成后再调用商品打包服务获取价格和图片信息(因为获取价格和图片前提是得到商品推荐服务返回的商品id,因此这里需要串行)

等待下游都ready并且返回后,我们再返回信息给商品详情推荐服务

投放系统的接口耗时=max(券推荐接口耗时,商品推荐接口耗时+商品打包接口耗时)+其他基础耗时

性能问题出现

商品推荐接口耗时高达300ms加上商品打包服务的时间,导致投放C端接口耗时接近400+ms

如果不进行优化会导致:商品详情场景服务调用投放C端接口超时,放弃等待响应接口,直接返回缺少banner的数据给前端------banner将无法进行展示

解决方案

3.1 朴素想法------加缓存

当接口速度慢的时候,很容易想法加缓存,用宝贵的内存换取更快的时间

当时在这个C端场景下------券和商品需要千人千面+用户的复访率不高

券和商品需要千人千面------缓存需要到用户纬度,也就是说用户id构成缓存key的一部分,并且推荐是动态变化的

用户的复访率不高------缓存命中率不高,用户一天很难多次访问

  1. 复访率不高么?

每一个商品展示的商品详情banner也是动态的,商品详情bannerA 和bannerB对应商品推荐底池也不一样

你可以认为缓存key需要是商品推荐底池底池id+用户id,因此"复访率"不高

  1. 推荐是动态的

假如用户短时间多次查看了瑞幸咖啡,那么banner上的推荐会感知用户当前行为,在底池中寻找相关商品,从而让banner推荐类似饮品

因此可以看到,这个场景下,缓存无用武之地,至少投放服务C端是无法针对商品推荐服务接口返回结果进行缓存的

3.2 朴素想法------服务内部并行

如上是我们目前设想出来的链路,其中我们能做到最大的并行程度是:券推荐和 商品推荐+商品价格图片获取进行并行

因此目前的耗时是max(券推荐接口耗时,商品推荐接口耗时+商品价格图片获取接口耗时)+其他基础耗时

其中商品推荐接口、商品价格图片获取接口调用有先后顺序依赖,必须获取了推荐的商品id后才能获取商品的价格+图片

似乎目前我们投放服务C端已经做到极限了,还能怎么进行优化呢?

商品图片价格获取和商品推荐并行

kaobei啊~,你不是说有先后依赖怎么并行

如果运营商品推荐底池只有少数个商品,比如50个,那么投放服务C端可以先调用商品打包服务对这50个商品进行价格和图片获取

与此同时,调用商品推荐接口,最终在内存中处理,只采纳推荐接口返回的商品就可以了

运营:看不起谁?我的招商都是10w级别的

因此这个方案不可行

push不了自己我还push不了商品推荐服务owner么

如上是算法的推荐流程,可以看到,算法推荐逻辑也是有先后顺序依赖的。因此基本上不太改变该流程进行并行优化接口速度

小陈:这样子啊,那你帮我先用粗排结果进行价格和图片的获取吧,然后接口加上价格和图片字段,这样我就不需要再次打包了,一定程度上进行了并行

了解到算法逻辑后,我想到:让算法先用粗排结果进行价格和图片的获取,并且和精排并行起来,这样就能一定程度上加快整个业务逻辑的执行了

算法:伟大的算法怎么能做接口仔的事情

但是算法同学拒绝了,原因:服务的职责不够单一,增加了算法服务的下游依赖,稳定性治理更为复杂

确实,我同意,单一职责不只是在类,在方法中要履行,在服务纬度也要遵守

3.3 变态想法------服务间并行

延续上面的商品推荐服务精排和商品价格图片获取并行的想法,算法同学提出:服务间并行。

具体来说是是这样:

调用算法接口,会先返回粗排结果,然后投放C端服务使用粗排的商品id调用商品打包服务获取价格和图片

与此同时算法服务会进行精排,精排结束后再通知投放C端服务精排结果

如果投放C端服务先一步完成,那么需要等待精排的通知,然后才能返回结果给商品详情场景服务

如何实现通知和等待

可以看到这里核心点在于通知和等待的实现

我简单的yy了一下,不引入其他复杂中间件的情况下

  1. 通知可以通过回调接口实现(k8s多实例部署的情况下,这意味着调用商品推荐服务接口的时候需要携带本地的ip和端口,然后商品推荐服务才可以调用到指定机器)

  2. 等待:在java中可以用阻塞队列实现,调用商品推荐接口的线程,可以先阻塞住,然后回调接口收到通知后再向阻塞队列写入数据,实现调用商品推荐接口的线程的唤醒

我果断拒绝这个方案

小陈:啊,这个方案实现了上线后,我每天晚上都睡不着觉

  1. 稳定性风险太大:如果没有合理的等待超时机制那么处理请求的线程一直阻塞,请求暴增的情况下会导致服务假死,流量一涨服务就一抖

  2. 链路复杂:涉及多次接口交互,实现复杂

我的提议

听了算法同学的提议后

我提出:要不你们直接把商品推荐拆分成粗排和精排两个接口吧,这样我就可以先调粗排,然后调用商品打包获取价格图片的同时调精排了

但是被无情拒绝,原因是工作量大

个人认为拆分成粗排+精排 远远远远远优于 回调通知+等待

你认为呢?

3.4 让产品方案和产品经理妥协

小陈:要不这个需求不做了doge

产品:???

如果一个问题解决成本很高,那么尝试绕过他

将banner拆分到次屏幕

其实这个banner是靠近商品详情底部的,那么为什么要跟着商品详情页面接口一起下发呢?为什么不等用户有下滑行为后再调用接口获取呢?

启发自笔者在淘宝工作的经验,那时候我们团队有如下优化手段

  1. 将页面拆分为多个区块,优先渲染可视区域(Above the Fold)

  2. 服务端渲染(SSR)​:服务器生成完整HTML,直接返回给浏览器,减少客户端渲染时间。

这个方案的优势在于

  1. 服务端改造小:基本上投放服务C端必须要任何改造,用原来的rpc接口构造一个http接口接口,经过一层api服务调用即可

  2. 服务端逻辑统一(和下面的懒加载方案比较)

劣势在于前端改造较大,实现较为复杂

懒加载

既然商品推荐太慢,那可以先展示banner图片,等用户下滑后再请求一次获取对应的商品推荐进行吧

思路类似于上面的拆分次屏,但是

  1. 对于前端来说可以先用banner图片撑开banner区域,不会出现下滑的途中突然挤出一个banner的情况

  2. 对于服务端来说,需要针对这种场景定制一条链路------获取推荐商品数据,代码逻辑分散,不够内聚

  3. 获取商品推荐接口可能失败,这导致会出现展示静态图片,而不展示推荐商品的情况!

  4. 服务端需要把第二次请求需要的一些参数,通过第一次请求的resp下发到前端,然后前端再请求一次

  5. 服务端要写大便,出现新的一条技术链路,后续迭代还需要考虑这条链路

如果选择了这个方案,那么如何把这个方案做的更好,更稳定呢?

  1. 第二次请求监控告警:理论上首次请求成功,那么要求第二次也成功的,虽然产品功能没有限制,但是本质上说这些数据应该一次性下发,因此针对第二次请求的成功率,耗时都应该进行监控

  2. 自适应降级:考虑极端情况下,算法推荐服务挂了,我们需要有一种机制让投放C端不下发这个需要算法推荐的banner(比如学习熔断机制,一个接口总是调用失败,那么直接快速失败------商品推荐一直失败,那么这个banner就不要下发了)