ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Cygwin下编译安装Varnish Cache:Windows环境HTTP加速与缓存实践

Cygwin下编译安装Varnish Cache:Windows环境HTTP加速与缓存实践

简介:适用于 Windows/Cygwin 的开源 Varnish Cache 适配包,为需要在非 Linux 环境部署高性能 HTTP 缓存的开发者与运维人员提供了可行方案。资源通过源码级修补,解决了文件路径、网络 I/O、线程管理等兼容性问题,使 Varnish 具备高速缓存、高并发、VCL 可编程策略等核心能力,并附带了最小化的 cygwin.dll 与 gcc 编译器分发版,无需安装完整 Cygwin 套件即可运行,降低系统占用和部署复杂度。压缩包共 118 个文件,体积仅 5.54MB,包含 36 个头文件、18 个静态库、14 个可执行程序、13 个动态链接库,以及 readme、bat、vcl、conf 等配置说明文件,结构清晰,便于集成与二次开发。目前已有 104 人学习使用。对于想在不完整安装 Linux 环境的情况下体验 Varnish Cache 高性能缓存服务的 Windows 用户,这是一份实用且轻量的开源选择。

1. Windows 上的开源 HTTP 加速器:为什么要在 Cygwin 里折腾 Varnish Cache

如果你做的是自建 CDN、接口聚合层或者给内部系统做缓存节点,Varnish Cache 这个名字你一定不陌生。它是开源软件里最老牌的 HTTP 加速器之一,VCL 配置语言可以精确控制哪些 URL 缓存、缓存多久、后端挂了怎么办,灵活度比 Nginx 的 proxy_cache 高一个量级。问题是它官方只支持 Linux/Unix 系平台,根本不提供 Windows 版本。想在 Windows 服务器上把它跑起来,目前最接地气的路径就是 Cygwin:用 POSIX 兼容层把 Varnish 从源码编译出来。这条路能走通,但共享内存挂载、VCL 编译链、线程模型稳定性这些坑会一个接一个冒出来。这篇笔记把从依赖安装到首屏缓存命中的完整过程拆开讲,适合正在做缓存层选型、或者被 Windows 环境绑住手脚又想用 Varnish 的开发者。先说结论:能跑,但前提是你知道坑在哪。

2. 先弄懂 Varnish 的加速原理:内存缓存、VCL 编译与请求生命周期

2.1 内存优先的缓存架构:为什么命中后响应可以极快

Varnish 和 Nginx 缓存最大的区别在存储介质。Nginx 的 proxy_cache 默认把缓存对象写到磁盘,命中后要从磁盘读回,性能受 I/O 队列长度影响;Varnish 默认把所有对象放在内存里,缓存命中就是一次内存拷贝,后端完全不参与。官方在公开演示里跑到过十万级 QPS,普通应用场景下只要命中率维持在 80% 以上,客户端感知到的延迟基本能压在 1ms 以内。这个量级是磁盘型缓存很难做到的。

这个“内存优先”的设计并不只是把文件从硬盘挪到 RAM。Varnish 自己维护了一套对象存储层,名字叫 stevedore,是插拔式的:默认的 malloc 引擎直接吃进程堆内存,按对象的 size、ttl、last use 做 LRU 淘汰;另有 file 引擎,用 mmap 映射磁盘大文件,对象仍然落在内存映射区域内。在 Cygwin 里我基本只敢用 malloc,因为 file 引擎依赖对大文件的 mmap,而 Cygwin 的 mmap 对超过 2GB 的大文件支持一直很玄学,跑起来容易翻车。

命中后的快速响应还依赖另一个设计:Varnish 的 HTTP 解析是自己在用户态完成的,不走系统 HTTP 库,连接管理尽量用非阻塞事件驱动。worker 线程只在有事件可处理时才被调度,而不是像传统多进程 Web 服务器那样一连接一进程。这个模型在 Linux 上非常成熟,在 Cygwin 里线程实现会打折扣,但事件驱动的大框架还是保留下来了,这也是为什么低并发时它依然能保持低延迟。

2.2 VCL 不是解释执行的:从配置到 C 代码再到共享库

VCL 是 Varnish Configuration Language 的缩写,很多人第一次接触会以为它像 Lua 一样被解释执行。实际上 VCL 文件的处理路径是:varnishd 启动时把 VCL 文件交给编译器(VCC),先做语法检查,然后生成一段 C 代码,再调用系统的 C 编译器把它编成共享库,最终动态加载进 varnishd 进程。

