1. 背景#
HAProxy 官方不提供 Windows 二进制文件。如果你在 Windows 环境下需要用 HAProxy 做反向代理或负载均衡,哪怕只是开发测试用途,要么自己编译,要么找别人编译好的。
我从 2021 年开始在 GitHub 上维护 HAProxyForWindows 项目,提供预编译的 Windows 版 HAProxy。最早的做法很原始:在本地 Windows 机器上装 Cygwin,手动下载源码、编译、收集 DLL、打包上传 Release。每次 HAProxy 发新版本都要重复一遍这个流程。
这个方式有几个明显的问题:版本更新靠人记得去做,经常滞后几周甚至几个月;编译环境的依赖全靠个人记忆维护,换台机器就可能编不过;DLL 收集容易遗漏,用户下载后跑不起来。
这篇文章记录了我如何把这个流程迁移到 GitHub Actions 上,实现全自动的多版本构建和发布。
2. 核心思路#
最终方案的设计目标是三件事:自动检测上游新版本并触发构建;支持所有在维护的 HAProxy 分支(LTS + Stable + Dev)同时构建;构建完成后自动创建 GitHub Release。
整个 workflow 拆成两个阶段。第一阶段在 Ubuntu runner 上检测版本、构建 matrix;第二阶段在自托管的 Windows runner 上执行实际编译。之所以分成两阶段,是因为版本检测只需要 curl 和 jq,跑在 GitHub 提供的免费 Ubuntu runner 上就够了,没必要占用自己的 Windows 机器。
为什么用自托管 Windows Runner 而非 GitHub 托管的 windows-latest#
| 方案 | 优点 | 缺点 |
|---|---|---|
GitHub 托管 windows-latest | 无需维护,开箱即用 | 每次都要重新安装 Cygwin(~5 分钟),构建环境不稳定 |
| 自托管 Windows Runner | Cygwin 预装,编译快,环境一致 | 需要维护一台 Windows 机器 |
最初我尝试过在 windows-latest 上动态安装 Cygwin,但每次 CI 跑起来光装环境就要好几分钟,而且 Cygwin 的包版本可能因为镜像更新而变化,导致构建结果不一致。最终选择自托管 runner,把 Cygwin 环境作为前置条件固定下来。
Runner 前置条件:Cygwin 环境准备#
自托管 runner 需要预装 Cygwin 和编译所需的全部开发包。以下是完整的包列表及其对应的编译参数:
| 包名 | 版本(参考) | 用途 |
|---|---|---|
| gcc-core | 13.4.0 | C 编译器 |
| make | 4.4.1 | 构建工具 |
| openssl | 3.0.19 | TLS/SSL 支持(USE_OPENSSL=1) |
| libpcre2-devel | 10.47 | 正则表达式 JIT(USE_PCRE2=1) |
| zlib-devel | 1.3.1 | 压缩支持(USE_ZLIB=1) |
| liblua-devel | 5.4.4 | Lua 脚本支持(USE_LUA=1) |
| lua | 5.4.4 | Lua 运行时 |
| libcrypt-devel | 4.5.2 | 加密支持(USE_CRYPT_H=1) |
| wget | 1.25.0 | 下载源码 |
| tar | 1.35 | 解压源码 |
| sed | 4.9 | Makefile patch |
一键安装命令:
| |
安装后验证环境:
| |
3. 技术选型与原理#
3.1 为什么是 Cygwin 而不是其他方案#
在 Windows 上编译 HAProxy 有几种可能的路径:
| 方案 | 可行性 | 说明 |
|---|---|---|
| Cygwin | ✅ 官方支持 | HAProxy Makefile 内置 TARGET=cygwin,是唯一官方认可的 Windows 编译目标 |
| MSYS2/MinGW | ❌ 不可行 | HAProxy 不支持 MinGW 编译目标,无法生成可用的二进制文件 |
| WSL2 | ❌ 产物不对 | 编译出的是 Linux ELF 二进制,不是 Windows PE,只能在 WSL 内运行 |
| 交叉编译 | ❌ 不可行 | HAProxy 没有 Windows 交叉编译的支持 |
没什么可选的,Cygwin 是唯一的路。Cygwin 提供了一个 POSIX 兼容层,让 HAProxy 的 Unix 代码能在 Windows 上编译运行,代价是运行时需要依赖 cygwin1.dll 等动态库。
3.2 编译参数选择#
最终使用的编译参数:
| |
几个关键决策:
USE_PROMEX=1 启用内置的 Prometheus exporter,这样用户不需要额外部署 exporter 就能接入监控。USE_LUA=1 启用 Lua 脚本支持,可以在配置中嵌入自定义逻辑。USE_GETADDRINFO=1 启用 DNS 解析,这个在早期版本里是不支持的,加上之后 HAProxy 可以解析后端的域名。USE_MATH=1 是 Lua 数学库依赖,不加的话某些 Lua 脚本会报错。
有些参数在 Cygwin 下是不可用的。USE_EPOLL、USE_LINUX_SPLICE、USE_LINUX_TPROXY 这些 Linux 专有特性在 Windows 上没有对应实现,不能开启。USE_QUIC 也不行,QUIC 支持依赖 Linux 的 UDP 相关系统调用。
3.3 Lua 5.3 到 5.4 的升级#
项目最初使用 Lua 5.3,后来升级到了 5.4。升级过程并不复杂,主要就是改编译参数里的 LUA_LIB_NAME=lua5.4,但需要确认几件事:
Cygwin 仓库里有 lua5.4-devel 包可用;编译后 haproxy -vv 输出中显示 Built with Lua version : Lua 5.4.x;运行时加载 Lua 脚本正常工作。我在本地 Windows 上用 PowerShell 调用 Cygwin bash 做了完整的验证,包括写一个简单的 Lua HTTP service 测试运行时兼容性,确认没问题后才改 workflow。
3.4 –export-dynamic 链接器警告的修复#
Cygwin 下编译 HAProxy 会产生一个烦人的警告:
| |
这是因为 HAProxy 的 Makefile 里写死了 -Wl,--export-dynamic,这是一个 ELF(Linux)的链接器选项,Cygwin 的 PE 格式不支持。虽然不影响编译结果和运行,但在 CI 中 PowerShell 会把 stderr 输出当作错误,导致 pipeline 失败。
解决方案是在编译前 patch Makefile:
| |
一行搞定,干净利落。
4. 具体实现#
4.1 整体架构#
flowchart TD
A["定时触发 / 手动触发"] --> B["detect-versions (Ubuntu)"]
B --> C{"扫描 haproxy.org\n获取所有分支最新版本"}
C --> D{"对比 GitHub Release\n过滤已发布版本"}
D --> E["输出 Matrix JSON"]
E --> F["build (Self-hosted Windows)\nMatrix 并行"]
F --> G["下载源码"]
G --> H["Patch Makefile"]
H --> I["Cygwin 编译"]
I --> J["收集 DLL 依赖"]
J --> K["打包 ZIP"]
K --> L["commit-to-repo\n更新 README"]
L --> M["创建 GitHub Release"]
4.2 版本检测与 Matrix 构建#
第一个 job detect-versions 负责扫描 HAProxy 官网,获取所有在维护分支的最新版本号,然后和已有的 GitHub Release 对比,只构建新版本。
核心逻辑是两层循环:先扫稳定版(2.8, 3.0, 3.1, 3.2, 3.3),再扫开发版(3.4)。稳定版和开发版的源码路径不同,稳定版在 /download/3.3/src/,开发版在 /download/3.4/src/devel/。这是一个实际踩过的坑——最初没区分这两个路径,导致开发版下载 404。
| |
workflow 同时支持两种触发方式:定时触发(每天 UTC 02:00 自动扫描)和手动触发(指定版本号或填 all 全量扫描)。
4.3 编译核心:PowerShell 调 Cygwin Bash#
在 Windows runner 上,GitHub Actions 的 shell 是 PowerShell,但编译需要在 Cygwin 环境下执行。这中间有一个关键的桥接函数 Invoke-Bash:
| |
这个函数解决了三个实际问题:
set -o igncr 让 bash 忽略 Windows 风格的 \r 回车符,否则脚本会报语法错误。$ErrorActionPreference = 'Continue' 临时关闭 PowerShell 的严格错误处理——Cygwin 编译器产生的 stderr 警告(比如前面提到的 --export-dynamic 警告)会被 PowerShell 视为 NativeCommandError 并终止整个 step。路径转换用 cygpath.exe 把 Windows 路径 D:\work\... 转成 Cygwin 路径 /cygdrive/d/work/...。
4.4 DLL 依赖收集#
编译出的 haproxy.exe 不能独立运行,需要带上 Cygwin 的 DLL。用通配符匹配依赖列表:
| |
使用通配符而非硬编码 DLL 名称很重要——OpenSSL 从 1.1 升级到 3.0 后,DLL 名从 cygcrypto-1.1.dll 变成了 cygcrypto-3.dll,通配符 cygcrypto-*.dll 可以自动适应这种变化。
4.5 打包与发布#
每个版本产出两个 ZIP 包:
haproxy-x.y.z-windows-full.zip:包含 haproxy.exe + 所有 DLL,解压即用haproxy-x.y.z-windows-nolib.zip:只有 haproxy.exe,适合已经安装了 Cygwin 的用户
Release 通过 softprops/action-gh-release 创建,开发版(版本号含 dev/alpha/beta)自动标记为 Pre-release。
5. 端到端流程#
完整的一次构建周期:
sequenceDiagram
participant Cron as 定时触发
participant Ubuntu as Ubuntu Runner
participant HAProxy as haproxy.org
participant GH as GitHub API
participant Win as Windows Runner
participant Release as GitHub Release
Cron->>Ubuntu: 触发 workflow
Ubuntu->>HAProxy: 抓取各分支最新版本
HAProxy-->>Ubuntu: 3.2.5, 3.3.2, 3.4-dev5...
Ubuntu->>GH: 检查已有 Release
GH-->>Ubuntu: 3.2.4 已发布,3.2.5 未发布
Ubuntu->>Win: Matrix: [version: 3.2.5, ...]
Win->>HAProxy: 下载 3.2.5 源码
Win->>Win: sed patch + make + 收集 DLL
Win->>Win: 打包 full.zip + nolib.zip
Win->>Release: 创建 v3.2.5 Release
异常处理:下载失败会直接终止该版本的构建但不影响其他版本(fail-fast: false);编译失败通过检查 haproxy.exe 是否存在来判断,而非依赖 make 的退出码;Matrix 中 max-parallel: 1 避免自托管 runner 上的资源竞争。
6. 踩坑记录#
坑 1:PowerShell 把编译器警告当错误#
编译过程中 Cygwin ld 产生的 --export-dynamic 警告被写到 stderr,PowerShell 的 $ErrorActionPreference = "Stop" 会把任何 stderr 输出视为致命错误并终止 step。
解决方案是在 Invoke-Bash 函数中临时将 $ErrorActionPreference 设为 Continue,执行完再恢复。同时用 sed patch 掉 Makefile 中的 --export-dynamic,从源头消除警告。
坑 2:开发版的下载 URL 路径不同#
稳定版源码在 https://www.haproxy.org/download/3.3/src/haproxy-3.3.2.tar.gz,开发版在 https://www.haproxy.org/download/3.4/src/devel/haproxy-3.4-dev4.tar.gz,多了一层 devel/ 目录。
最初的 URL 构造没区分这两种情况,导致开发版下载 404。修复方法是在 PowerShell 中根据版本号判断:
| |
坑 3:版本号解析——3.4-dev4 提取 series 失败#
用 cut -d. -f1,2 从版本号提取系列号时,3.4-dev4 会被提取为 3.4-dev4 而非 3.4。
改用正则:
| |
坑 4:CRLF 换行符导致 Cygwin bash 脚本执行失败#
GitHub Actions 在 Windows 上写入的临时脚本文件使用 Windows 的 CRLF 换行。Cygwin bash 不认 \r,会报各种诡异的语法错误。
解决方案已经在 Invoke-Bash 函数中处理:-replace "`r`n", "`n" 清理输入命令的换行符,set -o igncr 让 bash 忽略残留的 \r。
坑 5:haproxy -vv 输出无法正常捕获#
构建后需要运行 haproxy -vv 获取编译信息写入 Release notes。但这个命令的输出有时为空,原因是 PowerShell 和 Cygwin 进程之间的 stdout/stderr 管道在某些情况下会丢失数据。
最终采用多重回退策略:先在 PowerShell 中直接运行,失败则通过 Cygwin bash 运行,再失败则使用编译参数拼接的默认值。同时用 Base64 编码传递多行输出,避免 GitHub Actions output 中换行符被吞掉。
7. Windows 下 HAProxy 的性能局限性#
这是一个必须诚实说明的问题。HAProxy 在 Cygwin 下运行有本质的架构限制:
Cygwin 只提供 poll 和 select 两种 I/O 多路复用机制,Linux 下的 epoll 不可用。poll 的时间复杂度是 O(n),连接数上千后性能急剧下降。Cygwin 的 socket 实现是在 Winsock 上的模拟层,每个连接都有额外的用户态转换开销。pthread 也是用 Windows 线程模拟的,锁竞争开销比原生 Linux 大。
从 -vv 输出可以看到差异:
| |
实际测试中,Cygwin 下的 HAProxy 能稳定处理几百到低千级并发连接。超过这个范围就会明显感受到延迟上升。
如果在 Windows 上需要更高并发,替代方案是 WSL2(原生 Linux 版 HAProxy,有 epoll)或 Docker Desktop。但对于开发测试、内部工具、低流量的反向代理场景,Cygwin 版完全够用。
8. 局限性与边界#
适用场景:开发测试环境、Windows 服务器上的轻量反向代理(并发 < 1000)、需要 HAProxy 特定功能但无法部署 Linux 的场景。
不适用场景:高并发生产负载均衡(应使用 Linux 版)、需要 QUIC/HTTP3 支持的场景(Cygwin 不支持)、对延迟敏感的金融交易系统。
CI 层面的局限:版本检测依赖爬取 haproxy.org 的 HTML 目录页,页面结构变化会导致解析失败;自托管 runner 是单点故障,runner 挂了就没有构建能力;max-parallel: 1 限制了并行度,全量构建 6+ 个版本需要排队等待。
9. 结语#
从手工编译到全自动 CI,核心收益是消除了人为遗忘和环境不一致的问题。现在每天凌晨 workflow 自动扫描上游,有新版本就编译发布,整个过程不需要人工介入。
项目地址:github.com/xjoker/HAProxyForWindows
如果你也需要在 Windows 上使用 HAProxy,可以直接从 Releases 页面下载预编译的二进制文件。