知

模块四 · 工程与运维

第 7 / 11 节20 分钟阅读型

部署与诊断:容器、日志与线上排查

部署不是"发布一下",而是选择发布方式、配置健康检查、接好日志与追踪,并能在线上定位 CPU、内存与线程问题。

读完这一节,你能做到

  1. 区分框架依赖、自包含与 AOT 三种发布方式。
  2. 写出可用的容器与健康检查配置。
  3. 用 ILogger 与 OpenTelemetry 建立可观测性。
  4. 按 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。
  • 生产配置变更要可回滚,避免"改一个值重启全部"。

六、常见坑

  1. 容器内存 limit 很低,却用服务器 GC。
  2. 健康检查把数据库依赖放进 liveness,导致误重启。
  3. 日志打印敏感信息。
  4. 用 .Result 导致线程池饥饿。
  5. AOT 发布后反射相关功能失效。

七、动手任务:自写 → AI 写 → 对比 → 取舍

四步不跳:① 自己写(限时,关掉补全)→ ② 让 AI 写同一题 → ③ 逐条比对 → ④ 说出保留谁的写法。 完整动作与记录模板见《AI 协作与代码评审》。

① 自己写

  1. 分别用 FDD 与 SCD 发布,比较包体与启动时间。
  2. 配一个 liveness 与 readiness,模拟依赖故障观察行为。
  3. 用 dotnet-counters 观察一次压测的线程池队列。

② 让 AI 写同一题

把第 2 题(容器化部署)交给 AI,补一句 给出健康检查、日志与优雅停机的做法。

③ 对比要盯的点

  • 健康检查它分了 liveness 与 readiness 吗?混用会导致无谓重启。
  • 优雅停机它处理了吗?滚动发布时正在处理的请求会被切断。
  • 日志它是结构化输出吗?容器里 grep 纯文本日志基本没法用。

④ 一句话取舍

发布时它这套配置会让多少在途请求失败?

八、自测

  1. 三种发布方式的差异?
  2. liveness 与 readiness 的区别?
  3. 结构化日志的好处?
  4. 线程池饥饿的典型原因?
  5. 密钥应该放在哪?

参考答案要点:依赖运行时/自带/AOT;活着 vs 可接流量;可检索聚合;同步阻塞;密钥服务/环境变量。

附录:高频追问

问:AOT 适合所有项目吗? 不适合。依赖动态反射、动态编译、部分序列化的项目需要改造。

问:容器里怎么设置 GC? 按 CPU limit 选择工作站/服务器 GC,并设置堆硬限制,避免被 OOM Kill。

问:为什么健康检查会误杀? liveness 检查了下游依赖,下游抖动导致容器被重启,形成雪崩。

问:日志太多怎么办? 分级、采样、结构化字段,热路径降级;把日志当数据分析,而不是文本。

问:如何做优雅关闭? 监听 SIGTERM,停止接收新请求,等待进行中的请求完成,再释放资源。

问:OpenTelemetry 解决什么? 统一 traces/metrics/logs 的采集与导出,避免厂商锁定。

问:P99 高但平均正常说明什么? 长尾问题:锁竞争、GC 停顿、慢查询、线程池排队。

问:发布回滚怎么做? 镜像版本化 + 蓝绿/滚动发布;数据库变更要向前兼容。

问:配置热更新怎么实现? IOptionsMonitor 或配置中心推送;注意线程安全。

问:容器时区不对怎么办? 设置 TZ 环境变量或在镜像里装 tzdata。

附:CI/CD 流水线

一条可用的流水线:

  1. 还原依赖(缓存 NuGet)。
  2. 编译 + 静态分析,失败即停。
  3. 单元测试 + 集成测试。
  4. 构建镜像并打版本标签。
  5. 推送到镜像仓库。
  6. 部署到预发,跑冒烟测试。
  7. 灰度/滚动发布到生产,观察指标。

关键点:构建一次、多处部署;版本可追溯;回滚要能一条命令完成。

可观测性三支柱

支柱回答什么问题常用工具
日志发生了什么Seq、ELK、Loki
指标趋势好不好Prometheus、Grafana
追踪慢在哪一段Jaeger、Tempo

三者要用同一个 TraceId 串起来,否则排查时还是靠猜。

补充:发布前检查清单

  • 配置与密钥不包含在镜像里
  • 健康检查区分存活/就绪
  • 有超时、重试、熔断
  • 日志级别与脱敏已确认
  • 数据库迁移可回滚
  • 回滚镜像已准备好
  • 关键指标与告警已配置

缺任何一项,发布都会变成运气。

补充:容器安全

  • 使用非 root 用户运行。
  • 只读根文件系统,临时目录单独挂载。
  • 最小基础镜像,减少攻击面。
  • 镜像扫描与依赖审计进 CI。
  • 密钥通过环境变量/密钥服务注入,不打进镜像。

发布验收

发布后 10 分钟内看四项:健康检查全部就绪、错误率未上升、P99 未劣化、日志无异常峰值。任何一项异常,立即回滚。

三个不要

不要用 root 跑容器;不要把密钥打进镜像;不要让 liveness 依赖下游。

一个判断

发布前问三个问题:出问题能回滚吗?密钥在哪里?异常时先看哪个指标?三个都答不上来,就不要发。

验收

发布后 10 分钟看四项:健康检查就绪、错误率、P99、日志异常峰值;任何一项不对,立即回滚。

发布前问回滚,发布后看指标;没有回滚方案的发布,不叫发布,叫赌博。密钥不进镜像;健康检查分层;先演练。发布前先演练回滚,回滚先行,别赌。先演练,再发布。先演练。

配套题目

★ 越多越该先背;点题目可看完整答案与深入版。

提示:读完可以顺手做几道对应题目,检验是否真的理解