服务器CPU100%、WEB页面卡顿问题排查及解决步骤
系统现象:WEB页面请求响应慢、或者一直转圈;CPU100%
逆向分析问题:通过转储WEB站点的进程拿到【dump】文件进行分析
(1)!threadpool 结果分析:
- CPU 完全打满、
- 406 个工作线程全部在运行
- 0 个空闲
- 线程池已经被吃光
- 还有 35 个任务在排队,但没有线程可以处理

(2)e-CLRStack-a-命令结果-托管线程的托管 本机调用栈(看一下线程都卡在哪):发现大量的请求wcf线程正在等待返回结果

(3)!syncblk命令结果,查看一下程序的线程阻塞、锁竞争、死锁 问题

(4)再把有问题时的IIS请求日志拿来一块分析一下,发现有大量signalr请求,经过分析发现webSockets请求被降级为长轮询,这里应该也存在很大问题。

首先来看:signalR中WebSocket 握手失败的现象(net::ERR_CONNECTION_RESET)


再来看一下IIS请求日志:
2026-03-23 02:53:05 1xx.xxx.xxx.xxx GET /signalr/connect transport=webSockets&clientProtocol=1.5& connectionToken=CatYmwC5Lq8MxtIspjpNc2oZqGRLIU12IEKsnOBPY3pP1EGSxP%2Feyv7dwrveG89u2gnolo8CgQOM9 Aim5xO%2FlzNN6Xq9qsFGkWm2GvMHRrUNxLThuYL5K8YzuNX2kauH&connectionData=%5B%7B%22name%22%3A%22msgserverhub%22%7D%5D& tid=5 8889 - 192.168.8.185 Mozilla/5.0+(Windows+NT+6.1;+WOW64;+Trident/7.0;+rv:11.0)+like+Gecko - 400 0 0 15
其他IIS日志:
Edge 浏览器尝试了 WebSocket 但失败了,最终降级到 longPolling。
09:06:42 GET /signalr/connect transport=webSockets → 400 失败 09:06:47 GET /signalr/connect transport=foreverFrame → 200 (5303ms) 09:06:47 POST /signalr/connect transport=longPolling → 200
一、先解读这条日志的核心信息
| 字段值 | 含义 | 关键信息 |
|---|---|---|
2026-02-27 09:06:42 |
请求时间 | 特定时间点的请求 |
GET /signalr/connect |
请求路径 + 方法 | 客户端尝试通过 SignalR 建立 WebSocket 连接 |
transport=webSockets |
SignalR 传输方式 | 客户端优先选 WebSocket(长连接) |
connectionToken=XXX |
连接令牌 | 标识客户端连接 |
400 0 0 3 |
状态码 + 耗时 | ① 400=请求错误;② 3 = 耗时 3ms(但错误本身是关键) |
- WebSocket 连接失败:上图中
ws://localhost:7411/signalr/connect?transport=webSockets未返回响应,说明握手阶段被中断。 - 第二张图中:Server-Sent Events (SSE) 连接成功:
transport=serverSentEvents的请求返回200 OK,Content-Type: text/event-stream,说明降级传输方式可用。

