如何在Windows Server 2019的香港服务器中,通过IIS+ARR搭建SaaS负载均衡网关?

周五晚上十点,商务同事在群里一句“预售页面要上量了”,我盯了一眼 NOC 大屏,上行带宽已经在抖了。原来的应用层 Nginx 网关在内地 BGP 边缘点做得还不错,但跨境链路的抖动一来,SaaS 多租户的长尾流量就开始打小抖。我们临时决定在香港补一层Windows Server 2019 + IIS + ARR(Application Request Routing)的负载均衡网关,既做七层代理,又顺手把证书、压缩、限流、观测、灰度发布一把梭。下面是我从“一台裸机”到“可抗住凌晨流量尖峰”的完整过程与踩坑纪实。
拓扑与目标
目标:在香港机房落地一套“入口网关”,对外统一 HTTPS 入口,内部按租户/路径转发到多套后端(K8s Ingress、IIS 站点、.NET Core、Node 等),同时具备:
- 负载均衡与健康检查(后端就绪自动摘挂)
- TLS 终止、HSTS、HTTP/2
- 静态资源缓存与动态压缩
- 多租户 Host 头透传(SaaS 常见)
- 可灰度(按权重、按实例 drain)
- 可观测(真实客户端 IP、FREB、性能计数器)
- 基本防护(动态限速、请求过滤)
逻辑拓扑(文字版)
- Internet/CDN → HK Edge(任播/BGP) → IIS + ARR 网关 → 私网负载 (K8s NodePort / VM IIS / AppPool)
- 公网仅放通 80/443;内网与后端经专线/VPN 互通(10.0.0.0/16)。
环境与参数
| 项 | 规格/版本 | 备注 |
|---|---|---|
| 机型 | 2× Intel Xeon Silver(或同档 EPYC)+ 64GB RAM | 带宽敏感场景建议堆网卡队列和内存 |
| 存储 | NVMe SSD 1TB | 日志/缓存分区各自独立目录 |
| OS | Windows Server 2019 Datacenter (1809, 最新补丁) | 默认启用 TLS1.2, HTTP/2 支持 |
| 网卡 | 2×10GbE 绑定(或单口 10GbE) | RSS 开启、巨帧根据上游调优 |
| 站点 | IIS 10.0 + ARR 3.x + URL Rewrite 2.1 | 反向代理与规则引擎 |
| 证书 | *.example.com(PFX) | 网关终止 TLS,后端可 HTTP/HTTPS |
| 后端 | 10.0.1.11、10.0.1.12(K8s Ingress / IIS) | 提供 /healthz 200 |
注:Windows Server 2019 原生不含 TLS 1.3,HTTP/2 没问题;TLS 侧重点放在 TLS 1.2 加固。
一、系统与网络前置优化
基本网络
只在外网开放 80/443;对后端网段(10.0.0.0/16)放通 80/443/8080/8443 等必要端口。
DNS 指向近源(例如香港本地递归),减少握手时延。
TCP 栈简单自检(保守调优即可):
netsh int tcp show global
# 一般确保 RSS=enabled, Receive Window Auto-Tuning=normal
不建议一上来就动 MaxUserPort / TcpTimedWaitDelay,观察连接峰值和 TIME_WAIT 再定。
二、安装 IIS 及组件
PowerShell 一把梭(角色与常用模块):
Install-WindowsFeature Web-Server,Web-WebServer,Web-Common-Http,Web-Default-Doc,Web-Static-Content,Web-Performance,Web-Stat-Compression,Web-Dyn-Compression,Web-Filtering,Web-Http-Logging,Web-Http-Tracing,Web-Request-Monitor,Web-Http-Redirect,Web-WebSockets,Web-Mgmt-Tools,Web-Mgmt-Console,Web-Scripting-Tools,Web-IP-Security -IncludeManagementTools
安装 URL Rewrite 与 ARR 3.x(可用离线 MSI/企业软件源/包管理器,择一):
- 先装 URL Rewrite 2.1
- 再装 Application Request Routing 3.x(会带 WebFarm 框架)
安装完在 IIS 管理器里能看到:
- “Application Request Routing Cache”
- “URL Rewrite”
- “IP and Domain Restrictions”(用于动态限速)
三、导入证书与 HTTPS 绑定
证书导入(本地计算机 → 个人):
mmc → 证书 → “导入” PFX,勾选“将所有扩展属性标记为可导出”(方便应急迁移)。
站点绑定:
- Default Web Site 或新建 SaaS-Gateway 站点
- 绑定 https :443,选择证书,启用 SNI,主机名可留空(做总入口)
- 勾选 HTTP/2(2019 默认支持)
安全首部(HSTS 等,放在 web.config 或全局出站规则里):
<system.webServer>
<httpProtocol>
<customHeaders>
<add name="Strict-Transport-Security" value="max-age=31536000; includeSubDomains; preload"/>
<add name="X-Content-Type-Options" value="nosniff"/>
<add name="X-Frame-Options" value="SAMEORIGIN"/>
</customHeaders>
</httpProtocol>
</system.webServer>
TLS 加固:在 2019 上禁用 TLS1.0/1.1是常规动作(组策略/注册表),但请先确认老旧客户端影响范围,再决定变更窗口。
四、启用代理与全局代理参数(ARR)
IIS 管理器 → 服务器根节点 → Application Request Routing Cache → Server Proxy Settings:
- 勾选 “Enable proxy”
- Time-out (s):120(根据后端 P95 调整)
- Response buffer:启用
- Preserve client IP in X-Forwarded-For:启用(默认会加 X-Forwarded-For)
- Use URL Rewrite to inspect incoming requests:保持启用
五、创建 Server Farm(核心)
IIS 管理器 → 服务器根 → Server Farms → Create Server Farm…
名称:saas-gw
服务器:
- 10.0.1.11:443(权重 100)
- 10.0.1.12:443(权重 100)
勾选 “Automatically create rewrite rules”(会自动写入接入站点的反代规则)。
进入 saas-gw 细化配置:
Load Balance:
- 算法:Least Response Time(实战里波动小,尾延迟友好)
- Affinity(会话保持):默认关闭(SaaS 建议无状态。如果老系统粘会话,暂开 ARRAffinity 再逐步改造)
Health Test(非常重要):
- URL:https://{server}/healthz
- Interval:5s
- Timeout:2s
- HTTP status:200
后端必须提供轻量健康接口;应用探活≠进程存活。
Routing Rules:
- 勾选 “Use host name from incoming request”(保留 Host,多租户必需)
- SSL offloading:对外终止 TLS;对内可按需使用 HTTPS(推荐与后端同域证书,或内网 CA)
Proxy(farm 级别覆盖):
- HTTP version to servers:HTTP/1.1(ARR 到后端为 1.1,客户端可用 HTTP/2)
- Preserve client ip:已启用
- Disable SSL verification:不要勾,内部自签请导入信任链
六、接入站点与 Rewrite 规则
ARR 创建 farm 后,通常会自动在默认站点写入规则。建议显式、可读地定制:
入口站点 web.config(核心片段):
<configuration>
<system.webServer>
<rewrite>
<rules>
<!-- 1) 强制 HTTPS -->
<rule name="Redirect to HTTPS" stopProcessing="true">
<match url="(.*)" />
<conditions>
<add input="{HTTPS}" pattern="off" />
</conditions>
<action type="Redirect" url="https://{HTTP_HOST}/{R:1}" redirectType="Permanent" />
</rule>
<!-- 2) 静态资源直出(可结合 CDN 缓存) -->
<rule name="Bypass static" stopProcessing="true">
<match url="^assets/.*|.*\.(css|js|png|jpg|svg|ico|woff2?)$" />
<action type="None" />
</rule>
<!-- 3) 反向代理到 Server Farm -->
<rule name="ReverseProxy to saas-gw" stopProcessing="true">
<match url="(.*)" />
<action type="Rewrite" url="http://saas-gw/{R:1}" />
<conditions>
<add input="{CACHE_URL}" pattern=".*" />
</conditions>
</rule>
</rules>
<!-- 出站头部(可选):补充代理链信息 -->
<outboundRules>
<rule name="Add X-Forwarded-Proto" preCondition="IsHTML">
<match serverVariable="RESPONSE_X-Forwarded-Proto" pattern=".+" negate="true" />
<action type="Rewrite" value="{HTTPS:TRUE}" />
</rule>
</outboundRules>
<preConditions>
<preCondition name="IsHTML">
<add input="{RESPONSE_CONTENT_TYPE}" pattern="^text/html" />
</preCondition>
</preConditions>
</rewrite>
<!-- 压缩 -->
<urlCompression doStaticCompression="true" doDynamicCompression="true" />
<!-- 请求过滤(示例) -->
<security>
<requestFiltering>
<requestLimits maxAllowedContentLength="52428800" /> <!-- 50MB -->
</requestFiltering>
</security>
</system.webServer>
</configuration>
注意:url="http://saas-gw/{R:1}" 是一个 farm 逻辑名,不是域名。IIS 会把它路由到你刚建的 Server Farm 实例。
七、缓存与压缩策略
原则:HTML 不缓存、API 不缓存、静态资源强缓存。
启用 Static/Dynamic Compression(上面已装)
ARR 缓存更多用于“静态直出”与“后端抖动保护”(小流量),大规模静态建议走 CDN。
静态目录示例(web.config 局部):
<system.webServer>
<staticContent>
<clientCache cacheControlMode="UseMaxAge" cacheControlMaxAge="365.00:00:00" />
</staticContent>
</system.webServer>
八、日志与观测
真实客户端 IP
IIS 默认日志只有 c-ip(到网关的对端)。
在 Logging → Select Fields → Add Field,新增自定义字段:
- Source type: Request Header
- Header name: X-Forwarded-For
- Field name: X-Forwarded-For
FREB(失败请求跟踪)
对 4xx/5xx 建立规则,输出到独立目录,定位 502.3(后端不可达)、500.52(Rewrite 错)等。
性能计数器(perfmon 常看):
- Web Service(_Total)\Current Connections
- Web Service(_Total)\Requests/sec
- Process(w3wp)\% Processor Time
- HTTP Service Request Queues(*)\CurrentQueueSize
建议把 W3C 日志打进 ELK/云观测(Winlogbeat/Filebeat),做 P95/P99 延迟与错误率看板。
九、动态限流与防护
IP and Domain Restrictions → Dynamic IP Restrictions:
Deny IP based on number of concurrent requests:50(示例)
Deny IP based on number of requests over a period:100 requests / 10 seconds
动作:Deny(或 Abort 更省资源)
白名单:公司办公出口、上游 CDN 出口网段
Request Filtering 常规:禁止可疑扩展、限制动词、限制 URL 长度等。
十、灰度发布与无损摘挂
Server Farms → saas-gw → Servers
- 新增后端 10.0.1.13,初始权重设 10,观察错误率与延迟,再逐步拉到 100。
- 下线用 “Drain”(先不接新请求,等待连接跑空),再 Stop。
- 也可以按路径灰度(在入口站点加一条 Rewrite:^beta/ → 新 farm)。
权重切换 PowerShell 小工具(示例)
(ARR 的配置在 applicationHost.config 的 <webFarms> 节点,可用 XML 修改。下面示意读取/写入):
Import-Module WebAdministration
$webConfig = Join-Path $env:windir "System32\inetsrv\config\applicationHost.config"
[xml]$xml = Get-Content $webConfig
function Set-FarmWeight($farmName, $serverAddress, $weight) {
$ns = New-Object System.Xml.XmlNamespaceManager($xml.NameTable)
$ns.AddNamespace("a","urn:schemas-microsoft-com:applied-webhosting")
# 简化:不声明命名空间也可用 SelectSingleNode 搜索 webFarms 节点
$node = $xml.configuration.'system.applicationHost'.webFarms.webFarm |
Where-Object { $_.name -eq $farmName }
if(-not $node){ throw "Farm $farmName not found" }
$svr = $node.servers.add | Where-Object { $_.address -eq $serverAddress }
if(-not $svr){ throw "Server $serverAddress not found" }
$svr.weight = $weight
$xml.Save($webConfig)
iisreset /noforce
}
# 用法:
# Set-FarmWeight -farmName "saas-gw" -serverAddress "10.0.1.13" -weight 10
生产中建议做防呆:写入前备份 applicationHost.config,并优先用 GUI drain。
十一、常见坑与现场解决
502.3 Bad Gateway(后端 TLS)
原因:后端是自签或 SNI 不匹配。
处理:把后端证书链导入受信,或改走内网 HTTP;Farm 的 Routing Rules 勾“Use host name from incoming request”确保 SNI/Host 保持一致。
循环跳转(HTTP→HTTPS)
前端做了 HTTPS 强制,后端也做了 X-Forwarded-Proto 判断不当导致 301 循环。
处理:网关负责 HTTPS 强制;后端仅在识别到 X-Forwarded-Proto=http 时才跳。
真实 IP 丢失
上游 CDN 已写 CF-Connecting-IP,ARR 又叠加 X-Forwarded-For,应用只取第一个/最后一个产生偏差。
处理:统一规范:应用优先读 X-Forwarded-For 最后一个非保留地址;IIS 日志同时记录 XFF 与 CF-Connecting-IP。
缓存“脏”内容
误把 /api/ 缓存了,导致租户 A 看见租户 B 数据。
处理:严格路径与扩展白名单缓存;API 全部 Cache-Control: no-store。
健康检查误伤
/ 返回 302/登录页,Health Test 期望 200;Farm 频繁摘挂。
处理:后端专门做 /healthz 返回 200 与版本号。
HTTP/2 与后端兼容
ARR→后端为 HTTP/1.1,少数后端只监听 H2/TLS ALPN。
处理:后端同时启 1.1 或在网关转 HTTP 明文。
IIS 应用池队列打满(入口站点被打尖峰)
现象:HTTP Service Request Queues 上升,503。
处理:入口站点独立 AppPool,Queue Length 提到 10000;加动态限流与上游 CDN 盾。
十二、基线数据(样例)
业务:静态资源 + API 混合;指标采样 10 分钟。
| 指标 | 变更前(老入口) | 变更后(IIS+ARR) |
|---|---|---|
| 峰值 RPS | 6.2k | 7.8k |
| P95 首字节 (ms) | 420 | 280 |
| 5xx 比例 | 0.35% | 0.08% |
| 平均出网带宽 | 2.1 Gbps | 2.3 Gbps |
| 后端实例数 | 6 | 6(权重可灰度) |
十三、运维清单(可直接照抄)
- 安装 IIS、URL Rewrite、ARR
- 导入证书,绑定 443,启 H2
- ARR:Enable proxy、XFF、超时 120s
- Server Farm:Least Response Time、Health=/healthz
- 保留 Host,后端校验证书或走内网 HTTP
- Rewrite:HTTPS 强制、静态直出、其余转 farm
- 压缩:静/动启用;API 禁缓存,静态强缓存
- 日志:自定义 X-Forwarded-For 字段;开 FREB
- 防护:动态 IP 限制、请求过滤
- 灰度:按权重 + drain 下线
- 观测:P95/P99、错误率、队列长度看板
附:入口站点最小可用 web.config(整合版)
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<system.webServer>
<security>
<requestFiltering>
<requestLimits maxAllowedContentLength="52428800"/>
<fileExtensions allowUnlisted="false">
<add fileExtension=".json" allowed="true"/>
<add fileExtension=".xml" allowed="false"/>
</fileExtensions>
</requestFiltering>
</security>
<httpProtocol>
<customHeaders>
<add name="Strict-Transport-Security" value="max-age=31536000; includeSubDomains; preload"/>
<add name="X-Content-Type-Options" value="nosniff"/>
<add name="X-Frame-Options" value="SAMEORIGIN"/>
</customHeaders>
</httpProtocol>
<urlCompression doStaticCompression="true" doDynamicCompression="true"/>
<rewrite>
<rules>
<rule name="Redirect to HTTPS" stopProcessing="true">
<match url="(.*)"/>
<conditions>
<add input="{HTTPS}" pattern="off"/>
</conditions>
<action type="Redirect" url="https://{HTTP_HOST}/{R:1}" redirectType="Permanent"/>
</rule>
<rule name="Bypass static" stopProcessing="true">
<match url="^assets/.*|.*\.(css|js|png|jpg|svg|ico|woff2?)$"/>
<action type="None"/>
</rule>
<rule name="ReverseProxy to saas-gw" stopProcessing="true">
<match url="(.*)"/>
<action type="Rewrite" url="http://saas-gw/{R:1}"/>
</rule>
</rules>
</rewrite>
<handlers>
<add name="StaticFile" path="*" verb="GET,HEAD" modules="StaticFileModule" resourceType="Either" requireAccess="Read"/>
</handlers>
</system.webServer>
</configuration>
凌晨 2:40,尖峰来了。NOC 的曲线开始像心电图,但比刚上阵时稳。Least Response Time 在后端 12 核与 8 核之间自动找平衡,/healthz 在一次短暂 GC 抖动时把某个实例摘了两分钟又挂回;FREB 里那几个 502.3 也都能追到具体上游。到 5 点,带宽回落,最后几台机器 drain 下线,我给 farm 做了一次配置备份。走出机房,香港的天刚泛白。我知道,这套 IIS+ARR 的 SaaS 负载均衡网关不是“最酷的云原生词汇”,但它在这次真实的流量战斗里,干净利落地把问题解决了。