Varnish HTTP 加速器实战:VCL 规则、整页缓存命中率调优、purge 失效与 HTTPS 架构

Varnish:把 PHP 从请求链路上"摘出去"的 HTTP 加速器

网站变慢,绝大多数情况下慢的不是 Nginx,也不是网络,而是"每个请求都要叫醒 PHP,叫醒 PHP 就要连数据库"。一次首页请求要走 PHP-FPM、查十几条 SQL、拼模板、渲染输出——即使每次都优化过,这个流程也要几十毫秒。Varnish 的思路很直接:如果这个页面对所有访客都是同一份内容,为什么每次都要重新生成?把生成好的整页 HTML 存进内存,下一个访客直接拿走,PHP 和数据库全程不用参与。

这就是 Varnish 和 Nginx 的 proxy_cache、以及 Memcached 这类"对象缓存"的根本区别。Nginx 缓存把内容存在磁盘(或内存 zone),规则相对简单;Memcached 缓存的是应用主动存进去的"数据片段",需要改代码;而 Varnish 缓存的是完整的 HTTP 响应,用一套叫 VCL 的领域语言让你精细控制"什么能缓存、缓存多久、什么条件下绕过"。对个人站长来说,Varnish 最实际的价值是:不改一行业务代码,就能把动态站的 TTFB 从几百毫秒压到几毫秒。

架构位置:Varnish 应该站在哪里

Varnish 是一个反向代理,它必须挡在 Web 服务器前面。标准链路是:

访客 → Varnish(:80/:443 前置) → Nginx(:8080 后端) → PHP-FPM → MySQL

注意端口的变化:Varnish 抢占了 80/443,Nginx 必须退到高位端口(如 8080)。这是首次部署最容易卡住的地方——如果你直接 apt install varnish 然后启动,会发现 Nginx 和 Varnish 抢 80 端口导致其中一个起不来。

# 修改 Nginx 监听端口(/etc/nginx/sites-enabled/你的站.conf)
# listen 80;    →  listen 127.0.0.1:8080;

# 修改 Varnish 后端指向 Nginx(/etc/varnish/default.vcl)
backend default {
    .host = "127.0.0.1";
    .port = "8080";
}

# Varnish 监听端口(/etc/default/varnish 或 /etc/varnish/varnish.params)
# DAEMON_OPTS="-a :80 -T 127.0.0.1:6082 -f /etc/varnish/default.vcl -s malloc,256m"

现代发行版(Varnish 6/7)多半用 systemd,配置在 /lib/systemd/system/varnish.service 的 ExecStart 里,通过 systemctl edit varnish 做 override 更安全,避免升级时被覆盖。

VCL 不是配置,是程序:理解它的两个函数

VCL(Varnish Configuration Language)看着像配置,实际会被编译成 C 再编译成共享对象。写错一个分号,Varnish 就启动失败。它的核心是两个函数:vcl_recv(请求进来时)和 vcl_backend_response(后端返回时)。

vcl_recv 决定"这个请求要不要走缓存"。默认行为下,只要请求带 Cookie 或者是 POST,Varnish 就会 pass(直接转发给后端不缓存)——这对带登录态的站点是必要的,但也意味着如果你站点的每个响应都带 Cookie,Varnish 的缓存命中率会是 0。所以第一步永远是:把这些无关的 Cookie 在 Varnish 层剥掉。

vcl 4.1;

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

