TMC (Transparent Multilevel Cache) 在通用“分布式缓存解决方案(如 CodisProxy + Redis )”基础上,增加了以下功能:
- 应用层热点探测
- 应用层本地缓存
- 应用层缓存命中统计
以帮助应用层解决缓存使用过程中出现的热点访问问题
使用有赞服务的电商商家数量和类型很多, 商家会不定期做一些“商品秒杀”、“商品推广”活动, 导致“营销活动”、“商品详情”、“交易下单”等链路应用出现 缓存热点访问 的情况:
- 活动时间、活动类型、活动商品之类的信息不可预期,导致 缓存热点访问 情况不可提前预知;
- 缓存热点访问 出现期间,应用层少数热点访问 key 产生大量缓存访问请求:冲击分布式缓存系统,大量 占据内网带宽,最终影响应用层系统稳定性;
为了应对以上问题,需要一个能够 自动发现热点 并将 热点缓存访问请求前置在应用层本地缓存 的解决方案, 这就是 TMC 产生的原因。
TMC 架构
业务定位
整体流程
老的上报链路
旧架构强依赖 etcd 和 kafka
- etcd write 1w/s, 击穿时 tmc 产生的 变更事件远超这个量级;
- kafka 链路长,延迟较高且潜在风险较高。
新的上报通道
主要功能:热点上报、缓存失效广播、热点下发、服务依赖
主要接接口
- NotifierService#Report 供 sdk 上报新的热 key
- NotifierService#ExpireHotKeys 供 sdk 推送 key 过期通知
- NotifierService#Publish, 供 volcano publish 热 key
syntax="proto3";
package=v1;
service NotifierService {
rpc ExpireHotKeys (ExpireHotKeysRequest) returns (ExpireHotKeysResponse) {
}
rpc Report (ReportRequest) returns (ReportResponse) {
}
rpc Publish (PublishRequest) returns (PublishResponse) {
}
}
message ExpireHotKeysRequest {
repeated string appNames=1;
repeated bytes hotKeys=2;
string groupName=3;
}
message ExpireHotKeysResponse {
int32 code=1;
string msg=2;
}
message ReportRequest {
repeated ReportData reportData = 1;
string groupName = 2;
}
message ReportData {
string appName = 1;
string key = 2;
int32 accessTimes = 3;
int32 redisHitTimes = 4;
int32 localCacheHitTimes = 5;
int32 localCacheLossTimes = 6;
int32 version = 7;
int64 sendTime = 8;
}
message ReportResponse {
int32 code = 1;
string msg = 2;
}
message PublishRequest {
string groupName = 1;
repeated string appNames = 2;
repeated HotKeyChangeModel changeModels = 3;
int64 findAt = 4;
enum EventType {
NULL = 0;
DELETE = 1;
UPDATE = 2;
EXPIRE = 3;
FIND = 4;
}
EventType eventType = 5;
}
message HotKeyChangeModel {
string hotKey = 1;
int64 expireTime = 2;
}
message PublishResponse {
int32 code = 1;
string msg = 2;
}
// code 0: 正确
// code 400: 参数不对
- 按 group+key 维度聚合
- 跨注册中心的需求,采用 notifier 互调实现
- 对于 kvproxy 的 hotkey error,触发立即下发热点 key 的逻辑
notifier 可能会存在内存泄露问题
1、双缓冲队列:通过覆盖写的方式,只能解决突发流量洪峰,丢弃 limit 以外的流量;
2、下游 sweep(Tether 广播或者上报 volcano-server)性能受限时,内存中的 message 会产生积压,造成泄漏;
使用 LRU 缓存来替换抖动的数据映射来解决
热点计算
- 数据收集: 收集 Volcano-SDK 上报的 key 访问事件;
- 热度滑窗: 对 App 的每个 Key ,维护一个时间轮,记录基于当前时刻滑窗的访问热度;
- 热度汇聚: 对 App 的所有 Key ,以<key, 热度>的形式进行 热度排序汇总;
- 热点探测: 对 App ,从 热 Key 排序汇总 结果中选出 TopN 的热点 Key ,推送给 volcano-SDK;
volcano-sdk
对于 redis 的 Jedis 客户端中封装已有的 get、set、expire、delete 操作,做热点的发现统计,以及本地缓存变更通知。
volcano-server
对于收集的数据,进行滑动窗口计算,汇总,热点推送到各机器上
关于缓存击穿的处理
如果没有处理好并发问题, 业务方很容易出现, 在缓存数据过期时, 大量相同的请求回源到底层的 DB 读取数据, 导致大量无效回源请求, 增加 DB 的压力, 在大流量的情况下可能会导致 DB 崩溃, 从而导致雪崩。 对于这种情况, 我们在 redis-zan 框架中封装了一个简易接口来避免这种情况。
使用 redis-zan 的业务方, 可以通过封装的接口完成缓存击穿的简化处理, 缓存失效自动 DB 回源更新缓存的使用示例:
public void testStringGet() throws ExecutionException {
String key = "youzan:test:db_callback";
Supplier<String> supplier = () -> {
// 处理读取db的数据, 返回对应db的值
return ret;
};
// 此方法会从kv读取key的值, 如果不存在, 调用supplier回调函数从db中回源, 然后自动更新到kv, 并设置指定的ttl过期时间.
// 此方法会自动处理缓存失效时多个并发回源问题, 保证只有第一个线程的调用会回源db并更新kv. 后面的线程会排队等待第一个线程返回.
String value = redisClient.opsForString().get(key, 5, supplier);
}
注意, 此方式可以单独使用, 不需要开启 TMC 本地缓存功能也可以使用。
类似的, 对于读取业务本身就不存在的数据, 出现的缓存穿透情况, 业务可以设置一个业务认为不存在的标记, 来避免不存在的数据一直请求 DB。
KV 增强本地缓存 LRUs
默认的热点缓存一般只用于少量的热点数据缓存(几百以内的 key), tmc-kv 新功能新增支持本地 LRU 能力, 可以近实时的拦截热点(理论上可以立即缓存热点读请求), 也可以缓存更多的数据到本地(几十万 key), 并且通过淘汰算法自动驱逐非热点数据, 不过由于缓存的 key 更多了, 因此有可能会扩大数据不一致的影响范围(开启更新通知广播可以通过更新广播减少不一致窗口), 适合数据修改不频繁的业务场景, 需要的业务方可以联系开发手动开启本地 LRU 增强功能。
RPC 缓存
最近新增了一项 RPC 缓存功能, 来提供更加快捷的本地缓存使用方式, 可以直接缓存 RPC 的响应结果, 业务方不需要关注缓存细节。 通过 RPC 缓存,业务可以:
- 提升请求的处理速度,提升业务的吞吐量。从本地缓存中获取响应结果,性能远高于向后端发起 RPC 请求。
- 降低后端应用负载,节省成本。本地缓存可以有效的降低后端应用处理的 RPC 请求量(RPS,requests per second),进而降低后端应用的负载,节省成本。
- 流量削峰,提升应用稳定性。缓存命中率在秒杀等瞬间洪水流量场景下会显著提升,从而避免洪水流量瞬间冲击后端服务。
通过开启缓存配置,即可零开发零成本的享受到 RPC 缓存带来的收益。对于不需要针对 kv 具体缓存数据做读取操作的应用,或者需要 kv 之外数据缓存的业务, 可以使用 RPC 缓存更加简单的提升性能。