这意味着什么?意味着运行环境里除了 varnishd 本体,还必须有一套可用的编译链:gcc、make、libtool,缺一不可。在 Linux 上这些通常都预装了,在 Cygwin 里如果当初只装了最小包,第一次 vcl.load 就可能在“调用 cc 编译 VCL 生成的 C 文件”这个环节失败。后面避坑章节会专门写这个问题。这里先记住一个结论:VCL 的性能开销被编译这一步抵消了,运行时它和手写的 C 代码几乎没有差别;但配置出错时,报错信息往往不是逻辑错误,而是编译错误,排查起来要有点耐心。

VCL 有版本之分,常见的是 4.0 和 4.1。文件开头必须写vcl 4.1;这种版本声明,varnishd 才能在对应版本语义下解释。4.0 到 4.1 的差异不算大,但有些返回路径和函数行为变了,比如 vcl_recv 里 return (hash) 的语义在 4.1 里更明确。后面给最小配置时直接用 4.1 语法,如果你手里的源码是较老的版本,可能要退回 4.0。

2.3 请求的完整旅程:recv、hash、miss、fetch、deliver

理解 VCL 之前先看一个请求在 Varnish 内部的完整路径,我用一个简化流程说明:

  1. 客户端连接进入 acceptor,varnishd 解析出请求行与头部;
  2. 进入 vcl_recv:这里是所有缓存策略的入口,你可以决定请求走 hash、pass(穿透)还是 pipe(隧道透传);
  3. 走 hash 的请求会按“主机 + URL + 其他参与 hash 的字段”生成一个键,去全局哈希表里做 lookup;
  4. 命中的直接走 vcl_hit,然后 vcl_deliver 把对象拼装成响应发给客户端;
  5. 未命中的进入 vcl_miss,varnishd 替它选一个 backend,发起 fetch;
  6. 后端响应的内容流进 vcl_backend_response,在这里设置 beresp.ttl、beresp.grace 等缓存参数,对象存入 stevedore;
  7. 最后回到 vcl_deliver,客户端收到响应,这一次的 miss 已经被写进缓存,下一次相同 hash 键就变成 hit。

这套流程里的核心对象有三个:req 代表客户端请求,beresp 代表后端响应,resp 代表最终发给客户端的响应。很多人刚上手时容易把 beresp 和 resp 搞混:beresp 是你设置缓存决策的对象,resp 是在 vcl_deliver 里给你的响应做最后修改的对象。三者的定位差异可以这样区分:

对象来源典型用途常见动作
req客户端请求判断是否可缓存req.method、req.url、req.http.Cookie
beresp后端响应设置缓存参数beresp.ttl、beresp.status、beresp.grace
resp发给客户端的响应响应头改写resp.http.…

这个区分直接决定了你写的 VCL 对不对:缓存决策放错对象,最常见的后果就是 TTL 设置无效,缓存命中率上不去。

3. 在 Cygwin 下编译安装 Varnish:依赖清单、configure 参数与常见编译报错

3.1 环境准备:用 setup-x86_64.exe 把依赖一次装齐

Cygwin 的软件安装我一般直接用官方 setup-x86_64.exe,不要偷懒用网上找的省略版,包源不全会让后面编译到处碰壁。打开安装界面后,默认包基本不够用,需要手动搜索并勾选这些包。编译 Varnish 的核心依赖是 gcc-core(C 编译器)、gcc-g++(C++ 编译器)、make、autoconf、automake、libtool、flex、bison、pkg-config、libpcre2-devel(VCL 正则表达式依赖)、libedit-devel、ncurses-devel。另外建议把 python3 也装上,有些版本的构建脚本会用 python 生成文档,缺了会卡在最后一步。

安装完成后在 Cygwin 终端里验证一下版本,确认所有工具都能被正常调用:

gcc --version make --version flex --version bison --version pkg-config --version

这里要注意 flex 和 bison 的版本。我碰到过一个老环境 flex 是 2.5.x,VCL 解析阶段一直报语法错误,换到新版才过。如果包管理器里拉到的版本太老,去 Cygwin 的 package 列表里找更高版本,或者直接源码编译 flex,这一步别省。自动工具链的版本对 Varnish 这种重度依赖生成器的项目影响很大。

