淘宝购物车扩容与性能优化(上):https://developer.aliyun.com/article/1443492
网络包传输
▐ 包大小对网络rt影响分析
在当今的网络环境中,上下行带宽往往是不对称的,这种不一致性在移动网络中也很常见,网络运营商为了更有效地利用有限的无线频谱资源,往往会分配更多的带宽给下行链路,以提供更好的媒体消费体验。考虑到这种上下行带宽的不一致性,上行包的优化往往比下行包的优化收益更大。因为下行通常有更多的可用带宽,即使存在一些性能瓶颈,用户也可能不会立即感受到明显的性能下降。而上行链路由于其固有的带宽限制,任何性能的微小提升都能给用户带来明显的体验改善。优化上行数据包,例如通过减少发送的数据量、实现数据包的压缩、优化传输协议以及应用智能队列管理,能够有效降低延迟、提高上传速度,从而在带宽受限的情况下尽量减少阻塞和等待时间。
上行包优化收益略大于下行包优化收益。
▐ 购物车状态协议缓存
回到购物车的场景,上行包最直观的膨胀点就在于请接口求中params字段:
这一份长长的params里包含了,页面组件,页面状态等多个信息,其中页面组件是客户端渲染强依赖数据,而状态数据是服务端业务逻辑依赖数据。
状态数据同时存在于上下行协议中,且客户端不依赖其做逻辑处理,所以理论上这份数据可以不作为协议下发,并且在服务端的“历史长河”中,由于缺乏对该信息的管控,状态协议数据可以随意的追加,导致了这份大数据包逐渐成为上下行网络包中的一份包袱。
状态协议数据同时存在于上行与下行协议中,且客户端不依赖。
为了达到和客户端解耦的效果,最直观的方法就是将这份数据存到缓存中。然而在结合交易流量和现有计算资源做一个简单估算后,我们发现事情并没有那么简单。结合淘宝购物车的峰值流量来测算,整体产生的带宽量级会达到数十GB,在当前的规格资源下根本不够用。
- 压缩与缓存
前文也提到了我们的终极目标是把服务端依赖数据放到缓存中,完全与客户端解耦。经过结合业务逻辑的状态协议裁剪,包大小已经有所降低,带宽瓶颈与可用性问题确实得到了缓解。那么在将这份数据存到缓存之前,还有没有可以继续“压榨”的空间呢?答案是当然是有。
base64 和 压缩算法:在原有的模式下,状态协议通过客户端和服务端直接传输来做保持,故需要通过使用base64编码确保数据能正确地存储和传输,而不是通过原始的二进制格式。这种方式会带来大约33%的数据膨胀,在确定了状态数据往缓存中存储的优化方向后,状态数据可以不再做base64编码,而转为直接存字节数组。
- Base64编码原理:Base64编码将每组3个字节(共24位)的原始数据划分为4个单元,每个单元6位。由于每6位只能表示64种状态(2^6 = 64),Base64编码选择了一个64字符的集合来表示这些状态。这个字符集通常包括大写和小写英文字母(A-Z, a-z)、数字(0-9)、加号(+)和斜杠(/)。实际编码时,Base64处理器会查找这个字符集,将每个6位的值映射为相应的字符。
- 数据膨胀:考虑到大多数字符编码(如ASCII,UTF-8)中,一个字符通常占用8位(1字节)。在Base64编码中,每4个字符用来表示原始数据的3个字节。这意味着每个原始字节在编码后占用了8 * 4 / 3 = 10.666...位。换言之,原始数据的大小会增加大约1/3(33%)。
我们同时也测试了原始长度为36KB(未压缩状态数据)在各个压缩算法下的表现,虽然部分算法(例如Brotli)能实现更高的压缩比,但是综合解压和压缩时间来看,gzip仍然是最好的选型。
购物车状态缓存模式:经过上述优化后,状态数据的大小在网络传输包能进一步得到缩减,从而减小带宽和存储上的压力。状态数据在客户端与服务端的交互方式也由原先网络传输的模式,改为客户端解耦的服务端缓存模式。客户端上行数据中不再持有状态数据,业务状态的计算完全交付服务端,若出现缓存超时或异常则通知客户端降级为网络传输(原有模式)。
▐ 流式Api
经过上述的优化后,我们发现有一个遗留的“坑”点,为了能保证异常情况下用户操作能兜底上传一份状态数据,下行包中仍然保留了全量的状态协议。此时就需要我们在保证兜底能力的情况下,把下行包中的状态协议也进行剔除。
- 流式api介绍
参考业界流式API模型,说法各不相同,主要是以下三种:
- request streaming: 多个上行对应一个下行;
- response streaming: 一个上行对应多个下行;
- bidirectional streaming: 端云双向流式服务;
流式API是实现 “response streaming”,即一个上行多个下行,请求模型如下:
- 引入流式后
通过流式api的引入,下行包拆成了主包与副包,状态协议在副包中,其余数据保留在主包。客户端在收到主包后即可执行渲染逻辑,等效于状态数据在网络传输中的耗时,不再影响用户端到端体验耗时。
服务端接口
▐ 接口耗时分析
服务端耗时占比:从用户的操作数据来看,大部分用户操作商品数量较少,这些情况下服务端的性能表现尚可,这意味着服务端耗时对购物车这部分用户的影响不大。但是随着商品数量的增加,服务端耗时在端到端耗时中占比不断攀升,这意味着服务端性能对购物车的深度用户影响较大。
商品数据较少时:服务端优化点在于下发数据量,在网络包优化中已经得到了解决
商品数据较大时:服务端优化点在于降低耗时
▐ 并行化改造
购物车的服务端逻辑一大特点是平均商品数较大,其中对于商品数的处理在历史上多使用简单的foreach直接遍历做计算。针对解决过多foreach逻辑最有效的方法自然是做并行化处理。购物车的流程基于业务特性将各个独立域活动进行串联,流程简化图如下:
结合业务回头来看整个流程,会发现全串行流程中低效的点:
- 部分节点之间并没有严格的顺序关系,例如查服务数据在注入商品标签后即可执行,且服务数据当前只在视图层构建有用。
- 互不依赖的下游服务例如库存查询、营销计算、查服务数据是低效的串行查询。
- 购物车平均商品数过大的特点,导致和商品数相关的节点内部做foreach循环非常低效,典型的有例如注入商品标签、视图层构建。
找到问题后,我们最终采用基于ForkJoinPool的方式实现购物车并行化改造:
ForkJoinPool主要针对那些可以拆分成多个子任务的大任务进行优化。它的核心技术原理是基于“工作窃取”算法,每个工作线程都有自己的任务队列,当线程完成自己队列里的任务后,它可以从其他线程的队列尾部窃取任务来继续工作,这样可以保持所有工作线程的高效运行,避免了线程闲置。ForkJoinPool 通过 ForkJoinTask 的子类如 RecursiveAction 和 RecursiveTask 来支持任务的分割(fork)和结果的合并(join)。
这种面向多子任务且支持并行化的方式,就非常适用于购物车场景中的多商品处理,例如:
- 并行处理多商品任务:购物车的一大特点就是平均商品数多,每个商品标签注入、单个视图分组构建等操作都可以作为一个子任务独立执行。
- 任务可拆分性:购物车中的每个商品处理逻辑都是独立的,这使得任务可以很方便地被拆分成小任务并行执行。
- 提高响应性能:使用 ForkJoinPool 可以在多核处理器上同时执行多个商品的处理任务,从而减少总体的处理时间,提高系统的响应速度。
- 动态任务调度:"工作窃取"算法能够在运行时动态地重新平衡任务负载,如果某些商品的处理耗时较长,ForkJoinPool 能够确保其他线程不会闲置,而是帮助处理剩余的工作。
结合ForkJoinPool的方式做并行优化后的购物车流程简化图如下:
- 注入商品标签和视图层构建,从商品维度拆分子任务,使用并行化。
- 将部分仅视图层且互不依赖的下游数据节点做聚合,内部做并行查询。
团队介绍
看到这里相信诸位能够感受到购物车技术体系的复杂度和深度,笔者所在的团队是淘天集团交易前链路技术团队(购物车&下单),在这里,你能够不断被各种商业模式烧脑,也能够不断被各种新兴技术锤炼,更能收获一群志同道合的战友。值此变革关键时期,也急需有能力和有梦想的你一起参与:
1. 负责淘宝购物车、下单等面向全民用户的C端产品演进和迭代,每一次需求每一行代码都能创造巨大的商业价值。
2. 支撑集团16N组织下形态各异电商的购物车、下单平台应用的维护(buy2 carts2),在这里见证如何针对形态各异的电商进行架构抽象出电商内核,并通过高度灵活的开放扩展机制解决业务的差异性。
3. 操刀完整的端到端协议的设计、演进和优化(奥创),见证移动时代在客户端不发版的情况下,如何既能高效满足产品需求迭代,又能获得native一样优异的消费者体验。
4. 全程保障每一次大促流量洪峰背后的业务安全和稳定性,全力促成持续的平台架构演进,确保用户每一次购物车浏览,每一次下单能够丝滑顺畅。
如果您有兴趣,可以点击下面的链接或者通过邮箱bonan.hbn@alibaba-inc.com与我们联系和交流,期待您的加入。https://talent.taotian.com/off-campus/position-detail?lang=zh&positionId=1089401