一次 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 慢——看起来像是域名相关的网络问题。

第二轮:第三方抓包诊断

请网络同事做了客户端抓包分析,结论:
  1. 后端接口处理慢:13 个接口服务端等待合计 12,780ms
  2. 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:
  1. 登录/免登接口返回 JWT Token
  2. 前端存 localStorage,请求放 Authorization 头
  3. 后端从 Token 解析用户身份,不再碰 HttpContext.Session
  4. 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 锁在作祟。

 
posted @ 2026-10-21 19:57  周捷Jay  阅读(64)  评论(0)    收藏  举报