sub vcl_recv {
    # 只保留真正需要的 Cookie(会话、登录),其余全部清除
    # 很多站点被统计代码、CDN 探针塞了一堆无意义 Cookie,它们会毁掉命中率
    if (req.http.Cookie) {
        set req.http.Cookie = ";" + req.http.Cookie;
        set req.http.Cookie = regsuball(req.http.Cookie, "; +", ";");
        set req.http.Cookie = regsuball(req.http.Cookie, ";(PHPSESSID|wordpress_logged_in_[^=]*)=", "; \1=");
        set req.http.Cookie = regsuball(req.http.Cookie, ";[^ ][^;]*", "");
        set req.http.Cookie = regsuball(req.http.Cookie, "^[; ]+|[; ]+$", "");
        if (req.http.Cookie == "") {
            unset req.http.Cookie;
        }
    }

    # 已登录/已评论的访客不缓存(WordPress 场景示例)
    if (req.http.Cookie ~ "wordpress_logged_in") {
        return (pass);
    }

    # 后台、购物车、结账等路径永不缓存
    if (req.url ~ "^/(wp-admin|wp-login\.php|cart|checkout)") {
        return (pass);
    }

    # 只缓存 GET 和 HEAD,其余方法直接放过
    if (req.method != "GET" && req.method != "HEAD") {
        return (pass);
    }

    return (hash);
}

然后 vcl_backend_response 决定"这个响应该缓存多久":

sub vcl_backend_response {
    # 后端是 PHP 时,PHP 端会发 Cache-Control,我们以它为准
    # 但给一个兜底 TTL,防止 PHP 没发缓存头时把整页设成不缓存
    if (beresp.ttl <= 0s) {
        set beresp.ttl = 120s;
    }

    # 有 Set-Cookie 的响应默认不能进共享缓存(会串号),除非确认安全
    if (beresp.http.Set-Cookie) {
        return (pass);
    }

    # 静态资源给长 TTL
    if (bereq.url ~ "\.(css|js|png|jpg|jpeg|gif|webp|svg|woff2)$") {
        set beresp.ttl = 7d;
        unset beresp.http.Set-Cookie;
    }

    return (deliver);
}

三条会让缓存失效的隐形规则

配好 VCL 之后命中率还是上不去,九成是踩了下面三条。

第一,响应带了 Set-Cookie。只要后端在响应头里放了 Set-Cookie,Varnish 默认就认为"这个响应因人而异",不会缓存。很多 PHP 程序(尤其是框架)会在每个响应里塞一个 session cookie,即使访客没登录。要让整页缓存生效,必须在未登录时通过 PHP 或 VCL 把无关的 Set-Cookie 去掉。

第二,请求带 Authorization 头或 Cookie。见前面的 vcl_recv,Cookie 必须清理到"无状态"才可能命中。

第三,命中的 hash 里混入了变化的成分。Varnish 默认用 URL 做 hash(return(hash) 时),但如果你在 vcl_hash 里加了 User-Agent 或 Cookie,同一个页面就会被拆成无数份缓存,命中率归零。检查 vcl_hash,确保里面除了 req.url 和必要的 host 之外不加别的。

看命中率:varnishstat 才是裁判

Varnish 自己记录所有请求的命中情况,这是判断配置对错的唯一权威:

varnishstat

# 关键几行(括号内是社区版/商业版的叫法差异):
# MAIN.cache_hit          命中缓存直接返回的次数
# MAIN.cache_miss         未命中,转发给后端的次数
# MAIN.cache_hitpass      命中但被 pass 规则强制作废的次数
# MAIN.backend_conn        后端连接数
# MAIN.n_object           当前缓存的对象数量
# MAIN.n_expired          已过期但仍占内存的对象
# MAIN.sess_conn          总连接数

命中率 = cache_hit / (cache_hit + cache_miss)。内容站的整页缓存,60% 以上算及格,80% 以上算好。如果偏低,用 varnishtop 看看到底哪些 URL 在反复 miss:

# 实时显示最常出现的请求 URL(默认按 hit count 排序)
varnishtop -i ReqURL

# 看看后端都给响应打了什么 Cache-Control
varnishtop -i RespHeader:Cache-Control

# 排查为什么没缓存:看 TXURL 与 Miss 标记
varnishtop -i VCL_call

varnishtop -i ReqURL 是排查神器。如果看到某个 URL 反复出现且明明应该缓存,说明它的响应里带着 Set-Cookie,或者被你的 pass 规则误伤了。

