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

如何在香港服务器的 Windows Server 2016 上优化 IIS 队列与应用池,提升百万级请求的吞吐能力?

发布人:Minchunlin 发布时间:2025-08-20 10:03 阅读量:882


香港服务器第一次被“打穿”是在一个潮湿的周一清晨,监控墙上一片红:503.2—Queue Full、应用池频繁回收、平均延迟飙到秒级。客户活动提前上线,流量像海水一样灌进来。我一只手拎起螺丝刀把一根松掉的理线扣安好,另一只手在 RDP 里敲下第一条 appcmd。接下来六个小时,我们把 IIS 从“捱打”变成“抗打”。这篇文章,就是那次现场从排障到调优的完整复盘。

场景与目标

目标:在单台香港物理服务器(低延迟回国链路)上,优化 Windows Server 2016 + IIS 10 的队列、应用池与相关内核/网络参数,使网站在高并发活动中稳定承载**百万级请求(按日)**的吞吐,峰值 QPS 稳定、P95 延迟可控且无 503.2 队列溢出。

基础环境(真实可落地)

项目 参数
机房 香港将军澳(BGP 多线,回国优先 CN2/GIA)
服务器 Dell R740(或同级),单机作战
CPU Intel Xeon Silver 4210R ×1(10C/20T)
内存 64GB DDR4
系统盘 2×480GB SATA SSD(RAID1)
数据盘 2×1.92TB NVMe(RAID1/镜像)
网卡 2×10GbE(绑定 LACP,启用 RSS/RSC)
OS Windows Server 2016 Standard(1607+ 补丁齐全)
Web IIS 10.0(含动态压缩、URL Rewrite、Kernel Cache)
运行时 .NET Framework 4.7.2 / ASP.NET Core 6(ANCM 反代)二选一的思路均覆盖
压测 wrk / bombardier(压测机同城,不限速,开启 keep-alive)

备注:硬件不是“越猛越好”,而是“平衡”:CPU 足够、SSD 减少日志与临时文件 IO 抖动、10GbE 让内核栈不成为瓶颈。

痛点画像:IIS 是怎么“堵车”的?

  1. 503.2 – Queue Full:应用池队列 queueLength(默认 1000)被打满。
  2. 回收/重启抖动:定时回收与内存限制触发,连接迁移造成短时可用性下降。
  3. ASP.NET 与 HTTP.sys 双重排队:经典管道/集成管道差异、队列位置不同导致误判。
  4. 线程与 GC 压力:CLR 线程池迟迟扩容、工作线程饱和,GC 模式不匹配。
  5. 网络栈默认值保守:RSS/RSC 未开启或队列数偏低,导致单核过载。
  6. 日志 IO:W3C 日志落在系统盘或与应用共享卷,峰值时写入抖动放大延迟。

总体策略图(从外到内)

  1. 网络栈:NIC/RSS/RSC、多队列中断调优 → 减少内核抢占与单核瓶颈
  2. HTTP.sys / IIS 内核态缓存:能进内核缓存的尽量不进用户态
  3. IIS 队列/应用池:放大且“有序”地放大(避免一味无限)
  4. ASP.NET/ANCM/Kestrel:并发、线程池、GC、回收策略成体系
  5. 日志与磁盘:把日志与临时文件移到 NVMe,降落地延迟
  6. 观测:PerfMon/ETW 采样 + 压测闭环,按数据迭代参数

逐项实施(可直接抄)

1)网络栈与 NIC 调优

查看全局 TCP 设置

netsh int tcp show global

推荐设置(按需调整)

netsh int tcp set global rss=enabled autotuninglevel=normal ecncapability=disabled chimney=disabled rsc=enabled

NIC 队列与 RSS(在网卡驱动里确认启用 RSS,多队列≥CPU 物理核心数的一半;10GbE 一般支持 4–8 队列。)
同时在电源管理里关闭“允许计算机关闭此设备以节约电源”。

效果:降低单核中断风暴,减少队列堆积前的内核抖动。

2)启用内核模式缓存(Kernel Cache)与压缩策略

安装必要组件

dism /online /enable-feature /featurename:IIS-HttpCompressionStatic /NoRestart
dism /online /enable-feature /featurename:IIS-HttpCompressionDynamic /NoRestart
dism /online /enable-feature /featurename:IIS-Caching /NoRestart

开启压缩与缓存(站点级)

%windir%\system32\inetsrv\appcmd set config -section:urlCompression /doStaticCompression:"True" /commit:apphost
%windir%\system32\inetsrv\appcmd set config -section:urlCompression /doDynamicCompression:"True" /commit:apphost

