血泪复盘:一次由 HttpContext.Current 引发的 CPU 100% 惨案

做.NET开发的同学,对"CPU飙高"一定不陌生。很多时候,重启能解决一时,但解决不了一世。今天分享一个真实案例:一个看似无害的 TraceId 中间件,是如何通过 HttpContext.Currentlog4net 联手,把 CPU 打到 994%,让线程池彻底瘫痪的。


一、 案发现场:CPU 突然"起飞"

MES业务系统的某个核心应用节点突然告警:CPU 使用率逼近 100%

第一反应是重启,先恢复生产,重启后 CPU 确实回落了。但没过半小时,曲线再次陡峭上升,接口响应全线超时。

这种"重启缓解、随后复发"的模式,说明进程内部一定有持续消耗 CPU 资源 的逻辑在作祟。靠重启只能止血,不能根治。我们紧急对进程抓取了 Dump 文件,准备用 WinDbg​ 验尸。


二、 抽丝剥茧:WinDbg 三板斧

拿到 Dump,打开 WinDbg,加载 SOS 扩展。分析 CPU 飙高,我通常只用三板斧:

第一斧:确认 CPU 水位

执行 !tp 查看线程池状态:

解读 :CPU 直接干到 994%(多核总和),更可怕的是 182 个线程正在 Running。正常情况下大部分线程应该 Idle,这说明大量线程卡在了同一个地方疯狂自旋或切换上下文。


第二斧:查看线程状态

执行 ~*e !clrstack 查看所有线程的调用栈。我们发现了一个非常诡异的现象:

大量线程的调用栈中,都出现了 Monitor.Enterlock 等待状态。


第三斧:定位锁争用

!syncblk 查看同步块表,真相开始浮出水面:

关键发现

  1. 有一个 SyncBlockMonitorHeld 值极高,说明争用极其激烈。
  2. 锁的持有者是线程 253
  3. 锁关联的对象直指 log4net

切换到线程 253 查看调用栈:

调用栈清晰地显示:线程 253 正持有锁,执行 log4net 的日志写入操作,而其他 182 个线程全在门外等着。


三、 疑点重重:log4net 为什么要背锅?

看到这里,初步结论是:log4net 写日志引发了锁争用雪崩。

但作为老司机,我知道这还没完。log4net 虽然性能不算顶尖,但也不至于让 CPU 直接归零吧?

我们查看了当时的代码。为了链路追踪,写了一个中间件,给每个请求设置 TraceId

cs 复制代码
// 中间件代码
app.Use(async (context, next) =>
{
    TraceIdManager.SetLog4TraceId();
    await next.Invoke();
});


// TraceId 设置类
public static class TraceIdManager
{
    public static string SetLog4TraceId()
    {
         string traceId = string.Empty;
            try
            {
                if (HttpContext.Current != null && HttpContext.Current.Request != null)
                {
                    traceId = HttpContext.Current.Request.Headers["X-Eus-Requesttraceid"];
                }
                log4net.LogicalThreadContext.Properties["trace_id"] = string.IsNullOrEmpty(traceId) ? Guid.NewGuid().ToString("N") : traceId;
                return traceId;
            }
            catch { }
            return traceId;
    }
}

/// <summary>
/// 获取当前的请求入参
/// </summary>
public static Microsoft.AspNetCore.Http.HttpContext Current
{
    get
    {
        // 每次访问都在解析 DI 容器!
        object factory = ServiceProvider.GetService(typeof(IHttpContextAccessor));
        return ((HttpContextAccessor)factory).HttpContext;
     }
}

看起来人畜无害,对吧?但问题就出在这个 SetLog4TraceId() 方法内部。


四、 真相大白:隐藏在 HttpContext.Current 里的"毒药"

为了排查,我们做了一次对照实验

我们修改了代码,不再通过静态属性获取 HttpContext,而是由中间件直接传入参数

bash 复制代码
// 修复后的代码
public static string SetLog4TraceId(HttpContext context = null)
{
    string traceId = string.Empty;
    try
    {
        if (context != null && context.Request != null)
        {
            traceId = context.Request.Headers["X-Eus-Requesttraceid"];
        }
        // 同样是设置 log4net 上下文
        log4net.LogicalThreadContext.Properties["trace_id"] = traceId;
    }
    catch { }
    return traceId;
}

