1. 项目概述:当IIS突然甩出“Server is too busy”,你该信什么、改什么、查什么
“Server is too busy”——这行红色错误字眼,对任何正在线上跑着ASP.NET站点的运维或开发人员来说,不亚于半夜收到数据库主库宕机的告警。它不像500 Internal Server Error那样告诉你哪行代码崩了,也不像404那样明确指向资源缺失;它更像一个模糊但极具压迫感的警告:你的服务器已进入临界状态,请求正在被无情拒绝,而你手头没有任何堆栈跟踪可抓。我第一次在生产环境看到它时,正赶在电商大促前3小时,监控显示CPU和内存都才用掉40%,IIS队列长度却飙到200+,所有新请求直接返回这个错误页。后来复盘发现,问题根本不在硬件瓶颈,而在于web.config里一行被注释掉的httpRuntime配置——它默默关闭了请求排队机制,让IIS连最基本的缓冲都放弃了。
这个标题直击两个核心痛点:一是现象层(Server is too busy的应急处置),二是技术层(httpRuntime节点在web.config中的准确定位与参数逻辑)。它不是问“怎么装IIS”,也不是问“ASP.NET Core和Framework的区别”,而是聚焦在传统ASP.NET Framework(.NET Framework 4.x)托管在IIS下的经典并发瓶颈场景。关键词“httpRuntime”是解题钥匙,它不属于ASP.NET Core的配置体系,而是.NET Framework时代web.config中控制HTTP管道行为的核心节;而“web.config”则框定了操作边界——所有修改必须落在这个XML文件里,且必须在正确的sectionGroup下。适用人群非常明确:正在维护老系统、接手遗留项目、或面试中被问到IIS底层机制的开发者;尤其适合那些刚从ASP.NET Core转回Framework调试的老手——你会惊讶地发现,Core里靠Kestrel和中间件搞定的并发控制,在Framework里全靠web.config里几行XML和IIS应用程序池的配合。
很多人误以为这是IIS本身的问题,于是疯狂重启服务、调高应用程序池的“最大工作进程数”,甚至重装IIS。实测下来,90%的此类操作不仅无效,反而可能因进程回收加剧问题。真正有效的路径只有一条:先理解IIS请求处理管道如何与ASP.NET Runtime协同工作,再定位web.config中httpRuntime的maxRequestLength、executionTimeout、requestValidationMode等参数如何影响排队与超时,最后结合应用程序池的队列长度、CPU限制、空闲超时等设置做联动调整。这不是一个“改个数字就能好”的简单操作,而是一次对.NET Framework托管模型的深度校准。下面我会把整个排查链路拆解成可执行的步骤,包括每个参数背后的物理意义、为什么改这个值、改多少才合理,以及那些文档里绝不会写的“踩坑现场”。
2. 核心机制解析:IIS、应用程序池与httpRuntime的三层协作关系
2.1 IIS请求处理管道的三级缓冲结构
要真正解决“Server is too busy”,必须跳出“改配置”的思维,先看清IIS如何把一个HTTP请求从网卡送到你的C#代码里。这不是单线程直通,而是一个带三道缓冲闸的流水线:
第一道闸:HTTP.SYS内核态队列。当请求抵达服务器IP端口,Windows内核的HTTP.SYS驱动先接管,把它放进一个固定长度的内核队列(默认1000个请求)。这个队列不消耗用户态内存,纯内核空间管理。如果这里满了,客户端会直接收到TCP RST包,根本不会出现“Server is too busy”——因为错误发生在更底层。所以,当你看到这个错误,说明请求已经过了HTTP.SYS,进入了用户态。
第二道闸:IIS应用程序池的工作线程池。IIS为每个应用程序池维护一个线程池(默认大小由<processModel>节控制,但Framework 4.0+后通常由IIS自动管理)。线程池负责从HTTP.SYS队列取请求,交给ASP.NET Runtime处理。关键点来了:如果线程池所有线程都在忙(比如在等待数据库响应、调用外部API、或执行耗时计算),新请求就会被放入应用程序池级别的“请求队列”。这个队列长度就是web.config里<system.webServer><serverRuntime>节的appConcurrentRequestLimit属性(IIS 7.5+默认5000)。一旦这个队列也满了,IIS就不再接收新请求,直接返回“503 Service Unavailable”,页面上显示的就是“Server is too busy”。
第三道闸:ASP.NET Runtime的请求执行上下文。这才是httpRuntime真正起作用的地方。当IIS线程把请求交给ASP.NET后,Runtime会检查<httpRuntime>节的maxRequestLength(限制上传文件大小)、executionTimeout(单个请求最长执行时间)、requestValidationMode(请求验证模式)等。如果请求体超过maxRequestLength,会在解析阶段就被拒绝;如果执行时间超过executionTimeout,Runtime会主动终止线程并抛出ThreadAbortException。但注意:httpRuntime本身不控制队列长度,它只管单个请求的生命周期。真正决定“是否排队”的,是IIS的应用程序池队列和线程池状态。很多人混淆这点,以为调大executionTimeout就能缓解“too busy”,结果只是让每个坏请求占用线程更久,反而加剧队列堵塞。
提示:你可以用命令行快速查看当前应用程序池队列长度:
appcmd list apppool "YourAppPoolName" /text:queueLength。如果这个值持续接近或等于appConcurrentRequestLimit(默认5000),说明IIS层已饱和,必须优化代码或扩容;如果远低于此值,问题大概率在ASP.NET Runtime层,比如某个请求因锁竞争或死循环卡住线程。
2.2 httpRuntime节点的物理位置与嵌套规则
web.config是ASP.NET应用的中枢配置文件,但它不是扁平结构,而是分层嵌套的。<httpRuntime>节点必须放在<system.web>节内,且只能出现一次。它的父节点路径是:<configuration>→<system.web>→<httpRuntime>。常见错误是把它错放到<system.webServer>下(那是IIS 7.0+的模块配置区),或者放在<location>标签外导致全局失效。正确结构如下:
<?xml version="1.0" encoding="utf-8"?> <configuration> <system.web> <!-- 这里是httpRuntime的合法位置 --> <httpRuntime maxRequestLength="102400" executionTimeout="300" requestValidationMode="4.5" enableVersionHeader="false" relaxedUrlMapping="true" /> <!-- 其他system.web节点,如compilation、customErrors等 --> <compilation debug="false" targetFramework="4.7.2" /> <customErrors mode="On" defaultRedirect="Error.aspx" /> </system.web> <!-- 注意:system.webServer是IIS模块配置区,httpRuntime不能放这里 --> <system.webServer> <security> <requestFiltering> <requestLimits maxAllowedContentLength="1073741824" /> </requestFiltering> </security> </system.webServer> </configuration>这里有个关键细节:maxRequestLength单位是KB,而<system.webServer><security><requestFiltering>里的maxAllowedContentLength单位是字节(Byte)。两者必须匹配!比如你想允许100MB上传,maxRequestLength要设为102400(1001024),maxAllowedContentLength要设为1073741824(1001024*1024)。如果前者小后者大,ASP.NET Runtime会在解析请求体时先报错;如果后者小前者大,IIS会在接收数据时就截断并返回400 Bad Request。这种不一致是“Server is too busy”的隐形推手——用户上传大文件失败,重试多次,瞬间堆积大量半截请求。
2.3 为什么executionTimeout不是“越长越好”
executionTimeout参数常被误解为“延长请求存活时间”,但它的实际作用是设置ASP.NET Runtime监控单个请求执行时间的阈值。默认值是90秒(.NET Framework 4.0+),意味着如果一个请求在90秒内没返回,Runtime会强制终止它,并记录事件日志。问题在于:这个终止动作本身会消耗额外资源。当大量请求同时超时,Runtime要逐个清理线程、释放上下文、写日志,这反而加重服务器负担。我曾遇到一个报表导出接口,因数据库索引缺失导致查询耗时200秒,我把executionTimeout从90秒调到600秒,本意是让用户等得更久,结果发现服务器CPU在超时瞬间飙升30%,因为Runtime在集中处理一批被杀掉的线程。
更合理的做法是:对已知的长耗时操作,用异步模式(Async/Await)或后台任务(BackgroundService)解耦,而不是无脑拉长timeout。比如导出Excel,前端提交后立即返回任务ID,后端用Task.Run在独立线程生成文件,前端轮询状态。这样主请求线程几秒内就释放了,不会阻塞IIS线程池。executionTimeout应该作为安全兜底,而非业务逻辑的一部分。实测经验:对于95%的Web API,30-60秒足够;对于文件上传,按maxRequestLength反推,比如100MB上传,网络传输按10MB/s算,最多10秒,timeout设120秒足矣。
3. 实操配置指南:web.config中httpRuntime的参数详解与安全阈值
3.1 maxRequestLength:上传文件大小的双保险设置
maxRequestLength是<httpRuntime>中最常被调整的参数,它直接控制ASP.NET Runtime允许接收的最大请求体大小(单位:KB)。默认值是4096KB(4MB),这对现代Web应用显然不够——一张高清图片就可能超限。但调大它不是简单加个零的事,必须同步考虑三个层面:
第一层:IIS层的物理限制。IIS有自己的请求体大小限制,由<system.webServer><security><requestFiltering>的maxAllowedContentLength控制(单位:字节)。如果Runtime允许100MB,但IIS只让传30MB,请求在进入ASP.NET前就被拒。计算公式很简单:maxAllowedContentLength = maxRequestLength * 1024。例如:
maxRequestLength="102400"(100MB) →maxAllowedContentLength="1073741824"maxRequestLength="204800"(200MB) →maxAllowedContentLength="2147483648"
第二层:服务器内存压力。ASP.NET默认将整个请求体加载进内存再解析。一个200MB的上传请求,会瞬间吃掉服务器200MB内存。如果并发10个,就是2GB——这还没算你的业务逻辑内存开销。解决方案是启用<httpRuntime useFullyQualifiedRedirectUrl="true" />配合流式上传(Stream Upload),但Framework下需手动处理HttpContext.Current.Request.InputStream,不能依赖Request.Files。我在一个医疗影像系统里,把maxRequestLength设为500000(约488MB),但强制要求前端用分片上传(每片2MB),后端用FileStream直接写磁盘,避免内存暴涨。
第三层:安全风险。过大的maxRequestLength是DoS攻击的温床。攻击者发送海量超大请求,迅速耗尽服务器内存或磁盘空间。最佳实践是:按业务场景分级设置。比如:
- 普通表单提交:保持默认4096KB(4MB)
- 头像上传:设为10240KB(10MB)
- 文档上传:设为51200KB(50MB)
- 影像上传:单独路由,用
<location path="upload">节覆盖,设为500000KB(488MB)
<!-- web.config中按路径分级配置示例 --> <configuration> <system.web> <httpRuntime maxRequestLength="4096" executionTimeout="90" /> </system.web> <!-- 为/upload路径单独设置更大限制 --> <location path="upload"> <system.web> <httpRuntime maxRequestLength="500000" executionTimeout="600" /> </system.web> </location> </configuration>注意:
<location>节的path是相对于应用根目录的虚拟路径,不是物理文件路径。它必须放在<configuration>根节点下,且优先级高于全局<system.web>设置。
3.2 executionTimeout:超时阈值的动态平衡术
executionTimeout(单位:秒)是另一个高频调整项,但它背后藏着性能与用户体验的精细博弈。默认90秒看似宽松,但在高并发下,它可能成为压垮骆驼的最后一根稻草。原因在于:超时不是静默发生,而是触发Runtime的线程清理流程。当一个请求超时时,Runtime会:
- 调用
Thread.Abort()终止线程 - 执行
finally块和using语句的Dispose - 记录Windows事件日志(Application Log)
- 返回500错误给客户端
这一系列操作本身需要CPU和IO资源。如果100个请求同时超时,服务器会陷入“救火-超时-再救火”的恶性循环。我的实操经验是:对绝大多数API,executionTimeout应设为业务SLA的1.5倍,而非无限拉长。比如支付回调接口,业务要求3秒内响应,那么timeout设5秒足够;报表导出接口,用户可接受2分钟,timeout设180秒(3分钟)比600秒(10分钟)更安全——因为后者会让故障影响面扩大。
还有一个隐藏陷阱:executionTimeout对异步操作(async/await)的计时逻辑不同。在Framework中,await后的代码继续在原始上下文中执行,timeout计时器会暂停等待await完成。这意味着,如果你写await Task.Delay(10000),这10秒不计入timeout。但如果你用Thread.Sleep(10000),这10秒会计入。因此,务必用await替代Thread.Sleep,并确保所有IO操作(DB、HTTP)都异步化。否则,看似“异步”的代码,实际仍是同步阻塞,timeout照样会触发。
<!-- 推荐的httpRuntime timeout配置 --> <httpRuntime maxRequestLength="102400" executionTimeout="180" requestValidationMode="4.5" enableVersionHeader="false" />这里executionTimeout="180"(3分钟)是我在线上电商系统的标准值:覆盖了99.9%的正常请求,又为慢查询留出足够缓冲,同时避免超时风暴。测试方法很简单:用JMeter模拟100并发,请求一个故意Thread.Sleep(200000)的接口,观察服务器CPU和事件日志——如果CPU峰值超过70%,说明timeout值偏大,需下调。
3.3 requestValidationMode:绕过请求验证的安全代价
requestValidationMode参数常被用来解决“用户输入尖括号被拦截”的问题,比如富文本编辑器提交含HTML的内容。默认值是4.0(Framework 4.0行为),设为2.0可禁用请求验证,设为4.5则启用更严格的验证(.NET 4.5新增)。但很多人不知道,禁用请求验证(requestValidationMode="2.0")会带来XSS漏洞风险,且无法通过[ValidateInput(false)]特性完全规避。
根本原因在于:requestValidationMode="2.0"只关闭了ASP.NET对Request.Form和Request.QueryString的自动HTML编码检查,但Request.Files、Request.Headers等仍受保护。更危险的是,它会让<% %>服务器端代码块在用户输入中被执行——如果某处代码写了Response.Write(Request["userInput"]),攻击者提交<script>alert(1)</script>就能弹窗。我在一个CMS系统里见过真实案例:管理员用富文本发公告,黑客在公告里插入恶意JS,所有访问者打开页面就中招。
安全的替代方案是:保持requestValidationMode="4.5"(默认),对特定字段用HttpUtility.HtmlEncode编码输出,或用AntiXssEncoder类进行白名单过滤。比如:
// 安全写法:对用户输入的HTML内容进行编码后再输出 string userInput = Request["content"]; string safeOutput = HttpUtility.HtmlEncode(userInput); Response.Write(safeOutput); // 或使用AntiXssEncoder(需引用Microsoft.Security.Application) string safeHtml = Microsoft.Security.Application.AntiXss.HtmlEncode(userInput);如果业务确实需要存储原始HTML(如博客文章),应在存储前用HtmlSanitizer库清洗,而非在web.config里一刀切关验证。requestValidationMode的调整,永远应该是最后手段,且必须伴随全面的安全审计。
4. 全链路排查实战:从IIS日志到内存转储的七步定位法
4.1 第一步:确认错误来源——区分IIS原生503与ASP.NET自定义错误
“Server is too busy”这个提示,表面看是IIS返回的503错误,但实际有三种可能源头,必须先定位:
IIS原生503:由IIS应用程序池的
appConcurrentRequestLimit触发,日志在%SystemRoot%\System32\inetsrv\config\applicationHost.config中定义,默认5000。此时IIS Event Log(Windows日志 -> 应用程序)会记录事件ID 1009:“The application pool 'xxx' has reached the limit for concurrent requests.”。解决方法是调高appConcurrentRequestLimit或优化代码减少单请求耗时。ASP.NET自定义503:某些老版本.NET Framework或自定义HttpModule会主动返回503,日志在
Event Viewer -> Application中搜索“.NET Runtime”错误。这时要看<customErrors>配置是否开启,以及是否有未捕获异常。前端代理/CDN返回的503:如果用了Nginx、Cloudflare等,它们也有自己的队列限制。此时IIS日志里看不到对应请求,需检查代理层日志。
实操技巧:用curl -v http://yoursite.com/health(健康检查端点)对比。如果返回503 Service Unavailable且Server: Microsoft-IIS/10.0,基本是IIS层问题;如果返回503但Server: nginx,问题在代理层。
4.2 第二步:检查IIS应用程序池状态——用appcmd命令精准诊断
不要依赖IIS Manager图形界面,它有时会缓存状态。用命令行工具appcmd.exe获取实时数据:
# 进入IIS命令行工具目录(根据系统版本调整路径) cd C:\Windows\System32\inetsrv\ # 查看指定应用池的详细状态 appcmd list apppool "DefaultAppPool" /text:* # 关键字段解读: # queueLength : 当前排队请求数(重点关注!) # currentAppPoolState : Started/Stopped # idleTimeout : 空闲超时时间(秒),超时后工作进程回收 # cpuLimit : CPU使用率上限(百分比),超限后进程被杀 # pingingEnabled : 是否启用健康检查(ping)如果queueLength持续>1000,说明IIS队列已严重积压。此时检查idleTimeout是否过短(如设为1分钟),导致工作进程频繁启停,每次启动都要加载程序集,反而拖慢响应。我的标准配置是idleTimeout="1800"(30分钟),既保证资源回收,又避免冷启动。
4.3 第三步:分析IIS日志——用Log Parser Studio提取瓶颈请求
IIS日志(默认在%SystemDrive%\inetpub\logs\LogFiles\W3SVC1\)是黄金线索。用微软免费工具Log Parser Studio(LPS)快速分析:
- 打开LPS,连接日志文件夹
- 运行SQL查询:
SELECT cs-uri-stem AS RequestPath, COUNT(*) AS HitCount, AVG(time-taken) AS AvgTimeMs, MAX(time-taken) AS MaxTimeMs, STRCAT(TO_STRING(QUANTIZE(time-taken, 1000)), 's') AS TimeBucket FROM '[LOGFILEPATH]' WHERE sc-status = 503 GROUP BY cs-uri-stem ORDER BY HitCount DESC这个查询会列出所有返回503的URL及其平均耗时。如果发现/api/report/export平均耗时85000ms(85秒),那问题就锁定在这个接口——它很可能没做异步,或数据库查询没索引。接着查这个URL的time-taken分布,如果大量请求集中在90s附近,基本确认是executionTimeout触发的超时。
4.4 第四步:抓取内存转储——用ProcDump定位线程阻塞
当IIS队列满但CPU不高时(典型症状:CPU<30%,queueLength>4000),往往是线程被阻塞。用Sysinternals的ProcDump抓取转储:
# 下载ProcDump到C:\temp\ # 抓取w3wp.exe进程(IIS工作进程)的内存转储 procdump64.exe -ma -n 3 -s 10 w3wp.exe C:\temp\w3wp_dump.dmp # 参数说明: # -ma : 抓取完整内存 # -n 3 : 当CPU>70%时抓3次 # -s 10 : 间隔10秒用Visual Studio打开.dmp文件,调试->窗口->线程,看哪些线程状态是Wait。右键“切换到线程”,查看调用堆栈。如果大量线程停在System.Data.SqlClient.TdsParser.ReadNetworkPacket,说明数据库连接池耗尽;如果停在System.Threading.Monitor.Enter,说明有锁竞争。这是我处理过最典型的案例:一个静态字典被多线程读写,没加锁,导致Monitor.Enter死等。
4.5 第五步:验证web.config配置——用aspnet_regiis检查语法
web.config语法错误不会直接报错,但会导致配置不生效。用.NET Framework自带的aspnet_regiis.exe验证:
# .NET Framework 4.7.2路径 C:\Windows\Microsoft.NET\Framework64\v4.0.30319\aspnet_regiis.exe -k "DefaultAppPool" # 如果配置正确,返回"Configuration section registration completed successfully." # 如果有XML错误,会明确指出第几行第几列特别注意:<httpRuntime>节点不能有重复属性,比如maxRequestLength写了两次;属性值不能含空格,如executionTimeout = "300 "(末尾空格)会导致解析失败,Runtime用默认值。
4.6 第六步:压力测试验证——用Apache Bench模拟真实流量
改完配置后,必须用ab(Apache Bench)压测验证效果:
# 模拟100并发,持续60秒,请求首页 ab -n 10000 -c 100 http://localhost/ # 关键指标看: # Requests per second : 每秒请求数(越高越好) # Time per request : 平均响应时间(越低越好) # Failed requests : 失败请求数(应为0) # Percentage of the requests served within a certain time : 90%请求在多少毫秒内完成如果Failed requests> 0,且错误是Connection refused,说明IIS队列真的满了;如果是Socket receive failed,可能是网络层问题。我习惯用-c 50起步,逐步加到-c 200,观察Requests per second是否线性增长。如果到-c 100时TPS就停滞,说明瓶颈在代码或数据库,不是配置问题。
4.7 第七步:上线后监控——用PerfMon盯住四个黄金指标
生产环境不能只靠日志,要用Windows性能监视器(PerfMon)实时盯住:
| 性能对象 | 计数器 | 健康阈值 | 异常表现 |
|---|---|---|---|
| ASP.NET Applications | Requests Queued | < 10 | 持续>50,说明Runtime处理不过来 |
| Web Service | Current Connections | < 500 | 突然飙升到1000+,可能被CC攻击 |
| Processor | % Processor Time | < 80% | 长期>90%,CPU瓶颈 |
| Memory | Available MBytes | > 1024 | < 512MB,内存不足 |
创建Data Collector Set,每15秒采样一次,保存为BLG文件。当Requests Queued连续5分钟>20,自动触发邮件告警——这是我给所有线上系统标配的监控规则。
5. 常见问题速查表与独家避坑心得
5.1 常见问题速查表
| 问题现象 | 可能原因 | 快速验证方法 | 解决方案 |
|---|---|---|---|
| “Server is too busy”偶发,重启IIS临时解决 | 应用程序池idleTimeout过短,频繁回收进程 | appcmd list apppool /text:idleTimeout | 将idleTimeout从默认10分钟改为30分钟 |
| 大文件上传失败,返回400 Bad Request | maxAllowedContentLength与maxRequestLength不匹配 | 检查web.config中两个值的换算关系 | 确保maxAllowedContentLength = maxRequestLength * 1024 |
| 修改web.config后不生效 | IIS未检测到文件变更,或配置节位置错误 | 用aspnet_regiis -k验证语法;检查<httpRuntime>是否在<system.web>内 | 保存web.config后,手动回收应用程序池;确认XML嵌套层级 |
CPU很高,但queueLength很低 | 代码存在死循环、无限递归或锁竞争 | 用ProcDump抓取dump,分析线程堆栈 | 修复死循环;用ConcurrentDictionary替代static Dictionary;添加超时机制 |
| 本地IIS正常,部署到服务器报错 | 服务器缺少.NET Framework版本,或权限不足 | 在服务器运行dotnet --list-runtimes;检查IIS_IUSRS组对网站目录的读取权限 | 安装对应Framework版本;用icacls命令授予IIS_IUSRS完全控制权 |
5.2 我踩过的三个深坑与硬核心得
坑一:enableVersionHeader="false"引发的连锁反应
这个参数本意是隐藏ASP.NET版本号提升安全性,但我在一个金融系统里启用后,发现部分iOS设备无法上传文件。抓包发现,禁用版本头后,某些旧版iOS WebView的XMLHttpRequest会错误解析响应头。最终解决方案是:只对非敏感路径禁用,如<location path="api">中保留enableVersionHeader="true",而在<location path="public">中设为false。安全和兼容性必须做trade-off。
坑二:relaxedUrlMapping="true"打开后,URL编码被忽略
这个参数允许URL中包含{ } [ ]等字符,但副作用是%20(空格)不再被自动解码。用户提交/search?q=hello%20world,后端Request.QueryString["q"]拿到的是hello%20world而非hello world。修复方法:手动调用Uri.UnescapeDataString(),或在Global.asax的Application_BeginRequest中统一解码。
坑三:httpRuntime配置被machine.config全局覆盖machine.config(位于C:\Windows\Microsoft.NET\Framework64\v4.0.30319\Config\)中的<httpRuntime>会作为默认值,web.config里的设置是叠加而非覆盖。如果machine.config里executionTimeout="300",而你在web.config设"180",实际生效的是180秒;但如果machine.config设了maxRequestLength="2048",而你web.config设"102400",实际生效的还是102400——因为web.config优先级更高。唯一例外是enableVersionHeader,它在machine.config中设为true时,web.config设false才有效。所以,排查问题时,务必同时检查machine.config。
最后分享一个真实场景:上周帮一家物流公司排查,他们“运单查询”接口在每天早高峰(8-9点)必现“Server is too busy”。日志显示queueLength峰值4800,但CPU只有40%。用ProcDump抓dump,发现90%线程卡在System.Data.SqlClient.SqlCommand.ExecuteReader。查数据库发现,查询语句用了SELECT * FROM orders WHERE tracking_no LIKE '%{input}%',且tracking_no字段没索引。加了NONCLUSTERED INDEX后,queueLength降到50以下,TPS从80提升到320。所以记住:再完美的web.config配置,也救不了一个没索引的LIKE查询。性能优化的第一步,永远是看数据库执行计划。