简介:面向 Windows 平台运维人员与 Web 性能优化开发者的 Cygwin Varnish Cache 移植补丁包,针对 Varnish Cache 原生依赖 Linux 环境、直接编译运行困难的问题给出了可落地的适配方案。Varnish 以内存缓存 HTTP 对象、VCL 可编程缓存策略、事件驱动高并发与热升级见长,作者通过修改路径处理、网络 I/O、线程管理与信号处理等关键代码,使其在 Cygwin 中稳定工作,并附带精简的 cygwin.dll 与 gcc 分发版,免去完整安装 Cygwin 的负担。压缩包共 118 个文件、约 5.54MB,以 36 个 C 头文件、18 个静态库、14 个可执行程序与 13 个动态链接库为主体,配合 4 份 readme、若干批处理脚本与 vcl 示例配置,覆盖编译、运行与调试环节,另含 varnishd、varnishlog、varnishtop 等命令手册页,便于对照查阅参数与日志字段。已有 104 人学习,适合希望在 Windows 侧低成本验证缓存策略、观察请求日志与压测表现的中级开发者参考。
1. Windows 上想验证 HTTP 缓存行为,为什么我最后选了这个 Cygwin 版 Varnish
前端和后端都在 Windows 上跑,想在最前面挂一层 HTTP 缓存,看看 TTL 过期、Vary、Cookie 剥离这些决策到底会怎么走。Linux 上一条包管理命令就能解决,Windows 上没这个待遇。Cygwin Varnish Cache 这份开源资源解决的就是这件事:它借助 Cygwin 的 POSIX 层,把 Varnish Cache 这套开源软件编译出来,让你在 Windows 上跑一个行为跟线上足够接近的缓存实例。它的定位是给“我需要在本机复现一套缓存行为”的人用的,包括验证 VCL 写法、对照压测结果、给后端做一层临时缓存;生产边缘节点不要指望它,原因第 2 章会摊开讲。下面按我自己的构建顺序拆,命令能直接抄。
2. Cygwin 版 Varnish 的适配边界:它到底替你补了哪些系统调用
Varnish 不是那种“解压就能双击运行”的绿色软件。它是一个自建事件循环、多进程、多线程、共享内存、运行时编译 C 代码的服务端程序。想在 Windows 上把它跑起来,你要么重写一大半平台相关的代码,要么让 Cygwin 去承接这些系统调用。这份资源走的是后一条路。搞清楚它承接了什么、没承接什么,你才知道后面哪些报错是环境问题、哪些是这套组合先天不足。
2.1 Cygwin 的 POSIX 层与 Varnish 的依赖清单
Cygwin 的核心是cygwin1.dll,它在 Windows 的 Win32 API 之上实现了一层 POSIX 语义。Varnish 用到的几样关键能力,Cygwin 覆盖得还算齐:fork()、POSIX 线程与读写锁、mmap()的共享映射、dlopen()、poll()、信号、BSD socket。缺的那几样恰好是 Linux 上最“爽”的几样:epoll、sendfile、accept4、SO_REUSEPORT,以及各种内核零拷贝路径。
这带来的第一个直接后果是事件等待器。Varnish 在 Linux 上默认用 epoll,在 FreeBSD 上用 kqueue,autoconf 探测不到这些,就会退到poll等待器。你可以在启动参数里显式钉住它:
# 明确指定等待器,避免 autoconf 探测结果和你的预期不一致 /opt/varnish/sbin/varnishd -p waiter=poll ...poll能工作,但每次事件循环都要把 fd 数组整体扫一遍,连接数上千以后 CPU 会明显偏在 sys 时间上。这属于这套组合的固有成本,不是配置错了。
第二个后果是编译期的探测结果。autoconf 在 Cygwin 下会老老实实把sendfile、accept4、SO_REUSEPORT这些判为不可用,走功能降级分支,所以编译一般能过。真正容易卡住的是依赖库,Varnish 必须依赖 PCRE 做正则(vcl_recv里的~匹配全靠它),CLI 端依赖 libedit 和 ncurses 做行编辑和历史。PCRE 这块要注意分支差异:老分支吃的是 pcre1(Cygwin 包名libpcre-devel),新分支换成了 pcre2(libpcre2-devel)。别猜,直接看源码:
# 在源码根目录执行,比看任何文档都快 grep -rn -i -E 'pcre2?|libedit' configure.ac | head -n 20看到PKG_CHECK_MODULES里写的是libpcre2-8就走 pcre2,写的是libpcre就走 pcre1。装错版本的表现是 configure 阶段报Package requirements (libpcre2-8) were not met,别硬扛,回去换包。
2.2 master/child 双进程与 VSM 在 Windows 上的落点
Varnish 的进程模型是管理进程加工作进程。varnishd起来之后先当管理进程,随后 fork 出一个 child,也就是真正的 cache 进程,所有请求都由 child 处理。两者之间靠 VSM(Varnish Shared Memory)通信,本质是工作目录下的一个文件,双方mmap同一块区域,日志、统计、参数都从这块内存里读写。你敲的varnishstat、varnishlog、varnishadm,全都是去 mmap 这个文件,而不是去连一个 socket。
在 Cygwin 下这条链路是通的,原因是fork()和文件mmap都在 Cygwin 的能力范围内。但它带来两个 Windows 特有的注意点。
一是工作目录必须可写、且路径要干净。默认工作目录是/var/lib/varnish/<主机名>,你可以用-n覆盖。Cygwin 会把 POSIX 路径翻译成 Windows 路径,如果你的目录建在C:\Program Files下面,翻译过去之后普通权限写不进去,child 起不来,管理进程只在日志里丢一句创建失败就退了,看起来像“闪退”。我一般固定放在/var/lib/varnish/<实例名>,对应C:\cygwin64\var\lib\varnish\<实例名>,不存在权限问题。
二是管理进程没有守护进程托管。Linux 上有 systemd,进程挂了会被拉起来;Cygwin 下没有这层,要么你自己开个终端挂着-F前台跑,要么用 Cygwin 自带的服务包装器把它注册成 Windows 服务:
# 把 varnishd 注册成 Windows 服务,参数用单引号整体传入 cygrunsrv -I varnish-inst1 -p /opt/varnish/sbin/varnishd \ -a '-a 0.0.0.0:6081 -T 127.0.0.1:6082 -f /etc/varnish/default.vcl -s malloc,256m -n /var/lib/varnish/inst1 -j none' \ -e CYGWIN=nodosfilewarning cygrunsrv -S varnish-inst1 # 启动 cygrunsrv -E varnish-inst1 # 停止 cygrunsrv -R varnish-inst1 # 卸载-e CYGWIN=nodosfilewarning这行是血泪经验:不加它,Windows 事件日志里会持续刷 POSIX 路径警告,把真正的错误淹掉。
还有一个容易忽略的点:child 进程是要被管理进程重新 exec 起来的,也就是说varnishd会以内部参数再启动一次自己。如果你是从cmd.exe或 PowerShell 里手敲命令启动的,PATH 里没有 Cygwin 的/bin,第二次 exec 找不到cygwin1.dll,症状就是进程起来一瞬间就消失。要么在 Cygwin 终端里启动,要么把C:\cygwin64\bin加进系统 PATH。
2.3 什么时候该用这套组合,什么时候别硬上
我把这几年的使用场景归了两类,列表比任何说明都直观:
| 场景 | 建议 | 关键原因 |
|---|---|---|
| 本机或内网验证 VCL 写法、缓存策略 | 合适 | VCL 语法和后端行为与 Linux 版一致,改完立刻能验 |
| 给 Windows 上的后端加一层临时缓存减压 | 合适 | 单机低并发够用,配置成本远低于换系统 |
| 离线环境、不能装 Linux 的机器上做演示 | 合适 | 一套 Cygwin 加一份构建产物就能带走 |
| 团队只在 Windows 上开发,需要复现线上缓存逻辑 | 合适 | 共享同一份 VCL,减少“我本地是好的” |
| 生产边缘节点、日均千万级请求 | 不建议 | 无 epoll 无 sendfile,事件循环是 poll,长连接一多就顶到天花板 |
| 需要 HTTP/2、TLS 卸载、大规模并发 | 不建议 | 这些要么依赖 OpenSSL 的特定路径,要么依赖内核能力 |
| 要求毫秒级 P99 和稳定吞吐 | 不建议 | Windows 上的定时器和调度精度摆在那 |
一句话总结选型原则:把它当“行为验证器”,别当“性能组件”。你要验证的是缓存逻辑对不对,那它完全够;你要验证的是能扛多少 QPS,那这个数字在 Windows 上没有参考价值。
3. 从零构建一台可跑的实例:Cygwin 环境、configure 参数与首次启动
这一章是从零开始的完整链路。我假设你手上是一台干净的 Windows 加一个 Cygwin 安装器,源码包是这份资源里的压缩包。整个过程大概二十分钟,其中编译占大头。
3.1 用 setup 一次性装齐依赖
别一项一项装,装到一半发现缺autoconf再回头是最浪费时间的。直接一次装齐,装完重开一个终端让 PATH 生效:
# -q 静默模式,-P 后跟逗号分隔的包名,不加空格 ./setup-x86_64.exe -q -P gcc-core,gcc-g++,make,automake,autoconf,libtool,pkg-config,python3,libpcre2-devel,libedit-devel,ncurses-devel,dos2unix,patch,gzip,git,cygrunsrv,curl参数说明就三点。-P后面的包名之间不能有空格,有空格安装器会当成另一个参数。libpcre2-devel是给新分支准备的,如果你的源码是 pcre1 分支,换成libpcre-devel,两个都装上也无所谓。cygrunsrv只有在你打算把服务注册成 Windows 服务时才必要,纯手工调试可以不装。
装完先验一遍,这一步别省:
gcc --version | head -n 1 # 期望看到 gcc (GCC) x.y.z pkg-config --modversion libpcre2-8 # 期望输出一个版本号,报 not found 说明包装错了 pkg-config --modversion libedit # CLI 依赖,缺它 varnishadm 的交互模式会残废 python3 --version # 构建脚本要用,版本别太老任何一条报错,都别往下走。pkg-config找不到包,通常是装了运行库没装-devel包,头文件缺失,编译到一半才会炸。
3.2 源码落地与换行符预处理
源码包从 Windows 那边解压或者拷过来,十有八九带 CRLF 换行。shell 脚本里混进\r,报错信息会非常反直觉,比如$'\r': command not found,新手很容易以为是语法写错了。所以在跑 configure 之前,先把构建系统会读到的文件统一转成 LF:
mkdir -p /usr/src && cd /usr/src tar -xf /cygdrive/d/pkgs/varnish-cache.tar.gz cd varnish-cache-*/ # 只处理构建系统会解析的那几类文件,全量转换没必要还慢 find . -type f \( -name '*.ac' -o -name '*.am' -o -name '*.in' -o -name '*.m4' \ -o -name '*.sh' -o -name 'configure' -o -name 'Makefile*' \) -print0 \ | xargs -0 dos2unix # 兜底检查:还带 CRLF 就别继续 file configure | grep -i crlf && echo "还有 CRLF,回去再转一遍"xargs -0配-print0是为了兼容文件名里带空格的情况;dos2unix是幂等的,重复跑不会坏事。如果你是从 git 仓库拉的源码而不是压缩包,直接设git config core.autocrlf input再 checkout 一次,比事后转换干净。
源码如果是 git 快照而不是发布包,configure脚本可能压根不存在,需要自己生成:
# 只有 configure 不存在时才需要,发布包不用跑 autoreconf -fi-fi是强制重新生成并安装辅助脚本。这一步在 Cygwin 上偶尔会因为libtool版本对不上报错,遇到就升级libtool再来。
3.3 configure 的关键开关与 make
configure 我只用三个开关,其他的交给自动探测。多写反而容易踩到未测过的组合:
./configure \ --prefix=/opt/varnish \ --sysconfdir=/etc/varnish \ --localstatedir=/var \ CFLAGS="-O2 -g" \ 2>&1 | tee configure.log--prefix决定装到哪,我固定用/opt/varnish,好处是跟系统路径隔开,卸载直接删目录。--sysconfdir决定 VCL 和 secret 文件的位置,配成/etc/varnish是为了跟 Linux 上的习惯对齐,将来把配置搬到线上不用改路径。--localstatedir决定运行时的数据目录,也就是 VSM 文件的位置。CFLAGS="-O2 -g"保留调试符号,出问题时 gdb 才看得见东西,代价是二进制大一点,本机调试完全值得。
失败的时候别慌,答案只在两个地方:
# 90% 的构建失败原因在这两处能直接看到 grep -n -i -E 'error|not found|No package' configure.log | head -n 30 tail -n 60 config.logconfig.log才是真相,configure.log只是摘要。常见的失败就三类:缺-devel包、pcre 版本对不上、C 编译器被 Windows 上的其他工具链(比如某个 MinGW)抢了 PATH。第三类表现为config.log里出现x86_64-w64-mingw32-gcc,那就说明你在错误的终端里执行了命令。
configure 过了就编译。-j后面跟核数,Cygwin 下nproc能正常返回:
make -j"$(nproc)" 2>&1 | tee build.log make install编译期比较常见的警告是setproctitle和pthread_setname_np未声明,这两个只影响ps里看到的进程名,不影响功能,可以无视。如果报的是undefined reference到某个pcre2_*符号,那就是 pcre 版本装错了,回去第 2.1 节对一遍。
装完的目录结构值得看一眼,后面配路径要用:
ls /opt/varnish/sbin # varnishd varnishadm varnishstat varnishlog 等 ls /opt/varnish/share # vmod 的 .so/.dll 和文档share下面的 vmod 是运行时dlopen加载的,Cygwin 生成的是.dll后缀。如果你的 VCL 里import了某个 vmod 却报加载失败,先来这里确认文件到底叫什么名字。
3.4 首次启动:-j none、-s malloc 与最小验证
先建目录,然后把 VCL 单独编译一遍,最后才启动进程。这个顺序能帮你把“配置错”和“环境错”分开:
mkdir -p /var/lib/varnish/inst1 /var/log/varnish /etc/varnish # -C 只编译 VCL 不启动进程,语法错会直接指到行号,比启动后再看日志快十倍 /opt/varnish/sbin/varnishd -C -f /etc/varnish/default.vcl > /dev/null && echo "VCL OK" # 正式启动,前台跑,方便看管理进程和 child 的输出 /opt/varnish/sbin/varnishd \ -a 0.0.0.0:6081 \ -T 127.0.0.1:6082 \ -f /etc/varnish/default.vcl \ -s malloc,256m \ -n /var/lib/varnish/inst1 \ -j none \ -p default_ttl=120s \ -F每个参数都要说清楚,因为它们是最容易配错的一层:
-a是客户端监听地址。写127.0.0.1:6081只有本机能连,写0.0.0.0:6081才是所有网卡。第一次启动 Windows 会弹防火墙对话框,选允许,而且要注意它问的是varnishd.exe这个进程,不是端口。
-T是管理接口,只绑回环,别暴露到外网。varnishadm靠它连。
-f指向 VCL 文件。-s是存储后端,malloc,256m表示在内存里分配 256MB 做缓存,这是最简单的选择,也最不容易出问题。老版本还有基于文件 mmap 的file存储,Cygwin 下也能用,但 Windows 的文件锁和杀毒软件扫描会让它表现很不稳定,我一般不用。
-n是实例工作目录,必须提前建好并且可写。多个实例就换不同的-n,互不干扰。
-j none是这套组合的关键。默认的unixjail 会尝试降权到指定用户,Windows 上这套 uid/gid 映射语义对不上,child 起不来但报错很含糊。直接关掉降权,用当前用户跑,代价是安全性降低,但本机验证场景完全可以接受。
-p是启动时预设参数,这里把兜底 TTL 设成 120 秒。
启动之后另开一个终端验证。先看端口,再看响应头:
# Cygwin 里没有 ss,用 Windows 的 netstat 确认监听 netstat -ano | findstr :6081 curl -sI http://127.0.0.1:6081/ | head -n 20 curl -s -o /dev/null -w 'code=%{http_code} time=%{time_total}\n' http://127.0.0.1:6081/再敲一下管理口,确认 CLI 能通:
/opt/varnish/sbin/varnishadm -S /etc/varnish/secret -T 127.0.0.1:6082 "vcl.list" /opt/varnish/sbin/varnishadm -S /etc/varnish/secret -T 127.0.0.1:6082 "backend.list"-S指向 secret 文件,首次启动时varnishd会在--sysconfdir下面生成。如果这条命令报认证失败,去看启动日志里打印的 secret 路径,Cygwin 的路径翻译有时候会跟你以为的不一样。
4. VCL 落地:把默认配置改成能扛住真实流量的一套
默认 VCL 只是个能跑的最小样本,后端指向 127.0.0.1:8080,缓存策略基本等于没有。真要用起来,得把后端定义、缓存键、Cookie 处理和静态资源规则都补上。这一节的配置可以直接抄,但每段后面的说明要看,因为参数取值跟你的后端行为强相关。
4.1 VCL 版本声明与内置子程序执行链
VCL 是编译型 DSL,varnishd启动或者vcl.load时把它编译成 C,再编成共享库dlopen进来。所以 VCL 里的语法错误是启动期错误,不是运行期错误,这点跟 nginx 的配置很像,但它连类型都要检查,比 nginx 严格得多。
第一行必须是版本声明。写高了版本不支持,varnishd会明确告诉你最高支持到哪个版本:
vcl 4.1;请求进来之后的执行链,记住主干就行:vcl_recv决定"这个请求要不要查缓存",返回pass直接过、pipe走裸 TCP、hash进查找;然后vcl_hash算缓存键;命中走vcl_hit,未命中走vcl_miss再进后端;后端回来进vcl_backend_response,这是唯一能改beresp.ttl的地方;最后vcl_deliver把响应交出去,这里可以打调试头。
最容易搞混的是两个 return 的位置:在vcl_recv里写return (lookup)是"继续查缓存",写return (hash)也是同样效果(4.x 里两者等价,但语义上hash更明确);在vcl_backend_response里写return (deliver)是"把这个响应交给客户端并缓存",写return (abandon)是"我不想要了"。
4.2 后端定义与超时参数怎么定
后端定义里,除了 host 和 port,真正影响线上表现的是三个超时和一个连接上限:
backend default { .host = "127.0.0.1"; .port = "8080"; .host_header = "app.internal"; // 后端按 Host 分流时必须设 .connect_timeout = 1s; // 建连超时,内网可以很短 .first_byte_timeout = 15s; // 后端首字节,慢接口要放宽 .between_bytes_timeout = 2s; // 字节间隔,防后端半死不活挂着 .max_connections = 200; // 到后端的最大连接数 }.connect_timeout设 1 秒是我踩过坑之后的习惯:内网建连正常在毫秒级,1 秒还没连上说明后端进程已经挂了或者端口错了,早点失败让客户端重试比挂着强。
.first_byte_timeout才是最需要跟业务对齐的。后端有个报表接口要跑 30 秒,你这里设 15 秒,缓存层就先返回 503 了,而后端还在慢慢算。我一般按最慢的接口再留 50% 余量。
.between_bytes_timeout是防"半死"后端的:TCP 连上了,第一个字节也回来了,但后面每隔 10 秒才吐一点,没有这个超时就会一直挂着占线程。
.host_header特别容易被忽略。后端如果是虚拟主机部署,靠 Host 头分流,你不设这个字段,缓存层透传的是客户端原始的 Host(比如cache.local:6081),后端匹配不到虚拟主机,返回 404,然后你花半天查缓存配置。
4.3 缓存键、Cookie 与静态资源的三段处理
完整的 VCL 我拆成三段看。第一段在vcl_recv里做分流,核心思想是"只缓存该缓存的":
sub vcl_recv { # 只缓存 GET/HEAD,其他方法直接放行不管 if (req.method != "GET" && req.method != "HEAD") { return (pass); } # 带 Authorization 的一律不缓存,避免串号 if (req.http.Authorization) { return (pass); } # 静态资源剥掉 Cookie,让它能进缓存 if (req.url ~ "\.(css|js|png|jpg|jpeg|gif|ico|svg|woff2?)$") { unset req.http.Cookie; return (hash); } # 动态接口一律不缓存,交给后端 if (req.url ~ "^/api/") { return (pass); } set req.backend_hint = default; return (hash); }这段的关键在顺序:先排除非 GET/HEAD,再排除带认证的,最后才是静态资源剥离 Cookie。顺序反了,静态资源分支会先匹配上,带 Authorization 的静态请求也被缓存了,这就是安全问题了。
第二段在vcl_backend_response里,决定"后端返回的东西能不能缓存、存多久":
sub vcl_backend_response { # 后端明确说不缓存,就尊重它,但要留个兜底供调试 if (beresp.http.Cache-Control ~ "no-store|private") { set beresp.uncacheable = true; set beresp.ttl = 0s; return (deliver); } # 带 Set-Cookie 的响应默认不可缓存,静态资源例外 if (beresp.http.Set-Cookie) { if (bereq.url ~ "\.(css|js|png|jpg|jpeg|gif|ico|svg|woff2?)$") { unset beresp.http.Set-Cookie; } else { set beresp.uncacheable = true; return (deliver); } } # 静态资源给一天,动态内容沿用后端 TTL,没有就用兜底值 if (bereq.url ~ "\.(css|js|png|jpg|jpeg|gif|ico|svg|woff2?)$") { set beresp.ttl = 1d; } elsif (beresp.ttl <= 0s) { set beresp.ttl = 120s; } # 宽限期:TTL 过期后还能拿旧对象顶一段,配合后端故障时很有用 set beresp.grace = 10m; return (deliver); }beresp.uncacheable = true这个赋值必须跟return (deliver)成对出现,只设标志不 return,后面的逻辑还会继续跑,很容易出现"明明设了不可缓存但还是进了缓存"的玄学现象。
第三段在vcl_deliver里打命中标记,这是排查阶段最有用的东西,上线前别急着删:
sub vcl_deliver { if (obj.hits > 0) { set resp.http.X-Cache = "HIT"; } else { set resp.http.X-Cache = "MISS"; } set resp.http.X-Cache-Hits = obj.hits; return (deliver); }obj.hits是这个对象被命中的次数,MISS 时是 0。用curl -I连着打两次同一个 URL,第二次看到X-Cache: HIT就说明整条链路通了。
4.4 用 varnishstat 和 varnishlog 验证命中率
配置写完不算完,得看数据。varnishstat给的是全局计数器,-1表示只输出一次,-f过滤你关心的字段:
# 注意 -n 必须跟启动时的实例目录一致,否则看到的是另一个实例的数据 /opt <p> <a href="https://download.csdn.net/download/weixin_42118423/17429358" style="color:#ec7500;font-size:14px;"> 本文还有配套的精品资源,点击获取 </a> <img alt="menu-r.4af5f7ec.gif" src="https://csdnimg.cn/release/wenkucmsfe/public/img/menu-r.4af5f7ec.gif" style="width:16px;margin-left:4px;vertical-align:text-bottom;cursor:text;"> </p>