HAProxy、Varnish与Squid选型与缓存分工

先说结论:一个日 IP 三位数的技术博客,用 Varnish 做整页缓存是过度设计,用 Squid 做反向代理更是把简单问题复杂化,真正该考虑的是 Nginx 的 proxy_cache 或 fastcgi_cache。但如果你确实是给公司内部系统做前置缓存、或者站点前后端分离(Nginx 只做静态加反代,后端是 Node/Java),那 HAProxy 与 Varnish 的分工就值得认真算一次。这篇把三者的适用边界、Varnish VCL 的命中率调优、以及 fastcgi_cache 与 proxy_cache 的分工讲清楚,顺带给出一个个人站可以照抄的选型判断表。

一、三者解决的问题并不完全重叠

很多人把 HAProxy、Varnish、Squid 当成同类工具,其实它们的强项不同。

HAProxy 的核心是四层和七层负载均衡,缓存能力很弱(它没有真正的响应缓存),强项是连接管理、健康检查、会话保持、限流、ACL 路由和极低的资源占用。一台 1 核 1G 的机器跑 HAProxy 撑几万并发连接是常规操作。它的价值在"把流量正确分给后端"和"在后端出问题时优雅降级"。

Varnish 的核心是 HTTP 反向代理缓存,用 VCL 这门 DSL 描述缓存策略,命中时请求完全不到后端。它最擅长的是"同一份内容服务大量匿名用户"的场景:新闻站、文档站、电商的商品详情页。缺点是 VCL 学习曲线陡,缓存失效要靠 PURGE/BAN,一旦规则写错很容易出现用户互相看到对方数据的事故。

Squid 出身是正向代理(客户端代理),反向代理模式(http_port 80 accel vhost)也支持,但现在的新项目基本不会再选它。它的配置语法老派,缓存键和刷新逻辑不如 Varnish 精细,性能也不占优。除非你有历史遗留的内网代理需求,否则不用考虑。

还有一个小众但值得知道的选项是 Nginx 自带的 proxy_cache。它没有 VCL 那么灵活,但胜在不需要多装一个进程、和现有的 Nginx 配置在同一处维护,绝大多数个人站的需求它都能覆盖。

二、给个人站长的选型判断表

按实际场景对号入座,不用纠结:

后端是 PHP-FPM(WordPress/Typecho 等)
  → Nginx fastcgi_cache(直接缓存 PHP 输出,最省事)

后端是 Node/Java/Python 的 HTTP 服务
  → Nginx proxy_cache(配置成本最低)

单机已经扛不住,需要两台以上后端
  → 前面加 HAProxy 做负载均衡,缓存仍放 Nginx 层

内容极重、匿名流量大、需要精细缓存策略(ESI/边缘包含)
  → Varnish

内网历史遗留、需要正向代理
  → Squid(仅此一种情况)

个人博客站最常犯的错是"看到 Varnish 能提升 10 倍性能"就装一个,结果为了让它认识 WordPress 的 Cookie 规则,写了 200 行 VCL,最后因为登录态串号回滚。其实 Nginx 的 fastcgi_cache 配 20 行就够,效果在同一量级。

三、fastcgi_cache 与 proxy_cache 的分工

这两个指令经常被混用,区别其实很明确:

fastcgi_cache 缓存的是一段 FastCGI 响应,它的上游是 PHP-FPM 的 socket,缓存键默认基于 $request_uri 加 $fastcgi_script_name 等变量,需要用 fastcgi_cache_key 显式定义。它只对 location ~ \.php$ 这类走 FastCGI 的位置有意义。

proxy_cache 缓存的是任意 HTTP 上游的响应,上游可以是 http://127.0.0.1:3000 这样的应用服务,也可以是另一台 Nginx,甚至是一个外部的远程接口。它的配置指令是 proxy_cache_path、proxy_cache_key、proxy_cache_valid。

两者可以同时用,而且经常需要同时用。典型的分层是一个前后端分离的站点:/api/ 走 proxy_pass 到 Node 服务并做 proxy_cache,静态资源走本地磁盘加 expires,而如果后台还有一小块 PHP 管理页,那段单独走 fastcgi_cache。两套 keys_zone 要分开命名,别混用一个共享内存区,否则 proxy_cache_purge 和 fastcgi_cache_purge 会互相误伤。