3.2 获取源码与生成 configure:autogen.sh 背后的四件套

Varnish 的源码是高度自动化的 autotools 工程,很多历史版本发布时已经带上了生成好的 configure 脚本。但为了稳妥,我在 Cygwin 下从来都是先跑 autogen.sh 再 configure。这里解释一下为什么:Cygwin 的构建环境在系统头文件、链接库的默认路径上和 Linux 有差别,直接拿官方 configure 脚本跑有可能探测不到 pcre2 或 ncurses,自动重新生成一遍配置脚本,能让 configure 按当前 Cygwin 的实际环境重新探测。

autogen.sh 内部本质上是 autoreconf -fvi,依赖的四个工具正好是前面装的 autoconf、automake、libtool 和 pkg-config。在源码根目录执行:

# 进入源码根目录 sh autogen.sh

把四个工具装齐,这一步基本就是几秒钟的事,输出里能看到生成 configure、更新 aclocal.m4 之类的信息。如果这里报错 autoreconf: command not found,说明 autoconf 或 automake 没装全,回去补包再跑一遍。

3.3 configure 与 make:常见的三个编译报错

configure 的常用参数我先给一个能直接跑的版本:

./configure --prefix=/usr/local/varnish --localstatedir=/var/local/varnish make -j4 make install

--prefix 指定安装根目录,这里装到 /usr/local/varnish 下,所有二进制和配置集中在同一层,之后卸载或升级都方便;--localstatedir 指定运行时状态目录,varnishd 的 PID 文件、共享内存文件会被放在这里,分开管理比混在 prefix 里干净。make -j4 是四路并行编译,机器余量大可以开 -j8,但注意 Cygwin 的 make 对并行任务偶有偶发失败,真要是并行编译报错,先退回单线程 make 重试,不要纠结。

编译阶段最常见的三个报错。第一个是pcre2.h: No such file or directory,原因是缺 libpcre2-devel,回去装包。第二个是 flex 版本问题,报错信息里会有 syntax error in scanner 或者 vcc/yacc 相关的内容,解决方式是升级 flex。第三个是链接阶段找不到 ncurses 符号,通常是 ncurses 的 .a 归档格式与当前 gcc 不匹配,把 ncurses 和 ncurses-devel 全部重新安装一次就能解决。

configure 阶段报checking for pkg-config... no也比较常见,这个不用解释,pkg-config 包没装。这些报错在 Linux 上很少见,在 Cygwin 上几乎每个人都会碰上一两条,耐心看输出位置就能定位。

3.4 安装后的目录布局与 varnishd 自检

安装完成以后,最需要记住的目录是:二进制在 prefix/bin 和 prefix/sbin,默认 VCL 文件在 prefix/etc/varnish,运行时状态在 localstatedir 下。安装后立刻做一次版本自检:

/usr/local/varnish/sbin/varnishd -V

如果 -V 能正确输出版本号,说明二进制和动态库的路径都没有问题。这里有个坑:在 Cygwin 下动态库的搜索路径不完全等价于 Linux 的 LD_LIBRARY_PATH,如果自检时报 error while loading shared libraries,先 echo $PATH 确认 /usr/local/varnish/lib 和 /usr/local/varnish/lib/varnish 是否在 PATH 里,或者用 cygpath 转换路径后放到 PATH 前面。这一步虽然琐碎,但能提前挡掉后面一大半问题。

4. 写第一份 VCL 并启动 varnishd:最小缓存策略、启动参数与命中验证

4.1 最小 VCL 脚本:backend、缓存判定与 TTL 控制

先给一个最小可用配置,把它保存为 /usr/local/varnish/etc/varnish/default.vcl:

vcl 4.1; backend default { .host = "127.0.0.1"; .port = "8080"; } sub vcl_recv { # 非 GET/HEAD 请求不缓存,直接穿透到后端 if (req.method != "GET" && req.method != "HEAD") { return (pass); } # /admin 下面的动态接口强制穿透 if (req.url ~ "^/admin") { return (pass); } return (hash); } sub vcl_backend_response { # 默认给 200~400 之间的响应设置 1 小时 TTL if (beresp.status >= 200 && beresp.status < 400) { set beresp.ttl = 1h; set beresp.grace = 2h; } return (deliver); }

这段 VCL 的逻辑值得拆开看。vcl 4.1;必须写在第一行,缺了这行 varnishd 直接拒绝加载。backend default 声明了一个名为 default 的后端,指向本机 8080 端口。vcl_recv 里,所有非 GET/HEAD 请求和 /admin 开头的 URL 都返回 pass,不做缓存;其余请求走 hash 流程,也就是进入缓存查找。

vcl_backend_response 里做了缓存参数的设定:响应状态码在 200 到 400 之间时,设置缓存生命周期为 1 小时,宽限期 2 小时。这里的一个常见误区是忽略后端响应头里的 Cache-Control。如果后端返回了Cache-Control: no-cache,Varnish 默认会尊重这个头,导致你设置的 ttl 不生效,命中率上不去。想让 Varnish 强制覆盖,需要在 vcl_backend_response 里显式 unset beresp.http.cache-control,但做之前要想清楚后端是不是有意禁止缓存。

4.2 启动命令解析:-a、-s、-T、-n 四个参数

VCL 写好后,用下面的命令启动 varnishd:

/usr/local/varnish/sbin/varnishd \ -f /usr/local/varnish/etc/varnish/default.vcl \ -s malloc,1G \ -a :6081 \ -T :6082 \ -n /tmp/varnish

每个参数的含义和 Cygwin 下的注意点:

参数含义Cygwin 下的注意点
-f指定 VCL 文件路径必须使用 Cygwin 的 POSIX 路径,不要写 C:\ 形式
-s指定存储引擎与容量malloc,1G 表示内存上限 1GB,不建议用 file 引擎
-a对外监听地址放 6081 而不是 80,避开 Windows 常见端口占用
-T管理端口6082 是 varnishadm 的默认管理口
-n工作目录固定它,varnishstat、vcl 缓存都会在这里

启动后终端不会有任何输出,用 ps 确认进程状态:

ps -ef | grep varnishd

Cygwin 下会看到两个 varnishd 进程,一个是 manager,一个是 child,这是正常现象,别误判成重复启动。

4.3 用 varnishstat 验证第一次缓存命中

进程起来以后,先访问两次后端,再用 varnishstat 看命中率:

curl http://127.0.0.1:6081/ curl http://127.0.0.1:6081/

然后在另一个终端执行:

/usr/local/varnish/bin/varnishstat -n /tmp/varnish

注意 varnishstat 必须加 -n /tmp/varnish,和启动时的 -n 保持一致,否则它去默认目录找统计文件,什么都读取不出来。这个坑我踩过一次,统计结果全为 0,还以为是缓存没生效,实际上是指针找错了地方。

看 MAIN.cache_hit 和 MAIN.cache_miss 两个计数器:正常情况第二次 curl 会让 cache_hit 加 1,第一次访问之后 cache_miss 也为 1。如果 cache_hit 一直是 0,回到 VCL 里检查是不是哪个 return (pass) 把请求放走了,或者后端响应带了 Cache-Control: no-cache,Varnish 尊重了这个头,导致对象不可缓存。

5. Cygwin 环境下 Varnish 避坑指南:共享内存、编译报错与 VCL 版本血泪

5.1 Shared Memory 启动失败:/dev/shm 挂载与路径坑

现象:varnishd 启动时报 Could not open shm 或 Cannot create shared memory file;varnishstat 运行时报 Cannot open shared memory。带 -n 参数指定了工作目录也没用。

原因:Varnish 的统计和日志依赖 POSIX 共享内存,Cygwin 对这一块的实现位于虚拟文件系统的 /dev/shm。部分精简安装或安全策略下,这个目录不存在或没有被正确标记为共享内存挂载点,shm_open 就必然失败。

解决:先执行mkdir -p /dev/shm,再用mount命令确认 tmpfs 形式的挂载存在;如果没有,执行mount -t tmpfs none /dev/shm。之后重启 varnishd。这个坑在全新 Cygwin 环境里出现概率相当高,建议把这两条命令写进部署脚本,作为前置步骤。

5.2 编译报错:pcre2 头文件缺失与 flex/bison 版本不足

现象:configure 阶段直接报错退出,常见输出是 pcre2.h: No such file or directory;或者 make 进到一半在 VCC 解析器相关源文件上报 syntax error。

原因:前一种纯粹是缺 libpcre2-devel 包;后一种则是 flex/bison 版本太老,生成的 C 解析器和 Varnish 源码里的语法定义不兼容。Cygwin 软件源里有些旧版本 flex 的算法行为与众不同,生成的解析器会在某些输入序列上出错。

解决:缺头文件就补libpcre2-devel包,这个没有捷径。flex/bison 版本问题,先检查flex --version,如果低于某个较新的小版本,直接从 flex 官网源码编译安装一套新的,放到 PATH 最前面。替换完记得重跑 autogen.sh,让 VCL 编译器重新生成。

5.3 高并发下进程崩溃:Cygwin 线程模型的代价

现象:ab -n 20000 -c 200压测跑到一半,varnishd 无响应或进程直接消失;Windows 事件日志里能看到 varnishd.exe 崩溃记录。低并发或纯功能测试时一切正常,一上并发就翻车。

原因:Varnish 的 child 进程从 manager fork 出来,内部再起大量 worker 线程。Cygwin 的 fork 实现是通过复制父进程内存模拟的,多线程进程在 fork 时只保留调用线程,Windows 内核线程模型和 Linux pthread 的实现差异会在高并发时暴露,线程同步和内存可见性问题集中爆发。

解决:压测并发控制在 100 以内,同时用 varnishadm 调低线程池上限,降低 Cygwin 线程调度的压力:

/usr/local/varnish/bin/varnishadm -n /tmp/varnish param.set thread_pool_max 500 /usr/local/varnish/bin/varnishadm -n /tmp/varnish param.set thread_pool_min 50

这个环境的定位应该是开发测试或边缘缓存节点,Windows 生产环境真要扛大流量,还是建议虚拟机里跑 Linux。

5.4 VCL 加载失败:版本声明与语法差异血泪

现象:启动时报 Could not parse VCL file,具体输出可能是 Unsupported VCL version '4.1' 或 unknown parameter,但你确认语法写对了。

原因:VCL 的版本号和 varnishd 编译时的版本强绑定。老版本源码编出来的 varnishd 只认识 vcl 4.0;,你在配置头写了 4.1 就直接拒绝;反过来,太新的 varnishd 对旧版 VCL 的处理方式也可能有差异。这个问题的隐蔽性在于同样的语法,在不同版本里行为不完全一样。

解决:启动前先跑varnishd -V看版本,再决定写vcl 4.0;还是vcl 4.1;。如果源码包里带了多份 VCL 模板,按版本挑选最接近的那一份,不要盲目用最新的。加载失败时先看错误信息的行号,绝大多数是返回动作写错或函数签名不匹配,逐行对版本差异表检查。

6. 用 varnishadm 热加载 VCL:改配置不再重启的后悔药

Varnish 的 VCL 和 Nginx 配置不一样,它有原生支持的热加载机制:vcl.load 先把新配置编译好并挂一个标签,vcl.use 原子切换。这相当于给线上配置上了一道后悔药,编译失败时完全不影响当前运行中的配置。

我的习惯是先把当前生效的 VCL 存一个标签,再加载新版本:

/usr/local/varnish/bin/varnishadm -n /tmp/varnish vcl.load old_vcl /usr/local/varnish/etc/varnish/default.vcl /usr/local/varnish/bin/varnishadm -n /tmp/varnish vcl.use old_vcl

之后每次修改配置,都走“新标签加载 + 切换”的流程:

/usr/local/varnish/bin/varnishadm -n /tmp/varnish vcl.load new_vcl /usr/local/varnish/etc/varnish/default.vcl /usr/local/varnish/bin/varnishadm -n /tmp/varnish vcl.use new_vcl

一旦发现新配置下命中率暴跌或响应异常,一行命令切回旧标签:

/usr/local/varnish/bin/varnishadm -n /tmp/varnish vcl.use old_vcl

用 vcl.list 可以查看所有已加载标签和当前生效状态。这个流程比 restart 进程安全得多,因为 VCL 加载失败时,vcl.use 根本不会执行,当前配置原封不动。唯一要注意的是 vcl.load 阶段同样需要调用 gcc 编译生成共享库,所以如果报错信息指向链接阶段,先回 Cygwin 环境确认 gcc 和相关库还在。

从那以后我每次改 VCL 都强制走一遍 vcl.load + vcl.use,绝不直接重启 varnishd。这个习惯救过我两次线上配置事故,希望也帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进