缓存工程师亲历:三年跑通盈利的7次关键跃迁
|
凌晨三点零七分,告警声没响,是微信语音条先弹出来——DBA老周发的,三秒,就一声“缓存雪崩”,后面半句被空调嗡的一声吞了。我耳机还挂着,手已经按在键盘上,SSH连上第一台Redis集群节点,输完密码回车,卡住,两秒,再输一遍,还是卡。我抬头看监控大屏,QPS曲线斜着往下掉,像有人拿刀割了根线。不是缓存穿透,不是热key打穿,是下游DB直接夯死——但问题出在我们这层,你信不信?我信。因为昨天上线的那个“优雅降级兜底策略”,把所有miss都喂给本地Caffeine缓存,喂得太多,GC毛刺叠着来,CPU软中断飙到92%,而那个本地缓存的maxSize设的是八百,八百个键,我敢打包票,就八百,我亲手敲的,不是配置中心配的,是硬写进启动参数里的——结果呢?它真就卡在七百九十九,第七百九十九个key进去的时候,JVM把线程池干跪了。
文章配图,仅供参考 咖啡洒在工牌上了,深褐色,从“张工”两个字中间淌下去,盖住了工号后四位。我擦了一下,越擦越糊。对面坐的是后端小李,他换了一双拖鞋,灰蓝色,左脚那只后跟磨秃了,露出一点白边。他没说话,就盯着自己屏幕右下角的时间,17:43——可那会儿是凌晨。他调休单被驳回第三回了,理由是“业务高峰期”。我原以为驳回理由至少得提一句“缓存抖动风险”,结果邮件里就这一行,没标点。我说不准他是不是上周五故意把那个Lua脚本注释掉了三行,反正日志里没留痕,Redis慢查询里也查不到,就像从来没存在过。 第一次跃迁?二〇二六年四月,上线“分级驱逐队列”。名字是老板拍的,说听着像战略。其实就是把LFU改成带权重的——热点数据不光看访问频次,还得看下游服务SLA等级。DBA老周当场在评审会上笑出声,说这不就是当年淘宝Tair里拆过的老轮子?我点头,他没再说下去。可上线第二天,支付链路缓存命中率从91%掉到84%,不是跌,是台阶式地,咔、咔、咔,三步,每步三分钟。复盘发现:风控模块发来的key前缀带“_risk_”的,全被插队塞进高优队列,但风控本身根本没读这些key,是它上游一个埋点SDK误发的——那个SDK用的还是HTTP长连接心跳包,心跳里混着17个废弃key,其中5个是去年灰度期写的,连注释都写着“临时勿删”。没人删。我们也没权限删。后来我手动清掉三次,第三次清完,运维那边的监控报警器响了一声,没人接,响完就歇了,像累了。 第二次跃迁,是把淘汰策略从时间戳+LRU混合,换成基于内存碎片率的动态阈值触发。这个我真觉得是进步。但实施的时候,运维小陈非要用Ansible批量推送,不肯手工改一台试——他说“统一性重要”。结果推完三十七台,有两台的Linux内核版本低了0.02,meminfo里FragmentationRatio字段压根不吐数,直接返回空,空值进算法,触发了无限重入——那晚六台实例持续fork失败,OOM killer连续杀掉十二个worker进程。最后靠我ssh进那两台,手动echo 65 > /proc/sys/vm/overcommit_ratio才止住。这事我没报故障单,写了笔记:下次升级kernel之前,先扫/proc/sys/vm/下所有可读文件,哪怕只读一行。笔记到现在还在我桌面那个notepad++里,标签页没关过。 第三次……算了,第三次是老板拍桌子那次。他拍的不是桌子,是他那块Surface Pro,合上又弹开,笔掉地上,滚到机柜底下,谁也没捡。他说:“为什么用户看到‘加载中’要等十八秒?”我没接话。十八秒?我查过Trace,真实耗时十七秒五,差那五百毫秒是前端埋点上报延迟——但它确实在缓存层卡了十三秒,卡在序列化反序列化那一步,Gson用了默认配置,遇到嵌套Map嵌套七层,parse完要一百四十毫秒,而我们每秒扛三万请求。没人改。不是不能改,是改完要测兼容性,测完还要压测,压测资源排期到下季度。所以现在还在用。你说优胜劣汰?对。淘汰的就是不肯把Gson换成Jackson、或者肯换但不敢动的那批人。活下来的,要么是胆子比心跳快,要么是早就把panic注入到CI里,每次PR带测试报告,没过就拦。我属于后者。但上周三的panic注入脚本,跑歪了,把生产配置的密钥前缀刷成了test_,好在只刷了dev集群——DevOps那个实习生手抖点了确认,我看见他指甲缝里还有没洗掉的泡面汤渍。 第四次是冷热分离做实了,但热区容量不够,就硬加一层“伪写穿透”。写请求进来,不立刻刷DB,先写本地环形缓冲区,攒够五十条或超三百毫秒再批量flush。上线三天,DB负载降了四成,缓存失效风暴平了。可第五天凌晨,后端小李突然问我:“你是不是改了写缓冲的timeout?”我说没动,他甩给我一张截图,Buffer flush延时P99突然跳到2.7秒。我一看,是他自己写的异步提交逻辑,用了CompletableFuture.runAsync(),没指定线程池,吃的是ForkJoinPool.commonPool()——而commonPool当时正被另一个定时任务占着,那任务每十分钟跑一次,扫三百万条日志索引,一跑就五分钟。所以缓冲区就在那儿干等,等commonPool腾出手。这不是缓存的事,是Java线程池认知赤字。可这赤字,算在我们头上。告警归我们收,背锅归我们背。所以第五次跃迁,我提了个RFC:禁止任何团队往commonPool里扔IO型任务,违者,缓存层自动把他们的trace_id加进黑名单,降级到最低优先级队列——不是玩笑,真的上了。 第六次?忘了。记不清哪次是第六次。可能是把哨兵模式切集群模式那次,也可能是在K8s里给Redis StatefulSet加了topologySpreadConstraints,让pod尽量不在同一物理机——但这事做完之后,机房冷风突然变大了,吹得我后颈发凉,风扇转速表在监控屏角落一闪,0.8rpm,不对,是800rpm,单位标错了。我就盯着那串数字,盯了四十秒。没管。第七次……二〇二六年四月,其实就一个动作:把所有客户端里config.get("cache.ttl")的默认值,从null改成300。就这么改。但改完三天,订单查询RT下降11%,不是因为我们提速了,是因为上游服务终于敢把缓存TTL设成负数了——之前他们怕设了负数,我们的client会炸,所以全设0,强制直连DB。我们改了default,他们就敢改业务逻辑了。这就叫优胜劣汰。不是我们多牛,是我们先蹲下了,别人才敢站起来往前走。我现在在改一个更狠的:把get命令的默认超时,从两千毫秒,缩到三百。明天上线。今晚不睡了。对面小李那双拖鞋,右脚那只今天没穿,他脚上就一只左脚的,孤零零,露着脚踝。 哦对,值班表下周起改三班倒了,早八晚四、晚四早零、零点到早八。我排在零点那班。调休申请我刚又填了一张,理由写“需要校准本地时钟与NTP主源偏差”,不知道过不过。不过无所谓。反正凌晨三点零七分,告警总会响——只是这次,我先听。 (编辑:站长网) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |

