服务器CPU100%、WEB页面卡顿问题排查及解决步骤

系统现象:WEB页面请求响应慢、或者一直转圈;CPU100%

逆向分析问题:通过转储WEB站点的进程拿到【dump】文件进行分析

(1)!threadpool 结果分析:

  • CPU 完全打满、
  • 406 个工作线程全部在运行
  • 0 个空闲
  • 线程池已经被吃光
  • 还有 35 个任务在排队,但没有线程可以处理

企业微信截图_17748407669692

 

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

企业微信截图_17748403438767

 

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

企业微信截图_17748405454910

 

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

企业微信截图_17748402532670

 

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

企业微信截图_17742538582583

 

企业微信截图_17742541954777

 

再来看一下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=longPolling200

 

一、先解读这条日志的核心信息

先把日志字段拆明白(IIS 日志默认格式):
 
字段值含义关键信息
2026-02-27 09:06:42 请求时间 特定时间点的请求
GET /signalr/connect 请求路径 + 方法 客户端尝试通过 SignalR 建立 WebSocket 连接
transport=webSockets SignalR 传输方式 客户端优先选 WebSocket(长连接)
connectionToken=XXX 连接令牌 标识客户端连接
400 0 0 3 状态码 + 耗时 ① 400=请求错误;② 3 = 耗时 3ms(但错误本身是关键)
 
👉 核心结论:客户端发起的 SignalR WebSocket 连接请求,返回了 400 错误(请求无效)。

 

  • WebSocket 连接失败:上图中 ws://localhost:7411/signalr/connect?transport=webSockets 未返回响应,说明握手阶段被中断。
  • 第二张图中:Server-Sent Events (SSE) 连接成功: transport=serverSentEvents 的请求返回 200 OKContent-Type: text/event-stream说明降级传输方式可用

企业微信截图_17742574178431

 

 

问题分析:

结合之前的 net::ERR_CONNECTION_RESET 错误,问题聚焦在 IIS 对 WebSocket 的处理环节:

从截图中的请求路径看,这是基于 SignalR 的 WebSocket 连接(transport=webSockets),常见于一些实时通讯类系统。

遇到的是 WebSocket 握手失败:net::ERR_CONNECTION_RESET,本质是客户端与 WebSocket 服务器之间的连接被强制重置,导致握手无法完成。

ERR_CONNECTION_RESET 是服务器端主动断开或中间设备拦截的典型表现,而非客户端主动取消

 

二、【WebSocket 被禁用 / 无法使用 → SignalR 强制降级为 长轮询】对线程 \ 系统性能的影响

SignalR 是ASP.NET中实现实时通信的框架,基于长连接(WebSocket / 轮询),这个 400 错误,降级为长轮询后会引发连锁问题:
首先明确一点:WebSocket(高效,几乎不占线程)

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.线程 / 进程调度切换(最常见)

CPU 从线程 A → 线程 B
要保存:
  • 寄存器里的值
  • 栈状态
  • 内存页表
  • 各种状态标志
这是有开销的,切换多了 CPU 就忙死了。

2.系统调用(用户态 ↔ 内核态切换)

比如你调用一次 readwritesocket 都会触发:
用户态 → 内核态 → 用户态
这也是切换,也有开销,但比线程切换轻一点。

3.中断处理(硬件 / 软件中断)

硬件或软件事件触发中断时:

  • 网卡收包、磁盘 I/O 完成、时钟中断
  • 键盘鼠标输入、定时器到期
CPU 必须暂停当前任务,处理中断,再恢复原任务,会产生中断上下文切换。

4.阻塞与唤醒(I/O 等待)

线程等待 I/O、锁、信号、条件变量时:

  • 线程主动阻塞,让出 CPU
  • 等待事件完成后被唤醒,重新调度
典型场景:网络请求、磁盘读写、锁竞争、sleep。

5.锁竞争与同步(自旋锁 / 互斥锁)

多线程争抢锁时:

  • 获取不到锁 → 阻塞 / 挂起 → 上下文切换
  • 锁释放后唤醒 → 再次切换
高并发下锁争用会导致大量上下文切换,是性能瓶颈常见原因。

6.信号处理

进程收到信号(如 SIGINTSIGKILLSIGUSR1):

  • 暂停当前执行流
  • 进入信号处理函数
  • 处理完恢复
会触发一次上下文切换。

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,被浏览器安全策略拦截

 

企业微信截图_17742545302867

 

 

企业微信截图_177425430013

 

企业微信截图_17742544703864

 

企业微信截图_17742548205314

 

 

 

问题根源及最终解决方法:

不经意在windows日志发现如下异常

(这也是上面为什么websocket在浏览器端为什么不能用【握手失败】,生产错误:net::ERR_CONNECTION_RESET,被降级为长轮询的根本原因了):

企业微信截图_17742547451807

 

解决方法:在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

企业微信截图_17742533671457

 

企业微信截图_17742535249395

 

八、终极总结(一定要记住)

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

 

posted @ 2026-03-23 17:03  无聊的蚂蚁  阅读(61)  评论(0)    收藏  举报