把 Nginx 日志丢进 ClickHouse 练手后,我顺手给全站做了套"查询-缓存-静态资源"三级体检
上周闲着没事,把 Nginx access log 导进 ClickHouse 玩分析,结果一眼扫过去,自家站的毛病比客户站还多。干脆趁热打铁,把查询、缓存、静态资源这三块重新捋了一遍,记个流水账。
一、慢查询:别光盯着 EXPLAIN,先分清"该慢"和"不该慢"
之前我判断慢查询就一条标准:>1s 就杀。这次细拆了下,发现至少一半属于"该慢"——复杂报表、历史聚合,这种你加索引也白搭,该丢离线任务就丢。真正要掐的是"不该慢":单表点查、主键查询、简单 WHERE 走了全表。
有个典型:用户中心接口 `SELECT * FROM user WHERE phone = ?`,phone 有索引,但 `SELECT *` 把 text 类型的扩展资料也带出来了,InnoDB 回表拖成 180ms。改成只取 id + nickname + avatar 三列,再按需二次查,压到 4ms。不是索引问题,是"拿太多"的问题。
另外发现个坑:MySQL 8.0 的 `innodb_buffer_pool_size` 我设了 4G,但单表数据 6G,热点数据根本兜不住。不是查询慢,是内存 miss 了在等磁盘。调大到 12G 后,同一批查询平均下来少了 40%。
二、缓存层:我砍掉了 60% 的 Redis key,反而更稳了
以前我缓存用得特豪放,接口返回全塞 Redis,TTL 统一 3600。结果内存飙高不说,缓存穿透、雪崩、数据不一致全碰上了。这次重构的核心就一条:能算的不存,能合并的不拆。
具体干了这几件:
1. 对象缓存改聚合缓存。原先用户基础信息、权限、配置各一个 key,现在合并成 `user:profile:{id}` 一个 hash,字段分开取。少了两次网络 RTT,内存也省。
2. 热点数据本地缓存兜底。配置表、字典表这种极少变的,用 Swoole Table / APCu 做进程内缓存,Redis 只做集群同步。单机 QPS 从 3000 干到 18000,Redis 连接数降了 70%。
3. 删掉了所有"缓存即数据库"的写法。之前图省事,有些列表直接 `Cache::get('article_list')`,更新时删 key。现在强制走"缓存标记 + 懒加载":存一个 `article_list:version` 时间戳,接口先比对版本,命中才取完整数据,不命中就回源并异步回写。脏数据问题基本绝迹。
最爽的是砍 key 那下:原先 12 万个 key,现在 4 万出头,内存从 3.2G 降到 1.1G,监控图看着都舒服。
三、静态资源:CDN 不是万能膏药,源站得先把自己收拾利索
这块我之前踩过 `__STATIC__` 的坑(见历史帖),这次从协议层往下抠。
图片/视频:OSS + CDN 没错,但回源策略要细调
我把 OSS 回源 CDN 的 TTL 从默认 3600 改成按文件类型分:图片 86400、JS/CSS 604800、版本化资源(带 hash)31536000。同时开了 CDN 的 QUIC 和 Brotli,但 Brotli 压缩级别从默认 11 降到 4——11 的 CPU 耗时在边缘节点不划算,4 已经比 gzip 省 20% 体积,耗时只有 1/5。
字体文件:预加载策略比格式更重要
站里用了个 2.3M 的自定义字体,之前 `font-display: swap` 一上,FOUT 闪得用户骂娘。这次改成子集化:用 glyphhanger 只提取用到的字符,体积压到 180K。再加 `` 和 `font-display: optional`,慢网直接 fallback 系统字体,快网无缝切换。
一个意外收获:HTTP 状态码的缓存语义
CDN 配置里我原来 404/500 也缓存了 60 秒,想着减少回源。结果有一次后端服务异常,CDN 把 500 状态码缓存出去,用户刷了半天还是报错。现在 4xx/5xx 全部不缓存,3xx 跟 Location 走,2xx 才按类型分 TTL。这点特别容易被忽略。
四、最后:别迷信"最佳实践",先知道自己站的瓶颈在哪
这次优化前,我先用 ClickHouse 拉了七天的日志,按 URI 聚合 P99 延迟、回源率、缓存命中率,画了张热力图。一眼看出来:80% 的请求落在 20% 的接口上,静态资源里图片占了 73% 流量但只贡献 12% 的交互价值。
数据摆出来,优化方向自然就清楚了。反过来,如果上来就照着网上"Redis 必做集群、MySQL 必上分库分表"搞,大概率刀砍在棉花上。
现在全站首屏时间从 2.1s 到 0.8s,服务器从 4 台砍到 2 台还富余。最讽刺的是,改动最大的不是架构,是"少查点、少存点、少传点"这六个字。