缓存失效:purge 与 ban

文章更新后,旧缓存必须立刻作废,否则访客看到的是旧内容。Varnish 提供两种机制:purge 精确删一个 URL 的缓存,ban 按条件批量失效。

# 精确清除单个 URL(需要先在 VCL 里允许 purge)
varnishadm "ban req.url == /2026/10/new-post.html"

# 用正则批量失效(淘汰所有 .html)
varnishadm "ban req.url ~ \.html$"

# 查看当前的 ban 列表
varnishadm "ban.list"

要在 VCL 里开放 purge,得加一段 ACL 限制来源(绝不能对公网开放 purge,否则任何人都能刷掉你的缓存):

acl purge_acl {
    "127.0.0.1";
    "10.0.0.0"/8;
}

sub vcl_recv {
    if (req.method == "PURGE") {
        if (!client.ip ~ purge_acl) {
            return (synth(405, "Not allowed."));
        }
        return (purge);
    }
}

推荐做法是把 purge 逻辑挂进网站后台:文章发布/编辑的钩子里调用 varnishadm "ban req.url ~ ^/对应路径",或发一个 PURGE 请求给 Varnish。WordPress 有现成的 Varnish 插件做这件事,Typecho 可以写个简单的钩子脚本。

该不该上 Varnish:三种情况分清

适合上:内容型站点(博客、资讯、文档站),访客看到的基本是同一份页面,登录用户占比低,且你愿意接受"多一层需要维护的组件"。这类站点上 Varnish 后 TTFB 常有数量级的改善,后端 CPU 和数据库压力显著下降。

没必要上:纯静态站(Nginx 直接吐静态文件已经足够快,加 Varnish 是画蛇添足)、日访问量几百的极小站(瓶颈根本不在渲染)、以及缓存规则复杂的电商站(个性化太重,命中率上不去反而添乱)。

谨慎上:如果站点重度依赖登录态、或者每个页面都有个性化内容(推荐、购物车、用户头像),Varnish 的整页缓存几乎不会命中,收益有限。这种情况更适合用 Memcached/Redis 做对象级缓存,而不是整页缓存。

还有几个权衡点要清楚。第一,Varnish 在 HTTPS 上不做终止——它自己不处理 TLS(社区版要配合 Hitch 或用 Nginx 做 TLS 终止再转发给 Varnish),所以架构会变成"Nginx(TLS) → Varnish → Nginx(后端)",多一跳。第二,内存要留够,-s malloc,256m 是最小配置,缓存 1 万个页面大约需要几百 MB。第三,VCL 编译错误会导致 Varnish 拒绝启动,改配置务必先 varnishd -C -f /etc/varnish/default.vcl 语法检查再 reload。

HTTPS 架构:Varnish 不处理 TLS 的应对

社区版 Varnish 本身不做 TLS 终止,这在"全站 HTTPS"已是标配的今天是个必须提前设计的问题。常见有三种架构。第一种是用 Nginx 做 TLS 终止,然后明文转发给 Varnish,Varnish 再转发给后端的 Nginx——链路变成"Nginx(443) → Varnish(80) → Nginx(8080)",多一跳但配置最直观。第二种是引入 Hitch(Varnish 官方的 TLS 代理),由它专门监听 443 做 TLS 卸载,再把明文交给 Varnish,职责更单一,但要额外维护一个进程。第三种是用商业版 Varnish Enterprise 的原生 TLS,对个人站长来说成本不划算。

# 方案一:前置 Nginx 做 TLS 终止,转发给 Varnish 的 80
# /etc/nginx/sites-enabled/site-tls.conf
server {
    listen 443 ssl http2;
    server_name www.example.com;
    ssl_certificate     /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;

    location / {
        proxy_pass http://127.0.0.1:80;   # 交给 Varnish
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto https;
    }
}

