速成课 · No. 15
cache 是一个又小又快的地方,用来存放你已经算出来的答案,这样下次需要它时直接读出来,而不必把活儿重做一遍。它是计算里独一档的最大提速诀窍——同时也带来了计算里最著名的难题:判断那个记下来的答案什么时候已经过时。
只讲精髓 · 每个想法一个画面 · 掌握术语
cache 就是一个简单的想法:把答案的一份副本存在某个快的地方,省得你为同一份答案付两次代价。理解了这一点,你这辈子遇到的每一个 cache 都是同一个把戏,只是换了个地方。
cache 记住答案
你桌上放着一本记事本,旁边是一个慢吞吞的文件柜——一旦你查过某样东西,就把它记下来,下次直接读这张便条,而不必再穿过房间走一趟。
cache 是一个又小又快的存储,保存着昂贵工作的结果——一次数据库查询、一个算好的页面、一个下载好的文件——这样下一次请求它时就很便宜。昂贵的事只发生一次;便宜的读取发生很多次。这就是全部原理。其余的一切都是细节:记事本放在哪里,以及什么时候该相信上面写的东西。
命中和未命中是两种结果
你伸手去拿那张便条。要么它在那儿,你瞬间读到——要么它是空白的,你终究还是得走到文件柜那边去。
当答案在 cache 里时,这就是一次 cache hit——又快又便宜。当它不在时,这就是一次 cache miss——你得去做那份慢活,而且通常会把结果存下来,好让下次命中。这两个词描述了每一次 cache 交互。caching 这整盘棋就是把未命中变成命中:确保你伸手去拿的时候答案就在那儿。
它管用是因为重复
一家咖啡馆记住了常客们的点单——之所以值得,只因为同一拨人一次又一次要的是同样的东西。
当同样的答案被反复需要时,caching 才划算——而计算里几乎一切都是重复的。同一个热门页面、同一个用户的资料、同一次查找,被问上千次。因为访问不是随机的,而是集中在少数几个热点上,一个只存放热门项的小 cache 就能吸收掉一大部分工作。没有重复,就没有理由 cache。
cache 把一个昂贵答案的快速副本存起来。命中是读那张便条;未命中是走去文件柜那一趟。它的活儿就是把未命中变成命中。
caching 不是某一个地方的某一样东西——它是在用户与数据之间的每一层上重复出现的一种模式。副本离需要它的人越近,就越快。
cache 越近,命中越快
你在桌上、屋里、楼里、还有城那头的仓库里都备着东西——越近的拿得越快,但能存的越少。
在用户和真相之间有一架 cache 的梯子,每一级都比上一级更近、更快:浏览器把文件存在你的设备上,CDN 把副本存在离你城市很近的地方,应用把热数据放在内存里,而 CPU 拥有只在几纳秒之外的微型 cache。越近的 cache 越快,但越小。一次请求先试最近的,未命中时再向外回落。
像 Redis 这样的内存存储是主力
一个文员把问得最多的文件放在桌上、伸手可及,而不是每次都从地下室的档案库里取一份。
大多数应用会在应用和数据库之间放一个专门的内存 cache——像 Redis 或 Memcached 这样的工具。内存比磁盘快得惊人,所以把热门结果放在那里,就能把一次慢吞吞的数据库查询变成几乎瞬间的读取。当人们说「加个 cache」来给后端提速时,通常指的就是这个:在一个慢吞吞的真相来源前面放一个快速的内存存储。
CDN 把网络 cache 到离用户近的地方
热门书籍在各地的本地图书馆都有存货,于是没人需要往一个单一的中央档案库邮寄索取。
CDN(内容分发网络)是把 caching 用在了地理上:它把你的页面、图片和文件的副本放在世界各地的服务器上,从离每个用户近的那台来服务他。这就是为什么一个全球站点在哪儿都加载得快,而不是只在它的源服务器附近快。它和别处是同一个命中/未命中的想法——只是副本被铺开在地图上,用来打败距离。
caching 是在每一层重复出现的一种模式:浏览器、CDN、内存存储、CPU。越近越快但越小——而一次未命中会向外回落到下一层。
一个 cache 只有在它确实接住了大多数请求时,才值得它的那份复杂。一个数字——hit ratio——就告诉你你的 cache 是不是配得上它占的位置。
hit ratio 是真正算数的那个分数
一个守门员是靠他扑出的射门比例来评判的。一个扑出 95% 的门将能扭转乾坤;一个只扑出 10% 的几乎改变不了比赛。
hit ratio 是从 cache 而不是从慢速来源服务的请求所占的比例。95% 的 hit ratio 意味着二十个请求里只有一个抵达数据库——负载和延迟都大幅下降。10% 的 hit ratio 意味着这个 cache 大体上只是开销。这一个数字就是你判断一个 cache 是否在起作用的方式,而提高它正是你要调的主要那根杠杆。
一个 cold cache 必须先热起来
一家货架空空的新店一开始谁也服务不快——它随着顾客来问而进货,直到热门商品总是备在手边。
刚启动时,cache 是 cold 的——空的,所以每个请求都未命中、都去找慢速来源。随着真实流量涌入,它装满了热门答案,变得 warm,hit ratio 也随之爬升。这就是为什么性能在刚重启或刚部署后会看起来很糟,然后才稳定下来。有些系统会刻意 pre-warm 这个 cache,在用户到来之前就把已知的热数据加载进去。
cache 拿内存换速度
租一张更大的桌子,好让更多文件都在伸手可及的范围内——这要花更多空间,但你走去文件柜的次数少得多。
caching 不是白来的:你花掉内存(它有限且要花钱)来省下时间。一个更大的 cache 装得更多、命中得更频繁,但你没法把一切都 cache 起来——所以诀窍在于 cache 那些对的东西:那些热门的、昂贵的、被反复索取的答案,它们每存一个字节带来的提速最多。cache 是一桩有意为之的交换,而 hit ratio 告诉你这桩交换划不划算。
hit ratio 是 cache 的成绩单。你花内存来买速度——所以去 cache 那些热门、昂贵的答案,它们每个字节赚到的命中最多。
一个记下来的答案可能在你还信着它的时候就过期了。判断什么时候该不再信那份副本,是 caching 真正核心、也确实最难的问题。
陈旧数据是 caching 的代价
你记在便利贴上的那个电话号码是宝贝——直到你朋友搬了家,那张便条悄无声息地变错了为止。
记住一个答案的另一面是,真正的答案可能变了而你的副本没变。陈旧数据是一个不再和真相相符的 cache 值——旧价格、被删掉的帖子、上个钟头的库存数。每个 cache 都冒着服务陈旧数据的风险,而决定你能容忍多少陈旧,是任何 cache 的第一个真正的设计问题。
TTL:让它按计时器到期
盖着保质期的牛奶——过了期,即使它看上去还好好的,你也不会不检查就信它。
最简单的新鲜度工具是 TTL(生存时间):每个 cache 条目都拿到一个到期时间,过了之后这个条目就被当作没了,从而强制一次新的取数。短 TTL 意味着数据更新鲜但未命中更多;长 TTL 意味着命中更多但陈旧更多。选 TTL 就是在新鲜对速度的那桩交换上选你确切的落点——按每一种数据来选,依据是它实际变化得有多快。
为什么这件事出了名地难
要知道世界上别处某个事实在哪一瞬间变了,好让你在恰好正确的时刻撕掉你的便条——说起来容易,做起来要命。
有个流传的玩笑说,计算机科学里只有两个难题,而 cache invalidation——知道一个 cache 值什么时候必须被扔掉——就是其中之一。它难,是因为 cache 往往并不知道底层数据变了;那个变化发生在别处。下一节就是人们用来对付这个确切问题的那一套策略。
每个 cache 答案都可能变陈旧。TTL 让它按计时器到期——而准确地知道何时该 invalidate,是计算里确实最难的问题之一。
有两股力量决定着一个 cache 里有什么、以及它是否可信:当真相变化时你怎么更新它,以及当它满了时你怎么腾地方。两者都有值得记住的、有名字的策略。
cache-aside:由应用来填充 cache
你先翻你的记事本;如果它是空白的,你就走去文件柜,读那份文件,在继续之前把答案记下来。
最常见的模式是 cache-aside(懒加载):读取时,应用先查 cache;未命中时,它从来源取数、把结果存下来、再返回。cache 按需填充,装进去的正是实际被请求的那些东西。它简单又流行——而它的弱点是陈旧,因为一个值会一直待在 cache 里,直到有东西让它到期或失效。
write-through 和 write-around:让写入保持一致
当一个事实变了,你可以在更新主文件的同一刻就更新桌上那张便条——或者跳过那张便条,让它下次再被重新取一份新的。
当数据被写入时,你要选 cache 怎么跟上。write-through 把 cache 和来源一起更新,于是 cache 从不陈旧(代价是写入更慢)。write-around 只写来源,让 cache 之后再去未命中并重新加载,从而避免 cache 那些没人会重读的数据。这些名字描述的是一次写入落在哪里、以及 cache 什么时候才知道它——你选哪个,取决于你的读写比例。
淘汰:满了时决定丢掉什么
一张满了的小桌子——要加一份新文件,你就得收起一份,于是你拿走那份你很久没碰过的,而不是你每小时都用的那份。
cache 的空间有限,所以当它满了时它必须 evict(淘汰)掉点什么。最常见的策略是 LRU(最近最少使用):丢掉那个最久没被碰过的,赌它不会很快被需要。还有别的——LFU 丢掉使用最不频繁的。淘汰就是一个小 cache 自动保住热门项、甩掉冷门项的方式,让它的 hit ratio 维持得住,而不必无止境地长大。
失效决定一个 cache 值什么时候是错的;淘汰决定空间不够时丢掉什么。cache-aside、write-through、LRU——都是给这两份活儿起的名字。
caching 的威力带着锋利的边。那些经典的失败并不奇异——它们是可预料的、有名字的,而知道它们就等于躲开它们的一半。
一个热门 key 到期时的 stampede
一件热门商品卖光了,而就在它卖光的那一瞬,一百个顾客一齐冲向柜台来要它——把那个唯一负责补货的文员压垮。
当一个热门的 cache 条目到期时,每一个想要它的请求都在同一时刻未命中,并一起涌向慢速来源——这就是 cache stampede(又叫 thundering herd)。那个被遮挡了好几个钟头的数据库,突然一下子接住整群人,可能就此崩溃。补救办法是知道的——让一个请求去刷新而其他请求等待,或者把到期时间错开——但你得预先料到它;stampede 恰恰在流量最高时出现。
陈旧读取:又快、又自信、又错
从一张旧便利贴上读出一个价格、再自信满满地报出来——这个数字被瞬间送达,而它是错的数字。
cache 会乐呵呵地把一个陈旧值又快又满怀信心地服务出去。通常这没什么——一个稍微旧一点的浏览量数字伤不着谁。但对于那些正确性至关重要的数据——银行余额、库存、权限——一次陈旧读取可能是一个真正的 bug。本事在于知道哪些数据容忍陈旧、哪些必须永远新鲜,并且永远不要草率地 cache 后一种。
有些东西根本就不该被 cache
你不会去复印一份每分钟都被重写的文档,也不会把某人的私人信件复印一份放在共用的桌子上——那份副本要么是错的,要么压根不该在那儿。
不是什么都该进 cache。变个不停的数据带来糟糕的 hit ratio 和频繁的陈旧。敏感的或按用户区分的数据,一旦共享 cache 把一个用户的副本交给了另一个,就有泄露的风险。而极少被请求的数据只是白占地方。知道什么不该 cache——变化快的、敏感的、或冷门的数据——和知道该 cache 什么一样重要。
caching 那些经典的反咬都有名字:热门 key 到期时的 stampede、带着虚假自信被服务出去的陈旧读取、以及 cache 那些永远不该被 cache 的东西。
caching 是一件大杀器:用一点内存换来巨大的速度,代价是冒着服务过去的风险。用得有分寸,它就是计算里最划算的交换之一。
cache 那些热门、昂贵、变化慢的
你记住常客们惯常的点单、以及你一天要说十遍的那条路怎么走——而不是那些一次性的请求、或者那些每分钟都在变的东西。
理想的 cache 对象是被频繁读取、产出昂贵、且很少变化的——这个组合带来最多的速度、最小的陈旧风险。一个由慢查询构建、整天被一模一样地重算的热门页面,就完美。一个变个不停、极少被读、又敏感的值,则恰恰相反。大多数 caching 的胜利都来自找出那少数几个符合前一种描述的答案,并恰恰记住它们。
预先定好你的陈旧预算
事先约定好过时到什么程度是可以接受的——一个天气小组件可以是几分钟前的;一个银行余额则不能是几秒钟前的。
在 cache 任何东西之前,先决定它能容忍多少陈旧,因为这个选择驱动着其余的一切——TTL、你是否需要主动失效、它是否根本就能安全地被 cache。要按每一种数据把它说清楚。「它被允许错到什么程度、错多久?」正是这个问题,把 caching 从一个神秘 bug 的来源,变成一桩受控的、有意为之的交换。
- 这份数据热不热——被反复索取得够不够多,足以撑出一个像样的 hit ratio? - 它产出贵不贵,让一次命中真能省下有意义的工作? - 它能陈旧到什么程度——TTL 是多少,我需不需要主动失效? - 一个热门条目到期时会怎样——它会不会引发一次 stampede? - 它 cache 起来安不安全——不是把按用户区分的私密数据放进共享 cache 里? - 我怎么知道它在起作用——我有没有在度量 hit ratio?
- cache hit / miss——答案在 cache 里找到了,或者没有。 - hit ratio——从 cache 服务的请求所占的比例;那个真正算数的分数。 - cold / warm cache——空着、在未命中,对比装满了、在命中。 - TTL / 陈旧——那个到期计时器,以及一份不再和真相相符的副本。 - 失效 / 淘汰——把错的数据扔出去,以及为腾地方而丢掉数据(LRU)。 - cache-aside / write-through——应用懒着填它,或者写入立刻更新它。 - stampede / thundering herd——一个热门 key 到期时涌向来源的那阵冲锋。
- 你能说出你那些重要 cache 的 hit ratio。 - 每一样被 cache 的东西都有一个刻意定的 TTL,与它实际变化得有多快相匹配。 - 没有任何按用户区分的或私密的东西被不小心放进了共享 cache。 - 你的热门 key 不会在到期时一齐 stampede 那个来源。 - 你 cache 那些热门、昂贵、变化慢的答案——而把其余的都跳过。
caching 用一点内存换来巨大的速度,代价是冒着服务过去的风险。做得好,它就是有分寸的:对的那些答案,配上一份你有意选定的陈旧预算。