问题分析:
结合之前的 net::ERR_CONNECTION_RESET 错误,问题聚焦在 IIS 对 WebSocket 的处理环节:
从截图中的请求路径看,这是基于 SignalR 的 WebSocket 连接(transport=webSockets),常见于一些实时通讯类系统。
遇到的是 WebSocket 握手失败:net::ERR_CONNECTION_RESET,本质是客户端与 WebSocket 服务器之间的连接被强制重置,导致握手无法完成。
ERR_CONNECTION_RESET 是服务器端主动断开或中间设备拦截的典型表现,而非客户端主动取消
二、【WebSocket 被禁用 / 无法使用 → SignalR 强制降级为 长轮询】对线程 \ 系统性能的影响
1. 线程池可能耗尽:连接失败→客户端重试→大量无效请求占线程→客户端IE页面一直转圈圈
- WebSocket 连接失败后,SignalR 客户端会自动降级为「长轮询(Long Polling)」,或反复重试连接;
- 每一次重试都是一次新的 HTTP 请求,会占用线程池线程;
- 如果大量客户端都触发这个 400 错误→重试请求暴增→线程被大量无效请求占满→线程耗尽。
2.CPU 100% 的真凶 = ASP.NET SignalR 长轮询 + 高并发 + TimerQueue 锁雪崩的灾难【行业内公认】
几百连接就能把 CPU 拉满 100%,系统崩溃!!!
SignalR 长轮询为什么会创建Timer?长轮询工作流程:浏览器端发起请求 > 服务器 Hold 住请求 60~110 秒 >服务器必须定时检查有没有消息 > 这个定时检查 = 必须在后端创建一个 Timer
大量的SignalR 长轮询产生的大量 Timer 会把 TimerQueue 压垮
1.SignalR 长轮询 = 服务器必须用 Timer 维持连接
2.每个在线访问者 = 1 个长轮询连接 = 1 个 Timer
3.几百人在线请求 → SignalR 长轮询 → 创建几百Timer
4.这些timer全部挤在同一个 TimerQueue全部抢同一把全局锁
5.TimerQueue 堵死、锁不释放
几百个线程开始疯狂抢锁 → CPU 100%
先插入另一个话题:
什么是CPU的“上下文切换”?
上下文切换 = CPU 在不同任务之间 “换班干活”
想象:你是一个超级打工人(CPU),同一时间只能干一件事。但你面前有一堆活儿:
写代码(进程 A)
回消息(进程 B)
看视频(进程 C)
你不能真的同时干三件事,只能:
干一会儿 A
停下来,把 A 的进度记下来
切换去干 B
停下来,把 B 的进度记下来
再切回 A……
这个 **“停下→保存进度→切换任务→恢复进度”** 的过程,就叫 上下文切换(Context Switch)。
上下文切换:是 CPU 在不同任务 / 模式之间换班
切换越少,程序越快;切换太多,CPU 空转严重,系统变卡
几种常见的上下文切换(涉及CPU消耗)
1.线程 / 进程调度切换(最常见)
要保存:
- 寄存器里的值
- 栈状态
- 内存页表
- 各种状态标志
2.系统调用(用户态 ↔ 内核态切换)
read、write、socket 都会触发:用户态 → 内核态 → 用户态
3.中断处理(硬件 / 软件中断)
硬件或软件事件触发中断时:
- 网卡收包、磁盘 I/O 完成、时钟中断
- 键盘鼠标输入、定时器到期
4.阻塞与唤醒(I/O 等待)
线程等待 I/O、锁、信号、条件变量时:
- 线程主动阻塞,让出 CPU
- 等待事件完成后被唤醒,重新调度
5.锁竞争与同步(自旋锁 / 互斥锁)
多线程争抢锁时:
- 获取不到锁 → 阻塞 / 挂起 → 上下文切换
- 锁释放后唤醒 → 再次切换
6.信号处理
进程收到信号(如 SIGINT、SIGKILL、SIGUSR1):
- 暂停当前执行流
- 进入信号处理函数
- 处理完恢复
7. 虚拟机 / 容器场景的额外切换
- 虚拟机:VM Exit / VM Entry(客户机 ↔ 宿主机切换)
- 容器:虽然轻量,但仍会因调度、网络、IO 产生切换
看到了吧,第5条中讲到的“高并发下锁争用会导致大量上下文切换,是性能瓶颈常见原因”
回到上面的讨论:
SignalR 长轮询 为什么会产生 TimerQueue ?
机制如下:
浏览器发起一个请求 → 服务器hold 住不返回
服务器必须定时检查有没有消息要推送给前端
这个定时检查,.NET 底层必须用 System.Threading.Timer
每一个前端连接,就至少产生 1 个 Timer
所有 Timer 都进入 全局唯一的 TimerQueue
最恐怖的来了
几百个人同时在线
每个在线请求 = 1 个长轮询连接 = 1 个 Timer
结果:
这几百个Timer全部挤在同一个 TimerQueue全部抢同一把全局锁
先回答一个核心的问题
几100 个 Timer 抢的 “全局锁” 到底是哪个锁?
答案只有一句话:
这个锁 = .NET 底层写死的、唯一的、全局的 TimerQueue 内部锁
你代码里找不到,你也看不见,它是系统自带的!
二、我用大白话讲清楚它是什么
你可以把 TimerQueue 理解成:
整个程序只有 1 个 “公共定时器总台”
所有定时器(SignalR 心跳、长轮询、定时任务)全部必须在这里排队。
重点来了:
这个总台为了防止混乱,加了 1 把锁:
csharp
运行
private readonly object _lock = new object();
这把锁,整个进程只有 1 把!
所有 几100个 Timer 都必须抢这一把锁!
三、为什么 几100 个 Timer 要抢同一把锁?
因为:
TimerQueue 是全局单例(整个程序只有一个)
它里面的队列只有一条,不能并发修改
所以必须加锁
你可以理解为:
几100 个人,共用 1 个厕所,必须排队
厕所门 = 全局锁
几100 人 = 几100 个 Timer
排队 = 抢锁
注意:若不能用 WebSocket,必须降低心跳频率
<add key="SignalR.Transports.LongPolling.HeartbeatInterval" value="30000" />
或者升级到 ASP.NET Core SignalR(完全解决 TimerQueue 问题)
最终解决思路及方案:
1. 首先解决【SignalR + IIS WebSocket】环境配置问题,对以下项目进行检查
- ✅ Windows 功能:已勾选 WebSocket 协议,安装完整。
- ✅ IIS 配置:
system.webServer/webSocket全局enabled=True,且已解锁。 - ✅ 应用程序池:目标站点
xxx.web使用 .NET v4.0 + 集成模式,完全兼容。 - ✅ web站点nuget安装的SignalR 版本:前端
jquery.signalR-2.2.1.min.js与后端 .NET 4.0 匹配。 - ✅ 若为 HTTPS 站点,确保 WebSocket 连接使用
wss://协议,避免混合内容拦截
🚩 可能原因与排查方向
1. 网络 / 防火墙层面问题
- 防火墙 / 安全组拦截:服务器端防火墙、云服务商安全组、客户端本地防火墙,可能拦截了 WebSocket 端口(通常是 80/443,或自定义端口)
- 代理 / 反向代理配置错误:Nginx/Apache/IIS 等反向代理未正确配置 WebSocket 支持(缺少
Upgrade/Connection头转发) - 网络中断或路由问题:客户端与服务器之间的网络链路不稳定,导致连接被重置
2. 服务器端配置问题
- WebSocket 功能未启用:IIS/ASP.NET Core 等服务器未开启 WebSocket 支持(如 IIS 需安装 WebSocket 模块,且配置节未锁定)
- SignalR 服务未正常启动:后端 SignalR Hub 未启动、配置错误或进程崩溃
- 请求参数 / 鉴权失败:
connectionData/tid等参数无效,或鉴权 Token 过期,服务器主动拒绝连接
3. 浏览器 / 客户端层面问题
- 浏览器禁用 WebSocket:极少数情况下浏览器或插件会阻止 WebSocket 连接
- 混合内容问题:HTTPS 页面尝试连接 ws:// 协议的 WebSocket,被浏览器安全策略拦截




