0. 背景与动机:为什么要替换掉 Nginx?
在过去很长一段时间里,本站点的静态文件托管(Hugo)以及动态服务反代(SillyTavern)一直由 Nginx 承担。Nginx 久经考验、极其稳定,堪称业界标杆。但随着系统架构的演进和统一网关管理的考量,我们开始寻求更现代化的反向代理方案,并最终将目光投向了 Pingap。
为什么选择 Pingap?
- 现代化的底层基石:Pingap 基于 Cloudflare 开源的 Pingora 框架构建,采用 Rust 编写。Pingora 在 Cloudflare 内部已经受了万亿级请求的考验。得益于 Rust 的内存安全特性,它从根本上规避了传统 C/C++ 网关常见的内存越界和崩溃风险。
- 更直观的配置模型:Nginx 的配置语法虽然强大,但指令的上下文和隐式执行顺序往往让人头疼。Pingap 采用了基于 TOML 的声明式配置,将
Server、Location、Upstream和Plugin彻底解耦,配置的心智模型更加清晰,更利于后期的维护与扩展。 - 原生的高级特性支持:Pingap 对 HTTP/1.x、HTTP/2、WebSocket、gRPC-web、证书动态加载和插件化网关能力支持更直接。需要注意的是,HTTP/3/QUIC 这类能力应以当前官方 release 和文档为准,迁移生产入口时不要默认假设已经完整可用。
本次实战的核心目标:先在隔离端口上把新代理跑起来并完整验证,再用一次原子切换无缝替换 Nginx,全程保留 Nginx 配置以便出现问题时能够秒级回滚。
通过这种“先验证后切换”的安全策略,我们最终实测的切换中断窗口仅为 174 ms,线上流量几乎无感。
2026-05-17 复核:我重新 SSH 到服务器核对了一遍真实状态。当前服务器仍运行
pingap 0.13.2,pingap.service使用 daemon 模式,Hugo 静态文件由darkhttpd承载。本文以下配置以服务器现状为准;新装环境可以再按最新 release 调整。
1. 起点:现状盘点
服务器:阿里云 ECS(Alibaba Cloud Linux 3 / RHEL 兼容),跑 Nginx 1.20.1,承载两个站点:
| 域名 | 性质 | 后端 | 关键能力 |
|---|---|---|---|
cloudside.icyyan.com | Hugo 静态站点 | 本地文件 /var/www/hugo-site | HTTPS、HTTP→HTTPS 重定向、静态资源 1 月缓存、Gzip |
silly.icyyan.com | SillyTavern 反代 | 127.0.0.1:8000 | HTTPS、WebSocket、client_max_body_size 100M、长连接 proxy_read_timeout 86400s、X-Forwarded-* |
证书来自两个不同来源:
cloudside:手动签发,放在/etc/nginx/certs/cloudside.icyyan.com/silly:Let’s Encrypt(certbot 自动续期),放在/etc/letsencrypt/live/silly.icyyan.com/
第一件事是 不要乱动证书路径——直接复用,迁移完出问题可以一键切回 Nginx,证书原地一致,省去后续同步的麻烦。
2. Pingap 能不能替代 Nginx 做入口网关?
逐项对照之后,作为 HTTPS 入口、反向代理和路由网关是可行的;静态文件服务则要按站点形态选择 directory 插件或本地静态文件服务器。对应关系如下:
| Nginx 概念 | Pingap 对应 |
|---|---|
server { listen 443 ssl; } | [servers.https] + [certificates.X] |
proxy_pass http://upstream; | [upstreams.X] + [locations.X] upstream = "X" |
root /var/www/... 静态文件 | directory 插件,或本地静态文件服务器 + Pingap 反代 |
return 301 https://$host$request_uri; | [plugins.X] category = "redirect" http_to_https = true(默认行为需按当前版本验证) |
proxy_set_header X-Real-IP ... | enable_reverse_proxy_headers = true 自动注入 X-Real-IP / X-Forwarded-* |
client_max_body_size 100M; | location 级 client_max_body_size = "100mb" |
proxy_read_timeout 86400s; | upstream 级 read_timeout = "86400s" |
| WebSocket Upgrade | 原生支持,无需任何额外配置(pingora 底层透传) |
| 多证书 SNI | certificates.X.domains = "..." 自动匹配 |
唯一需要注意:Pingap 不是 Nginx 那种 server { location { ... } } 嵌套模型。Location 是全局定义的路由规则,但每个 [servers.X] 需要通过 locations = [...] 显式绑定自己要使用的规则;请求进入某个 server 后,再按 location 的 host、path 和权重匹配。
3. 安装
二进制是 musl 静态链接,挑这个版本是因为不依赖系统 glibc,跨发行版兼容性最稳。本文记录的服务器当前版本是 0.13.2:
# 下载并安装(musl 版本,静态链接)
curl -L https://github.com/vicanso/pingap/releases/download/v0.13.2/pingap-linux-musl-x86.tar.gz \
-o /tmp/pingap.tar.gz
tar -xzf /tmp/pingap.tar.gz -C /tmp
install -m 755 /tmp/pingap-linux-musl-x86 /usr/local/bin/pingap
# 准备配置目录和日志目录
mkdir -p /etc/pingap /var/log/pingap
pingap --version
# pingap 0.13.2
4. 配置:六个文件搞定
Pingap 的配置可以一个大文件,也可以拆成多个文件放在一个目录里(启动时 -c /etc/pingap 自动扫描合并)。强烈建议拆分,方便后续单独 reload 某一类配置。
最终方案里,Hugo 静态文件没有直接挂 directory 插件,而是由 darkhttpd 在服务器本机提供 9000 端口,Pingap 通过 127.0.0.1:9000 反代过去。实际 ss 看到 darkhttpd 监听的是 0.0.0.0:9000,所以应依赖云安全组/防火墙避免公网直接访问这个端口。选择这个方案,是因为当次迁移使用的 Pingap 版本在 Hugo 子目录 index.html 场景下有兼容问题,细节见 10.5。
4.1 /etc/pingap/basic.toml — 全局
[basic]
name = "pingap"
log_level = "info"
pid_file = "/run/pingap.pid"
4.2 /etc/pingap/certificates.toml — 证书
注意
domains字段是 SNI 路由的关键,必须填上。
[certificates.cloudside]
domains = "cloudside.icyyan.com"
tls_cert = "/etc/nginx/certs/cloudside.icyyan.com/cloudside.icyyan.com.pem"
tls_key = "/etc/nginx/certs/cloudside.icyyan.com/cloudside.icyyan.com.key"
[certificates.silly]
domains = "silly.icyyan.com"
tls_cert = "/etc/letsencrypt/live/silly.icyyan.com/fullchain.pem"
tls_key = "/etc/letsencrypt/live/silly.icyyan.com/privkey.pem"
4.3 /etc/pingap/upstreams.toml — 后端
[upstreams.hugo_files]
addrs = ["127.0.0.1:9000"]
[upstreams.sillytavern]
addrs = ["127.0.0.1:8000"]
# WebSocket / 长连接:和 nginx 的 proxy_read_timeout / proxy_send_timeout 一一对应
read_timeout = "86400s"
write_timeout = "86400s"
idle_timeout = "86400s"
4.4 /etc/pingap/plugins.toml — 插件
# HTTP -> HTTPS 重定向
[plugins.httpToHttps]
category = "redirect"
http_to_https = true
4.5 /etc/pingap/locations.toml — 路由规则
# Hugo 静态站点,HTTPS
[locations.cloudsideHttps]
host = "cloudside.icyyan.com"
upstream = "hugo_files"
# SillyTavern 反代,HTTPS
[locations.sillyHttps]
host = "silly.icyyan.com"
upstream = "sillytavern"
client_max_body_size = "100mb"
enable_reverse_proxy_headers = true # 自动注入 X-Real-IP / X-Forwarded-For / X-Forwarded-Proto / X-Forwarded-Host
# 所有 HTTP 进来一律 301/307 跳到 HTTPS
[locations.redirectHttp]
plugins = ["httpToHttps"]
4.6 /etc/pingap/servers.toml — 监听端口
这是验证阶段和切换阶段唯一需要改动的文件。 验证时用非标端口,避免和 Nginx 抢端口。
验证阶段(与 Nginx 共存):
[servers.http]
addr = "0.0.0.0:8080"
locations = ["redirectHttp"]
[servers.https]
addr = "0.0.0.0:8443"
locations = ["cloudsideHttps", "sillyHttps"]
global_certificates = true # 启用全局证书池,按 SNI 自动选择
enabled_h2 = true # HTTP/2
切换阶段(接管标准端口):
[servers.http]
addr = "0.0.0.0:80"
locations = ["redirectHttp"]
[servers.https]
addr = "0.0.0.0:443"
locations = ["cloudsideHttps", "sillyHttps"]
global_certificates = true
enabled_h2 = true
4.7 启动本地 Hugo 静态文件服务
因为上面的 hugo_files upstream 指向 127.0.0.1:9000,所以在验证 Pingap 前,先确保本机静态服务已经跑起来:
# /etc/systemd/system/hugo-static.service
[Unit]
Description=Hugo Static File Server
After=network.target
[Service]
Type=simple
ExecStart=/usr/sbin/darkhttpd /var/www/hugo-site --port 9000 --no-server-id
Restart=on-failure
RestartSec=2s
[Install]
WantedBy=multi-user.target
systemctl daemon-reload
systemctl enable --now hugo-static
curl -I http://127.0.0.1:9000/
5. 验证:在 8080/8443 上把所有路径全跑一遍
启动前先验证配置语法:
pingap -c /etc/pingap -t
# 看到 "Validate config success" 才继续
后台启动:
pingap -c /etc/pingap -d --log=/var/log/pingap/pingap.log
ss -tlnp | grep pingap # 确认监听了 8080 和 8443
跑 5 项验证(每一项都对应一个 nginx 时代的功能):
# 1. HTTPS + SNI -> Hugo 静态 200
curl -ksI --resolve cloudside.icyyan.com:8443:127.0.0.1 \
https://cloudside.icyyan.com:8443/
# 2. HTTPS + SNI -> SillyTavern 反代(401 = 后端 Basic Auth,说明转发链路正常)
curl -ksI --resolve silly.icyyan.com:8443:127.0.0.1 \
https://silly.icyyan.com:8443/
# 3. HTTP -> HTTPS 重定向 307
curl -sI --resolve cloudside.icyyan.com:8080:127.0.0.1 \
http://cloudside.icyyan.com:8080/
# 4. SNI 证书匹配
echo | openssl s_client -connect 127.0.0.1:8443 \
-servername cloudside.icyyan.com 2>/dev/null \
| openssl x509 -noout -subject -dates
echo | openssl s_client -connect 127.0.0.1:8443 \
-servername silly.icyyan.com 2>/dev/null \
| openssl x509 -noout -subject -dates
# 5. WebSocket Upgrade 握手
curl -sk --resolve silly.icyyan.com:8443:127.0.0.1 -i -N --max-time 3 \
-H "Connection: Upgrade" \
-H "Upgrade: websocket" \
-H "Sec-WebSocket-Version: 13" \
-H "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==" \
https://silly.icyyan.com:8443/socket.io/?EIO=4\&transport=websocket
这一步是这次迁移的安全网。 任何一项不通就回头改配置,绝不带着问题去做切换。
6. systemd 服务化
# /etc/systemd/system/pingap.service
[Unit]
Description=Pingap Reverse Proxy
After=network.target
Wants=network.target
[Service]
Type=forking
PIDFile=/run/pingap.pid
ExecStartPre=/usr/local/bin/pingap -c /etc/pingap -t
ExecStart=/usr/local/bin/pingap -c /etc/pingap -d --log=/var/log/pingap/pingap.log
ExecReload=/bin/kill -HUP $MAINPID
KillMode=mixed
Restart=on-failure
RestartSec=2s
LimitNOFILE=65535
[Install]
WantedBy=multi-user.target
systemctl daemon-reload
ExecStartPre 加了配置语法预检——任何错配会让 systemd 直接拒绝启动,比起 pingap 起来后才报错友好得多。当前服务器就是这个 daemon 模式:systemctl status pingap 里能看到主进程命令为 /usr/local/bin/pingap -c /etc/pingap -d --log=/var/log/pingap/pingap.log。
7. 切换:让中断窗口尽可能短
切换之前先把 servers.toml 的端口从 8080/8443 改成 80/443,并把验证阶段启动的 pingap 进程清掉,然后一次性 stop nginx + start pingap:
# 1. 清掉验证阶段的 pingap 进程
pkill -f /usr/local/bin/pingap
# 2. 改 servers.toml(80/443 版本),再 systemctl daemon-reload 一次(unit 没变也无害)
systemctl daemon-reload
# 3. 切换(中断窗口就是这两条命令的间隔)
systemctl stop nginx && systemctl start pingap
# 4. 等待 socket 出现
for i in 1 2 3 4 5; do
ss -tln | grep -q ":443 " && break
sleep 0.2
done
# 5. 立即验证
systemctl is-active nginx pingap
ss -tlnp | grep -E ":(80|443)\b"
curl -ksI --resolve cloudside.icyyan.com:443:127.0.0.1 https://cloudside.icyyan.com/
我这次实测从 nginx stop 到 pingap accept 总共 174 ms,外部观察基本无感。
8. 收尾
systemctl enable pingap # 开机自启
systemctl disable nginx # 保留 nginx 但不开机自启(方便回滚)
# 把验证阶段的旧目录、nginx 配置都打个备份保险
mv /root/pingap-old /root/pingap-old.bak.$(date +%Y%m%d) 2>/dev/null
cp -a /etc/nginx /root/nginx-backup.$(date +%Y%m%d)
刻意 不 卸载 Nginx:
- nginx 的 conf 是已经验证过能 work 的"参考实现",留着便于以后写新 location 时对照
- 一旦 pingap 出问题,回滚就是
systemctl stop pingap && systemctl start nginx,秒级
9. 回滚预案
写在 ~/rollback-pingap.sh,应急时一行命令搞定:
#!/bin/bash
set -e
systemctl stop pingap
systemctl start nginx
systemctl status nginx --no-pager
证书路径完全没改、nginx 配置原封不动,所以回滚是真正的"按下就回去"。
10. 踩坑:本次迁移真正花时间的部分
10.1 SNI 拦截 / ICP 备案
迁移完成后从浏览器访问域名,出现 ERR_CONNECTION_CLOSED,第一反应是 pingap 有问题。但一通排查后定位到根因和 pingap 完全无关:
症状对比:
| 测试 | SNI | 结果 |
|---|---|---|
https://silly.icyyan.com/ | silly.icyyan.com | ❌ ERR_CONNECTION_CLOSED |
https://120.xx.xx.xx/(直接 IP) | 无 / IP | ✅ TLS 握手成功(证书 CN 不匹配但能握手) |
带 SNI 就被切,不带 SNI 就能握手 —— 经典的 SNI 阻断 模式。
根因:阿里云对未在阿里云 ICP 备案的域名会在境内入站方向做 SNI 检查,发现未备案的域名直接 TCP RST。境外 IP 出口因为路径不同绕过这层检查,所以从美国 IP 看 nginx access log 一切正常。这套机制和反代是 nginx 还是 pingap 毫无关系。
走过的弯路:
- 误以为是 pingap 启用 HTTP/2 / TLS 1.3 / ECH 不兼容
- 回滚到 nginx 后从我电脑测试发现同样的
Connection reset by peer,才意识到不是反代的问题
判别方法很简单:用 https://<IP>/ 直接访问,如果连证书警告页都打不开 → 端口 / 网络问题;如果能弹证书警告(CN 不匹配也行)→ 端口完全正常,问题在 SNI / 域名层面。
解决方案二选一:
- 补 ICP 备案
- 把域名挂 Cloudflare 真正代理(橙云),让阿里云那一段看到的 SNI 是 Cloudflare 的而不是你的
10.2 配置里 domains 字段不要忘
[certificates.X] 必须填 domains = "...",否则 global_certificates = true 没法按 SNI 路由证书,会用默认证书或报握手错误。
10.3 静态文件服务不在 [locations],在 [plugins]
Pingap 没有 nginx 的 root 指令。静态文件是通过 category = "directory" 插件提供,再在 location 的 plugins = [...] 数组里挂上。这一点和 nginx 的心智模型差别比较大,第一次看 pingap 文档时容易卡住。
10.4 category = "redirect" 的 http_to_https 默认行为要单独验证
当次迁移时,http_to_https 的表现更接近“统一跳转 HTTPS”的网关插件,而不是 Nginx 里随手写一条 return 301。如果某些路径不希望强制跳 HTTPS(比如 .well-known/acme-challenge 给 Let’s Encrypt 验证用),需要为这个路径单独建一个 location,匹配权重高于带 httpToHttps 插件的那条。新版本如果需要固定 301/302/307,也应按当前 redirect 插件文档验证状态码配置。
10.5 当时版本的 directory 插件对子目录 index.html 不稳定
这是本次迁移最大的功能陷阱。现象是:首页 / 正常,点进 /posts/、/tags/ 这类 Hugo 生成的目录页时返回 Not Found,即便对应目录下存在 index.html。
这条结论严格限定在当次迁移环境和当时使用的 Pingap 版本。当前 Pingap 官方文档已经明确描述 directory 插件支持目录 index,所以新版本应重新实测,不能直接照搬这个坑。
当时为了降低迁移风险,我没有继续把生产入口压在这个行为上,而是改成本地跑一个轻量静态文件服务器,Pingap 只做统一 TLS 终止、SNI、HTTP 跳转和反代。最终配置就是前面 4.7 的 darkhttpd + upstream 模式:
[upstreams.hugo_files]
addrs = ["127.0.0.1:9000"]
[locations.cloudsideHttps]
host = "cloudside.icyyan.com"
upstream = "hugo_files"
这样 pingap 负责 TLS 终止、SNI、HTTP→HTTPS,darkhttpd 负责正确提供目录 index.html,各司其职。需要注意:当前 hugo-static.service 没有额外绑定 localhost,ss -tlnp 显示它监听在 0.0.0.0:9000;如果安全组放行了 9000,就会被公网直接访问。生产上最好只在安全组里放行 80/443/22,或改用支持绑定 127.0.0.1 的静态文件服务。
11. 还没做但应该做
- certbot 续期 hook:silly.icyyan.com 用 Let’s Encrypt,续期默认 reload nginx,现在 nginx 不在跑了。更稳的做法是在 certbot deploy hook 里执行
systemctl restart pingap,确保证书文件更新后新 TLS 配置一定被进程读到。 - HTTP/3 / QUIC:不要在迁移文档里默认开启。Pingap 当前公开文档主要覆盖 HTTP/1.x、HTTP/2、gRPC-web、h2c;如果后续 release 明确支持 QUIC,再单独验证 UDP 443、安全组、回滚策略和客户端兼容性。
- 指标 / 监控:
prometheus_metrics = "/metrics"暴露给 Prometheus,或webhook接钉钉/企业微信告警 - 管理面板:
[plugins.adminWebPage]启用后可以在浏览器里改配置并热加载,但生产环境记得加 IP 白名单和强密码
一个总结
这次迁移的核心方法论是 “先建后切” —— 永远不要在生产端口上做实验。验证窗口越长越好,切换窗口越短越好;旧服务的配置和数据在切换后保留至少一个版本,回滚路径是肌肉记忆。
Pingap 的配置心智模型干净(upstream → location → server,配上 plugin 横切关注点),Rust 写的内存占用也确实小(~40MB resident,对比 nginx 多 worker 大约持平)。从这次站点入口迁移看,用 Pingap 替代 Nginx 承担 HTTPS 终止、反向代理和统一网关职责是可行的;静态文件这种细节能力则要结合版本和站点形态单独验证。