你的.NET应用为什么越来越慢?问题从来不在代码
一、问题往往不是出在你以为的地方系统变慢的时候大多数人的第一反应都很一致是不是SQL写得不够好是不是哪里没加缓存是不是算法可以再优化一下。然后开始改查询、加索引、做缓存甚至加机器。短时间内可能确实有效但过一段时间问题又会回来而且通常比之前更严重。这类问题有个共同点你每次都在“修表象”但真正的原因一直没动。在.NET体系里代码执行并不是直接发生的中间隔着一整套运行时机制——GC、线程池、JIT/AOT、内存分配。这些东西平时你几乎感觉不到但一旦规模上来它们就会成为主要成本来源。所以很多时候并不是代码慢而是系统为了执行这些代码付出的代价越来越高。二、真正拖慢系统的通常是“分配”如果你看.NET这几年的演进方向会发现一个很明显的趋势一直在减少分配、减少内存占用、减少运行时参与。比如AOT、Trimming、Span这些东西本质都在做一件事——降低运行成本。这背后其实说明了一件很现实的事情在大多数业务系统里计算本身并不贵贵的是“围绕计算产生的开销”。最常见的就是对象分配。每一次new都会进入托管堆参与GC。当请求量上来之后对象数量会迅速膨胀GC开始频繁工作。GC一旦频繁触发CPU时间就会被大量消耗在扫描、移动对象和暂停线程上。这时候你看到的是接口变慢但实际发生的是系统在忙着回收你刚刚创建的那些对象。还有一些更隐蔽的情况比如LINQ链式调用、Lambda闭包、字符串拼接、装箱拆箱。这些写法本身没有问题但它们在背后会不断制造临时对象。在低并发下几乎感觉不到但在高并发场景里这些“微小成本”会被放大成真实瓶颈。很多人优化代码时完全没意识到这一点还在不断引入新的分配点最后变成一个典型现象代码越来越“优雅”系统却越来越慢。三、分层没有错但很多系统已经分“过头”了Controller、Service、Repository这一套本来是为了让代码更清晰、更易维护。但在很多项目里它已经变成了一种惯性不管业务复杂与否先分三层再说。问题在于每一层都不是免费的。一次请求进来可能要经过多次方法调用、对象转换、序列化处理最后才真正触达到数据库。单看每一步都没什么问题但叠加起来之后请求路径会变得很长而真正“做业务”的部分反而只占很小一段。更麻烦的是这些层之间往往还伴随着额外的抽象比如DTO转换、接口封装、动态代理。这些设计在代码层面看起来很规范但在执行层面其实是在不断增加系统负担。你会看到一种现象业务很简单但调用链很复杂逻辑没多少但耗时却不低。这类问题很少能通过“优化某一层代码”解决因为问题本身就不在某一层而是在整体结构。四、很多性能问题其实是“无效工作”太多如果把这些问题放在一起看会发现一个共性系统在做很多“没有必要的事情”。为了抽象多走了几层调用为了通用性引入反射为了方便开发创建了大量短生命周期对象为了代码优雅用了复杂的表达式。这些决策单独看都合理但叠加起来之后就变成了一种负担。在过去这种负担还能被JIT和运行时“部分消化”。但现在情况在变。随着AOT、Trimming、Source Generator这些能力越来越成熟运行时正在逐步“退场”。很多过去可以在运行时动态处理的事情现在必须在编译阶段就确定下来。这意味着多余的抽象、多余的依赖、多余的代码都会变成真实成本而不是可以被隐藏的细节。归根结底系统的执行方式越来越“直接”也越来越“诚实”。你写了多少它就执行多少不再帮你兜底。在这种情况下真正影响性能的不再是某一段代码写得好不好而是你整个系统到底做了多少无效工作。五、优化的方向其实很简单让系统少做事当你把问题看清楚之后很多优化思路反而变得简单了。减少不必要的对象创建控制好数据结构和生命周期让GC压力保持在可控范围在读多写少的场景里减少不必要的抽象必要时直接打通数据访问路径对高频接口优先考虑执行路径而不是代码“是否优雅”。还有比较重要的一点是不要把所有问题都交给运行时解决。过去你可以依赖它帮你兜底但现在这种兜底空间正在变小。很多时候真正有效的优化不是把某段代码改得更复杂而是反过来问一句这段逻辑是不是可以不做或者换一种更直接的方式完成。结语系统变慢很少是因为某一行代码写错了更多是因为整体成本在不断累积而你没有察觉。.NET这几年的演进其实一直在强调一件事减少运行时参与减少不确定性让执行路径更简单、更直接。如果顺着这个方向去看你会发现很多问题的答案并不复杂——不是做得更聪明而是做得更克制。