问题根源及最终解决方法:
不经意在windows日志发现如下异常
(这也是上面为什么websocket在浏览器端为什么不能用【握手失败】,生产错误:net::ERR_CONNECTION_RESET,被降级为长轮询的根本原因了):

解决方法:在webconfig增加配置项:
<system.web>
<httpRuntime targetFramework="4.5" ........
问题解决,连接成功:
2026-03-23 08:30:59 ::1 GET /signalr/connect transport=webSockets&clientProtocol=1.5& connectionToken=F1vPbOsx%2B2C7bWSLSYo0WQU7eDfBBrzQkSrrj1ZcvdfoxX%2FJ2zfX9judlejXUNnwzml UbDbYGQXeznV0NQX1SNr7c60ne%2BQFSxHtts7BhIIr%2FisYkprOB4NesFl1zFsJ&connectionData=%5B%7B%22name%22%3A% 22msgserverhub%22%7D%5D&tid=4 7411 - ::1 Mozilla/5.0+(Windows+NT+10.0;+Win64;+x64)+AppleWebKit/537.36+ (KHTML,+like+Gecko)+Chrome/145.0.0.0+Safari/537.36 - 101 0 64 1420332
2026-03-23 08:09:53 ::1 GET /signalr/start transport=webSockets&clientProtocol=1.5& connectionToken=XCrtdZpPnhk4y9UPFN1oALO3IHbuyif%2BB7wXE9XRUBmbwyZoU8tK%2Fgn3m0UPlo5FRqhOGDIgt6KdiK8N%2BOG89 dBItLoA4HN7%2Fek3TPh5QmUWGtmTLx9L%2FRu3uCxLHFyy&connectionData=%5B%7B%22name%22%3A% 22msgserverhub%22%7D%5D&_=1774253392595 7411 - ::1 Mozilla/5.0+(MSIE+9.0;+Windows+NT+10.0;+WOW64;+Trident/7.0; +rv:11.0)+like+Gecko http://localhost:7411/OpenBidMain/Index 200 0 0 3


八、终极总结(一定要记住)
1. 全局锁 = .NET 底层 TimerQueue 内部唯一锁
2. 代码里没有,看不见,改不了
3. 所有 Timer 都必须抢它(全局锁)
4. SignalR 长轮询 = 1 个用户 = 1 个 Timer
5. 1000 用户 = 1000 个 Timer 抢一把锁
6. 抢锁 = CPU 自旋 = 100% CPU
其实以上只讲了系统一半的问题,因为最前面我们看到的:
(1)系统244 个线程在疯狂等待这个 TimerQueue 锁!这个应该上面的SignR问题,已经解决掉了。
(2)第2行中21 个线程在等待这个对象锁这个问题我们还没解决。这个问题也比较严重,而且页面卡顿和这个也有很大关系。
0:000> !syncblk Index SyncBlock MonitorHeld Recursion Owning Thread Info SyncBlock Owner 85 02cb632c 5 1 02c09760 509c 117 0e3fd18c System.Object 90 02cb6228 488 0 00000000 none 213b68c4 System.Threading.TimerQueue 303 55cfbe50 43 1 400231e8 4758 237 0d3a85b8 System.Object ----------------------------- Total 317 CCW 4 RCW 5 ComClassFactory 0 Free 97

浙公网安备 33010602011771号