血泪复盘:一次由 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,差点让整个产线停摆。

相关推荐
政企项目老覃15 小时前
边缘 AI 推理部署:安防零售场景下的模型裁剪与端侧落地实践
人工智能·程序人生·算法·性能优化·vllm
唐青枫15 小时前
别急着拆服务:C#.NET 微服务架构从边界设计到实战
c#·.net
Doris__HE21 小时前
【元脑服务器NF8260G7-NF8260M7技术规格分享】
运维·服务器·网络·数据库·性能优化
yunwei3721 小时前
eBPF 教程:BPF 调度器入门
linux·后端·性能优化
Doris__HE1 天前
【元脑服务器NF8480G7-NF8480M7技术规格分享】
运维·服务器·数据库·缓存·性能优化
淡海水1 天前
12-02-性能-数据结构性能调查案例1-5
数据结构·性能优化·c#
UWA1 天前
让UE性能分析真正穿透指标:GPU看堆栈,Texture拆到Group
性能优化·游戏开发
唐青枫2 天前
别把 Razor 当成几段 HTML:C#.NET Razor Pages、MVC 视图与实战详解
c#·.net
爱喝水的鱼丶2 天前
SAP-ABAP:MM 模块库存管理开发:出入库增强、批次管理与库存盘点功能开发
性能优化·sap·abap·增强·开发运维·经验交流·mm模块
宝桥南山2 天前
Microsoft Fabric - 简单尝试一下Microsoft Fabric .NET SDK
microsoft·微软·.net·.netcore·powerbi·fabric