做电商大促、直播、榜单、秒杀、热搜这类业务的团队,几乎都会在某一刻撞上同一个现象:整体流量看起来还在容量之内,监控上的平均响应时间也说得过去,但某一个接口的超时率突然拉起来,缓存层的 CPU 曲线只有一根线顶到顶,其余节点全都闲着。运维的第一反应通常是"缓存没命中,加机器",加完之后问题照旧,甚至更糟。
这题的根因不在缓存命中率,而在请求分布。热 key 是访问量严重倾斜的结果:某一个或者极少数几个 key 的访问量远超均值,把压力全部压在一个点上。它不是缓存没命中,恰恰相反,热 key 通常是命中的,正因为每次都命中,才每次都要回到同一个地方去取。理解了这一点,后面的手段排序才有意义。
本文面向的是业务有明显峰值和爆款内容的团队,讨论五件事:热 key 到底压在哪一层、为什么加副本经常不管用、本地缓存为什么是正解但要付一致性代价、探测本身要花多少成本、以及哪些处置是不该做的。文中不给出具体的 QPS 数字和延迟数字,因为那些数字高度依赖业务形态,照搬没有意义;给出的是判断结构和取舍顺序。
这三种现象的表象非常接近:缓存层响应变慢、回源量上升、下游数据库压力陡增、接口超时率抬头。但它们的成因完全不同,处置手段也完全相反,误判就等于用错药。
缓存穿透指的是查询一个根本不存在的 key。每次请求都会穿过缓存打到数据库,缓存层形同虚设。判别特征是缓存命中率异常低,且数据库上的查询条件里出现大量不可能存在的键。它的处置方向是在缓存层挡住空结果、在入口做合法性校验和布隆过滤器一类的存在性判断。
缓存雪崩指的是大量 key 在同一时刻集体过期。缓存层瞬间失去作用,所有请求同时回源,数据库被瞬时流量打穿。判别特征是过期时间点高度集中,故障时点与某个批量写入或预热动作的时间点吻合。它的处置方向是打散过期时间,而不是加机器。
热 key 与前两者的判别差异非常明确:命中率不低,甚至很高;故障不是全链路的,而是集中在某一个 key 或某一小批 key 上;分片集群的监控会显示单个分片明显偏高,其他分片平稳。如果你的监控能看到分片维度的量,那么这张图基本能一眼定性。看不到分片维度的话,这一节本身就是第一个要补的能力。
还有一个容易混淆的场景是"大 key"。大 key 是单个 key 的体积过大,导致序列化、网络传输和内存分配上的开销集中;热 key 是访问次数集中。两者可以同时出现在同一个 key 上,处置手段也不同:大 key 要拆结构,热 key 要拆压力。先分清楚是哪一种,再决定动手方向。
这是整篇文章里最需要说清楚的一条几何关系。分片集群按 key 做哈希路由,一个 key 只会落到一个分片上。无论集群里挂了多少个节点,这个 key 的每一次读、每一次写,都会打到同一个分片的同一个主节点上。
于是就有了那个让人困惑的现象:集群整体 CPU 平均百分之二三十,内存也宽裕,但某一个分片的 CPU 已经到顶,客户端在这个分片上的排队开始拉长,超时开始出现。加机器几乎不改变这个局面——新加的节点承担的是别的 key,热 key 的那一份压力一个字节都没有被分走。
扩容在热 key 场景里失效,不是因为扩容这个动作错了,而是因为扩容的粒度(节点)和压力的粒度(key)不匹配。节点是横向的资源单位,key 是纵向的访问单位,两者之间隔着一层哈希路由。只要路由规则不变,热 key 的压力就锁死在它命中的那个分片上。
这也是为什么"把整个集群翻一倍"经常在压测里看起来有用、到了真实峰值时又失效。压测如果用的是均匀分布的 key 集合,扩容当然线性有效;真实流量的分布是倾斜的,而且倾斜程度会随着爆款内容的出现而剧烈变化。压测的 key 分布不还原真实倾斜度,压测结论就不可信。
顺着这条几何关系往下推,能得出一个直接的结论:要解决热 key,必须让"访问这个 key 的请求"不再全部走到同一个分片,或者让它们根本不走网络。前一条对应拆 key,后一条对应本地缓存。其他所有手段都是在缓解后果,而不是消除原因。
在动手之前必须先知道热的是哪个 key。这件事听起来简单,做起来有明确的成本,而且三条可选路径各有各的代价。
第一条是客户端统计。在缓存客户端里嵌入计数逻辑,对本进程访问过的 key 做频次统计,周期性上报。它的优势是准确——统计的就是真实发生的访问,且天然带有业务上下文,能直接定位到是哪个接口、哪个参数导致的。代价是侵入业务进程:客户端要改,内存要占,上报链路要建,而且每个进程只能看到自己那一份,全局视角需要在上报侧聚合。
第二条是代理侧或服务端统计。如果架构里已经有代理层,或者缓存服务端本身提供了热点识别能力,那么在流量汇聚点上做统计是最省事的:所有请求必然经过,不需要改业务代码,看到的也是全局视角。代价是这层统计依赖代理或服务端的实现,粒度未必能到单个 key,且一旦代理层成为瓶颈,统计本身会加剧问题。另外,服务端侧的统计通常只能看到命令层面,很难知道这条命令来自哪个业务场景。
第三条是旁路采样。不对每一次访问计数,而是按一定比例采样,用样本推断全局。它的优势是成本可控,可以做得很轻;代价是准确性依赖采样率,短时突发的热点可能刚好落在采样窗口之外,等发现时峰值已经过去了。
三条路径在准确性、侵入性、时效性上的权衡可以这样记:客户端统计准确但侵入重,代理侧统计全局但粒度粗,旁路采样轻量但滞后。实际工程里往往是组合使用——用旁路采样做常态巡检,用客户端统计在已知的高风险接口上做精确观测,用代理侧的监控做交叉验证。只依赖一条路径,出问题是迟早的事。
为什么不能对每一个 key 都精确计数?因为 key 的数量太大。一个中等规模的缓存集群里,key 的空间是千万级甚至更高的,为每一个 key 维护一个计数器,光是计数器本身的内存开销就已经和缓存数据本体在一个量级上,更别说还要维护哈希表的结构开销和更新时的写放大。
更麻烦的是,精确统计要求的是"每个 key 一个槽位",而热 key 问题的本质是"绝大多数槽位永远是零"。为了找出几个异常值,付出与全量数据规模成正比的内存,这笔账在海量 key 场景下算不过来。所以全量精确统计在实践中不可行,不是工程能力不够,而是代价结构不对。
概率数据结构就是为这个代价结构准备的。以 Count-Min Sketch 一类为例:它用一个二维计数数组和多组独立哈希函数,每个 key 映射到每组中的一列并累加。查询时取多组中的最小值作为估计。它的内存占用是固定的,与 key 的实际数量无关,只与你预设的精度和置信度有关。
它的特点很明确:只会高估,不会低估。因为哈希冲突只会让计数变大,不会变小。对于找热 key 这个用途,这个方向性反而是好事——宁可把一些不那么热的 key 误判为热,也不要漏掉真正的热 key。漏掉的代价是线上故障,误判的代价只是多做一些准备工作。
误差换来的是可控内存和常数级的更新时间。你需要接受的是:这个计数不是准确值,是估计值,不能拿来写进报表当精确指标用,只能用来做排序和阈值判定。把概率计数当精确值用,是这类方案落地时最常见的错误。
还有一层常被忽略的设计:统计窗口。热 key 是时间现象,一个在全天尺度上很普通的 key,可能在某几分钟内是绝对的热点。用长窗口统计会把短时热点平滑掉,用短窗口统计则会让排名抖动。窗口长度必须和业务的峰值形态对齐——秒杀的窗口和榜单的窗口显然不该是同一个长度。
窗口的实现上,滑动窗口比固定窗口更适合抓突发,但内存要多存几份;衰减计数(随时间对已有计数做衰减)则能让排名自然反映"最近"的热度,代价是绝对计数不再准确。如果目的只是排序取 Top N,衰减计数的效果通常更好;如果需要知道绝对值去算容量,那就不能衰减。
最后一个实践提醒:探测结果要能被下游自动消费。很多团队做了热 key 探测,但结果只是打在日志里等人看,等到人看见的时候峰值已经过了。把 Top N 的 key 列表输出到一个能被缓存访问层直接读取的地方,让本地缓存和拆 key 的规则能自动跟着列表走,探测才真正闭环。
拆 key 是最早被广泛使用的工程手段,思路直白:既然一个 key 只能落到一个分片,那就把它拆成多个 key,让它们按哈希散到不同分片上。具体做法是在原 key 后面加随机后缀或者序号后缀,读的时候随机选一个后缀去读,写的时候把所有后缀都写一遍。
它确实解决了那个几何问题。压力被均摊到多个分片上,单点的排队消失,而且这个方案对缓存服务端没有任何特殊要求,任何支持普通读写命令的集群都能做。
代价同样清晰。第一,写入端和读取端必须遵循同一套后缀规则,且规则一旦定了就不能轻易改——改规则意味着存量数据的迁移和双写,这是实打实的业务侵入。第二,读的时候是随机选后缀,所以无法保证每次读到的是最新的那一份,除非写入时严格同步写全部副本,而这又把写放大了 N 倍。第三,聚合查询变复杂:原本一个 key 能拿到的东西,现在要合并多个 key 的结果。第四,过期和剔除也变成 N 份,内存占用跟着涨。
所以拆 key 的适用面其实比较窄:它适合读极多、写极少、且数据结构简单(比如一个计数器、一段 JSON、一个状态标记)的场景。写多读少、或者需要强一致的 key,拆了之后的一致性成本会超过收益。
还有一点必须提醒:拆 key 的份数 N 是拍出来的。拍小了不够用,拍大了白白增加写放大和内存。N 的合理值应该来自实际测到的热度,而不是"先拆 10 份试试"。没有探测数据支撑的拆 key,本质上是在用内存和写放大换心理安慰。
本地缓存是热 key 这道题的正解,因为它从根本上改变了压力的落点:请求不再走网络去某个分片,而是从应用进程自己的内存里读。热 key 被扛在应用进程内,分片的压力直接归零,集群里有多少个应用进程,就有多少份读能力。这是唯一一种能随着应用扩容线性增长的方案。
但本地缓存立刻带来一个新问题:进程内存里的数据是副本,源数据在缓存集群里被改了,本地这份怎么知道?
现代 Redis 一类服务端提供的客户端缓存能力(client-side caching,通常称为 tracking)就是为这个问题设计的。机制是:服务端记录每个客户端读过哪些 key,当这些 key 发生变更时,服务端主动向对应客户端推送失效通知,客户端收到后把本地副本删掉。这样本地缓存既能长期持有热点,又能在源数据变化时及时失效。
这个机制把一致性问题从"客户端猜"变成了"服务端通知",方向是对的。但它引入了新的失效模式:如果通知丢了怎么办。网络闪断、客户端重连、服务端在通知发出前重启、客户端正在处理通知时进程崩溃——任何一种情况下,客户端都会持有一份已经过期但自己不知道的脏数据,而且这份脏数据会一直服务下去,直到本地缓存的兜底过期时间到了为止。
这就是为什么即便用了 tracking,仍然必须给本地缓存设一个兜底 TTL。TTL 是最后一道防线,tracking 只是让这道防线在正常情况下不必被触发。把 tracking 当成一致性的全部保障,是这套方案最容易踩的坑。
另一个实践细节是失效通知的通道开销。服务端要维护"哪个客户端读过哪个 key"这张表,这张表本身占内存;热点 key 的失效通知要广播给所有读过它的客户端,这在进程数很多时会形成一次扇出。所以客户端缓存不是免费的,它把压力从缓存分片转移到了服务端的状态维护和通知链路上。只是这笔代价比分片被打爆要小得多,且可控得多。
本地缓存还有一个更朴素的方案:不依赖服务端通知,只用过期时间来控制新鲜度。这个方案的关键设计有两个,一个是随机过期,一个是提前刷新。
随机过期解决的是"同时失效"。如果一批本地缓存项是同一时刻写入、同样 TTL,那么它们会在同一时刻集体过期,所有进程在同一瞬间回源,形成一次人为的流量尖峰——这就是本地缓存版本的雪崩。给 TTL 加一个随机抖动,让过期时刻散开,尖峰就被摊平了。
提前刷新解决的是"过期后的回源穿透"。缓存项过期之后,第一个请求必须等待回源完成,这个请求的响应时间会明显变长;如果此时正好是高并发,多个请求会并发回源。提前刷新的做法是:在过期之前的某个时间点,由后台异步触发回源并更新缓存,让缓存项尽量不真正进入过期状态。
这两个设计凑在一起,就有了那个必须自己定的标:命中率和新鲜度之间的取舍。本地缓存的 TTL 越长,命中率越高、回源越少、分片压力越小,但数据越旧;TTL 越短,数据越新,但回源变多、保护效果变弱。这个标没有通用答案,它取决于业务对"旧"的容忍度。
定标的方法是把数据按容忍度分层,而不是全局设一个值。商品的价格和库存属于低容忍,TTL 要短,甚至不适合本地缓存;商品的图文详情属于高容忍,TTL 可以长;榜单类数据本身就是定时生成的,容忍度由生成周期决定。混在一个 TTL 里,必然是两头都不讨好。
还有一条容易被忽略的规则:本地缓存必须有容量上限。热 key 场景下进程内存是稀缺资源,如果不设上限,异常流量带来的大量冷 key 会把内存吃光,反而把真正的热点挤出去。用 LRU 一类淘汰策略加上明确的条目数或字节数上限,是本地缓存上线前的硬要求。
请求合并指的是把同一时刻针对同一个 key 的多个并发回源合并成一次,其余请求等待这次结果。它对热 key 的效果非常直接:热 key 的特点就是同一时刻有大量并发请求打同一个 key,合并之后回源次数从 N 降到 1。
这个手段的价值在于它几乎不需要改业务逻辑,通常在缓存访问层或库层面接入即可,业务侵入很低。对于回源代价高的场景——比如回源要查数据库、要做复杂计算、要调用下游服务——合并带来的收益是立竿见影的。
但必须说清楚它的边界:请求合并解决的是回源侧的问题,解决不了分片被打爆的问题。合并之后,访问缓存集群的请求次数并没有减少,只是回源次数减少了。如果热 key 的压力体现为分片上的连接数、网络带宽、命令处理量被打满,那么合并对此毫无帮助——那些请求还是要发到那个分片上去。
所以请求合并在手段排序里的位置是明确的:它是回源保护,不是热点治理。把它当成热 key 的主方案,会出现"回源量降下来了但接口还是超时"的困惑。正确的用法是把它和本地缓存、拆 key 这类真正改变压力落点的手段搭配使用。
实现上还有一个细节要注意:合并的粒度是 key,但等待的请求需要有超时保护。如果那唯一一次回源卡住了,所有等待者都会一起卡住,故障面反而被放大了。给等待设置独立超时和降级路径,是使用请求合并的前提。
回到开头那个"加副本不管用"的问题,这里把边界说完整。副本不是完全没用,它有用,但范围有限。
有用的部分:副本能摊薄读流量。如果热 key 所在分片的主节点因为读太多而 CPU 到顶,把一部分读引流到副本上,确实能缓解。对于读多写少、且能接受副本延迟的业务,这是成本最低的常规扩容手段。
没用的部分有三块。其一,写路径不会改善。热 key 如果有写,所有写仍然必须走主节点,副本数量不影响写的压力。其二,一致性代价会随副本数上升。副本靠异步复制同步,主节点上的变更到达副本有延迟,副本越多、复制拓扑越复杂,延迟和不一致的窗口就越不可控。对于热 key 这种被高频读取的数据,读到旧值的概率和影响都会被放大。其三,副本数量上去之后,主节点要维护的连接数、要发出的复制流量都跟着涨,主节点本身的负担不降反升,到某个点之后再加副本是负收益。
另外还有一层架构现实:客户端或代理要能正确地把读请求路由到副本,且要能感知副本的可用状态。这个路由层本身就是新的复杂度和新的故障点。为了治理一个热 key 而引入一套读写分离的路由治理,投入产出比通常不划算。
结论是:副本扩容是容量问题的常规答案,不是热 key 问题的答案。在热 key 场景里,它可以作为缓解手段之一,但不要指望它成为解法。把扩容预算花在副本上,而把真正的热点留在单分片上,是最常见的资源错配。
当热点已经压到瓶颈、且技术侧的手段来不及上或者上了也不够时,必须承认这题只能有损。有损不是失败,有损是主动选择——用一部分请求的实时性或完整性,换整条链路不超时雪崩。
第一类有损是本地返回兜底值。当回源失败或者超过阈值时,直接返回上一次成功缓存的值、或者返回一个预设的降级内容(比如榜单返回上一次计算好的版本、商品详情返回不含实时库存的静态版本)。用户看到的是略旧的信息,但请求是成功的。这里的要点是兜底值必须提前准备好并常驻内存,不能在降级时再去计算。
第二类是有损限流。按用户、按来源、按接口维度限制通过量,超出部分快速失败。要点是限流的粒度要能区分价值——把所有请求一视同仁地限掉,等于把正常用户的体验也牺牲了。按来源限流、按用户分层限流,比全局限流合理得多。另外,限流要配合明确的返回,让客户端知道这是限流而不是故障,避免客户端重试把问题放大。
第三类是队列削峰。把瞬时涌来的请求先收进队列,按后端能承受的速率消费。它把尖峰摊成平峰,代价是排队时间在峰值期间明显拉长。队列必须有长度上限和等待超时,否则队列本身会成为新的积压点和内存风险。
启用顺序上,建议是:先上请求合并(无损且便宜),再上本地缓存兜底值(有损但用户几乎无感),再上队列削峰(有损且有感但请求不失败),最后才是限流快速失败(有损且明确失败)。这个顺序的依据是用户可感知的损害程度,而不是实现难度。
还有一条必须提前定的规则:熔断的阈值和恢复条件要在故障之前就写好并演练过。等到线上开始超时再去讨论"多少错误率算该熔断",讨论本身就是不可用时间。
有损手段里还有一类容易被误用的做法:无差别重试。客户端发现超时就重试,重试又超时,流量被自己放大成好几倍,原本只是慢的链路被彻底压死。任何重试都必须配指数退避和次数上限,并且在服务端已经明确返回限流或熔断时不重试。把重试当成可用性手段,是热 key 场景下最快的雪崩加速器。
另一类不该做的是在故障中做重数据结构调整。热 key 已经压到瓶颈的时候,去改分片规则、去迁移数据、去重建索引,这些动作本身都会产生大量额外流量,把已经脆弱的链路推过临界点。故障中的动作原则只有一条:只做能立刻摘掉的动作,重结构的事留到事后。
技术侧的抢救做得再好,也不如让峰值根本不发生。业务侧的预防之所以通常更有效,是因为它改变的是"什么时候计算",而不是"怎么扛住计算"。
第一条是预热。对于可以预见的聚焦——大促的开卖商品、直播的开播房间、榜单的候选条目——在流量到来之前就把数据加载进缓存,包括本地缓存。预热的关键不只是写进缓存集群,而是要让每个应用进程的本地缓存也提前持有这份数据,否则第一波请求仍然会全部回源。预热动作本身要限速,避免预热流量把集群打垮。
第二条是整块替换。榜单类、排行类、聚合类数据最典型的错误是"每次请求现场算"。这类数据在峰值下会被反复计算同样的结果,而且计算的中间状态还会互相干扰。正确做法是定时计算一次,把结果整块写入一个 key,请求只做一次读取。整块替换还有个额外好处:替换是原子的一次写入,不会出现"读到算了一半的状态"。
第三条是错峰。如果业务允许,把可预期的高频操作在时间上打散——比如把批量任务的触发时间随机化、把定时刷新的时间点分散开。很多所谓的"突发热点",其实是多个定时任务撞在同一个时间点上造成的。
第四条是把热点从关键路径上剥离。以商品详情为例,库存、价格这类强实时字段和图文、评价这类弱实时字段应该分开取,前者走短 TTL 或直连,后者走长 TTL 本地缓存。混在一个大 key 里,就会因为一小部分字段的实时性要求,导致整块数据都无法被本地缓存。
这四条的共同点是把"不可预测的瞬时压力"转化为"可计划的批量动作"。转化的前提是知道哪些资源会被瞬时聚焦,而这又回到了探测那一节——业务侧的预防和技术侧的探测是同一件事的两面。
把前面讨论的手段放在一起横向对照,能更清楚地看到各自的适用位置。这张表按"解决什么 / 不解决什么 / 业务侵入 / 适用阶段"四个维度排列,实际选型时建议先定位自己处在哪个阶段,再看这一阶段有哪些手段可用。
| 手段 | 解决什么 | 不解决什么 | 业务侵入 | 适用阶段 |
|---|---|---|---|---|
| 拆 key 加随机后缀 | 把单 key 的读写压力摊到多个分片 | 规则耦合、写放大、聚合查询成本 | 高,读写两端都要按同一规则改 | 热点已确认且数据结构简单可读多写少 |
| 副本与读写分离 | 摊薄读流量、提升读可用性 | 单分片写路径与复制延迟导致的不一致 | 低,主要在配置与路由层 | 读多写少的常规容量扩容 |
| 客户端缓存与 tracking 失效通知 | 把热点扛到应用进程内存,分片压力归零 | 失效通知丢失时的脏读、服务端状态表开销 | 中,需接入失效通道并设兜底 TTL | 热点已定位且能接受短暂不一致 |
| 随机过期加提前刷新 | 避免同时失效后的回源尖峰 | 新鲜度与命中率之间的固有取舍 | 中,需按数据容忍度分层定标 | 长期存在的准静态热点 |
| 请求合并 singleflight | 消除同一 key 的并发回源风暴 | 分片上的连接数与带宽被打满 | 低,通常库层面接入 | 回源代价高且并发集中的任何阶段 |
| 兜底值、队列削峰、分层限流 | 保住整条链路不超时不雪崩 | 实时性与完整性,属于主动牺牲 | 低到中,取决于降级粒度 | 已压到瓶颈时的应急与常态兜底 |
读这张表时最需要注意的是"不解决什么"这一列。多数线上事故的根源不是选错了手段,而是选对了手段却误以为它能解决全部问题——比如上了请求合并就以为热点治好了,比如加了副本就以为单分片压力消失了。
热 key 治理在租用服务器环境里有一个额外的现实约束:资源是按需付费的,扩容有交付周期,不是想要就能立刻拿到。所以落地顺序必须考虑"什么能马上做"和"什么需要提前准备"。
第一步是先把量测出来。在动手改任何代码之前,先把分片维度的访问量监控补上,把按 key 的频次统计接上(先用旁路采样即可,成本低)。没有这层可观测性,后续所有决策都是猜的:不知道热的是哪个 key,就不知道该拆哪个 key、该给哪个 key 上本地缓存。
第二步是定标。定的是业务容忍度:哪些数据能接受多久的陈旧、哪些请求可以降级、哪些来源可以限流。这一步是业务决策不是技术决策,但它决定了后面所有 TTL、所有降级阈值、所有限流额度的取值。定标定不下来,技术方案就只能拍脑袋。
第三步才轮到上手段,且顺序有讲究。先上无损的:请求合并、本地缓存容量上限与淘汰策略、过期时间打散。再上有损但用户无感的:兜底值、提前刷新。最后才是改变数据结构的:拆 key、字段拆分。这个顺序的依据是回滚成本——越靠前的手段越容易摘掉,越靠后的越难回退。
扩容次序上,建议把预算优先投在应用层的横向扩容,而不是缓存集群的副本数量。因为本地缓存的读能力随应用进程数线性增长,而副本扩容对单 key 无效。多开几个应用进程,配合本地缓存,比给缓存集群加几倍副本来得有效,也更符合租用环境下按台计费的成本结构。
缓存集群本身的扩容则应该瞄准内存容量和分片数,而不是副本数。分片数增加意味着单个 key 在集群中的相对占比变化,虽然不能直接摊薄热 key,但能为拆 key 后的多份数据提供落点空间。内存容量则是所有本地缓存和整块替换方案的底座。
最后一条是演练。预热脚本、降级开关、限流阈值这些在故障时才启用的东西,必须在平时就跑通。租来的服务器资源允许你在非峰值时段做完整的压力还原——用真实倾斜度的 key 分布去压,而不是均匀 key 集合。压测不还原倾斜度,等于没测。
演练还应该覆盖"热点转移"这个场景。真实业务里的热 key 不是固定的:这一场直播的热点是 A 房间,下一场就是 B 房间;这一轮榜单的第一名和下一轮不是同一个。所以本地缓存和拆 key 的规则必须能跟着探测结果动态调整,而不是把已知的 key 硬编码进配置。硬编码名单能应付已知爆款,应付不了下一个爆款。
从成本角度看,租用环境下的治理投入应该集中在可复用的部分:可观测性、降级框架、压测脚本这几项是一劳永逸的,做完之后每上一个新业务都能直接用;而针对具体 key 的拆 key 规则和本地缓存配置则是一次性的,做完就绑定在那个业务上。把预算和人力优先投在前者,长期回报明显更高。
热 key 治理到最后会落回到一个容量问题:你需要多少应用进程来扛本地缓存、需要多大的缓存集群内存来放整块数据、需要多少分片来容纳拆开后的 key。这三个数字算清楚了,机型和台数的选择才有依据;算不清楚,选多大都是凭感觉。
算这笔账时要注意,本地缓存把压力从缓存分片转移到了应用进程的内存和 CPU 上。这意味着应用服务器的内存配置不再只由应用本身决定,还要加上本地缓存的容量预算。很多团队上完本地缓存之后发现应用进程开始频繁 GC 或者被 OOM 杀掉,原因就是这块预算根本没算。
一万网络深耕 IDC 行业 19 年(成立于 2007 年),在华南、华东、华北及中国香港、海外多地整合自营节点与 BGP 多线资源,提供服务器租用、机柜托管与带宽接入的一体化交付;具体机型、带宽与报价以官网实时信息或咨询确认为准。
第一件是确认分片维度的监控已经就位,且告警阈值是基于单分片而不是集群均值设置的。用均值告警会永远发现不了热 key,因为均值本来就是正常的。
第二件是确认本地缓存有容量上限、有兜底 TTL、有淘汰策略,三者缺一不可。没有上限会吃光内存,没有 TTL 会在失效通知丢失时长期服务脏数据,没有淘汰策略会让冷数据挤掉热点。
第三件是确认降级路径(兜底值、限流、队列)已经在生产环境演练过,并且有明确的开关和回滚方式。演练要包括"降级生效"和"降级解除"两个方向,只测过生效的降级在恢复时同样会出问题。
第四件是确认压测用的 key 分布还原了真实的倾斜度。这一点最容易被跳过,也最容易导致"压测通过、线上崩溃"。建议在压测脚本里直接注入不同量级的热 key 比例,观察各手段在不同倾斜度下的表现差异。
把这四件事做完,热 key 就从一个会让人半夜起床的突发事件,变成了一个有预案、有阈值、有回退路径的常规运维项。这之间的差距不在技术选型,而在是否提前把量测、定标、演练这三步真的走过一遍。
Copyright © 2013-2020 idc10000.net. All Rights Reserved. 一万网络 科技有限公司 版权所有 深圳市科技有限公司 粤ICP备07026347号
本网站的域名注册业务代理北京新网数码信息技术有限公司的产品