优化你的 .NET 应用以用于生产环境 - 完整清单(2)

Avatar
不若风吹尘
2025-12-28T14:00:59
17
0

6) 遥测(日志、指标、追踪)

Telemetry

以下代码添加了 OpenTelemetry 来收集 .NET 应用中的日志、指标和追踪。

builder.Services.AddOpenTelemetry()
  .UseOtlpExporter()
  .WithMetrics(m => m.AddAspNetCoreInstrumentation().AddHttpClientInstrumentation())
  .WithTracing(t => t.AddAspNetCoreInstrumentation().AddHttpClientInstrumentation());
  • UseOtlpExporter() 告诉它将遥测数据发送到哪里。通常是一个 OTLP 收集器(如 Grafana、Jaeger、Tempo、Azure Monitor)。这样你就可以在仪表板中可视化指标和追踪。
  • WithMetrics() 意味着它将收集指标。这些指标包括请求率(RPS)、请求持续时间(延迟)、GC 暂停、异常、HTTP 客户端计时。
  • .WithTracing(...) 意味着它将收集分布式追踪。当你的应用调用其他 API 或微服务时,这很有用。你可以看到一个请求从一项服务到另一项服务的完整路径,包括时间和瓶颈。

.NET 诊断工具

当你的应用上线运行时,你应该了解以下工具。你知道飞机上有一个黑匣子记录仪,用于了解飞机失事的原因。对于 .NET 来说,以下就是我们的黑匣子记录仪。它们在不附加调试器的情况下捕获发生的情况。

工具 功能 使用时机
dotnet-counters CPU、GC、请求率等实时指标 监控运行中的应用
dotnet-trace CPU 采样与性能追踪 查找运行缓慢的代码
dotnet-gcdump GC 堆转储(分配情况) 诊断内存问题
dotnet-dump 完整进程转储 调查崩溃或挂起
dotnet-monitor 暴露上述所有功能的 HTTP 服务 通过 API 收集遥测数据

7) 以正确的方式在 Docker 中构建和运行 .NET 应用

Docker

多阶段构建是一种 Docker 技术,你使用一个镜像来构建应用,使用另一个更小的镜像来运行它。我们之所以进行多阶段构建,是因为 .NET SDK 镜像很大,但包含了所有构建工具。而 .NET 运行时镜像则很小,并为生产环境优化。你只将构建阶段发布的输出复制到运行阶段。

FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build
WORKDIR /src
COPY . .
RUN dotnet restore
RUN dotnet publish -c Release -o /app/out -p:PublishTrimmed=true -p:PublishSingleFile=true -p:ReadyToRun=true

FROM mcr.microsoft.com/dotnet/aspnet:9.0
WORKDIR /app
ENV ASPNETCORE_URLS=http://+:8080
EXPOSE 8080
COPY --from=build /app/out .
ENTRYPOINT ["./YourApp"]  # 或者 ["dotnet","YourApp.dll"]

我来解释一下这些 Dockerfile 命令:

阶段1:构建

  • FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build 使用包含编译器和工具的 .NET SDK 镜像。AS build 命名允许你稍后引用这个阶段。
  • WORKDIR /src 设置容器内的工作目录。
  • COPY . . 将你的源代码复制到容器中。
  • RUN dotnet restore 还原 NuGet 包。
  • RUN dotnet publish ...Release 模式构建项目,为生产环境优化,并输出到 /app/out。 相关标志:
    • PublishTrimmed=true -> 移除未使用的代码
    • PublishSingleFile=true -> 将所有内容捆绑到一个文件中
    • ReadyToRun=true -> 预编译代码以实现更快的启动

阶段2:运行

  • FROM mcr.microsoft.com/dotnet/aspnet:9.0 使用更轻量级的运行时镜像,没有编译器,只有运行时。
  • WORKDIR /app 你的应用将在容器内存放的位置。
  • ENV ASPNETCORE_URLS=http://+:8080 让应用监听 8080 端口(以及所有网络接口)。
  • EXPOSE 8080 记录你的容器使用的端口(用于 Docker/K8s 网络)。
  • COPY --from=build /app/out .构建阶段 复制发布的输出到这个最终镜像。
  • ENTRYPOINT ["./YourApp"] 定义容器启动时运行的命令。如果你发布为单文件,则是 ./YourApp。否则,使用 dotnet YourApp.dll

8) 安全性

Security

无处不在的 HTTPS,即使在代理后面

即使你的应用运行在像 Nginx、Cloudflare 或负载均衡器这样的反向代理后面,也要始终强制使用 HTTPS。为什么?因为如果不使用 SSL,内部流量仍然可能被截获,而且 Cookie、HSTS、浏览器 API 都需要 HTTPS。在 .NET 中,你可以像这样轻松地强制使用 HTTPS:

app.UseHttpsRedirection();

在生产环境中使用 HSTS

HSTS(HTTP 严格传输安全)告诉浏览器:

始终对此域名使用 HTTPS——甚至不要再尝试 HTTP!

一旦设置,浏览器会缓存此规则,因此用户不会意外访问不安全版本。你可以像下面这样轻松地强制执行:

if (!app.Environment.IsDevelopment())
{
    app.UseHsts();
}

当你使用 HSTS 时,它会向浏览器发送这个 HTTP 头部: Strict-Transport-Security: max-age=31536000; includeSubDomains。浏览器将在 1 年(31,536,000 秒)内记住此设置,即该站点必须仅使用 HTTPS。并且 includeSubDomains 选项将此规则也应用于所有子域名(例如:api.abp.iocdn.abp.ioaccount.abp.io 等)。