举一个实际的分流配置:

proxy_cache_path /var/cache/nginx/api levels=1:2 keys_zone=APIZONE:32m
                 inactive=10m max_size=1g use_temp_path=off;

location /api/public/ {
    proxy_cache APIZONE;
    proxy_cache_key "$scheme$host$request_uri";
    proxy_cache_valid 200 60s;
    proxy_cache_lock on;
    proxy_cache_use_stale error timeout updating http_502 http_503;
    add_header X-API-Cache $upstream_cache_status always;
    proxy_pass http://127.0.0.1:3000;
    proxy_set_header Host $host;
}

location /api/private/ {
    proxy_cache off;
    proxy_pass http://127.0.0.1:3000;
}

关键在于把"可缓存的公开接口"和"不可缓存的私有接口"在 URL 层面就分干净。靠正则判断 Cookie 来区分,永远有漏网之鱼;靠路径前缀区分,规则一眼可见,新人接手也不会改错。

四、Varnish 的 VCL:命中率是怎么被拉低的

假设你确实要用 Varnish,那么命中率调优集中在 vcl_recv 和 vcl_backend_response 两个子程序里。内置的默认 VCL 已经定义了基本规则:带 Cookie 或 Authorization 的请求直接 pass 给后端。这条规则对博客是灾难,因为统计脚本、主题设置、广告插件几乎都会种一个 Cookie,于是所有请求都变成不可缓存。

正确的做法是在 vcl_recv 里主动剥离无关 Cookie,只保留真正影响内容的:

sub vcl_recv {
    # 登录态相关 Cookie 才 passthrough
    if (req.http.Cookie ~ "(wordpress_logged_in|PHPSESSID|comment_author)") {
        return (pass);
    }
    # 其余 Cookie 一律在进缓存查找前丢弃
    unset req.http.Cookie;

    # 后台与登录页不缓存
    if (req.url ~ "^/(wp-admin|admin|login)") {
        return (pass);
    }

    # 规范化查询串,避免 ?utm_source= 之类的参数打碎缓存
    if (req.url ~ "(\?|&)(utm_[a-z]+|fbclid|gclid)=") {
        set req.url = regsuball(req.url, "([\?&])(utm_[a-z]+|fbclid|gclid)=[^&]*", "\1");
        set req.url = regsub(req.url, "[&]+$", "");
    }
}

注意 unset req.http.Cookie 必须在判断登录态之后。如果顺序写反,先 unset 再判断,条件永远不成立,登录用户也会拿到访客的缓存页面——这是 Varnish 配置里最经典的事故,症状是"我明明登录了,页面顶栏显示未登录"。

第二个降低命中率的元凶是后端返回的缓存头。vcl_backend_response 里应该显式覆盖后端给的 TTL,不要全信 PHP 输出的 Cache-Control:

sub vcl_backend_response {
    # 后端说不要缓存,我们也给一个短 TTL(除非明确 no-store)
    if (beresp.http.Cache-Control ~ "no-store") {
        set beresp.uncacheable = true;
        return (deliver);
    }

    if (bereq.url ~ "\.(css|js|png|jpg|webp|woff2)$") {
        set beresp.ttl = 7d;
        unset beresp.http.Set-Cookie;
    } else if (beresp.status == 200) {
        set beresp.ttl = 10m;
        set beresp.grace = 1h;
        unset beresp.http.Set-Cookie;
    } else if (beresp.status == 404) {
        set beresp.ttl = 1m;
    }
}

beresp.grace = 1h 是 Varnish 的一个亮点:即使对象过期了,在后端短暂不可用时仍然可以继续发旧内容,比 Nginx 的 use_stale 粒度更细。

看命中率用 varnishstat,重点看 MAIN.cache_hit 与 MAIN.cache_miss 的比值,以及 MAIN.n_object 和 MAIN.s_resp_hdrbytes 是否符合预期。如果 hit 一直很低,用 varnishlog -g request -q "VCL_call eq 'MISS'" 抓几条真实请求,看它的 URL 和 Cookie 长什么样,基本一眼就能定位到是哪条规则把它踢出了缓存。

