所谓的"最佳实践"未必正确。
🧠 本文价值
我们曾有一个遵循所有"最佳实践"的 .NET 微服务——异步、缓存、Minimal API 一应俱全。但它仍在 3K RPS 时崩溃。原因何在?我们轻信了宣传,而非性能分析器。
本文用真实指标揭穿 .NET 最顽固的性能神话,并提供可落地的解决方案。
🚫 真相:过度缓存反而损害性能
盲目缓存会导致:
💡 解决方案:
var cacheEntryOptions = new MemoryCacheEntryOptions
{
SlidingExpiration = TimeSpan.FromMinutes(10),
Size = 1 // 限制条目大小
};
_memoryCache.Set("user:123", userData, cacheEntryOptions);
📉 反例:
缓存分页列表或实时仪表盘数据 → 低重用率 + 数据过时
🚫 真相:GC 是 .NET 的优势,而非敌人
问题通常源于:
💡 解决方案:
var pool = ArrayPool<byte>.Shared;
byte[] buffer = pool.Rent(1024); // 租用缓冲区
// 使用缓冲区...
pool.Return(buffer); // 归还资源
📉 反例:
在单例服务缓存大型结果集 → 内存固定 + 阻止 Gen0/1 回收
🚫 真相:云环境中会导致 DNS 陈旧问题
后端 IP 变更时持续访问旧地址
💡 解决方案:
services.AddHttpClient("Weather", client =>
{
client.BaseAddress = new Uri("https://api.weather.com");
}); // 连接池自动处理 DNS 轮转
🚫 真相:误用 struct
导致装箱开销
struct Point : IComparable // 反例
{
public int X, Y;
public int CompareTo(object obj) => 0; // 引发装箱
}
💡 适用场景:
🚫 真相:滥用导致栈溢出/缓冲区溢出
💡 适用场景:
Span<byte> buffer = stackalloc byte[256]; // 高效切片
ReadOnlySpan<byte> header = buffer.Slice(0, 4);
⚠️ 禁用场景:
stackalloc
)🚫 真相:提升扩展性而非原始速度
💡 解决方案:
// I/O 密集型:使用 async
await _httpClient.GetAsync(url);
// CPU 密集型:显式卸载
await Task.Run(() => DoHeavyCalculation());
🚫 真相:真实负载下性能差距可忽略
💡 适用场景:
🚫 真相:未启用高级优化仍存瓶颈
💡 解决方案:
<PropertyGroup>
<TieredCompilation>true</TieredCompilation> <!-- 分层编译 -->
<ReadyToRun>true</ReadyToRun> <!-- 预编译优化 -->
</PropertyGroup>
🚫 真相:单次迭代时强制转 List 增加内存压力
💡 解决方案:
IEnumerable<int> GenerateSequence() // 惰性求值
{
for (int i = 0; i < 1000; i++)
yield return i;
}
🚫 真相:清晰度优于过早优化
💡 高性能技巧:
if (items.TryGetNonEnumeratedCount(out int count)) // .NET6+ 特性
{
// 避免不必要的枚举
}
| ✅ 正确做法 | ❌ 致命陷阱 |
|---------------------------|---------------------------|
| 缓存昂贵+低频变更操作 | 盲目缓存所有数据 |
| 监控 GC 的 %Time 指标 | 忽视 Gen2 回收次数 |
| 使用 IHttpClientFactory | 滥用 HttpClient 单例 |
| 值类型用于小型数据结构 | 值类型实现接口引发装箱 |
| Span
性能神话不仅浪费 CPU 周期——更浪费工程师生命。
不要追随最响亮的声音,
遵循你的性能分析器、监控指标和官方指南。
记住:当 3AM 的生产告警响起时,只有真实数据能拯救你。