结果令人震惊:同样的配置,CPU 飙高的问题再也没有发生过。


根因剖析:为什么改了一行代码,救了整个系统?

关键在于旧代码中获取 HttpContext 的方式。旧代码里有一个静态属性:

bash 复制代码
public static HttpContext Current
{
    get
    {
        // 每次访问都在解析 DI 容器!
        object factory = ServiceProvider.GetService(typeof(IHttpContextAccessor));
        return ((HttpContextAccessor)factory).HttpContext;
    }
}

这就是那颗"定时炸弹"。

  1. 高频 DI 解析 :每一个请求进来,都要通过 ServiceProvider.GetService() 去解析服务。在 Castle Windsor 或内置 DI 中,解析过程涉及容器内部锁
  2. 跨线程访问陷阱HttpContextAccessor.HttpContext 底层是 AsyncLocal。当 log4net 的异步线程(非请求线程)尝试写日志时,访问这个属性会触发复杂的上下文传播逻辑。
  3. 死锁链形成
    • 请求线程:拿 DI 锁 → 拿 log4net 锁 → 写日志
    • log4net 线程:拿 log4net 锁 → 尝试获取 HttpContext → 拿 DI 锁
    • 两条链路交叉,形成了跨层级的锁依赖链。

在高并发下,这 182 个线程就在 DI 容器锁和 log4net 锁之间反复横跳,CPU 被上下文切换彻底烧干。


五、 解决方案与总结

临时止血

当时我们采取了临时措施:关闭 Trace 级别日志,屏蔽 trace_id 设置。日志量下降后,锁争用瞬间缓解,CPU 回归正常。

最终方案

  1. 干掉 HttpContext.Current :在 ASP.NET Core 中,严禁在高频路径(如中间件、日志上下文)使用静态方式反向解析 HttpContext
  2. 参数传递优于静态获取 :像上面修复的代码一样,通过参数直接传入 HttpContext,彻底绕开 DI 解析和 AsyncLocal 的跨线程访问。

六、 给.NET开发者的避坑清单

  1. 不要在高频路径上用 ServiceProvider.GetService(),尤其是中间件和日志组件。
  2. 不要在非请求线程(如后台线程、Task.Run)访问 HttpContext
  3. HttpContext.CurrentASP.NET 时代的遗留产物,在 Core 时代请直接扔掉。
  4. 对照实验是排查问题的利器:当你怀疑某个组件有问题时,写个 Demo 或改一行代码验证,往往比看 100 行源码更有效。

写在最后

这次事故让我深刻体会到:**有时候,最危险的代码往往看起来最无辜。**​ 一行 HttpContext.Current,差点让整个产线停摆。

相关推荐
晓晓_za89866836 分钟前
GEO 搜索源码白帽合规改造:适配各大 AI 信源收录规则
java·开发语言·人工智能·性能优化·开源
yunwei372 小时前
CPU 噪声会拖慢 GPU 推理吗:用 eBPF 定量测量调度器与 IRQ 影响
linux·人工智能·性能优化
ChaITSimpleLove4 小时前
用 .NET 10 技术栈 “玩转 AI 学习” 的路线图:从 Minimal API 到 AI Agent 产品
人工智能·.net·学习 ai
kyle~4 小时前
x86 汇编LOCK前缀 --- 硬件架构向软件提供的核心同步原语
汇编·c++·性能优化·硬件架构·实时系统
devilnumber15 小时前
MySQL 性能优化・诗意化记忆
数据库·mysql·性能优化
ly768915 小时前
Web 性能优化实战:从 Core Web Vitals 到工程化性能治理
前端·性能优化
李昊哲小课18 小时前
SpringBoot4 云端咖啡站 阶段四:安全、文件与性能
spring boot·安全·性能优化·文件·性能
小小杨树19 小时前
【C++】学习:双缓冲视频流水线深解析
c++·性能优化·音视频开发
半亩码田1 天前
【.NET新特性·第14篇】.NET 10 运行时与 SDK 新特性
.net