五、HAProxy 在个人站里真正有用的部分

如果站点规模不需要缓存集群,但确实有两台后端,HAProxy 的价值主要体现在这几项配置上:

global
    maxconn 4096
    stats socket /run/haproxy/admin.sock mode 660 level admin

defaults
    mode http
    timeout connect 3s
    timeout client  30s
    timeout server  30s
    option httplog
    option forwardfor

frontend web
    bind *:80
    acl is_api path_beg /api/
    use_backend api_pool if is_api
    default_backend web_pool

backend web_pool
    balance leastconn
    option httpchk GET /healthz
    http-check expect status 200
    server web1 10.0.0.11:8080 check inter 2s fall 3 rise 2
    server web2 10.0.0.12:8080 check inter 2s fall 3 rise 2 backup

backend api_pool
    balance roundrobin
    server api1 10.0.0.21:3000 check inter 1s fall 2 rise 3

几个参数值得说。fall 3 rise 2 意味着连续 3 次健康检查失败才摘除节点,连续 2 次成功才恢复,这个组合能避免后端偶发一次 500 就被踢出集群。backup 标记的机器平时不接流量,主节点全挂时才顶上来,适合做保底。option httpchk GET /healthz 要求后端提供一个只返回 200 的轻量端点,别把首页当健康检查地址——首页要查数据库,数据库慢的时候会把所有节点判死。这是用 HAProxy 最常见的自作自受。

六、决策清单:三步确定你要不要装

第一步,量一下现在的 TTFB。用 curl -o /dev/null -s -w '%{time_starttransfer}' https://your.site/ 连打十次,看输出的秒数。如果稳定在 300ms 以内,说明现在的缓存已经够了,装 Varnish 也不会更快,因为瓶颈在别处(比如 TLS 握手、CDN 回源)。

第二步,看匿名请求占比。登录用户占比超过 30% 的站点(内网系统、会员站),整页缓存收益很低,因为大部分请求根本不能缓存。这种情况下把精力放在 APCu 和数据库查询上更划算。

第三步,看这台机器还能不能加进程。Varnish 默认要占一块共享内存(-s malloc,256m),HAProxy 虽然轻但也要额外端口和配置维护量。2G 内存的机器同时跑 Nginx + PHP-FPM + MySQL + Varnish,很容易在流量高峰被 OOM Killer 干掉 MySQL,得不偿失。

三个问题都过了,再上。任何一个不过,把现有 Nginx 的 proxy_cache/fastcgi_cache 调好,收益已经接近 80%。

七、上线后的观测与常见误判

换上前置缓存后,最容易出现的误判是"命中率 90% 说明效果好"。命中率高不等于用户快,如果命中的都是 404 页面或静态图片,对 TTFB 帮助有限。正确的观测方式是分 URL 类型看命中率和响应时间:文章详情页的命中率应接近 95%,首页和列表页 80% 以上,搜索页和后台应该全部 BYPASS。

第二个误判是"缓存了就不需要优化数据库"。整页缓存失效的瞬间(发布新文章、缓存过期、被 PURGE)所有并发会同时回源,这时候数据库如果本来就慢,会直接被打到超时。所以 proxy_cache_lock on 或者 Varnish 的 request coalescing(默认开启)必须配上,同时数据库那侧也要有基本的索引优化兜底,否则缓存只是把问题推迟到过期时刻。

第三个是日志分析失真。前置缓存命中时请求不进后端,应用日志里看不到这些访问,用应用日志统计 PV 会严重偏低。统计要以缓存层的 access log 为准,Varnish 用 varnishncsa -a -w /var/log/varnish/access.log,Nginx 则直接看 access.log,别混着算。

最后,任何前置缓存上线前都要准备好清缓存的入口。内容改了但页面还是旧的,是最常见的工单来源。Varnish 用 varnishadm "ban req.url ~ ^/post/123",Nginx 用 proxy_cache_purge(需 ngx_cache_purge 模块)或者干脆删缓存目录下的对应文件后 reload。这个入口在发布脚本里就要接上,别等出事再手工找。

Last modification:October 20th, 2026 at 10:25 pm

Leave a Comment