1. 通用主机与 IHost
一句话总结: IHost 是 .NET 应用的运行时外壳,统一管理依赖注入、配置、日志与后台任务,Web 与 Worker 应用共用同一套托管模型。
从 .NET Core 3.0 起,IHost 成为所有应用的托管基础:它持有 DI 容器、配置系统、日志管道与生命周期,WebApplication 是它在 Web 场景的便利封装。理解 IHost 就理解了「应用是怎么被启动、编排、停止的」。
// 通用主机最小示例(Worker Service 模板本质)
var builder = Host.CreateApplicationBuilder(args);
builder.Services.AddHostedService<Worker>();
var host = builder.Build();
await host.RunAsync();
// WebApplication 也是建立在 IHost 之上
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
var app = builder.Build();
app.MapControllers();
await app.RunAsync();
// 访问底层 IHost 的能力
var lifetime = app.Services.GetRequiredService<IHostApplicationLifetime>();
| 组件 | 职责 |
|---|---|
IHost | 应用生命周期外壳 |
IServiceProvider | 依赖注入容器 |
IConfiguration | 配置系统 |
IHostedService | 随主机启停的后台服务 |
IHostApplicationLifetime | 启动/停止通知 |
避坑:
Host.CreateApplicationBuilder默认不带 Web 能力;Web 场景要用WebApplication.CreateBuilder。两者都注册了同一个 IHost,所以 Hosted Service、配置、日志在两种模板里的行为完全一致——这是「通用主机」的含义。
2. BackgroundService 与 IHostedService
一句话总结: IHostedService 让代码随主机启动与停止,BackgroundService 提供了基于 Task 的简洁基类,是定时任务与消息消费的标准宿主。
后台任务(定时清理、消息轮询、指标采集)不该塞进请求处理线程。IHostedService.StartAsync 在应用启动时被调用,BackgroundService 进一步把 ExecuteAsync 抽象成无限循环,配合 CancellationToken 实现优雅停止。
public class DailyCleanupService : BackgroundService
{
private readonly ILogger<DailyCleanupService> _logger;
private readonly IServiceScopeFactory _scopeFactory;
public DailyCleanupService(ILogger<DailyCleanupService> logger,
IServiceScopeFactory scopeFactory)
{
_logger = logger;
_scopeFactory = scopeFactory;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
_logger.LogInformation("清理服务已启动");
using var timer = new PeriodicTimer(TimeSpan.FromHours(24));
while (await timer.WaitForNextTickAsync(stoppingToken))
{
// 每次创建作用域:注入 Scoped 服务
using var scope = _scopeFactory.CreateScope();
var repo = scope.ServiceProvider.GetRequiredService<IOrderRepository>();
var deleted = await repo.CleanExpiredAsync(DateTime.UtcNow.AddDays(-90));
_logger.LogInformation("清理过期订单 {Count} 条", deleted);
}
}
}
// 注册
builder.Services.AddHostedService<DailyCleanupService>();
| 类型 | 特点 |
|---|---|
IHostedService | 最底层,手动实现 Start/Stop |
BackgroundService | 基类,抽象 ExecuteAsync 循环 |
PeriodicTimer | 周期性触发,可取消 |
IServiceScopeFactory | 后台任务创建 DI 作用域 |
避坑: 后台服务是单例宿主,直接注入 Scoped 服务(如 DbContext)会异常。必须用
IServiceScopeFactory.CreateScope()在循环体内创建作用域。另外ExecuteAsync抛出的未处理异常会悄悄终止该服务——要在循环里捕获并记录,或用IHostedService实现失败即停机的策略。
3. 启动顺序与状态感知
一句话总结: 多个 Hosted Service 按注册顺序启动,HostedService 与请求管线存在竞态,IHostApplicationLifetime 事件与健康检查用于协调启动时序。
应用启动不是一瞬间:配置加载、服务注册、Hosted Service 启动、Kestrel 监听、请求处理逐层展开。IHostApplicationLifetime.ApplicationStarted 在所有服务启动完成后触发,HealthChecks 让负载均衡器知道「真的准备好了」而非「端口开了」。
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddHostedService<CacheWarmer>(); // 先启动
builder.Services.AddHostedService<MessageConsumer>(); // 后启动
// 健康检查
builder.Services.AddHealthChecks()
.AddDbContextCheck<ShopDbContext>()
.AddCheck<ReadyCheck>("ready");
var app = builder.Build();
app.MapHealthChecks("/healthz");
var lifetime = app.Services.GetRequiredService<IHostApplicationLifetime>();
lifetime.ApplicationStarted.Register(() =>
{
Console.WriteLine("所有服务启动完成,应用可服务流量");
});
// 就绪探针:依赖预热完成
public class ReadyCheck : IHealthCheck
{
private readonly IOptionsMonitor<CacheWarmerState> _state;
public ReadyCheck(IOptionsMonitor<CacheWarmerState> state) => _state = state;
public Task<HealthCheckResult> CheckHealthAsync(
HealthCheckContext context, CancellationToken ct) =>
Task.FromResult(_state.CurrentValue.Warmed
? HealthCheckResult.Healthy()
: HealthCheckResult.Unhealthy("缓存尚未预热完成"));
}
| 阶段 | 事件/信号 |
|---|---|
| 服务注册完成 | ApplicationStarted |
| 停止开始 | ApplicationStopping |
| 停止完成 | ApplicationStopped |
| 存活 | /healthz(进程活着) |
| 就绪 | /ready(可接流量) |
避坑: 部署平台(K8s/编排器)同时用两种探针——存活探针判断进程是否假死,就绪探针判断能否接流量。把「数据库连不上」写进就绪探针会导致 Pod 反复重建,写成存活探针则可能掩盖依赖故障——探针语义选错,故障定位就难了。
4. 优雅关闭与资源释放
一句话总结: 优雅关闭让进程在退出前停止接收新工作、把在途工作跑完并释放资源,Kestrel 的连接关闭与 Hosted Service 的 CancellationToken 是关键两环。
收到 SIGTERM(K8s 停止 Pod、Ctrl+C)时,IHost 触发 ApplicationStopping,Kestrel 停止接受新连接,CancellationToken 通知所有 Hosted Service 收尾。ShutdownTimeout 限制总关闭时长,超时则强制退出。
var builder = WebApplication.CreateBuilder(args);
// 默认 30 秒,按需调整
builder.Host.ConfigureHostOptions(o => o.ShutdownTimeout = TimeSpan.FromSeconds(20));
var app = builder.Build();
app.Lifetime.ApplicationStopping.Register(() =>
{
Console.WriteLine("正在优雅关闭,等待在途请求完成...");
});
await app.RunAsync();
// Hosted Service 响应取消,主动收尾
public class MessageConsumer : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
await Task.Delay(TimeSpan.FromSeconds(5), stoppingToken);
// 处理一批消息后检查取消
}
// 退出前刷新缓冲
Console.WriteLine("消费服务已停止,剩余消息留待下次处理");
}
}
| 资源 | 关闭动作 |
|---|---|
| Kestrel | 停止接受连接,等待在途请求 |
| Hosted Service | CancellationToken 触发,循环退出 |
| DbContext/连接池 | 随 DI 容器释放 |
| ILogger | 刷写日志缓冲 |
| 总时长 | ShutdownTimeout(默认 30s) |
避坑:
Task.Delay(..., token)与WaitForNextTickAsync(token)都必须传入 CancellationToken——否则取消只停止你的循环检查,阻塞点不响应,优雅关闭形同虚设。另外容器里优雅关闭靠 SIGTERM,Windows 上 Ctrl+C 是 SIGINT,两种信号都要处理。
5. Kestrel 配置与调优
一句话总结: Kestrel 是 ASP.NET Core 的高性能内置服务器,端点、并发、超时与 HTTP/2 都由 ConfigureKestrel 精确控制,反向代理常在前方终结 TLS。
Kestrel 是跨平台的 libuv/Kestrel 传输层,性能优异且无需 IIS。生产部署最常见的拓扑是 Nginx/网关终结 TLS + 转发 HTTP 到 Kestrel,此时 Kestrel 只监听内网端口。端点、请求体上限、超时、连接并发都是常见调优项。
var builder = WebApplication.CreateBuilder(args);
builder.WebHost.ConfigureKestrel(options =>
{
// 显式端点(跳过 UseUrls 的环境变量)
options.Listen(IPAddress.Any, 8080);
// HTTPS 端点由反向代理终结时不需要
// 直接终结 TLS 的场合:
options.Listen(IPAddress.Any, 8443, listen =>
listen.UseHttps("cert.pfx", "password"));
// 请求体上限
options.Limits.MaxRequestBodySize = 10 * 1024 * 1024;
// 超时控制
options.Limits.KeepAliveTimeout = TimeSpan.FromSeconds(120);
options.Limits.RequestHeadersTimeout = TimeSpan.FromSeconds(30);
// 并发连接
options.Limits.MaxConcurrentConnections = 10_000;
});
| 配置 | 默认 | 说明 |
|---|---|---|
MaxRequestBodySize | 30MB | 请求体上限 |
KeepAliveTimeout | 130s | 空闲连接保持 |
MaxConcurrentConnections | 无限 | 并发连接数 |
MinRequestBodyDataRate | 240 B/s | 慢速攻击防护 |
避坑: 反向代理模式下不要把 HTTPS 也交给 Kestrel——代理终结 TLS、内网 HTTP 转发,性能和证书管理都更优。此时设置
ForwardedHeaders中间件让应用信任X-Forwarded-*,否则重定向与客户端 IP 会全错。
6. Docker 容器化
一句话总结: 多阶段构建把 SDK 镜像编译与运行时镜像分离,ASPNET 基础镜像内置非 root 用户与生产优化,暴露端口 + 健康检查让容器进入编排体系。
.NET 官方镜像分 sdk(构建)与 aspnet/runtime(运行),多阶段构建能显著减小镜像体积。ASPNET 镜像已包含非 root 用户、时区与生产环境变量默认值,安全与合规基线更稳。
# 构建阶段
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /src
COPY . .
RUN dotnet publish "src/Shop.Api/Shop.Api.csproj" -c Release -o /app/publish
# 运行阶段
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS runtime
WORKDIR /app
COPY --from=build /app/publish .
EXPOSE 8080
ENV ASPNETCORE_URLS=http://+:8080
ENTRYPOINT ["dotnet", "Shop.Api.dll"]
# docker-compose.yml 片段
services:
api:
build: .
ports:
- "8080:8080"
environment:
- ASPNETCORE_ENVIRONMENT=Production
- ConnectionStrings__Default=Server=db;Database=shop;...
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/healthz"]
interval: 10s
timeout: 5s
retries: 3
depends_on:
db:
condition: service_healthy
| 环节 | 要点 |
|---|---|
| 基础镜像 | sdk 构建 / aspnet 运行 |
| 端口 | 显式 EXPOSE + ASPNETCORE_URLS |
| 用户 | 运行阶段用非 root 用户 |
| 健康检查 | /healthz 交给编排器 |
| 配置 | 全部走环境变量注入 |
避坑: 容器里运行默认会以 root 身份——aspnet 镜像提供了
app用户,建议USER $APP_UID切换。另外不要在镜像里打日志文件,日志走 stdout 由编排平台收集;PID 1 由dotnet直接担任,否则收不到 SIGTERM 优雅关闭会失效。
7. 发布与部署拓扑
一句话总结: dotnet publish 生成可部署产物,部署拓扑决定运维方式——单机 systemd、容器编排、Azure App Service 各有侧重,健康检查与滚动更新是共同底线。
发布产物是自包含的 dll + 依赖,配合 dotnet publish 的优化选项(裁剪、单文件、ReadyToRun)可以调整体积与启动速度。部署拓扑的选择影响配置注入、日志收集与伸缩方式。
# 基础发布
dotnet publish -c Release -o out
# 单文件 + 裁剪(减小体积,牺牲 JIT 灵活性)
dotnet publish -c Release -r linux-x64 \
--self-contained true /p:PublishSingleFile=true /p:PublishTrimmed=true
# 预览发布内容
ls out
# Linux systemd 单元(单机部署)
[Unit]
Description=Shop API
After=network.target
[Service]
WorkingDirectory=/opt/shop
ExecStart=/usr/bin/dotnet /opt/shop/Shop.Api.dll
Environment=ASPNETCORE_ENVIRONMENT=Production
Restart=always
RestartSec=10
User=www-data
[Install]
WantedBy=multi-user.target
| 部署方式 | 优点 | 注意 |
|---|---|---|
| systemd | 简单、自控 | 手动滚动更新 |
| Docker + Compose | 环境一致 | 单机编排 |
| Kubernetes | 伸缩、自愈 | 运维复杂度高 |
| Azure App Service | 免运维 | 平台锁定 |
避坑: 容器/编排环境里永远不要用
Restart=always以外的自写守护,也不要让应用自行 fork 子进程——编排器管理进程生命周期。发布前务必用dotnet publish而不是 build 后的 bin 目录直接拷,后者缺依赖且路径硬编码。
8. 总结
| 环节 | 要点 |
|---|---|
| IHost | 统一托管外壳,Web 与 Worker 共用 |
| BackgroundService | 定时/消费任务的标准宿主,DI 作用域隔离 |
| 启动时序 | Hosted Service 顺序 + 健康检查协调就绪 |
| 优雅关闭 | CancellationToken 贯穿,ShutdownTimeout 兜底 |
| Kestrel | 反向代理终结 TLS,内网 HTTP 转发 |
| Docker | 多阶段构建,非 root 用户,健康检查 |
| 发布 | publish 产物 + 部署拓扑与滚动更新 |
托管与部署是 .NET 应用「从代码到可用」的最后一段路:生命周期决定资源何时创建与释放,部署拓扑决定运维如何介入。把优雅关闭、健康检查、配置注入这三件事在第一天就做对,后续的伸缩、灰度、故障恢复都会顺理成章——它们是云原生 .NET 服务的及格线。
延伸阅读
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。