当缓存朝错误的方向增长
这个服务的缓存目录里有四个小文件。最大的一个 35 字节。它们合起来装着三个计数器编号,而且每一次计数器请求都会读它们。
它们存在,是为了让计数路径不必向数据库提四个几乎总是回答「没有」的问题。这个计数器排除站长自己的访问吗?统计外链点击吗?跟踪站内路径吗?自己重新加载吗?对超过 99 % 的计数器来说,每个答案都是没有,而一个装着少数几个「有」的编号的文件,查起来比四条带索引的查询便宜。
它确实便宜。它也是那种会翻转过来的便宜。
测了什么
同一个问题 —「这个计数器在名单里吗?」— 用两种方式问,在六种名单长度上问。文件的路子:读进来、解码 JSON、找那个编号。数据库的路子:一条预编译语句,查带索引的列。各重复两千次,跑五轮,取中位数。
在今天这个规模上,文件快八倍:0.0202 毫秒对 0.1630 毫秒。到一千个编号时两者持平。到一万个,文件贵十二倍;到十万个,文件贵 135 倍,因为那时候每次访问都得读进并解析 578 千字节。
数据库那条线不动。两行是 0.16 毫秒,十万行还是 0.16 毫秒:索引就是干这个的,而人们很容易忘记,这条无聊的线背后藏着多少工作。
让人不舒服的部分
交叉点落在 100 到 1,000 个编号之间。这个服务有 2,218 个计数器在过去三十天里有过流量。
所以,如果排除功能真的成功了 — 如果每一个有计数器的人都打开「不要统计我自己的访问」— 这个文件就会装着 2,218 个编号,重 11 千字节,每次访问要花 0.43 毫秒而不是 0.02 毫秒。按昨天的 18,423 次访问算,就是每天八秒钟的工作,只为了省下一条本来要花三秒的查询。
这项优化在功能被用得最少的时候最快。它不像慢查询那样随着负载逐渐变差。它随着采用率变差,而采用率是唯一没人盯着的那根轴,因为采用率上升本来应该是好消息。
为什么它还留着
因为它今天是对的,而「今天是对的」可以成为一件事情的理由,只要有人写下了它什么时候不再成立。
三个编号,三个文件。比另一种做法便宜八倍,而这台机器上,计数路径是唯一必须快的东西。现在就把它换成那条被它打败的查询,等于用一个假设换来一个更差的系统。
缺的是绊线,不是重写。这个文件本来就是从数据库写出来的,由那段改设置的代码来写 — 那正是发现名单已经越过几百条、并且说出来的自然位置。有写明上限的缓存是一个决定。没有上限的缓存是一场没人同意过的赌。
一般化的形状
这不是反对把缓存放在文件里的论点。这是支持弄清楚你的缓存朝哪个方向增长的论点。
大多数缓存在负载下会变好:请求更多,命中更多,比例更好。这一个属于另一类。它的单次请求开销取决于它装了多少,而它装的东西,随着服务想要鼓励的那件事一起增长。每一次读都要为每一条记录付钱,包括那 2,215 条与此刻正在被统计的这位访客毫无关系的记录。
对任何放在内存里或文件里的查找表,该问的不是「它有多快」,而是「什么让它变大,以及那件事顺利时会怎样」。如果答案是「它会变慢」,那么规模上限属于代码,就放在写这个文件的地方旁边,在你把它造出来的那一天。