%windir%\system32\inetsrv\appcmd set config -section:httpCompression /+"dynamicTypes.[mimeType='application/json',enabled='True']" /commit:apphost
%windir%\system32\inetsrv\appcmd set config -section:system.webServer/caching /enableKernelCache:"True" /commit:apphost

典型动态页面短缓存(命中即可“直出”内核)

<!-- web.config (示例:对可缓存的 JSON 接口缓存 10 秒) -->
<configuration>
  <system.webServer>
    <caching enabled="true" enableKernelCache="true">
      <profiles>
        <add extension="*" policy="CacheForTimePeriod" duration="00:00:10"
             kernelCachePolicy="CacheForTimePeriod" location="Any" />
      </profiles>
    </caching>
    <httpCompression directory="%SystemDrive%\inetpub\temp\IIS Temporary Compressed Files" />
  </system.webServer>
</configuration>

关键:能进内核的让它进内核。哪怕只缓存 5–10 秒,高并发下能把大量“热点”挡在用户态之外。

3)放大 IIS 应用池队列 & 预热

把队列从 1000 拉到按峰值估算的安全值(例如 65535)

%windir%\system32\inetsrv\appcmd set apppool "ProdPool" /queueLength:65535

AlwaysRunning + 站点预加载,避免冷启动

%windir%\system32\inetsrv\appcmd set apppool "ProdPool" /startMode:AlwaysRunning
%windir%\system32\inetsrv\appcmd set app "Default Web Site/" /preloadEnabled:true

Rapid-Fail 防抖

%windir%\system32\inetsrv\appcmd set apppool "ProdPool" /failure.rapidFailProtection:true /failure.failureInterval:00:05:00 /failure.maxFailures:100

注意:放大队列不是万能。队列变长只是在短峰时“削峰填谷”,根因还是要靠内核缓存、并发与线程池匹配吞吐。

4)回收与并发:.NET Framework 方案

避免高峰期定时回收(改为凌晨固定窗口,允许重叠)

%windir%\system32\inetsrv\appcmd set apppool "ProdPool" /recycling.periodicRestart.time:00:00:00
%windir%\system32\inetsrv\appcmd set apppool "ProdPool" /recycling.periodicRestart.schedule."[value='03:30:00']":true
%windir%\system32\inetsrv\appcmd set apppool "ProdPool" /recycling.disallowOverlappingRotation:false

Server GC(通常 ASP.NET 站点默认已是 Server GC,但建议显式声明)

<!-- web.config -->
<configuration>
  <runtime>
    <gcServer enabled="true"/>
    <gcConcurrent enabled="true"/>
  </runtime>
</configuration>

请求并发(集成管道下一般不再使用经典时代的 minFreeThreads 等;重点放在线程池与异步 I/O)

  • 尽量使用 async/await,避免阻塞线程。
  • 连接池(数据库/Redis)上限与 IIS 并发匹配。
  • 禁用 Session 粘性或把 Session 放到分布式(如 Redis),否则 web garden/多进程会互相“绊倒”。

可控的 Web Garden(仅当应用完全无状态时)

%windir%\system32\inetsrv\appcmd set apppool "ProdPool" /processModel.maxProcesses:2

警告:Web Garden 能提升 CPU 利用与并发度,但对 会话/缓存/锁 有严格要求;大多数传统站点不建议开。

5)回收与并发:ASP.NET Core(ANCM)方案

InProcess 模式 web.config

<configuration>
  <system.webServer>
    <handlers>
      <add name="aspNetCore" path="*" verb="*" modules="AspNetCoreModuleV2" resourceType="Unspecified"/>
    </handlers>
    <aspNetCore processPath="dotnet" arguments="MyApp.dll"
                hostingModel="InProcess"
                stdoutLogEnabled="false"
                requestTimeout="00:02:00"/>
  </system.webServer>
</configuration>

线程池预热(Program.cs)

让 CLR 线程池在启动时就拉高最小线程,避免突发扩容带来的排队。

using System.Threading;

ThreadPool.GetMinThreads(out var wt, out var cpt);
ThreadPool.SetMinThreads(Math.Max(wt, 200), Math.Max(cpt, 200));

Kestrel 并发与 HTTP/2(如果走反代 OutOfProcess)

webBuilder.ConfigureKestrel(o =>
{
    o.AddServerHeader = false;
    o.Limits.Http2.MaxStreamsPerConnection = 128;
    o.Limits.MaxConcurrentConnections = null; // 不限
    o.Limits.MaxConcurrentUpgradedConnections = null;
});

6)HTTP.sys 头与请求体限制(避免 400/431 类错误)

适当放大请求头字节数(仅在明确需要时)

