聊一聊方案中心性能优化中做的缓存设计
本篇文章主要是对方案性能优化2.0中,所做的缓存设计的过程、方案、结果做一个总结。
一、前言
二、优化2.0面临什么问题
2.1 1.0优化做了什么
-
降低CPU资源消耗
-
规则引擎表达式预先编译并且缓存 -
减少大对象深度复制,快递场景可以完全不复制 -
避免通过JSON序列化,再反序列化实现创建对象并且赋值 -
底层元数据查询方法,避免使用第三方封装的校验框架 -
日志打印优化,debug添加isDebug判断
-
tair使用优化
-
mget替代tair get方法使用,降低网络资源消耗
2.2 计费流程的核心问题--嵌套循环查询
-
匹配运力线方案 1<=n<=10,查DB or localCache -
计算物流方案 1<=n<=?,视服务商报价而定,如有多条方案,则计算多条。 -
计算销售价/计算成本价,分别计算两个报价。 -
匹配报价版本,匹配当前生效的报价版本。查DB 或 tair -
匹配费用项报价1<=n<=30+,视不同运力线而定。查DB 或 localCache
以上只是简化流程,依然有很深的嵌套循环。
2.3 计费数据涉及模型简述
2.4 当前缓存模型
2.4.1 Tair缓存
-
为什么缓存?要获取 当前报价配置,从DB查询获取,查询涉及表较多,不做缓存,那肯定顶不住。
-
为什么用Tair?现在看来,是因为获取 当前报价配置 模型的查询复杂性,通过分布式缓存,尽可能降级查询DB次数。
优点:
-
使用Tair缓存当前报价配置,批量读取的方式,可以一定程度缓解DB查询压力
-
目前Tair缓存模型设计,没有把最核心、查询量级最大的方案和报价进行缓存,没解决真正痛点;
-
在计费过程中仍需要根据每个方案+费用项构造相应的缓存key,需要费用项多多情况下,仍然需要多次查询tair;
2.4.2 本地缓存
-
实现简单,不用对数据做新的聚合设计,调mapper接口级别缓存。于前期临时、快速解决性能问题的本地缓存方案;
-
大面积、细粒度使用本地缓存,集群机器本地缓存数据还不一致,易造成客户体验割裂问题(测试有时候都搞不清是bug还是缓存)
-
粒度太细,计费流程与数据存储层的交互还是嵌套分散在深层次循环流程的内部,当缓存失效,依然会有大量DB查询(特别是循环嵌套最深的报价查询)
-
不太能支持水平扩容(尝试过,DB扛不住)
-
缓存数据无法预热,面对大流量场景,程序重启易出现成功率下跌(优化前每次发布基本都会发生)
三、新的缓存
3.1 为什么需要新的缓存设计
3.2 对询价计费流程的重新定义
-
前置费率匹配:小二启用运力线新报价的时候,通过精卫监听报价表变更,为每个物流方案提前匹配费用项报价,并且匹配结果保存在清洗出来的产品线路费率表中。客户侧查价时,根据from-to线路信息即可获取到该线路所有的费用项的报价,不需要在运行时逐个费用项取匹配费率,大幅减少查价运行时匹配报价的tair请求以及逻辑运算。
-
减少DB访问:配置型数据通过方案中心本地缓存框架访问获取,数据量大的费率模型数据,从tair缓存获取。通过此方案,可以大幅减少DB访问。
-
减少网络开销:在费用计算前组装好计费所需的数据上下文,通过批量tair查询读取费率,避免在核心流程循环访问获取费率数据,减少RPC请求次数。
图3.2-3 报价缓存失效后的查询高峰
3.3 缓存模型
3.3.1 Tair缓存模型
Tair缓存模型,这里指的是方案和报价的缓存。这里的Tair缓存模型的设计,需要满足两个关键点:可批量查和高速查。贴合业务场景来进行设计,提高匹配方案报价的效率以及命中率。
图3.3-1 Tair缓存模型设计
prefixGets
之前考虑的使用mget可能会受限于key分片问题导致查询缓存性能不稳定。对于方案/报价查询,改用prefixPut,prefixGets进行批量缓存存取,一个主key,多个子key的情况下,以获得更好的批量读取性能。通过prefixGets,可以高效批量获取一次查价中多个sku的方案的缓存数据 或者 报价的缓存数据。
prefixPut
prefixPut发生在查询不到报价、或者方案的时候,进行惰性加载
缓存key设计
-
主key:对于方案/报价,都可以用SPU维度区分主key,如上图所示。 -
方案查询子key:
-
对于国际快递,匹配线路方案的核心条件,只有一个destinationCountry,以destinationCountry作为缓存key,可以缓存每次对应目的国的方案查询结果。 -
同时在查方案的时候,已经指定了sku_id和生效的报价version_id,因此设计查询方案缓存key由 sku_id+version_id+destinationCountry组成。如上图所示。另外需要注意使用version_id作为缓存key一部分,可以对数据做较长时间的缓存,避免了频繁失效要重新查询方案数据,并且将Tair缓存的实时性控制,转移为对version_id的实时性控制。
-
报价查询子key:
-
查价流程,先根据destinationCountry查询方案缓存, 得到方案线路列表,一条线路信息包含【destinationCountry、warehouseCode】。匹配报价的查询条件是warehouseCode + destinationCountry,相似地,设计查询报价的缓存key由 sku_id+version_id+destinationCountry+warehouseCode组成。 -
为什么不直接用destinationCountry?报价数据对象相对而言比较大,受限于业务场景与value大小限制,缓存粒度拆分相对需要更小. -
同样地,可以对数据做较长时间的缓存,避免了频繁失效要重新查询方案数据,并且将Tair缓存的实时性控制,转移为对version_id的实时性控制。
value设计
-
方案value:没啥特殊的,结构比较简单
图3.3-2 方案缓存模型value
-
报价value:结构类似方案,核心是多了报价信息result【JSON结构数据】
图3.3-3 报价缓存模型value
-
报价记录value结构
3.3.2 本地缓存模型
询价计费时依赖的配置型数据,分成3大类,按照sku、resource、spu做聚合缓存。
key设计
key以 类型+ 大类的主键构成:sku+sku_id, resource+resource_id, spu+spu_id
value设计
图3.3-4 本地缓存模型value
通过这样的聚合模型设计,询价过程中,通过用skuID可以在本地缓存中检索到任意想要的服务表达定义相关的数据。另外这里缓存的实时性和写逻辑控制,在后面展开。
聚合模型每个子属性更新,需要更新整个模型的数据,这里为什么考虑要做聚合,而不采用每个表的数据都单独一个key缓存的实现方式呢?
-
每个表的单独key,缓存结构零散,需要管理更多的Key。 -
每个表的单独key,缓存结构零散,在读取缓存的业务层,不同业务场景诉求下,需要实现比较复杂的关联组装逻辑。 -
配置数据并不多变,少更新,聚合模型更新读取DB的次数有限,较少。
综上,相关的配置数据聚合管理的好处大于缺点。
四、缓存读写
4.1 本地缓存
4.1.1 缓存预热
本地缓存预热,程序启动时,根据程序内置逻辑定义的本地缓存Key集合,提前加载缓存到应用内存,保证提供服务时,缓存已经加载。
4.1.2 缓存更新
简单来说就是通过监听精卫,借助广播能力,通知集群更新本地缓存。这里的缓存是一直存在于堆内存,不会失效,只会广播刷新。每次刷新缓存,按照图3.3-4 本地缓存模型value描述的聚合模型,每次更新最小粒度为一个ConfigDTO。
4.1.3 解决了什么
-
解决应用服务器本地缓存方案缓存实时性问题,实现应用服务器集群本地缓存方案的准实时刷新。 -
通过广播数据实体变更,触发本地缓存刷新,解决应用服务器集群多节点本地缓存不一致的问题。例如之前经常出现,因为本地缓存问题,查询方案多次不一致的问题。 -
数据启动时预热,解决了之前每次发布,程序重启都会出现服务成功率下跌的问题。 -
对于已缓存数据,在数据使用的业务流程中,可完全屏蔽数据库查询,对水平扩容友好。可以解决扩容时,DB瓶颈问题。
4.2 tair缓存
查询没啥输的,按照 tair缓存模型设计 的key-value,进行prefixGets,prefixPut。需要注意的是,key设计的粒度、报价value大小限制。
4.2.1 缓存预热
这里需要做预热的场景,基本就只有新运力线上线了,一般日常还是没问题的。新运力上线,目前要求是分批灰度,等Tair缓存命中率上去了,继续开启灰度,这是比较保守的做法。
4.1.2 缓存更新
前面有提到,Tair缓存数据的实时性控制,是依靠version_id的实时性控制,方案或者报价的version_id通过本地缓存准实时更新,能够保证version_id的准实时性,从而保证每次查询Tair缓存数据的实时正确性。因为每次获取到的version_id是最新的,拼接出来的Key自然也是查询最新的缓存的Key
4.3 缓存读写总结
图4.3-1 缓存读写总结
-
配置型数据:稳定,量不大,查价计费时需要经常读取的数据。例如运力线配置、运力线资源、报价版本、费用项、运力资源关联关系等。 -
方案报价型数据:量大,无法本地缓存,具备版本特性,可以长时间存储在tair。
五、缓存脏数据处理
5.1 本地缓存
尽管精卫很强大,但也不是100%保证没有意外,为避免脏数据产生,因此会采用定时任务刷新的方式来定时更新本地缓存。
5.2 tair缓存
前面的设计有提到,目前的方案/报价缓存子key,是带版本号的,只要版本号正确,就不存在缓存脏数据的问题,而版本号数据实时性,依赖于本方案中的本地缓存实现,二者相互结合,保证查询Tair缓存数据的正确性。另外使用版本号作为缓存key还可以对数据做较长时间的缓存,避免了频繁失效要重新查询报价数据。
六、单点资源瓶颈
6.1 Tair瓶颈
对整个应用集群来说,支撑更大的流量,绕不开单点资源瓶颈,水平扩容更加绕不开单点资源瓶颈。不巧,最近在接入更大的流量场景的时候,就遇到Tair瓶颈问题
图6.1-1 切流Tair出现瓶颈监控视图
图6.1-2 缓存穿透后DB的QPS视图
可以看到出现大量的Tair限流,解决处理方向有几种,简单说一下
方向1
很简单,如果是原本Tair的限流阈值很低,那么可以申请扩容。需要注意的是,申请扩容的容量评估,需要结合我们查询缓存方式来评估,鹰眼上看的仅仅是对Tair发起RPC请求的统计,服务端限流统计是按照真正的key个数统计的。例如使用到prefixGet,那么就按Skey个数统计。
方向2
如果扩容不能满足,那么就需要回到代码中,看看有没有什么不必要的Tair查询,进行优化。
方向3
针对热点Key做一层本地缓存,如果应用服务器的热点本地缓存中包含key,那么就不需要查询Tair了,可以直接返回结果,降低对Tair的压力。热点key的识别可依赖Tair内嵌的LocalCache功能,或者我们自己实现,动态配置热点Key。
方向4
使用RDB。对持久化有需求,并且缓存QPS确实很高,如果当前使用的是LDB,那么可以考虑使用RDB,LDB成本比较高,没那么多资源。RDB成本相对较低,可以有更多资源。
方案中心怎么做
这次遇到Tair瓶颈,方案中心是先从简单的方向1、2入手。
首先申请扩容,一开始评估预计QPS,按照的鹰眼平台展示的来估,因为方案中心使用了prefixGet,因此估少了,扩容完成后发现还是限流。
无奈,但也没继续申请扩容,而是到业务、代码中,分析可以减少的查询Tair的点。
七、总结
7.1 数据
通过本地缓存配置型数据 + tair缓存方案报价型数据的组合,缓存命中的场景下,查价计费链路已经可以实现无DB查询。目前线上稳定支持水平扩容,按照压测数据预估,单机支持180QPS,单集群50台机器支撑9000+qps
结合新的缓存组合,代码路径实现调整如下:
-
获取N个运力线方案版本+报价版本:查询本地缓存
-
批量获取N个运力线方案:查询tair or DB
-
批量获取N个方案的报价:查询tair or DB
作者|申怀
本文来自博客园,作者:古道轻风,转载请注明原文链接:https://www.cnblogs.com/88223100/p/Talk-about-cache-design-in-performance-optimization-of-the-solution-center.html