注意 X-Forwarded-Proto https 这一行。Varnish 到后端是明文 HTTP,如果不在请求头里标明原始协议是 https,后端的 PHP 程序(以及重定向逻辑)会以为访客走的是 http,从而产生重定向死循环或者生成错误的绝对链接。这是 HTTPS 架构里最常见的坑。

内存估算与启动参数

Varnish 最耗的是内存,因为它把整个响应体都放在 RAM 里。启动参数里的 -s malloc,256m 只是下限,估算公式大致是"平均页面大小 × 期望缓存的对象数 × 1.5"。一个页面 HTML 平均 50KB,想缓存 5000 个页面,就要 50KB × 5000 × 1.5 ≈ 360MB,给 512MB 更稳。

# 语法检查(改完 VCL 必须先跑这步,否则 reload 会让服务起不来)
varnishd -C -f /etc/varnish/default.vcl

# 优雅重载 VCL,不中断服务
varnishreload
# 或
varnishadm "vcl.load myvcl /etc/varnish/default.vcl"
varnishadm "vcl.use myvcl"

varnishadm 还能在不停机的情况下做很多事:查看后端健康状态、临时屏蔽某个 URL、动态调整线程池。它是 Varnish 的"管理控制台",出事时比翻日志快得多。

# 进入交互式管理控制台
varnishadm

# 常用命令
backend.list        # 后端健康状态与探测结果
param.show          # 当前运行参数
vcl.list            # 已加载的 VCL 版本
status              # 子进程运行状态

监控与容量:把 TTFB 的改善量化出来

上线 Varnish 之后一定要用数据验证效果,否则你无法判断它是真在帮忙还是白占资源。方法是对比上线前后的 TTFB(首字节时间)和缓存命中率。测量 TTFB 可以用 curl 的 -w 格式化输出:

# 连续测量 10 次首页的 TTFB,观察第一次(miss)与后续(hit)的差距
for i in $(seq 1 10); do
    curl -o /dev/null -s -w "TTFB=%{time_starttransfer}s total=%{time_total}s\n" \
        -H "Host: www.example.com" http://127.0.0.1/
done

# 典型结果:
# TTFB=0.312s   <-- 第一次 miss,走了后端
# TTFB=0.002s   <-- 之后都是 hit,直接从内存返回
# TTFB=0.002s
# ...

从 0.3 秒降到 0.002 秒,就是 Varnish 命中缓存的真实收益——两个数量级。如果命中后 TTFB 依然在 0.1 秒以上,说明你的缓存没真正命中,去查 varnishstat 的 cache_miss 为什么一直涨。

长跑的监控建议:把 varnishstat -1(一次性输出全部指标)接进你现有的监控(Netdata、Zabbix 或简单的 shell 脚本 + 告警),重点盯三个数——cache_hit 的增速、n_expired(过期对象是否积压占内存)、backend_fail(后端失败次数)。这三个数异常,分别对应"缓存没生效""内存要加""后端挂了"三种问题,比翻日志直观得多。

结语:让"重复的活"不再重复干

Varnish 背后的思想是 Web 性能优化里最朴素也最有效的一条:不要为同一个结果计算两次。当站点 90% 的请求都是访客读取同样的内容时,让 PHP 每一次都重新渲染,本质上是在浪费 CPU 和数据库资源。Varnish 把这些重复劳动挡在内存层,把宝贵的动态处理能力留给真正需要它的请求——登录、下单、提交评论。

对个人站长来说,它是一层"配置一次、长期受益"的基础设施。前提是理解它的边界:它是 HTTP 层的缓存,不解决应用逻辑慢的问题,也不改变数据本身。用对了,它是你网站上性价比最高的一次提速;用错了(比如对个性化站点硬上整页缓存),它只会带来"内容串号""数据错乱"这类更难查的问题。

Last modification:October 1st, 2026 at 09:26 pm

Leave a Comment