IIS上SignalR连接数过载导致API阻塞?排查与解决方案 这个标题里的现象我印象太深了。之前帮朋友排查一个部署在 Windows Server 上的 .NET Core 项目项目里挂了 SignalR 做实时通知结果只要在线客户端数量到 10 个左右Web API 的请求就全部卡住转圈转到超时。一开始我还以为是代码里有死锁后来才发现问题出在 IIS、SignalR 和 ASP.NET Core 并发模型这三者的配合上。如果你也遇到类似情况建议先别急着改业务代码这篇文章我会把整个排查过程和解决方案拆开讲清楚为什么 10 个连接会成为临界点、怎么确认当前 SignalR 到底走的什么传输模式、IIS 上需要做什么配置以及代码里哪些隐藏坑会直接放大这个问题。1. 故障现象与问题定位1.1 现象描述在线连接数一到 10 个左右API 全部转圈先还原一下现场。项目是标准的前后端分离架构后端是 ASP.NET Core Web API部署在 Windows Server 的 IIS 上前端通过 SignalR 客户端接收服务端推送的消息。项目刚发布时一切正常但运行一段时间后只要 SignalR 的在线连接数达到 10 个左右再发起任何一个 Web API 请求浏览器端就会一直处于 pending 状态直到 IIS 返回 504 或 503。这个现象有几个非常明显的特征SignalR 连接本身没有立刻断开客户端显示已连接。新的 HTTP 请求进不来但已经建立的连接还能维持一段时间。重启 IIS 应用程序池之后系统短暂恢复但连接数再次上来后又开始卡。卡住的时间不固定有时几十秒有时直接超时。这种“连接数一到某个量级就整体阻塞”的现象很容易让人误判成数据库连接池耗尽或者某个 Redis 连接问题。但仔细观察会发现即使是不查数据库的静态接口也卡住说明问题出在更底层的 HTTP 请求处理链路上而不是某个具体业务模块。1.2 最先想到的检查项排除死锁和接口慢查询遇到这种问题我第一反应是排查代码里有没有同步阻塞比如在 async 方法里用了 .Result 或者 .Wait()。因为 SignalR 的 Hub 方法和 Web API 是跑在同一个进程里的如果某个 Hub 方法里有同步阻塞调用线程池线程会被快速耗尽新请求自然进不来。我先做了三件事用日志记录每个请求的进入时间和离开时间看是根本没进来还是进来了但处理很慢。把数据库、Redis 等外部依赖全部暂时屏蔽测一个纯内存返回的接口看是否还会卡。检查所有 Service 注册的生命周期确认没有把 DbContext 这种 Scoped 服务错误注册成 Singleton导致并发竞争。结果出乎意料纯内存接口在连接数上来之后照样卡。也就是说问题不在这几个常见坑里而是 IIS 和 SignalR 之间的请求处理方式出了问题。1.3 明确的排查方向SignalR 的传输模式当排除了业务代码后我把目光放到了 SignalR 的传输机制上。SignalR 为了兼容不同环境默认支持三种传输方式WebSocket、Server-Sent EventsSSE、Long Polling。这里有一个很关键的知识点并不是所有部署环境都能真正走 WebSocket。SignalR 客户端启动时会先发一个 negotiate 请求然后根据服务器返回的能力和当前网络环境选择最优传输方式。如果 IIS 上没装 WebSocket Protocol 功能或者中间有代理/负载均衡器没有正确转发 Upgrade 头SignalR 就会自动回退到 SSE 或 Long Polling。而 SSE 和 Long Polling 有一个致命问题每一个在线客户端都会长期占住一个 HTTP 请求不释放。服务端处理这些请求需要占用线程或异步连接当连接数量达到一定临界值后剩余的处理能力就无法响应新的 API 请求了。这就解释了为什么看起来像是“10 个连接之后开始阻塞”——实际上这取决于服务器配置的并发上限、CPU 核数、线程池初始线程数等因素10 只是一个常见临界值。2. 为什么 10 个连接就能把请求堵死核心原理拆解2.1 IIS 托管 .NET Core 的请求处理模型要理解阻塞的根因必须先知道 .NET Core 应用发布到 IIS 后请求是怎么流动的。当前主流的部署方式是进程内托管In-Process也就是说 ASP.NET Core 应用直接跑在 IIS 的应用程序池工作进程里IIS 接收到 HTTP 请求后直接交给应用内的 Kestrel 服务器处理不再经过一个独立的 Kestrel 进程。进程内托管的好处是性能更好少一次进程间通信。但它也意味着应用和 IIS 共享同一个进程的资源包括线程池。IIS 本身对并发请求是有队列管理的应用程序池有一个“队列长度”参数默认是 1000。也就是说如果请求处理不过来新请求会在 IIS 队列里排队而不是直接进到应用代码里。但这里要注意IIS 的队列长度默认 1000理论上不会在 10 个连接时就爆掉。真正的瓶颈出现在 ASP.NET Core 的线程池和 SignalR 连接模型之间的冲突上。2.2 SignalR 的三种传输方式与连接占用差异SignalR 的三种传输方式对连接资源的占用是完全不同的WebSocket一次 HTTP 握手后协议升级为长连接数据双向实时传输。这种连接不占用 HTTP 请求处理线程对服务器压力最小。Server-Sent Events服务器向客户端单向推送客户端通过 EventSource 接收。服务器会保持一个 HTTP 响应连接长期不关闭每个客户端占一个请求槽。Long Polling客户端发一个请求服务器有消息就立即返回没消息就挂住等有消息或超时后再返回客户端收到后立刻发起下一个请求。这种模式下每个客户端在任何时刻也至少会有一个挂起的 HTTP 请求。麻烦就出在后两种模式。如果 SignalR 因为某种原因没有升级到 WebSocket而是回退到了 SSE 或 Long Polling那么每一个在线客户端就相当于一个永不结束的 HTTP 请求。在 ASP.NET Core 中这种长时间挂起的请求会占用线程池的可用线程。虽然异步请求不会一直占着线程不放但 SignalR 在处理消息调度、连接生命周期等逻辑时仍然需要线程池分配线程来执行回调。一旦这种半挂起的任务数量多了线程池就会进入“饥饿”状态。2.3 线程池饥饿真正压垮系统的最后一根稻草.NET 的线程池有一个动态调节机制。它会根据任务的到达速率和完成速率自动增加或减少线程数。增加线程是有成本的需要消耗 CPU 和内存所以线程池有一定的“惰性”。当任务量突然增大且部分任务长时间不完成时线程池会尝试每 500 毫秒左右增加一个线程。听起来好像问题不大但如果你的 SignalR 连接模式是 SSE 或 Long Polling这些连接会把线程池的“活跃请求数”撑起来但每个请求都不结束。线程池判断系统仍然繁忙于是不断加线程直到线程数达到上限。而线程数到达上限后新进来的 API 请求会进入线程池的全局队列。上面说的是理论上最典型的情况还有一个容易被忽略的技术细节线程池的“最小线程数”。默认情况下.NET 线程池的最小线程数通常等于 CPU 核心数。如果一个部署环境是 4 核甚至 2 核那么最小线程数很低一旦连接数稍微上来一点线程池就很容易进入饥饿策略。实际上线程池饥饿时不一定真的加不上线程而是加线程的速度赶不上请求堆积的速度。加上很多开发者在 Hub 方法里习惯性地写了同步代码比如用 HttpClient 的 .Result、用 Thread.Sleep、用 lock(obj)这会让线程被真正占用而不是异步挂起进一步加速线程池耗尽。2.4 为什么临界点看起来是“10 个连接”而不是其他数字很多人在排查时会纠结“为什么是 10”甚至去 IIS 配置里找有没有哪项限制是 10。我排查过不少案例得出的结论是10 并不是 IIS 写死的数字而是你的服务器资源、线程池配置和应用代码共同决定的临界值。可以做一个简单推算假设服务器 CPU 是 2 核线程池默认最小线程数可能就是 2。当有 10 个 SignalR 客户端在线且每个客户端因为传输模式回退都保持着一个 SSE 或 Long Polling 请求这时候线程池里至少有 10 个长期挂起的异步操作。如果其中一部分 Hub 方法还做了同步阻塞调用那么真正能用来处理新请求的线程可能已经所剩无几。新请求进来后排队等待表现得就像被“阻塞”了一样。所以重点不是去找一个隐藏的 10 连接上限而是先确认你的 SignalR 在 IIS 上到底走了哪种传输模式如果走的是 WebSocket10 个长连接根本不叫事如果走的是 SSE 或 Long Polling50 个、100 个连接迟早会暴露问题。2.5 另一个隐藏因素ASP.NET Core 版本和托管模式差异不同版本的 ASP.NET Core 在 IIS 上的并发行为也有差别。比如 .NET Core 3.1 到 .NET 6、.NET 8虽然整体模型一致但在 ThreadPool 的默认策略和 ANCM 的转发细节上会有微调。另外如果你的部署选择了进程外托管Out-Of-Process请求会先经过 IIS再由 ANCM 转发给独立的 Kestrel 进程。这种模式下IIS 和 Kestrel 之间有额外的进程间通信开销同时 IIS 的一些机制可能会在传输升级时产生额外的握手消耗也会影响连接临界值。如果你不确定自己项目是进程内还是进程外托管最简单的方法是看 web.config 里的 aspNetCore 节点。hostingModelinprocess 就是进程内hostingModeloutofprocess 就是进程外。3. 排查实录从复现到定位的完整过程3.1 第一步复现问题并确认连接方式和数量排查任何这类问题复现是第一位的。我写了一个简单的 SignalR 客户端模拟器循环创建 30 个连接连接到目标 Hub。每建立一个连接就调用一次 Web API观察是否出现阻塞。复现的同时在浏览器开发者工具里查看 SignalR 的 negotiate 请求和后续请求协议。如果能看到 101 Switching Protocols说明走的是 WebSocket。如果看不到而是一直有状态为 pending 的 GET 请求那基本可以确定走的是 SSE 或 Long Polling。如果项目是 C# 客户端还可以在 HubConnectionBuilder 里直接指定传输方式来做对照试验var connection new HubConnectionBuilder() .WithUrl(https://your-server/notificationHub, options { options.Transports HttpTransportType.WebSockets; options.SkipNegotiation true; }) .WithAutomaticReconnect() .Build();注意SkipNegotiation 只有在你明确使用 WebSocket 传输时才可以使用。如果服务器端不支持 WebSocket这种写法会直接抛异常而不是自动回退。这在排查时非常有用如果指定了 WebSocket 后全部连接失败说明问题就出在 WebSocket 握手链路不通。我在那个项目里做了同样的验证发现客户端指定 WebSocket 传输后连接全部失败。再回头查 IIS 功能果然WebSocket Protocol 没有安装。问题到这里已经锁定了八成。3.2 第二步检查 IIS 的 WebSocket Protocol 功能Windows Server 上 IIS 默认不会安装所有功能模块。WebSocket Protocol 属于“应用程序开发”类别下的一个子功能如果在安装 IIS 时没有手动勾选默认是缺失的。可以用 PowerShell 检查当前是否安装了 WebSocket ProtocolGet-WindowsFeature Web-WebSockets如果 Installed 状态是 False说明 IIS 缺少 WebSocket 支持。安装命令也很简单Install-WindowsFeature Web-WebSockets安装完成后建议重启 IISiisreset这里有一个容易踩的坑WebSocket Protocol 的安装可能需要重启服务器或者至少重启 IIS 服务才能生效。如果只装了功能不重启SignalR 握手仍然可能失败。3.3 第三步用 dotnet-counters 观察线程池状态为了拿到更直接的数据我使用了 dotnet-counters 这个诊断工具。如果你的服务器上还没装可以先安装dotnet tool install --global dotnet-counters然后找到应用进程 ID开始监控线程池和 CPU 状态dotnet-counters monitor --process-id PID --counters System.Runtime重点关注这几个指标ThreadPool Thread Count线程池当前线程数、ThreadPool Queue Length线程池队列长度、CPU UsageCPU 使用率。在复现阻塞时观察到的现象往往是线程池线程数在不停上涨但队列长度也在增长CPU 使用率却不高。这说明系统不是忙到算不过来而是有大量线程被“卡住”了典型特征就是线程饥饿。如果 ThreadPool Queue Length 持续高于 0而 Thread Count 已经达到一个较高值比如几百几乎可以断定代码或信号连接中存在着长时间占用线程的操作。3.4 第四步开启 stdout 日志确认 ANCM 的报错信息在 IIS 上排查 ASP.NET Core 问题一个很大的困惑是应用崩溃或连接异常时错误信息不会直接显示在浏览器里而是被 ANCMASP.NET Core Module吞掉了。这时候可以临时开启 stdout 日志来看 ANCM 层面的错误。在 web.config 中修改 aspNetCore 节点aspNetCore processPathdotnet arguments.\YourApp.dll stdoutLogEnabledtrue stdoutLogFile.\logs\stdout hostingModelinprocess environmentVariables environmentVariable nameASPNETCORE_ENVIRONMENT valueDevelopment / /environmentVariables /aspNetCore注意保存 web.config 后 IIS 会自动重启应用这个特性有时候很贴心有时候也会造成连接闪断。开启日志后让问题复现一次再去 logs 目录下查看最新的 stdout 日志。日志里可能不会直接写“WebSocket 不可用”但往往能看到连接被拒绝或升级失败的相关记录。另外如果你有 Failed Request Tracing 的权限也可以配置一条跟踪规则专门监听 HTTP 500 和 HTTP 502 请求能够看到更详细的内部错误链。对于长期运行的生产环境我建议在 Web.config 里保留 stdoutLogEnabledfalse改成按需开启避免日志文件暴涨占用磁盘空间。3.5 第五步查看应用池的队列长度与回收策略如果 WebSocket 已经安装了、代码也确认没有明显的同步阻塞问题仍然存在那就要看 IIS 应用程序池的配置了。在 IIS 管理器中右键应用程序池选择“高级设置”会看到几个关键参数队列长度Queue Length默认 1000。如果请求堆积超过这个数IIS 会直接返回 503。最大工作进程数Maximum Worker Processes默认 1。如果你的应用配置成了多个需要额外注意会话一致性和 SignalR 跨进程消息的问题。SignalR 默认是无粘性的多个进程时需要用 Redis Backplane 之类的方式同步消息。回收时间Regular Time Interval默认 1740 分钟即 29 小时。但如果你在代码里手动调用了 GC.Collect 或某些特殊操作应用池回收也会造成连接全断。对于 SignalR 场景不要在应用池上开启“重叠回收”因为 SignalR 连接在回收期间会全部断开客户端虽然会自动重连但会产生一次明显的消息丢失窗口。合理的做法是把应用池回收时间改到凌晨低峰期或者设置成根据虚拟内存/私有内存使用量来回收。3.6 第六步代码里的静态全局状态排查线程池饥饿还有一个不太容易察觉的原因SignalR Hub 中使用了静态变量或静态集合并且没有加锁。比如一个在线用户列表如果是用静态 Dictionary 维护多个连接同时写入时会造成竞争。下面这种写法在并发下很危险public class NotificationHub : Hub { private static readonly Dictionarystring, string _connections new(); public override async Task OnConnectedAsync() { _connections[Context.ConnectionId] Context.UserIdentifier; await base.OnConnectedAsync(); } }如果写入时没有线程安全的保护可能在低并发时没问题但到一定并发量后触发内部哈希碰撞或扩容问题导致请求卡住。这个和 IIS 无关但会叠加在 SignalR 的并发问题上让系统看起来更早到达临界点。建议要么使用 ConcurrentDictionary要么给字典操作加锁。这里用 ConcurrentDictionary 就行private static readonly ConcurrentDictionarystring, string _connections new();排查这一步时可以在静态变量的写入逻辑处加日志观察阻塞发生时这些静态操作是否耗时异常。4. 解决方案从 IIS 配置到代码改造的完整落地4.1 优先保证 SignalR 走 WebSocket而不是降级到 SSE解决这个问题的第一优先级是让 SignalR 尽可能使用 WebSocket。只要传输模式是 WebSocket就不会出现“每个客户端占住一个 HTTP 请求”的情况后面讲的很多配置优化才真正有意义。服务端确保支持 WebSocket 的代码很简单。在 Program.cs 或 Startup.cs 中需要显式调用 UseWebSocketsvar builder WebApplication.CreateBuilder(args); builder.Services.AddSignalR(); var app builder.Build(); app.UseWebSockets(); app.MapHubNotificationHub(/notificationHub); app.Run();这里有一个顺序问题UseWebSockets 必须放在 UseRouting 之后、MapHub 之前调用同时要放在任何可能短路请求的中间件之前比如身份验证中间件通常要放在它之前否则有些请求在验证时就被拦截了。4.2 安装并验证 IIS 的 WebSocket 支持前文已经提到安装命令这里再整理一遍图形化操作路径打开“服务器管理器”。点击“添加角色和功能”。选择“基于角色或基于功能的安装”。在“Web 服务器IIS”下展开“Web 服务器” - “应用程序开发”。勾选“WebSocket 协议”。完成安装后执行 iisreset。安装完成后可以用握手测试确认 WebSocket 已经启用。最直接的方式是通过浏览器的开发者工具观察 SignalR 连接是否返回 101 状态码。如果项目不是托管在默认站点下而是部署在虚拟目录或使用 https 协议要确认 WebSocket 的升级请求能正常到达应用而不是被 URL Rewrite 等规则拦截了。4.3 调整线程池最小线程数提升系统抗突发能力即使解决了 WebSocket 支持我仍然不建议跳过线程池优化。因为 SignalR 的消息调度、心跳检测、断线重连等机制仍然会异步地使用线程池。生产环境中偶尔的线程池饥饿可能不表现为“10 连接就阻塞”而是表现为“高峰期 CPU 不高但接口响应极慢”。可以在应用启动早期调整线程池最小线程数。我会在 Program.cs 的 Main 方法最开始处加这样一段ThreadPool.GetMinThreads(out int workerThreads, out int ioThreads); ThreadPool.SetMinThreads(workerThreads 100, ioThreads 100);这段代码的意思是把线程池最小工作线程数和 IO 线程数各增加 100。这里的 100 不是固定标准值而是根据实际并发量来估算的。如果你的 SignalR 同时在线连接在 2000 以内加 100 已经能有效缓解饥饿如果更高建议做压测来找到更适合的值。但要注意不要一次性把最小线程数调得过大比如直接设成 1000。线程池的最小线程数决定了系统在请求峰值到来时能多快创建线程。设得太大意味着程序一启动就会创建大量线程白白消耗内存设得太小则在高并发时线程池会花时间慢慢加线程表现为响应时间一路上升。4.4 用配置来强制 SignalR 只走 WebSocket可选方案如果在你的业务场景里客户端环境完全可控比如是自家公司的内部系统、PC 端浏览器统一为现代浏览器可以考虑强制 SignalR 只使用 WebSocket这样可以从根上消除 SSE 和 Long Polling 造成的请求占用问题。服务端在 MapHub 之前加一个检查中间件app.Use(async (context, next) { if (context.Request.Path.StartsWithSegments(/notificationHub) !context.WebSockets.IsWebSocketRequest) { context.Response.StatusCode StatusCodes.Status400BadRequest; await context.Response.WriteAsync(WebSocket only.); return; } await next(); });客户端也需要做对应的设置。在前端 JavaScript 中创建连接时指定传输方式const connection new signalR.HubConnectionBuilder() .withUrl(/notificationHub, signalR.HttpTransportType.WebSockets) .build();如果你看到请求直接返回 400而且服务端没有其他日志那基本可以断定服务器端不支持 WebSocket需要回过头检查 IIS 功能和网络代理。这种强制方案的缺点也很明显如果服务器前面还有一层负载均衡器比如 F5、Nginx需要负载均衡器也支持并放行 WebSocket 的升级请求。很多负载均衡器默认只转发普通 HTTP对 Upgrade 头处理不当会导致强制 WebSocket 后所有信号连接直接失败。因此在强制方案前务必确认全链路都支持 WebSocket。4.5 如果 WebSocket 不可用退而求其次的调优方案有些团队的技术栈限制比较大比如客户端位于某些安全等级很高的内网环境网关会屏蔽掉所有非 80/443 端口的协议升级。这种情况下 WebSocket 可能真的无法使用SignalR 只能长期跑在 SSE 或 Long Polling 下。这时要做的是尽量减小“每个连接占一个请求”的负面影响。可以从几个方向入手提高 ASP.NET Core 请求队列的处理能力也就是合理地增加线程池最小线程数让系统在 1000 个挂起请求时仍能保持部分线程响应 API。给 SignalR Hub 方法设置合理的超时时间避免某个连接长时间不释放。将 SignalR 和 Web API 拆分到两个不同的应用池甚至两台服务器上这样实时连接即使拖垮了 SignalR 应用池也不会影响主站 API。拆分方案是我比较推荐的。因为从架构上看实时推送和 REST API 对资源的占用模型差异很大。实时推送是长连接密集型REST API 是短请求密集型。混跑时长连接容易饿死短请求。分开部署后两边可以各自调优互不干扰。4.6 使用进程外托管模式减少 IIS 层影响如果你排查了很久发现是 IIS 层对连接升级或请求队列的处理有问题而你的项目又恰好是进程内托管可以考虑切换为进程外托管。进程外托管的优势在于ASP.NET Core 应用跑在独立的 Kestrel 进程中IIS 只作为反向代理。这种模式下请求在 IIS 和 Kestrel 之间多了一次转发单请求性能会略降但隔离性更好。如果你的应用本身没有性能瓶颈只是被 IIS 的某些连接管理机制拖住这种隔离可能会让问题消失。修改 web.configaspNetCore processPathdotnet arguments.\YourApp.dll stdoutLogEnabledfalse hostingModeloutofprocess /aspNetCore进程外托管模式下SignalR 连接的实际承载者是 KestrelIIS 只负责将升级后的 WebSocket 流量透明转发。在 Windows 的 HTTP.sys 层这种转发的处理效率和稳定性通常比进程内更好。缺点是进程外模式会额外占用一个进程的内存部署结构相对复杂一些。5. 常见问题与避坑经验速查表5.1 连接数到临界点就整体卡死怀疑是 IIS 限制现象可能原因处理方法连接数到 10-20 之后就阻塞SignalR 走 SSE/Long Polling占满线程池安装 WebSocket Protocol让 SignalR 升级为 WebSocket纯内存 API 也卡死CPU 却不高线程池饥饿调大线程池最小线程数检查 Hub 内同步阻塞代码浏览器 Network 里没有看到 101 状态码WebSocket 握手未完成检查 IIS 功能、反向代理的 Upgrade 头配置请求直接返回 503应用池队列长度超限或应用无响应检查应用池队列长度确认应用池未频繁回收指定 WebSocket 传输后连接全失败服务器端不支持 WebSocket确认 Web-WebSockets 功能已安装5.2 SignalR 断线自动重连时的隐藏坑SignalR 的自动重连机制在客户端断线后会以指数退避的方式尝试重新连接。每次重连都会重新发起 negotiate 请求如果客户端量非常大重连风暴造成的瞬时并发会对服务器造成巨大压力。我曾见过一个项目因为半夜发布导致所有连接断开客户端默认配置在断开后 0 秒、2 秒、10 秒、30 秒等时间点集中重连。当时同时在线约 5000 个客户端重连风暴直接把 API 打垮。后来在客户端加了随机延迟const connection new signalR.HubConnectionBuilder() .withUrl(/notificationHub) .withAutomaticReconnect([0, 1000, 5000, 15000, 30000]) .build();同时也建议服务端配置 ClientTimeoutInterval 和 KeepAliveInterval避免服务端因为网络抖动误杀活跃连接。一个常见的配置是services.AddSignalR(options { options.ClientTimeoutInterval TimeSpan.FromSeconds(60); options.KeepAliveInterval TimeSpan.FromSeconds(15); options.EnableDetailedErrors true; });5.3 Hub 生命周期与依赖注入的坑ASP.NET Core 的 SignalR Hub 在每次方法调用时是瞬时创建的不是单例。如果你在 Hub 的构造函数里注入了 DbContext而 DbContext 本身是 Scoped这通常没问题。但如果你在 Hub 里使用了 IHubContext 来从外部推送消息并且这个 IHubContext 被注入到一个 Singleton 服务中那么在这条调用链上使用 DbContext 就会出问题因为 Singleton 服务无法拿到 Scoped 的 DbContext。这种问题在高并发时不一定会立刻报错但当连接数多了以后可能会出现数据库连接被意外释放或线程阻塞的诡异现象。排查时可以看看 Hub 中是否有复杂的依赖链如果有尽量让 Hub 的方法只做消息转发把业务逻辑下沉到独立的 Service 中并明确生命周期。5.4 使用反向代理时 WebSocket 升级头被过滤如果你在 IIS 前面配置了 Nginx、ARR 或其他反代需要确认以下请求头能够正确传递Upgrade: websocket Connection: Upgrade在 Nginx 中针对 WebSocket 的代理需要额外配置 Upgrade 头。虽然标题场景是 IIS但很多企业网络是 IIS 外层还套一个 Nginx 统一入口。这种情况下IIS 本身的 WebSocket 配置正确还不够外层 Nginx 也得放行。如果你在生产环境看到“服务器已安装 WebSocket 功能但客户端始终无法升级”的情况大概率就是反代层过滤了 Upgrade 请求头。5.5 不要忽视 DNS 和负载均衡层的超时设置SignalR 长连接在传输层上是保持不关闭的。很多负载均衡器默认有一个“空闲连接超时”比如 60 秒内无数据传输就自动断开连接。如果 SignalR 的心跳间隔大于这个超时时间连接会被负载均衡器静默切断。客户端表现为连接断开随后自动重连重连成功后再次被切断形成周期性断连。解决方法是调节 SignalR 的心跳频率或者把负载均衡器的空闲超时时间调大。服务端和客户端都有对应的 KeepAlive 配置必须保证心跳间隔小于负载均衡器的空闲超时时长。我建议在部署前先向网络团队确认反代层的空闲连接超时时间再反推 SignalR 的心跳间隔设置。6. 最后一次压测验证与实际心得解决完全部问题后用压测工具模拟了 2000 个 SignalR 并发连接同时持续调用 Web API。这次的现象和之前完全不一样了API 不会阻塞SignalR 连接也都稳定在 WebSocket 状态。最初那台服务器配置并不高4 核 8GB 内存通过合理配置后承载 2000 个长连接没有太大压力。回顾这次排查我最大的体会是遇到 IIS .NET Core SignalR 的组合问题不要一上来就怀疑 IIS 版本或框架 bug。90% 的情况是传输模式没有按预期走 WebSocket或者代码中某个不起眼的同步阻塞放大了并发问题。下次你再看到“连接数到 10 个就卡死”这类描述第一件事就去确认浏览器 Network 面板里有没有 101 Switching Protocols。如果始终没有那跟线程池较劲没意义先把 WebSocket 的握手链路打通再说。SignalR 本身的设计是支持大规模长连接的IIS 上要做的只是把这条通道正确建起来别让它降级到 HTTP 轮询模式去硬扛。

相关新闻

最新新闻

完美主义者的挂机课:接受95%成功率的那晚

完美主义者的挂机课:接受95%成功率的那晚

完美主义者的挂机课:接受95%成功率的那晚 一个完美主义者的心态转折点: 「我曾要求每个任务100%成功,做不到就反复调流程。有晚挂机跑了180个任务,成功171个——9个失败让我整晚睡不着,第二天逐个复盘到崩溃。后来想通…

2026/9/7 20:14:04
字体与插件枚举指纹:风控的边角检测维度

字体与插件枚举指纹:风控的边角检测维度

字体与插件枚举指纹:风控的边角检测维度 一个冷知识引发的自查: 「做防风控这么久,滑块也过了、webdriver也抹了,验证还是弹。后来看到一篇文章说风控会枚举系统字体列表——我服务器上那个纯净系统,字体干净得像个刚…

2026/9/7 20:14:04
Windows下OSG+OSGEarth+GDAL 3.12编译实战:CMake配置与版本匹配

Windows下OSG+OSGEarth+GDAL 3.12编译实战:CMake配置与版本匹配

前阵子刚好要在Windows上把这套组合捞起来:OSG 3.6.5 OSGEarth 3.7.2 GDAL 3.12,要做三维地形数据调度和矢量数据可视化方向的预研。网上的编译教程大多停留在OSGEarth 2.x时代,照着走一遍几乎没戏。折腾了一个周末,把依赖顺序、…

2026/9/7 20:14:04
Frida动态插桩入门:核心架构、Hook原理与Android实践

Frida动态插桩入门:核心架构、Hook原理与Android实践

1. Frida到底是什么,为什么大家都在学 提起Frida,做移动安全、应用逆向、自动化测试的朋友都不会陌生。我最早接触Frida是在一个App的协议分析项目里,当时用抓包工具看流量、用jadx翻Smali代码,改完还要重打包、重签名、绕过校验&…

2026/9/7 20:14:04
VS Code + MinGW + OpenCV 环境配置完全指南:从零到图像处理实战

VS Code + MinGW + OpenCV 环境配置完全指南:从零到图像处理实战

1. 这套环境方案,到底解决什么问题如果你在 Windows 上做过 OpenCV C 开发,应该对“环境配置”这四个字有刻骨铭心的体会。Visual Studio 固然省心,新建项目、配置附加目录、链接库都是图形界面点一点的事,但整套 IDE 几个 GB 的体…

2026/9/7 20:14:04
资深白帽的内功:如何系统性开展白盒代码审计?(附 Java/PHP 高频逻辑漏洞解析)

资深白帽的内功:如何系统性开展白盒代码审计?(附 Java/PHP 高频逻辑漏洞解析)

引言:白盒审计的必要性与痛点 在软件安全测试的生态中,白盒代码审计(White-Box Code Auditing)一直是资深安全工程师的必修内功。它与黑盒(Black-Box)或灰盒审计不同,意味着审计者拥有完整的源代…

2026/9/7 20:09:04