一、我死磕了一天半网络二、报告指着会话锁,可现场对不上三、同事随口一句,方向全变了四、那 12 秒是两笔账叠出来的五、为什么测试环境从来没慢过六、止血和根治,四条路七、三天的账
这套系统我们内部用了两年多,一直没出过什么大毛病。直到上个月有同事在群里丢了一句:从外网进来打开首页要十二秒,从内网零点几秒就开了,是不是服务器该换了?
我第一反应也是资源不够。上去看了一眼,CPU 才 10%,内存有一半空着,IIS 日志干干净净。服务器明显没被压着。
更邪门的是同事后来补的第二个现象:前端页面和接口用不同的端口,快得跟内网一样;只要挪回同一个端口,当场变慢。同一个页面,同一台服务器,就换了两个数字。
先怀疑链路,这几乎是不用过脑子的顺序。
架构也不复杂:外网用户进来走公有云的 28085,NAT 映射到内网 IIS 的 18082;内部同事走域控 DNS,直接解析到内网 IP。
我在纸上列了三个能怀疑的对象:公网端口转发、TLS 握手、证书的 OCSP 吊销检查。这三个都是「外网才有的开销」,跟现象长得完全对得上。
然后被打脸。在服务器本机上 curl 18082 和 28085,都快。从客户机用 IP 直连 18082 快,换成域名走 28085 就慢。看着特别像「跟域名有关的东西」——握手?证书?DNS?
折腾到第二天,我请网络的同事帮忙做了客户端抓包。那份报告是整件事的转折点,两行结论:
- • 13 个接口,服务端等待时间合计 12,780 毫秒
- • 请求返回是一条一条出来的,大约 500 毫秒一档,像台阶
第二行才是重点。网络慢是「一起慢」,不是「排队慢」。台阶状的返回只有一种解释:后端在串行处理。
报告把矛头指向 ASP.NET 的 Session 会话锁。这机制我大致知道:只要某个接口声明了自己要用 Session,同一个 SessionId 上来的请求就会被排他锁串起来,前一个处理完才放行下一个。13 个接口乘 500 毫秒,量级正好对上。
但有个地方对不上。IP 直连的时候浏览器根本不会带 Cookie,域名访问才带。照「带 Cookie 才锁」的说法,应该能解释——可我做了一个更狠的实验:用 IP 访问,手动把域名下的 ASP.NET_SessionId 塞进请求头。结果,还是快。
这一下把假设推翻了。后来才想明白,Session 的锁不是看到 Cookie 就上,得代码里明确声明「我要用 Session」(IRequiresSessionState那套)才会排他。带了 Cookie 但接口不要 Session,一点事都没有。
那我手动塞的 Cookie 到底进去没有?打开 F12 看 Network,IP 访问的 Request Headers 里干干净净,压根没有 Cookie。被浏览器的同源策略挡在门外了,我以为我在做实验,其实什么也没验证。
![]()
第三天下午,一个同事在工位上随口说了句:你把接口端口改成跟页面不一样的试试。
改完,两秒以内。
他为什么这么想我不知道,但这个结果直接把问题的形状说清楚了:慢不慢,取决于端口在不在同一个 origin 上,跟域名、证书、链路一点关系都没有。
![]()
注意,换端口不是修好了,是绕开了。它只是让浏览器多给了几个连接名额,锁还在那儿,账也还在那儿。
到这儿两块拼图能合上了。把当天的情况拆开看:
- • 浏览器对同一个 origin 的并发连接有上限,Chrome 和 Edge 都是 6 个
- • 页面本身要先吃掉两个(HTML 加静态资源)
- • 剩下 4 个名额,13 个接口请求得排着队上车
- • 先上车的这几批,每一个都带着同一个 SessionId
- • 后端一把一把地上锁,500 毫秒放一个,全程串行
- • 请求处理完连接才释放,后面的继续等连接、等锁
所以那 12 秒不是一次慢,是两次排队叠在一起:等连接排一次,等锁再排一次。单看接口,500 毫秒乘 13 是 6.5 秒;多出来的那五六秒,全花在门口等位子上。
换端口为什么有效?浏览器是按 origin 发名额的。页面在 28085 拿 6 个,接口在 28086 再拿 6 个,一共 12 个并发。锁还是那把锁,但同时挤进去的请求变多了,总时长自然掉下来。
这是整件事里最让我后怕的一段。
发现问题之后我第一件事是去测试环境复现。同一个 origin,一样两个端口,一打开——慢,一模一样的慢。
也就是说这个问题一直在,只是没人看见。测试环境数据少、接口快、同时用的人少,锁一排下去也就几十毫秒,谁都不会觉得卡。等上了生产,数据量一大、接口一慢、并发一上来,同一把锁就变成了雪崩。
我们之前所有的验收,看的是「页面能不能打开」「功能对不对」。
先把火灭了,再谈重构。当天我们按优先级排了四步。
第一条,给只读接口声明 ReadOnly,解除排他。这是见效最快的一刀:
![]()
这行注解只对读 Session 的接口有效。要往 Session 里写东西的接口不行,该排他还是排他。但读接口占绝大多数,先把大部分救回来。
第二条,在反向代理层把 SessionId 抹掉。零代码改动,代价是代码不能依赖 Session 取值:
![]()
剥完 Cookie 后端拿不到 SessionId,锁自然不触发。但如果接口靠 Session 读值,剥完就直接返回空,这一点得先确认清楚。
第三条,拆 origin。把页面和接口挪到不同子域名,浏览器各给 6 个名额,并发直接翻倍,架构层面零代码。
第四条,干脆别用服务端 Session。登录换 JWT,前端存本地,请求带 Authorization 头,后端从 Token 里取身份,web.config 里把 sessionState 设成 Off。这是这套架构本来的解法,只是我们一直没走到这一步。
顺带说一句,如果你们的 H5 是嵌在钉钉、飞书这类 IM 里,免登之后往 Session 里塞 UserId、后面接口带 Cookie,一模一样会踩这个坑。
![]()
排查三天,最后落地的是两件事:一行 [SessionState(ReadOnly)],和一份后面要做的 JWT 改造排期。
回头看,真正耽误时间的不是技术有多难,是我一开始就认定「外网慢就是网络问题」。链路、TLS、证书吃掉了一天半,而抓包报告里「500 毫秒一档」那行字,第一眼就该让我想到串行。
你们遇到过这种「同域慢、跨域快」的怪事吗?或者更离谱的性能问题也行——评论区说说,我最近有点上瘾。
特别声明:以上内容(如有图片或视频亦包括在内)为自媒体平台“网易号”用户上传并发布,本平台仅提供信息存储服务。
Notice: The content above (including the pictures and videos if any) is uploaded and posted by a user of NetEase Hao, which is a social media platform and only provides information storage services.