将密钥存储在环境变量或密钥存储中

切勿将密码、连接字符串或 API 密钥存储在代码或 Git 中。那么我们应该把它们保存在哪里呢?

  • 最好/最实用的方式是使用环境变量。你可以在类 Unix 系统中轻松设置环境变量,如下所示:

    export ConnectionStrings__Default="Server=...;User Id=...;Password=..."
    
  • 你可以像这样轻松地从 .NET 应用中访问这些环境变量:

    var conn = builder.Configuration.GetConnectionString("Default");
    

或者使用密钥存储,例如:Azure Key Vault、AWS Secrets Manager、HashiCorp Vault。

为公共端点添加速率限制

别忘了,使用你应用的可能不只是"天真的人"!我们过去在面向公众的网站上多次遇到这个问题。所以,要保护你的公共 API 免受滥用、机器人和 DDoS 攻击。使用速率限制!!!阻止暴力破解攻击,防止你的资源耗尽……

在 .NET 中,有一个内置的速率限制功能(System.Threading.RateLimiting):

builder.Services.AddRateLimiter(_ => _
    .AddFixedWindowLimiter("default", options =>
    {
        options.PermitLimit = 100;
        options.Window = TimeSpan.FromMinutes(1);
    }));

app.UseRateLimiter();

安全的 Cookie

Cookie 通常是攻击的良好目标。你必须正确保护它们,否则可能面临 Cookie 窃取或 CSRF 攻击。

options.Cookie.SecurePolicy = CookieSecurePolicy.Always;
options.Cookie.SameSite = SameSiteMode.Strict; // 或者 Lax
  • SecurePolicy = Always -> 仅通过 HTTPS 发送 Cookie
  • SameSite=Lax/Strict -> 防止 CSRF(跨站请求伪造)
    • Strict = 最安全
    • Lax = 对于登录会话来说是良好的平衡

9) 启动/冷启动

Cold Start / Startup

保持分层 JIT 开启

JIT(即时)编译器 在代码运行时将你应用的中间语言(IL)转换为本机 CPU 指令。分层 JIT 意味着运行时使用 2 个阶段的编译。实际上,这个设置在现代 .NET 中默认是启用的。所以只需保持开启。

  1. 第 0 层(快速 JIT): 快速、低优化的编译 -> 让你的应用尽快运行。 (在启动时使用。)
  2. 第 1 层(优化 JIT): 随后,运行时会以更深入的优化重新编译热点方法(经常使用的方法),以提高速度。

使用 PGO(配置文件引导优化)

PGO 让 .NET 从你应用的实际使用中学习。它分析哪些函数最常被使用,然后根据该模式重新优化构建。你可以把它想象成运行时说:

我已经看到了你的应用实际在做什么……我会相应地重新安排和优化代码路径。

在 .NET 8+ 中,你不必手动启用 PGO(配置文件引导优化)。JIT 收集运行时分析数据(例如哪些类型是常见的,分支预测)并随后使用它来生成更优化的代码。在 .NET 9 中,PGO 得到了改进:JIT 将 PGO 数据用于更多模式(如类型检查/转换)并做出更好的决策。


10) 优雅关闭

Shutdown

当我们和爱人分手时,常常会争吵并在事后后悔。当应用程序与操作系统"分手"时,应该处理得好一些 😘 ... 当你的应用停止时,可能是因为你部署了新版本,或者 Kubernetes 重启了 Pod…… 操作系统会发送一个名为 SIGTERM(终止)的信号。 优雅关闭 意味着妥善处理该信号,完成正在运行的任务,清理资源,然后干净地退出(像个成年人一样)!

var app = builder.Build();
var lifetime = app.Services.GetRequiredService<IHostApplicationLifetime>();
lifetime.ApplicationStopping.Register(() =>
{
    // 停止接受新请求,完成进行中的请求,刷新遥测数据
});
app.Run();

在 K8s 上,设置 terminationGracePeriodSeconds 并配置就绪/启动探针。


11) 负载测试

Load Test

有时候和爱人争吵是件好事。我们可以在结婚前看清他/她的真面目 😀 使用 k6bombardier,并使用真实的负载和类似生产的限制进行测试。别等到你的应用在生产环境运行时才感到惊讶!这些主题都应该被测试:CPU %GC 中的时间LOH 分配ThreadPool 队列长度Socket 耗尽

关于 K6

关于 Bombardier

[Bombardier vs K6

总结

总而言之,我列出了 11 项用于优化生产环境 .NET 应用的内容;涵盖了构建配置、托管设置、运行时行为、数据访问、遥测、容器化、安全性、启动性能以及在负载下的可靠性。通过应用本系列第一和第二部分中的清单,利用诸如裁剪发布、服务器 GC、最小化负载、池化 DbContext、OpenTelemetry、多阶段 Docker 构建、HTTPS 强制实施以及适当的关闭处理等技术——你将提升应用在真实流量和生产约束下的耐久性、可扩展性和可维护性。每一项都是一个检查点,你将能够交付一个健壮、高性能的 .NET 应用,为真实用户做好准备。

译自:https://abp.io/community/articles/optimize-your-dotnet-app-for-production-for-any-.net-app-2-78xgncpi

Last Modification : 12/29/2025 6:58:25 PM


In This Document