模块四 · 工程与运维
部署与诊断:容器、日志与线上排查
部署不是"发布一下",而是选择发布方式、配置健康检查、接好日志与追踪,并能在线上定位 CPU、内存与线程问题。
读完这一节,你能做到
- 区分框架依赖、自包含与 AOT 三种发布方式。
- 写出可用的容器与健康检查配置。
- 用 ILogger 与 OpenTelemetry 建立可观测性。
- 按 CPU/内存/线程池三条线排查线上问题。
一、三种发布方式
| 方式 | 特点 | 适用 |
|---|---|---|
| 框架依赖(FDD) | 包小,依赖运行时 | 服务器统一装运行时 |
| 自包含(SCD) | 带运行时,包大 | 目标机无运行时 |
| AOT | 编译为原生,启动快、内存低 | 冷启动敏感、无动态加载 |
AOT 的限制:不能用动态反射、表达式树编译、部分序列化特性;EF Core 需要编译模型。选之前先确认依赖兼容性。
裁剪(Trimming)能减小包体,但可能裁掉反射用到的类型,需要 DynamicDependency 或根描述符。
二、容器与健康检查
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base
WORKDIR /app
EXPOSE 8080
COPY publish .
ENTRYPOINT ["dotnet", "App.dll"]
要点:
- 用非 root 用户运行。
- 设置内存与 CPU limit,并让 GC 感知(
DOTNET_GCHeapHardLimit)。 - 健康检查分 liveness(活着)与 readiness(能接流量),不要混用。
- 优雅关闭:处理 SIGTERM,等待进行中的请求。
三、日志与追踪
ILogger<T>用结构化日志,不要拼字符串。- 日志级别按环境配置,生产默认 Information,热路径用 Debug。
- 请求 ID/追踪 ID 要贯穿,方便串联。
- OpenTelemetry 采集 traces/metrics/logs,导出到 Jaeger、Prometheus 或云厂商。
三个常见指标:请求量、错误率、延迟分位(P95/P99);再加 GC、线程池、连接池。
四、线上排查三条线
| 现象 | 先查 |
|---|---|
| CPU 高 | dotnet-trace 采样、热点方法、GC 频率 |
| 内存高 | dotnet-counters、gcdump、大对象与泄漏 |
| 线程池饥饿 | 队列长度、阻塞调用、同步等待异步 |
线程池饥饿很隐蔽:大量同步阻塞(.Result、Thread.Sleep)会耗尽线程,表现为"所有接口都慢"。用 dotnet-counters 看队列长度与线程数。
五、配置与密钥
- 配置分层:appsettings → 环境变量 → 密钥服务。
- 密钥不要进代码库,用环境变量、User Secrets(开发)或 Key Vault。
- 生产配置变更要可回滚,避免"改一个值重启全部"。
六、常见坑
- 容器内存 limit 很低,却用服务器 GC。
- 健康检查把数据库依赖放进 liveness,导致误重启。
- 日志打印敏感信息。
- 用
.Result导致线程池饥饿。 - AOT 发布后反射相关功能失效。
七、动手任务:自写 → AI 写 → 对比 → 取舍
四步不跳:① 自己写(限时,关掉补全)→ ② 让 AI 写同一题 → ③ 逐条比对 → ④ 说出保留谁的写法。 完整动作与记录模板见《AI 协作与代码评审》。
① 自己写
- 分别用 FDD 与 SCD 发布,比较包体与启动时间。
- 配一个 liveness 与 readiness,模拟依赖故障观察行为。
- 用 dotnet-counters 观察一次压测的线程池队列。
② 让 AI 写同一题
把第 2 题(容器化部署)交给 AI,补一句 给出健康检查、日志与优雅停机的做法。
③ 对比要盯的点
- 健康检查它分了 liveness 与 readiness 吗?混用会导致无谓重启。
- 优雅停机它处理了吗?滚动发布时正在处理的请求会被切断。
- 日志它是结构化输出吗?容器里 grep 纯文本日志基本没法用。
④ 一句话取舍
发布时它这套配置会让多少在途请求失败?
八、自测
- 三种发布方式的差异?
- liveness 与 readiness 的区别?
- 结构化日志的好处?
- 线程池饥饿的典型原因?
- 密钥应该放在哪?
参考答案要点:依赖运行时/自带/AOT;活着 vs 可接流量;可检索聚合;同步阻塞;密钥服务/环境变量。
附录:高频追问
问:AOT 适合所有项目吗? 不适合。依赖动态反射、动态编译、部分序列化的项目需要改造。
问:容器里怎么设置 GC? 按 CPU limit 选择工作站/服务器 GC,并设置堆硬限制,避免被 OOM Kill。
问:为什么健康检查会误杀? liveness 检查了下游依赖,下游抖动导致容器被重启,形成雪崩。
问:日志太多怎么办? 分级、采样、结构化字段,热路径降级;把日志当数据分析,而不是文本。
问:如何做优雅关闭? 监听 SIGTERM,停止接收新请求,等待进行中的请求完成,再释放资源。
问:OpenTelemetry 解决什么? 统一 traces/metrics/logs 的采集与导出,避免厂商锁定。
问:P99 高但平均正常说明什么? 长尾问题:锁竞争、GC 停顿、慢查询、线程池排队。
问:发布回滚怎么做? 镜像版本化 + 蓝绿/滚动发布;数据库变更要向前兼容。
问:配置热更新怎么实现? IOptionsMonitor 或配置中心推送;注意线程安全。
问:容器时区不对怎么办? 设置 TZ 环境变量或在镜像里装 tzdata。
附:CI/CD 流水线
一条可用的流水线:
- 还原依赖(缓存 NuGet)。
- 编译 + 静态分析,失败即停。
- 单元测试 + 集成测试。
- 构建镜像并打版本标签。
- 推送到镜像仓库。
- 部署到预发,跑冒烟测试。
- 灰度/滚动发布到生产,观察指标。
关键点:构建一次、多处部署;版本可追溯;回滚要能一条命令完成。
可观测性三支柱
| 支柱 | 回答什么问题 | 常用工具 |
|---|---|---|
| 日志 | 发生了什么 | Seq、ELK、Loki |
| 指标 | 趋势好不好 | Prometheus、Grafana |
| 追踪 | 慢在哪一段 | Jaeger、Tempo |
三者要用同一个 TraceId 串起来,否则排查时还是靠猜。
补充:发布前检查清单
- 配置与密钥不包含在镜像里
- 健康检查区分存活/就绪
- 有超时、重试、熔断
- 日志级别与脱敏已确认
- 数据库迁移可回滚
- 回滚镜像已准备好
- 关键指标与告警已配置
缺任何一项,发布都会变成运气。
补充:容器安全
- 使用非 root 用户运行。
- 只读根文件系统,临时目录单独挂载。
- 最小基础镜像,减少攻击面。
- 镜像扫描与依赖审计进 CI。
- 密钥通过环境变量/密钥服务注入,不打进镜像。
发布验收
发布后 10 分钟内看四项:健康检查全部就绪、错误率未上升、P99 未劣化、日志无异常峰值。任何一项异常,立即回滚。
三个不要
不要用 root 跑容器;不要把密钥打进镜像;不要让 liveness 依赖下游。
一个判断
发布前问三个问题:出问题能回滚吗?密钥在哪里?异常时先看哪个指标?三个都答不上来,就不要发。
验收
发布后 10 分钟看四项:健康检查就绪、错误率、P99、日志异常峰值;任何一项不对,立即回滚。
发布前问回滚,发布后看指标;没有回滚方案的发布,不叫发布,叫赌博。密钥不进镜像;健康检查分层;先演练。发布前先演练回滚,回滚先行,别赌。先演练,再发布。先演练。
配套题目
★ 越多越该先背;点题目可看完整答案与深入版。
- ★★★★☆ .NET 应用在容器里跑,有哪些必须调的参数?
- ★★★☆☆ 线上 .NET 服务变慢或内存涨,你会用哪些工具排查?
- ★★★☆☆ .NET 容器镜像怎么优化体积和启动速度?
- ★★★☆☆ CI/CD 是什么?最小落地怎么做?
提示:读完可以顺手做几道对应题目,检验是否真的理解