Varnish Cache 实战:反向代理缓存原理与 VCL 配置

大家好,我是程序员天天困。

想象一个很常见的场景:一个电商项目,商品详情页接口 RT 平时稳在 80ms,大促一到飙到 3s,数据库连接池被打满,Nginx 后面一排 502。团队第一反应通常是加机器------加了,效果撑不过半小时,因为根因是所有请求都在回源查库。这时候就该 Varnish Cache 这类 HTTP 缓存出场了。

这种情况下真正救场的往往不是加机器,而是在源站前加一层缓存。Varnish Cache 就是专门干这件事的工具------把商品详情这种热点页面整个拦住,命中率跑到 90% 之后,后端 DB 的 QPS 能直接掉一个数量级。这篇我想把它从头到尾讲清楚:是什么、凭什么快、VCL 这套语言怎么读怎么写、命中率上不去该查哪里。不堆术语,能看懂就能动手。点个收藏,我们开始。

一、Varnish Cache 到底是什么

Varnish Cache:一款开源的 HTTP 反向代理缓存(reverse proxy cache),用 C 写成,架在源站服务器前面,把可缓存的 HTTP 响应存在内存或磁盘里,后续相同请求直接由它返回,不用再回源。你可以把它理解成小卖部的前台货架------热门商品摆在货架上,顾客要直接拿,不用每次都跑去仓库翻。

注意它的定位是反向代理:代理的是服务端,对客户端来说它就是源站;这跟 VPN 那种替客户端上网的正向代理正好相反。它不是 Web 服务器,不是应用框架,就专门干缓存这一件事,而且干得很极端。维基百科、纽约时报、Vimeo 这些高流量站点都在生产环境用它。

二、它凭什么能这么快

Varnish 的快,不是因为 C 语言写得快,而是因为它把"缓存"单独抽象成了一条专门的流水线,每个环节都为命中服务。

1)进程模型:Manager 盯梢,Child 干活

Varnish 跑起来是两个进程:Manager 进程负责读取配置、编译 VCL、监控子进程;真正处理请求的是 Child(也叫 Cacher)进程,内部是一堆线程池------Acceptor 线程接连接,Worker 线程处理请求,Expirer 线程清理过期对象,Backend 线程跟源站通信。多线程 + 线程池的设计让它能把多核吃满,而不是卡在单核上。

2)VCL 不是每次请求解析,是预先编译成 C

这是 Varnish 最特别的一点。你写的 VCL 配置不会在每个请求里被解释执行,而是在加载时先翻译成 C 代码,再调用 gcc 编成 .so 动态库加载进内存。这相当于给前台写好一本接待手册,而不是每来一个顾客就打电话问经理。改配置也不用重启进程,vcl.load + vcl.use 秒级热加载,语法错了直接拒绝切换,老配置继续跑。

3)日志写共享内存,不写磁盘

Varnish 把请求日志写在一块共享内存(VSL)里,而不是逐条 fwrite 到磁盘文件,日志开销极低。varnishstatvarnishlog 这些工具再从这块内存里读数据。这也是它在高 QPS 下还敢默认记录每个请求细节的底气。

4)存储引擎可插拔

  • malloc:纯内存存储,最快,重启就没,是大多数高并发场景的首选;
  • file:mmap 一个文件当缓存,容量可以比内存大,性能介于内存和磁盘之间。

缓存这东西本来就是可重建的,丢了下回源站拿就是了。真要"掉电不丢"的持久化,那是业务数据库该操心的事,别压在缓存层上。

三、VCL 状态机:一个请求的一生

VCL 不是一份静态配置,而是一组在请求不同阶段被回调的钩子函数。

VCL(Varnish Configuration Language) :Varnish 的领域特定语言(DSL),用 sub vcl_xxx 子例程定义请求在各个阶段的行为,通过 return(动作) 决定下一步走向。你可以把它理解成给快递分拣线写的工位规则------每个工位干自己的活,return() 就是分拣口那个拨杆,往哪拨走哪条道。

一个请求在 Varnish 里主要经过这些工位:

子例程 什么时候触发 你常在这里干什么 常见 return
vcl_recv 请求刚到、解析完 判断要不要缓存、选哪个后端、删 Cookie hash / pass / purge
vcl_hash 生成缓存键 自定义缓存 key,比如忽略无关参数 hash
vcl_hit 查到缓存对象 命中后改响应头 deliver / restart
vcl_miss 没查到缓存 决定要不要回源 fetch / pass
vcl_backend_fetch 发往后端之前 改发给源站的请求头 fetch
vcl_backend_response 后端响应回来 设 TTL、决定是否缓存、剔除 Set-Cookie deliver / abandon
vcl_deliver 交给客户端之前 X-Cache: HIT 这类调试头 deliver

