上一篇 下一篇 分享链接 返回 返回顶部

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

发布人:Minchunlin 发布时间:2025-09-23 09:56 阅读量:902


周五晚上十点,商务同事在群里一句“预售页面要上量了”,我盯了一眼 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:

  1. 勾选 “Enable proxy”
  2. Time-out (s):120(根据后端 P95 调整)
  3. Response buffer:启用
  4. Preserve client IP in X-Forwarded-For:启用(默认会加 X-Forwarded-For)
  5. 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(权重可灰度)

十三、运维清单(可直接照抄)

  1.  安装 IIS、URL Rewrite、ARR
  2.  导入证书,绑定 443,启 H2
  3.  ARR:Enable proxy、XFF、超时 120s
  4.  Server Farm:Least Response Time、Health=/healthz
  5.  保留 Host,后端校验证书或走内网 HTTP
  6.  Rewrite:HTTPS 强制、静态直出、其余转 farm
  7.  压缩:静/动启用;API 禁缓存,静态强缓存
  8.  日志:自定义 X-Forwarded-For 字段;开 FREB
  9.  防护:动态 IP 限制、请求过滤
  10.  灰度:按权重 + drain 下线
  11.  观测: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 负载均衡网关不是“最酷的云原生词汇”,但它在这次真实的流量战斗里,干净利落地把问题解决了。

目录结构
全文