Windows Registry Editor Version 5.00

[HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\HTTP\Parameters]
"MaxFieldLength"=dword:00010000        ; 65536
"MaxRequestBytes"=dword:00010000       ; 65536

修改后需重启。不要盲目拉满,防止被大头攻击“炸死”。

7)站点连接/超时与日志 IO

连接超时与最大连接数

%windir%\system32\inetsrv\appcmd set site "Default Web Site" /limits.connectionTimeout:00:02:00 /limits.maxConnections:4294967295

移动日志到 NVMe(并按站点分目录)

%windir%\system32\inetsrv\appcmd set config -section:sites /siteDefaults.logFile.directory:"D:\IISLogs" /commit:apphost
%windir%\system32\inetsrv\appcmd set config "Default Web Site" -section:system.applicationHost/sites /+"[name='Default Web Site'].logFile.directory:'D:\IISLogs\DefaultWebSite'" /commit:apphost

临时压缩目录也移走

<system.webServer>
  <httpCompression directory="D:\IIS_Compress_Temp" />
</system.webServer>

8)观测与压测:用数据说话

关键 PerfMon 计数器

  • HTTP Service Request Queues(*)\CurrentQueueSize
  • Web Service(_Total)\Current Connections
  • ASP.NET\Requests Queued(老站点可参考)
  • Process(w3wp)\% Processor Time
  • Processor(_Total)\% Processor Time
  • TCPv4\Connections Established

快速采集(5 秒采样,30 分钟)

logman create counter iis_perf -f bincirc -max 200 -si 00:00:05 ^
 -c "\HTTP Service Request Queues(*)\CurrentQueueSize" ^
 "\Web Service(_Total)\Current Connections" ^
 "\Process(w3wp)\% Processor Time" ^
 "\Processor(_Total)\% Processor Time" ^
 "\TCPv4\Connections Established"

logman start iis_perf

压测建议

  • 压测机同城同运营商,延迟 2–5ms;生产回国链路另做端到端压测。
  • wrk -t16 -c1000 -d60s --latency http://your.host/endpoint
  • 着重看:RPS、P95/P99、非 2xx 比例、503.2。

实际对比数据(节选)

业务:读多写少的 JSON 接口;动态可内核缓存 10 秒;落地 Redis/SQL 连接数有限。

压测:同城 2ms RTT、10GbE,不限速。

阶段 核心改动 峰值 RPS P95 延迟 503.2 占比
A(基线) 默认 IIS,队列 1000,未启用内核缓存 13.8k 420ms 3.7%
B 队列 65535 + AlwaysRunning + 预加载 18.9k 260ms 0.9%
C 启用 Kernel Cache(热点 10s)+ 动态压缩 31.4k 92ms 0.1%
D 线程池预热 + 日志与压缩目录迁移 NVMe 36.8k 70ms 0.02%

说明:RPS 并非“无限增长”。从 C→D 的收益在于稳定性与低尾延迟,活动高峰的“抖针”明显减少。

常见坑与现场解法

放大队列后 503.2 仍然出现

排查 HTTP Service Request Queues(*)\CurrentQueueSize 是否持续上涨且不回落。

若是持续不回落:吞吐不足 → 优先核查内核缓存命中与业务热点是否可缓存。

若是锯齿状回落:可能是回收/GC 抖动 → 调整回收窗口、查看 GC 模式与线程池。

Web Garden 开启后用户登录“掉线”

会话未外置。解决:Session 转 Redis/SQL 或改无状态令牌;或关闭 web garden。

ANCM OutOfProcess 下 Kestrel 成为瓶颈

观察 dotnet 进程 CPU 与 w3wp 的关系,必要时提升 ThreadPool.SetMinThreads 并开启 HTTP/2 multiplexing;或者切回 InProcess。

动态压缩 CPU 打满

对热点 JSON 才启用动态压缩,或只在中高体量响应启用;对超小响应禁压缩。

IIS 中设置按 mimeType 精准白名单,不要一刀切。

日志写入卡顿

日志目录与应用目录分卷,并放 NVMe;必要时使用集中日志(ETW/Agent)异步收集。

偶发 400/431(Header 超限)

检查网关/前端是否带了异常大的 Cookie 或自定义头;在 HTTP.sys 合理放大 MaxFieldLength/MaxRequestBytes,同时压缩或分片前端头部。

TLS 版本与 HTTP/2

Server 2016 上 HTTP/2 需要 TLS1.2 与支持的套件;确认未被组策略关闭。

若需要强制,检查注册表 EnableHttp2Tls(1 为启用),证书链完整且使用 ECDSA/RSA 现代套件。