vcl_recv 是大门口的安检,决定这个请求走缓存通道还是直接放行;vcl_hash 负责贴单号;vcl_hit/vcl_miss 是查货架;vcl_backend_response 是货到了检查值不值得上架;vcl_deliver 是最后交到客户手上。理解了这条线,VCL 就不吓人了。

可能有人会问:VCL 语法写错了会不会把线上搞挂?

不会。Varnish 加载 VCL 时会先编译,编译失败直接报错并拒绝切换,线上跑的还是老配置。这一点比改完配置直接 reload、写错了就起不来要让人安心不少。

四、VCL 配置实战:从最小可用到 Grace 兜底

最小可用的 VCL 其实就两块:定义后端、写规则。下面示例基于 VCL 4.1 语法(Varnish 4.0 及以上,2026 年 8 月仍在维护的 7.x 系列通用),具体指令请以你所用版本的官方文档为准。

1)最小配置

vcl 复制代码
vcl 4.1;

backend default {
    .host = "127.0.0.1";
    .port = "8080";
}

就这几行,Varnish 就能跑起来,其余行为走内置 VCL 的默认逻辑:缓存 GET/HEAD 的安全响应,带 Cookie 的不缓存,等等。

2)静态资源缓存久一点,并清掉 Cookie

Cookie 是缓存命中率的第一杀手------Varnish 默认认为带 Cookie 的请求和响应是私有的,不缓存。静态资源根本不需要 Cookie,主动清掉:

vcl 复制代码
sub vcl_recv {
    if (req.url ~ "\.(jpg|jpeg|png|gif|css|js|woff2?)$") {
        unset req.http.Cookie;
        return(hash);
    }
}

sub vcl_backend_response {
    if (bereq.url ~ "\.(jpg|jpeg|png|gif|css|js|woff2?)$") {
        unset beresp.http.Set-Cookie;
        set beresp.ttl = 7d;
    }
}

req 是客户端请求对象,bereq 是发给后端的请求对象,beresp 是后端返回的响应对象------这三个变量别搞混,是写 VCL 最容易出错的地方。

3)Grace 模式:后端挂了也能用旧缓存顶一阵

Grace(优雅模式) :Varnish 在缓存对象过期后仍保留一段时间(由 beresp.grace 控制),当后端不健康或正在后台异步回源时,先把这份"过期但还能用"的内容交给客户端的机制。

vcl 复制代码
sub vcl_backend_response {
    set beresp.ttl = 1h;
    set beresp.grace = 6h;
}

这功能平时不起眼,后端发版重启那几分钟,它就是救命稻草------用户看到的是稍旧的页面,而不是 502。

改完用 varnishadm 热加载,不用重启:

bash 复制代码
varnishadm vcl.load myconf /etc/varnish/default.vcl
varnishadm vcl.use myconf

五、Varnish vs Nginx vs Squid:到底怎么选

选型不是选"谁最强",是选"谁更匹配你的瓶颈"。

维度 Varnish Cache Nginx(proxy_cache) Squid
定位 专用 HTTP 缓存 Web 服务器/反向代理附带缓存 老牌正向/反向代理缓存
多核并发 强,多线程流水线 强,事件驱动 单核瓶颈较明显
策略灵活度 极高,VCL 可编程 中等,指令式配置 较高,但配置语法古老
缓存性能 极高,专为缓存优化 良好 一般
HTTPS 终止 原生不支持,需前置 Nginx/Hitch 原生支持 支持
持久化 弱,默认内存,重启即失 支持文件缓存 支持
学习成本 VCL 有门槛 中高
适合场景 高并发热点内容/API 缓存 中小规模一体化部署 正向代理、传统缓存场景

Varnish 还有个 Nginx 很难比的能力:ESI(Edge Side Includes,边缘侧包含) 。它允许在一个页面里埋 <esi:include src="..."> 片段标签,Varnish 会把每个片段按各自的 TTL 单独缓存,再拼装成完整页面返回。要用得在配置里开 set beresp.do_esi = true;,不开的话标签会原样发给浏览器。遇到"页头是登录态千人千面、商品主体可以缓存一分钟"这种页面,ESI 能让你只缓存能缓存的部分,而不用整页放弃。Nginx 靠第三方模块,支持有限。

可能有人会问:Varnish 不支持 HTTPS,那线上怎么用?

