一次 ASP.NET Session 锁引发的"同域慢、跨域快"诡异性能问题排查实录
前言
最近排查了一个让人抓耳挠腮的性能问题:同一个系统,内网 IP 直连飞快,外网域名访问却要 12 秒以上;更诡异的是,前端页面和 API 用不同端口就快,同端口同 origin 就慢。
排查过程从网络链路、TLS 握手、证书吊销一路摸到 ASP.NET 的 Session 会话锁,最终在浏览器的同源并发限制上完成了闭环。记录于此,希望能帮到遇到类似问题的同学。
一、问题背景
系统架构:
外网用户 → 公有云 :28085 → NAT映射 → 内网 IIS :18082 (HTTPS)
内网用户 → 域控DNS解析 → 内网IP :28085 → IIS
- 前端:纯前后端分离 H5 页面
- 后端:ASP.NET WebAPI(IIS 托管)
- 前端代码和 API 部署在同一个站点、同一个端口
现象:外网域名
https://mhrms.xxx.cn:28085 访问首页极慢(12s+),内网 http://IP:8082 飞快(<1s)。二、排查过程
第一轮:网络链路怀疑
最初怀疑是公有云端口转发、TLS 握手、证书 OCSP 吊销检查导致的延迟。用
curl 在服务器本机测试 18082 和 28085,均快。客户端用 IP 直连
:18082 快,域名 :28085 慢——看起来像是域名相关的网络问题。第二轮:第三方抓包诊断
请网络同事做了客户端抓包分析,结论:
- 后端接口处理慢:13 个接口服务端等待合计 12,780ms
- ASP.NET 会话独占锁:请求呈现约 500ms 一档的阶梯返回,明显是串行化处理
抓包报告指向 Session 锁,但有个矛盾:IP 访问不带 Cookie,域名访问带 Cookie。
第三轮:手动注入 Cookie 验证
在 IP 访问时手动注入域名下的
ASP.NET_SessionId Cookie,结果——依然很快。这推翻了"带了 SessionId 就触发锁"的简单假设。因为 ASP.NET 的 Session 锁不是"有 Cookie 就锁",而是代码声明了需要 Session 才锁。如果接口没实现
IRequiresSessionState,带了 Cookie 也不会锁。第四轮:F12 实锤
查看 IP 访问的 F12 Network 面板,Request Headers 里根本没有 Cookie。之前手动注入没生效(同源策略限制)。
结论翻转:
|
访问方式
|
有无 Cookie
|
是否触发 Session 锁
|
结果
|
|---|---|---|---|
|
IP 直连
|
❌ 无
|
不触发
|
快
|
|
域名访问
|
✅ 有
|
触发
|
慢
|
第五轮:web.config 关 Session
确认了锁的存在,尝试在
web.config 加:<system.web>
<sessionState mode="Off" />
</system.web>
结果接口直接报错——代码里在读取
Session["xxx"]。硬关不行。三、关键突破:同 origin vs 不同 origin
这时候一个偶然的测试改变了方向。
同事发现:如果把前端调用的 API 端口改成和前端页面不同的端口,速度就恢复正常。
|
场景
|
结果
|
|---|---|
|
前端
:28085 + API :28085(同 origin) |
慢(12s+)
|
|
前端
:28085 + API :28086(不同 origin) |
快(<2s)
|
为什么?
这是浏览器同源并发限制 + ASP.NET Session 锁的叠加效应:
Chrome/Edge 对同一 origin 的并发 HTTP/1.1 连接限制 = 6 个
同端口时:
浏览器加载页面(占 2 个连接)
↓
JS 发起 13 个 API 请求 → 最多同时发 6 个
↓
这 6 个请求都带 SessionId → ASP.NET Session 锁 → 串行处理(500ms/个)
↓
连接长时间不释放 → 剩余 7 个请求排队等连接 + 等锁 → 双重排队 → 12s+
不同端口时:
前端 :28085 → 浏览器给 6 个连接槽
API :28086 → 浏览器再给 6 个连接槽(不同 origin)
↓
总共 12 个并发连接,Session 锁串行 6 个
另外 6 个能跑起来,整体等待时间大幅缩短
本质上是不同端口绕开了浏览器的 6 连接限制,不是真正解决了慢。
测试环境验证
在测试环境用同 origin 访问,同样复现了慢的问题。这彻底坐实:
Session 锁 100% 是根因。测试环境之前"不慢"只是因为并发少、接口快,锁的代价被掩盖了。
四、根因总结
┌─────────────────────────────────────────────────────────┐
│ 前端 H5 和 WebAPI 同域同端口部署 │
│ ↓ │
│ 浏览器同 origin 限制 6 个并发连接 │
│ ↓ │
│ 13 个 API 请求排队,前 6 个到达 IIS │
│ ↓ │
│ 请求带 ASP.NET_SessionId Cookie │
│ ↓ │
│ 后端接口声明了 Session 依赖(隐式或显式) │
│ ↓ │
│ ASP.NET 对每个 SessionId 加排他锁 │
│ ↓ │
│ 6 个请求被串行化,500ms 一档阶梯返回 │
│ ↓ │
│ 连接长时间不释放 → 后续请求排队 → 雪崩 │
│ ↓ │
│ 用户感知:页面加载 12 秒+ │
└─────────────────────────────────────────────────────────┘
五、解决方案
方案 1:后端接口加 ReadOnly(推荐,P0 止血)
对只读接口声明只读 Session,解除排他锁:
// MVC Controller
using System.Web.SessionState;
[SessionState(SessionStateBehavior.ReadOnly)]
public class FlowController : Controller
{
// 接口逻辑不变,不再排他
}
// .ashx Handler
public class RuleExecuteHandler : IHttpHandler, IReadOnlySessionState
{
public void ProcessRequest(HttpContext context)
{
// 只读 Session,不排他
}
}
方案 2:Nginx 剥离 SessionId Cookie
在 Nginx 反代层把
ASP.NET_SessionId 从请求头里剥掉,让后端拿不到 SessionId,不触发锁:http {
map $http_cookie $strip_session_cookie {
~*(.*)(?:^|;\s*)ASP\.NET_SessionId=[^;]*($|;)(.*) $1$2;
default $http_cookie;
}
server {
listen 28085 ssl http2;
server_name mhrms.xxx.cn;
location / {
proxy_pass http://127.0.0.1:8082;
proxy_set_header Cookie $strip_session_cookie;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto https;
# ... 其他头
}
}
}
⚠️ 前提:代码不依赖 Session 读值。如果依赖,剥离后相关接口会返回 null/报错。
方案 3:拆 origin(架构层面)
- 前端页面和 API 用不同子域名:
h5.xxx.cn+api.xxx.cn - 浏览器按不同 origin 各给 6 个连接,并发能力翻倍
- 配合 Nginx 反代,零代码改动
方案 4:彻底去 Session 依赖(根治)
纯前后端分离架构本不应依赖服务器端 Session:
- 登录/免登接口返回 JWT Token
- 前端存
localStorage,请求放Authorization头 - 后端从 Token 解析用户身份,不再碰
HttpContext.Session web.config设置sessionState mode="Off"
六、延伸:IM 内嵌 H5 场景
企业微信、钉钉内嵌 H5 同样受同源并发限制影响。如果 IM 集成方案是:
- 免登 → 写
Session["UserId"]→ 后续 API 带 Cookie → 会触发同样的锁问题
推荐做法:
- 免登换 userid → 签发 JWT → 前端存
dd.setStorage/localStorage - 请求用
Authorization头,不依赖 Cookie - WebAPI 端禁用 Session
七、经验总结
|
教训
|
说明
|
|---|---|
|
不要在同 origin 下放大量并发 API
|
浏览器 6 连接限制是硬约束
|
|
纯前后端分离不要用 ASP.NET Session
|
无状态 Token 才是正道
|
|
Session 锁 + 并发 = 雪崩
|
单个接口 500ms × 串行 13 个 = 6.5s,加上连接排队直接 12s+
|
|
测试环境"不慢"不代表没问题
|
数据量小、并发低时锁的代价被掩盖,生产一放大就爆
|
|
Nginx 反代剥离 Cookie 是零代码止血手段
|
但前提是代码不依赖 Session 读值
|
排查耗时:约 3 天 | 根因:一行
[SessionState(ReadOnly)] 就能解决如果你也遇到"同域慢、跨域快"的诡异性能问题,不妨看看是不是 Session 锁在作祟。

浙公网安备 33010602011771号