一键化操作脚本(PowerShell 片段)

安全起见,先在测试环境执行;生产逐条验证。

# 变量
$PoolName = "ProdPool"
$SiteName = "Default Web Site"
$LogDir   = "D:\IISLogs"
$ZipTmp   = "D:\IIS_Compress_Temp"

# 创建目录
New-Item -Force -ItemType Directory $LogDir,$ZipTmp | Out-Null

# 启用组件
dism /online /enable-feature /featurename:IIS-HttpCompressionStatic /NoRestart | Out-Null
dism /online /enable-feature /featurename:IIS-HttpCompressionDynamic /NoRestart | Out-Null
dism /online /enable-feature /featurename:IIS-Caching /NoRestart | Out-Null

# 队列与预热
& $env:windir\system32\inetsrv\appcmd.exe set apppool "$PoolName" /queueLength:65535
& $env:windir\system32\inetsrv\appcmd.exe set apppool "$PoolName" /startMode:AlwaysRunning
& $env:windir\system32\inetsrv\appcmd.exe set app "$SiteName/" /preloadEnabled:true

# 日志与压缩目录
& $env:windir\system32\inetsrv\appcmd.exe set config -section:sites /siteDefaults.logFile.directory:"$LogDir" /commit:apphost
& $env:windir\system32\inetsrv\appcmd.exe set config "$SiteName" -section:system.applicationHost/sites /+"[name='$SiteName'].logFile.directory:'$LogDir\$SiteName'" /commit:apphost

# 压缩与内核缓存
& $env:windir\system32\inetsrv\appcmd.exe set config -section:urlCompression /doStaticCompression:"True" /commit:apphost
& $env:windir\system32\inetsrv\appcmd.exe set config -section:urlCompression /doDynamicCompression:"True" /commit:apphost
& $env:windir\system32\inetsrv\appcmd.exe set config -section:httpCompression /+"dynamicTypes.[mimeType='application/json',enabled='True']" /commit:apphost
& $env:windir\system32\inetsrv\appcmd.exe set config -section:system.webServer/caching /enableKernelCache:"True" /commit:apphost

变更与回滚策略(生产必要)

灰度顺序:网络栈 → 内核缓存(仅命中可回滚)→ 队列放大 → 回收窗口/线程池 → Web Garden(若启用)。

回滚点:每一步改动前 appcmd list config 导出快照;注册表变更做 .reg 备份。

窗口选择:活动外低峰期执行“重启需”的操作(如 HTTP.sys 参数)。

监控阈值:503.2 比例>0.1%、P95 超出基线 30%、CurrentQueueSize 连续 60s 不回落 → 自动回滚上一阶段参数。

FAQ:面对百万级请求,究竟什么最关键?

命中率。让 60%+ 的热点命中内核缓存,吞吐就“上楼梯”。

短队列 + 充足并发。队列不是越大越好;要结合线程池/CPU 实际承载。

稳定的 IO。日志与临时目录分卷,很多“玄学抖动”就会消失。

回收要有“节奏”。允许重叠 + 定点回收,活动期间不要“定时爆破”。

活动当天晚上 23:40,我们在机房外的便利店喝了罐温的维他柠檬茶。监控墙恢复成了清爽的绿:RPS 平稳在 3 万多,P95 压在两位数,503.2 几乎看不见。凌晨 1 点,我把最后一条 logman stop iis_perf 敲下,像把门轻轻带上。
有同事笑我:这套调优笔记写得像给别人看的。我说:是写给“下一次的自己”的。现场永远会有新的坑,但方法论不会变——先把流量挡在内核,队列只做缓冲,并发与线程池对齐 CPU,IO 抖动切干净,用数据验证假设。
如果你也在香港的机房里和我一样,被 503.2 追着跑,希望这篇文能帮你把局面“稳住”,然后漂亮地反击。

附:检查清单(上线前 3 分钟快速扫一遍)

  1.  queueLength 已调整并与吞吐预估匹配
  2.  AlwaysRunning + 站点预加载开启
  3.  Kernel Cache 命中正常(通过响应头/ETW 验证)
  4.  动态压缩按 MIME 白名单启用,CPU 余量>20%
  5.  日志/压缩/临时目录在 NVMe 独立卷
  6.  回收窗口落在低峰且允许重叠
  7.  线程池最小线程预热到合适水平
  8.  RSS/RSC 启用,多队列正常
  9.  PerfMon 与告警阈值到位,回滚脚本已就绪

以上内容都是我在真实现场踩坑后的总结。你可以不必一次把所有“开关”拉满,但请一定先测再上、一步一验证。祝你这次活动稳稳拿下。

目录结构
全文