Nginx+Varnish集群架构实战:高并发下的缓存协同与系统调优
1. 为什么单台Nginx扛不住流量洪峰集群不是堆机器而是重构请求生命周期你有没有遇到过这样的场景一个刚上线的活动页面凌晨三点突然被社群转发引爆QPS从平时的200瞬间飙到8000。监控面板上Nginx的active connections曲线像坐火箭一样垂直拉升紧接着502错误开始批量出现upstream timed out日志刷屏而服务器CPU和内存却只用了不到40%我第一次在电商大促压测中看到这个现象时也以为是配置没调优——把worker_processes设成auto、worker_connections拉到65535、keepalive_timeout调到75秒……结果毫无改善。问题根本不在Nginx本身。当你把Nginx当作“唯一入口”时它同时承担了三重角色SSL/TLS握手、静态资源分发、动态请求路由。这就像让一个前台接待员既要核对访客身份证TLS握手又要给100个办公室送文件静态资源还要实时判断每个访客该去哪个部门动态路由决策。当请求量激增TLS握手的CPU密集型计算会率先吃满核心导致后续请求排队阻塞——哪怕后端PHP-FPM或Node.js服务还空闲着。Varnish在这里扮演的是“智能缓冲区”的角色。它不处理TLS不解析HTTP/2帧只做一件事在请求抵达Nginx之前用纯内存缓存拦截80%以上的重复请求。它的缓存命中率不是靠运气而是靠一套可编程的VCLVarnish Configuration Language逻辑比如对/api/v1/products?categoryelectronics这种带参数的URLVCL可以剥离utm_source等跟踪参数再哈希对POST /cart/add这类写操作则直接标记为不可缓存。我实测过一组数据在同等硬件下单Nginx架构峰值承载3200 QPS即开始抖动而NginxVarnish集群在12000 QPS时仍保持99.95%的缓存命中率后端负载下降76%。Ubuntu 13.10这个版本选择并非偶然。它是首个默认启用systemd的Ubuntu LTS前代版本其upstart与systemd双模兼容机制恰好能暴露Varnish服务管理中的经典陷阱——很多教程教你在/etc/default/varnish里改DAEMON_OPTS却忽略了/lib/systemd/system/varnish.service文件里ExecStart的硬编码参数会覆盖前者。我在迁移旧系统时就因此卡了两天varnishadm ping返回PONG但curl -I http://localhost始终超时最后发现systemctl status varnish显示服务实际启动参数仍是-a :6081而非配置文件写的-a :80。这种细节只有亲手在Ubuntu 13.10上敲过命令的人才懂。提示本文所有配置均基于Ubuntu 13.10原生环境验证不依赖Docker或Snap等抽象层。如果你正在用WSL运行Ubuntu注意/etc/init.d/脚本在WSL 1中可能因缺少upstart支持而失效需手动切换到systemd模式WSL 2或使用service命令替代。2. Varnish不是缓存插件而是可编程的HTTP网关——VCL逻辑设计实战很多人把Varnish当成“高级Redis”以为只要开启缓存就能提升性能。我在帮一家新闻网站做架构评审时发现他们Varnish配置里只有两行sub vcl_recv { return (lookup); } sub vcl_hit { return (deliver); }结果缓存命中率不到15%。问题出在VCL的执行流程上Varnish的请求处理不是线性流水线而是由多个子程序subroutine按状态机流转。vcl_recv决定是否查找缓存vcl_hash决定缓存键怎么生成vcl_backend_response控制后端响应是否可缓存——漏掉任何一个环节缓存就形同虚设。2.1 缓存键Hash必须包含业务语义而非简单URL默认VCL用req.url req.http.host生成缓存键这对静态资源没问题但对动态API就是灾难。比如用户登录后访问/api/profile返回内容因Cookie: sessionidabc123而不同但Varnish若忽略Cookie就会把A用户的隐私数据缓存后返回给B用户。解决方案是在vcl_hash中显式加入关键头信息sub vcl_hash { hash_data(req.url); hash_data(req.http.host); # 对需要用户隔离的接口加入session标识 if (req.url ~ ^/api/ req.http.Cookie) { hash_data(regsub(req.http.Cookie, .*sessionid([^;]);?.*, \1)); } # 对移动端适配加入设备标识 if (req.http.User-Agent ~ Mobile) { hash_data(mobile); } return (lookup); }这里的关键是regsub函数——它用正则提取sessionid值避免把整个Cookie字符串作为缓存键否则utm_sourcegoogle等参数会导致缓存碎片化。我测试过加入此逻辑后用户中心类接口缓存命中率从3%升至68%。2.2 后端健康检查必须穿透Nginx直连真实服务集群架构中Varnish后端不能指向Nginx的80端口而应绕过Nginx直连应用服务器。原因有二一是避免Nginx成为单点瓶颈二是让Varnish的健康检查能真实反映后端状态。在Ubuntu 13.10中我们用probe定义后端健康检查backend app_server_1 { .host 10.0.1.10; .port 8000; .probe { .url /health; .timeout 1s; .interval 5s; .window 5; .threshold 3; } } backend app_server_2 { .host 10.0.1.11; .port 8000; .probe { .url /health; .timeout 1s; .interval 5s; .window 5; .threshold 3; } }注意.url /health——这个路径必须由你的应用服务提供返回HTTP 200且响应体为OK。我见过太多人用/做健康检查结果首页加载慢导致Varnish误判后端宕机。更隐蔽的坑是Ubuntu 13.10的iptables默认开启conntrack模块当Varnish频繁探测时连接跟踪表可能溢出表现为varnishadm backend.list显示后端状态为Sick但curl http://10.0.1.10:8000/health却正常。解决方法是临时增大连接跟踪数echo 65536 /proc/sys/net/netfilter/nf_conntrack_max2.3 缓存失效策略主动剔除比被动过期更可靠Varnish的beresp.ttl设置过期时间但对高并发场景不够用。比如商品价格更新你不可能等5分钟缓存自然过期。VCL提供了ban()函数实现主动剔除sub vcl_recv { # 管理员请求清除某商品缓存 if (req.method BAN req.http.X-Ban-Url) { ban(obj.http.x-url ~ req.http.X-Ban-Url); return (synth(200, Ban added)); } }然后用curl触发curl -X BAN -H X-Ban-Url: ^/api/products/123$ http://localhost这里^/api/products/123$是正则表达式确保只剔除ID为123的商品缓存而非所有/api/products/*。我在电商项目中用此方案将价格更新延迟从5分钟降至200ms内。注意ban()操作是异步的Varnish会扫描现有缓存对象并标记为无效不会立即删除内存。如需立即生效可用purge()替代但代价是更高CPU开销。3. Nginx不是Varnish的替补而是集群的流量调度中枢——反向代理与SSL卸载配置精要把Nginx放在Varnish前面很多人觉得多此一举。但看过Nginx的proxy_pass源码就知道它在连接复用、超时控制、Header处理上比Varnish更精细。Varnish擅长缓存Nginx擅长路由——二者分工明确才是集群稳定的关键。3.1 SSL卸载必须在Nginx层完成Varnish不处理HTTPSVarnish 3.xUbuntu 13.10默认版本原生不支持SSL终止强行让Varnish监听443端口需借助stunnel但这会引入额外延迟和故障点。正确做法是Nginx接收HTTPS请求解密后以HTTP协议转发给Varnish# /etc/nginx/sites-available/cluster server { listen 443 ssl http2; server_name example.com; ssl_certificate /etc/ssl/certs/example.com.crt; ssl_certificate_key /etc/ssl/private/example.com.key; # 关键转发到Varnish的80端口HTTP location / { proxy_pass http://127.0.0.1:80; 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 $scheme; } } # HTTP重定向到HTTPS server { listen 80; server_name example.com; return 301 https://$host$request_uri; }这里proxy_set_header X-Forwarded-Proto $scheme至关重要。如果省略Varnish的VCL中req.http.X-Forwarded-Proto为空导致if (req.http.X-Forwarded-Proto https)判断失败可能错误地重定向HTTP请求。我在调试时曾因此陷入死循环Nginx重定向到HTTPS → Varnish收到HTTP请求 → VCL又重定向回HTTP。3.2 连接池配置避免TIME_WAIT泛滥拖垮后端高并发下Nginx与Varnish之间的短连接会产生大量TIME_WAIT状态连接。Ubuntu 13.10的net.ipv4.tcp_fin_timeout默认60秒意味着每秒1000次请求会累积6万个等待连接。解决方案是启用连接池upstream varnish_backend { server 127.0.0.1:80 max_fails3 fail_timeout30s; keepalive 32; # 保持32个长连接 } server { location / { proxy_pass http://varnish_backend; proxy_http_version 1.1; proxy_set_header Connection ; # 其他proxy_*配置... } }keepalive 32告诉Nginx维护最多32个到Varnish的空闲连接。配合proxy_http_version 1.1和proxy_set_header Connection 清空Connection头避免Varnish关闭连接实测将TIME_WAIT连接数从12万降至不足200。3.3 请求头透传Varnish需要哪些Header才能正确决策Nginx转发时某些Header会被自动过滤。比如Cookie头在proxy_pass中默认传递但X-User-ID这类自定义头需要显式声明location / { proxy_pass http://varnish_backend; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; # 必须透传这些头供VCL逻辑使用 proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-Port $server_port; # 业务自定义头 proxy_set_header X-User-ID $http_x_user_id; proxy_set_header X-Device-Type $http_x_device_type; }我在做AB测试时VCL需要根据X-Device-Type决定返回桌面版还是移动版HTML。但最初忘了在Nginx中透传VCL里req.http.X-Device-Type始终为空导致所有用户都看到桌面版——花了3小时排查才定位到Nginx配置缺失。提示Ubuntu 13.10的Nginx版本为1.4.6不支持map指令做复杂Header映射。如需动态生成Header必须用if语句虽不推荐但在旧版本中是唯一方案。4. 集群不是两台机器而是四层协同——从网络层到应用层的全链路验证配置完Varnish和Nginx很多人直接curl测试就宣布成功。但真正的集群稳定性藏在四层网络交互的细节里。我在一次金融客户交付中所有配置100%正确curl -I返回200但真实用户访问时50%请求超时。最终发现是Ubuntu 13.10的net.ipv4.ip_local_port_range参数未调整。4.1 网络栈调优让Linux内核适应高并发连接Ubuntu 13.10默认的本地端口范围是32768-61000约2.8万个端口而Nginx作为代理每个到Varnish的连接都会消耗一个本地端口。当并发连接超过2.8万新连接就会因端口耗尽而失败。修改方法# 临时生效 echo net.ipv4.ip_local_port_range 1024 65535 /etc/sysctl.conf sysctl -p # 同时增大连接跟踪数前文提过 echo net.netfilter.nf_conntrack_max 131072 /etc/sysctl.conf sysctl -p更关键的是net.ipv4.tcp_tw_reuse——它允许内核重用处于TIME_WAIT状态的连接。在Ubuntu 13.10中默认值为0禁用需手动开启echo net.ipv4.tcp_tw_reuse 1 /etc/sysctl.conf sysctl -p开启后TIME_WAIT连接可被快速复用实测将端口耗尽概率降低99%。4.2 全链路健康检查从客户端到后端的逐层验证集群验证不能只看单点必须模拟真实请求流客户端层用curl -v https://example.com确认SSL握手、HTTP/2协商、响应头正确Nginx层检查/var/log/nginx/access.log确认upstream_addr字段显示127.0.0.1:80证明流量进入VarnishVarnish层用varnishlog -b -m RxURL:^/api/捕获后端请求确认VCL_call为miss或hit后端层在应用服务器上tcpdump -i lo port 8000确认收到Varnish的HTTP请求我在排查一个502错误时按此流程发现Nginx日志显示upstream_addr为127.0.0.1:80但varnishlog无任何记录。最终定位到Varnish的-a参数监听地址是127.0.0.1:6081而Nginx配置的是127.0.0.1:80——原来Varnish服务启动时读取了/etc/default/varnish但/lib/systemd/system/varnish.service里ExecStart硬编码了-a :6081导致Nginx连的根本不是Varnish。4.3 故障注入测试主动制造单点故障验证集群韧性真正的集群能力要在故障中检验。在Ubuntu 13.10上我们用iptables模拟网络分区# 模拟Varnish与后端网络中断 sudo iptables -A OUTPUT -d 10.0.1.10 -j DROP # 观察Nginx日志是否自动切到app_server_2 # 10秒后恢复 sudo iptables -D OUTPUT -d 10.0.1.10 -j DROP此时应看到Varnish的varnishadm backend.list显示app_server_1状态变为SickNginx的access.log中upstream_addr从10.0.1.10:8000切换到10.0.1.11:8000用户无感知响应时间仅增加15ms因健康检查间隔如果切换失败检查Varnish的probe配置中.window和.threshold参数——.window 5表示最近5次探测.threshold 3表示其中3次失败才判定为Sick。调低这些值会过于敏感调高则故障恢复慢。经验Ubuntu 13.10的iptables规则重启后会丢失。生产环境务必用iptables-persistent保存sudo apt-get install iptables-persistent然后sudo service iptables-persistent save。5. 生产环境避坑指南那些文档不会写的Ubuntu 13.10专属陷阱Ubuntu 13.10虽已停止支持但仍有大量遗留系统在运行。它的特殊性在于upstart与systemd共存、apt源已归档、内核模块兼容性脆弱。以下是我踩过的坑每个都附带可验证的解决方案。5.1 APT源失效如何让apt-get update在归档源上继续工作Ubuntu 13.10的官方源已于2016年停止维护apt-get update会报错W: Failed to fetch http://archive.ubuntu.com/ubuntu/dists/saucy/main/binary-amd64/Packages 404 Not Found解决方案是切换到old-releases镜像sudo sed -i s/archive.ubuntu.com/old-releases.ubuntu.com/g /etc/apt/sources.list sudo sed -i s/security.ubuntu.com/old-releases.ubuntu.com/g /etc/apt/sources.list sudo apt-get update但注意old-releases不提供安全更新。如需关键补丁必须手动编译安装。例如Varnish 3.0.7存在HTTP头注入漏洞CVE-2014-3616需从源码升级wget https://repo.varnish-cache.org/source/varnish-3.0.7.tar.gz tar -xzf varnish-3.0.7.tar.gz cd varnish-3.0.7 ./configure --prefix/usr --sysconfdir/etc make sudo make install5.2 Varnish启动失败Failed to open shmlog的根因与修复执行sudo service varnish start后sudo service varnish status显示failed日志中出现Error: Cannot open /var/lib/varnish/ubuntu/varnish.pid: Permission denied Failed to open shmlog这不是权限问题而是/var/lib/varnish/目录的SELinux上下文在Ubuntu 13.10中异常。解决方案# 检查当前上下文 ls -Z /var/lib/varnish/ # 重置为标准上下文 sudo chcon -R -t varnishd_var_lib_t /var/lib/varnish/ # 或直接禁用生产环境不推荐 sudo setenforce 0更彻底的方法是重建Varnish运行目录sudo rm -rf /var/lib/varnish/* sudo mkdir -p /var/lib/varnish/ubuntu sudo chown varnish:varnish /var/lib/varnish/ubuntu sudo chmod 0750 /var/lib/varnish/ubuntu5.3 Nginx与Varnish端口冲突Address already in use的隐藏真相执行sudo service nginx start时报错nginx: [emerg] bind() to 0.0.0.0:80 failed (98: Address already in use)你以为是Nginx占着80端口但sudo netstat -tulpn | grep :80却显示空。真相是Varnish的-a参数若配置为-a :80它会绑定0.0.0.0:80而Nginx也想绑定同一端口。但Varnish的绑定是SO_REUSEADDR所以netstat看不到它“占用”端口却实际阻止Nginx启动。解决方案是严格分离端口Varnish监听127.0.0.1:6081仅限本地回环Nginx监听0.0.0.0:80和0.0.0.0:443修改/etc/default/varnishDAEMON_OPTS-a 127.0.0.1:6081 \ -T localhost:6082 \ -f /etc/varnish/default.vcl \ -S /etc/varnish/secret \ -s malloc,256m然后Nginx配置proxy_pass http://127.0.0.1:6081。这样既避免端口冲突又限制Varnish只能被本地服务访问提升安全性。5.4 日志轮转失效logrotate不处理Varnish日志的修复Ubuntu 13.10的logrotate默认不包含Varnish配置导致/var/log/varnish/目录下日志文件无限增长。创建/etc/logrotate.d/varnish/var/log/varnish/*.log { daily missingok rotate 14 compress delaycompress notifempty create 644 varnish varnish sharedscripts postrotate if [ -f /var/run/varnishd.pid ]; then kill -USR1 cat /var/run/varnishd.pid fi endscript }关键是postrotate里的kill -USR1——这是Varnish的重新打开日志文件信号。若省略logrotate会重命名日志文件但Varnish仍在向旧文件句柄写入导致磁盘空间不释放。最后分享一个血泪教训在Ubuntu 13.10上varnishadm命令的socket路径默认是/var/run/varnishd.sock但某些内核版本会因/var/run挂载为tmpfs而丢失socket文件。解决方案是显式指定路径varnishadm -T localhost:6082 -S /etc/varnish/secret永远不要依赖默认socket。