标准解法是前置一层 Nginx 或 Varnish 官方的 Hitch 做 TLS 终止:客户端 HTTPS 先到 Nginx/Hitch 解密成 HTTP,再转给 Varnish,Varnish 回源走 HTTP 或另一层内网 TLS。别指望 Varnish 自己终止 HTTPS,它从设计上就没打算干这事。

我的判断是:如果瓶颈就是缓存命中率和吞吐,且你愿意为 VCL 付学习成本,Varnish 在这个细分场景里目前没有对手;如果就一个小站、HTTPS 也想一锅端,Nginx 自带的 proxy_cache 完全够用,别为了用 Varnish 而用 Varnish。

六、命中率上不去的几个坑

命中率低,99% 不是 Varnish 的问题,是响应头和 Cookie 在拖后腿。

1)Cookie 没清干净 :前端请求带 Cookie、后端响应带 Set-Cookie,都会让 Varnish 绕开缓存。静态资源、可公开的接口在 vcl_recvvcl_backend_response 两头都要 unset。

2)Cache-Control 头打架 :源站返回了 Cache-Control: no-storeprivate,或者带了 Set-Cookie,Varnish 默认就不会缓存这个响应(注意 no-cache 不是不缓存,而是"每次用前都要回源校验一次",命中率照样上不去)。要么改源站,要么在 vcl_backend_response 里按路径强行 set beresp.ttl 并清掉矛盾的头。

3)Vary 头滥用Vary: User-Agent 会让每一种浏览器 UA 各存一份缓存,缓存体积膨胀、命中率暴跌。除非真的按 UA 返回不同内容,否则去掉它。

4)URL 带随机参数?utm_xxx、时间戳、防缓存随机数会让每个 URL 都是新 key。在 vcl_hash 之前归一化 URL,把无关参数剔掉:

vcl 复制代码
sub vcl_recv {
    # 去掉 utm_xxx=yyy 及其值,避免每个渠道一个缓存 key
    set req.url = regsuball(req.url, "utm_[^&]+&?", "");
    set req.url = regsub(req.url, "[?&]+$", "");
}

5)POST 请求:Varnish 默认不缓存 POST,这是对的,别硬改。登录后的个性化内容本来就不该在这一层缓存,该上 Redis 上 Redis。

排查时别上来就乱调 TTL,先用工具看清楚:

bash 复制代码
varnishstat   # 看全局命中/未命中比例
varnishlog -g request -q 'ReqURL eq "/product/123"'   # 跟踪单个 URL 为什么 miss

varnishstatMAIN.cache_hitMAIN.cache_miss 的比值,心里有底;再用 varnishlog 追一个具体 URL,看它是被 Cookie 挡了、被 Cache-Control 挡了,还是 hash key 算歪了。对着具体原因改,比瞎调参数强得多。

说白了,Varnish Cache 做的事,就是把"缓存策略"从框架写死的默认值里解放出来,交到你自己手里。它不是银弹------不原生支持 HTTPS、内存缓存重启即失、VCL 有学习成本------但在高并发热点内容这个场景里,它依然是最锋利的那把刀。


我是程序员天天困,持续分享编程干货。觉得有用的话记得点赞收藏和关注~也欢迎在评论区聊聊:你在项目里用过 Varnish 吗?命中率最高跑到过多少,踩过最坑的配置是什么?

相关推荐
Nil2081 小时前
leetcode 146LRU缓存
算法·leetcode·缓存
ZJU_统一阿萨姆1 小时前
【算子开发】全局内存访问与合并访存
java·服务器·网络·人工智能·语言模型
7177772 小时前
不止工具集成:基于 Gitee 软件工厂构建 DevSecOps 研发治理底座
java·服务器·gitee
gis开发之家2 小时前
Spring Boot 4 深度解析——参数接收大全:@RequestParam、@PathVariable、@RequestBody
java·spring boot·后端·spring
(轻舟已过万重山)2 小时前
D1 · 融合蓝图:Spring AI + 虚拟线程 + 服务网格——现代后端统一底座
java·人工智能·spring
SamDeepThinking2 小时前
警惕那些很长时间没有编写任何代码、却在设计系统的人
java·后端·架构
莫得感情 o2 小时前
并发 13 · 异步编排
java·并发
TizzyGoodhealth2 小时前
从零到一搭建Spring‑AI原生Tool‑Calling AI Agent|架构对比+完整实战源码+踩坑实录
java·ai·知识库·rag·ai agent·spring ai
mmsx2 小时前
置灰按钮为什么自己又“亮“了?一次状态缓存“双写冲突“的排查记录
java·缓存·bug·livedata
rannn_1112 小时前
【力扣hot100】图论专题+模板|DFS、BFS、拓扑排序...
java·算法·leetcode·深度优先·图论