CodeRoadMap
首页路线题库资源库文章社区
CodeRoadMap

程序员的学习成长路线图。内容免费开放,登录后可同步路线图与课时进度。

由 程序员柏风 独立维护 · 关于作者

学习

  • 路线图
  • 课程
  • 面试题
  • 文章
  • 我

更多

  • 资源
  • 社区
  • 关于作者
  • 内容说明

© 2026 CodeRoadMap

津ICP备2026012044号-1·津公网安备12011002026155号·coderoadmap@126.com
ASP.NET Core 实战入门/课时/日志、任务与工具链
RawBlame
课时目录12 节

ASP.NET Core 实战入门

  1. 01ASP.NET Core 基础
  2. 02C# 与 .NET 基础
  3. 03Entity Framework Core
  4. 04依赖注入
  5. 05REST / GraphQL / gRPC
  6. 06数据存储与缓存
  7. 07SignalR 实时通信
  8. 08测试与 E2E
  9. 09消息队列
  10. 10微服务与网关
  11. 11日志、任务与工具链
  12. 12综合练习

日志、任务与工具链

Serilog、Hangfire、AutoMapper、Blazor/MAUI 概览。

35 分钟

打包部署与可观测(Docker + CI/CD)

这一课你将能做到

  1. 写出多阶段 Dockerfile,把 API 与判题 Worker 打成小体积镜像,并写一键启动全平台的 docker-compose.yml
  2. 搭 GitHub Actions 流水线:构建 → 测试 → 推镜像 → 部署
  3. 配好结构化日志(ILogger/Serilog)、健康检查与 Prometheus 指标,对照 actuator/Logback 完成可观测三件套映射

为什么要学

代码写完只算 70 分,剩下 30 分在「能可靠地跑在生产并被人看懂」。Spring Boot 的肌肉记忆是 fat jar + actuator + Logback + CI/CD;.NET 侧对应 dotnet publish + 健康检查中间件 + ILogger/Serilog + 同一套 CI/CD。差异最大的是镜像构建,原理相同但细节值得逐行看。

先建立图像

部署形态对照:

环节Spring BootASP.NET Core
产物Boot fat jar(内嵌 Tomcat)dotnet publish 输出 + Kestrel
运行时宿主装 JDK 或 jlink 裁剪基础镜像 aspnet:8.0 或 self-contained
健康端点/actuator/health/healthz(自己 Map)
日志门面SLF4J + LogbackILogger + Provider(Serilog/NLog)
指标Micrometer + PrometheusOpenTelemetry / prometheus-net
CIGitHub Actions / Jenkins同(构建命令不同)

跟着做

步骤 1:多阶段 Dockerfile(API 与 Worker 通用模板)

# 阶段 1:构建(SDK 镜像大,只用于编译)
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /src
COPY *.sln .
COPY Oj.Api/*.csproj Oj.Api/
COPY Oj.Worker/*.csproj Oj.Worker/
RUN dotnet restore                      # 先只拷 csproj:依赖层缓存,源码改动不触发重装包
COPY . .
RUN dotnet publish Oj.Api -c Release -o /app/publish /p:UseAppHost=false

# 阶段 2:运行(只带运行时,体积 ~100MB 级)
FROM mcr.microsoft.com/dotnet/aspnet:8.0
WORKDIR /app
COPY --from=build /app/publish .
ENV ASPNETCORE_URLS=http://+:8080
EXPOSE 8080
USER app                                # 非 root 运行,安全基线
ENTRYPOINT ["dotnet", "Oj.Api.dll"]

判题 Worker 同理,入口换 Oj.Worker.dll;需要编译器/沙箱(g++、isolate)就在运行阶段 apt-get install。

步骤 2:docker-compose 一键起全平台

services:
  api:
    build: .
    environment:
      - ASPNETCORE_ENVIRONMENT=Production
      - ConnectionStrings__Default=Host=db;Database=oj;Username=oj;Password=oj123   # 双下划线=层级配置
      - ConnectionStrings__Redis=redis:6379
      - RabbitMq__Host=rabbitmq
    depends_on:
      db: { condition: service_healthy }
    ports: ["8080:8080"]
  worker:
    build:
      context: .
      dockerfile: Dockerfile.worker
    environment:
      - ConnectionStrings__Default=Host=db;Database=oj;Username=oj;Password=oj123
      - RabbitMq__Host=rabbitmq
    deploy:
      replicas: 2                        # 判题能力横向扩容就改这个数字
  db:
    image: postgres:16
    environment:
      - POSTGRES_DB=oj
      - POSTGRES_USER=oj
      - POSTGRES_PASSWORD=oj123
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U oj"]
      interval: 5s
  redis:
    image: redis:7
  rabbitmq:
    image: rabbitmq:3-management
    ports: ["15672:15672"]               # 管理界面

环境变量 ConnectionStrings__Default 覆盖 appsettings.json 同名层级——容器化配置注入的标准姿势,等价 Spring 的 SPRING_DATASOURCE_URL 宽松绑定。

步骤 3:GitHub Actions 流水线

name: ci
on:
  push: { branches: [main] }
jobs:
  build-test-push:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-dotnet@v4
        with: { dotnet-version: "8.0.x" }
      - run: dotnet restore
      - run: dotnet build --no-restore -c Release
      - run: dotnet test --no-build --collect:"XPlat Code Coverage"
      - name: Build & push image
        run: |
          docker build -t ${{ secrets.REGISTRY }}/oj-api:${{ github.sha }} .
          docker push ${{ secrets.REGISTRY }}/oj-api:${{ github.sha }}
      - name: Deploy
        run: |
          kubectl set image deploy/oj-api oj-api=${{ secrets.REGISTRY }}/oj-api:${{ github.sha }}
          kubectl rollout status deploy/oj-api --timeout=120s

对比 Spring 项目:只有构建命令从 ./mvnw package 换成 dotnet publish,流水线骨架(构建→测试→镜像→部署→健康确认)一致。

步骤 4:结构化日志(ILogger 惯例 + Serilog 落 JSON)

builder.Logging.ClearProviders();
builder.Services.AddSerilog((ctx, lc) => lc
    .ReadFrom.Configuration(ctx.Configuration)
    .Enrich.FromLogContext()
    .WriteTo.Console(new JsonFormatter()));      // 生产 JSON 行 → Loki/ELK 直接吃
// 用模板占位而非字符串插值:结构化字段,可在日志平台按 submissionId 检索
logger.LogInformation("提交 {SubmissionId} 判题完成,结果 {Verdict},耗时 {ElapsedMs}ms",
    submissionId, verdict, sw.ElapsedMilliseconds);

这与 SLF4J 的 log.info("... {}", arg) 惯例同源:占位符保住结构,插值($"...")会破坏它。MDC 对应物是 LogContext.PushProperty("UserId", id)。

步骤 5:指标与健康检查落地

dotnet add package OpenTelemetry.Extensions.Hosting
dotnet add package OpenTelemetry.Exporter.Prometheus.AspNetCore
dotnet add package AspNetCore.HealthChecks.NpgSql
builder.Services.AddOpenTelemetry()
    .WithMetrics(m => m.AddAspNetCoreInstrumentation()      // 内置 HTTP QPS/耗时
                      .AddRuntimeInstrumentation()          // GC/线程池(对应 JVM metrics)
                      .AddPrometheusExporter());

app.MapPrometheusScrapingEndpoint("/metrics");
app.MapHealthChecks("/healthz");

K8s 部署片段(探针 + HPA):

livenessProbe:  { httpGet: { path: /healthz, port: 8080 }, initialDelaySeconds: 10 }
readinessProbe: { httpGet: { path: /healthz, port: 8080 }, periodSeconds: 5 }
resources: { requests: { cpu: 250m, memory: 256Mi }, limits: { cpu: "1", memory: 512Mi } }

步骤 6:发布策略(滚动更新验证)

dotnet publish -c Release -r linux-x64 --self-contained false   # 依赖框架发布
kubectl rollout restart deploy/oj-api                           # 触发滚动替换
kubectl rollout undo deploy/oj-api                              # 一条命令回滚

就绪探针未就绪的 Pod 不会接流量,配合 EF 迁移在启动任务(Job)里执行,实现零停机发布。

讲清楚要点

  1. 多阶段构建的经济学:SDK 镜像 ~700MB 只存在于构建期;运行镜像只含运行时与产物。依赖层缓存(先 COPY csproj 再 restore)让改代码的镜像构建从分钟级降到秒级。
  2. 配置分层优先级:命令行 > 环境变量 > appsettings.{Env}.json > appsettings.json。容器里靠环境变量改行为;敏感信息进 K8s Secret。
  3. 日志三原则:结构化(模板占位)、带上下文(traceId/userId 属性)、分级克制(Information 业务事件、Warning 可自愈、Error 需人工)。日志是给机器检索的,不是给人翻的。
  4. 可观测三支柱在 .NET 的落位:日志(Serilog → Loki/ELK)、指标(OpenTelemetry → Prometheus)、追踪(OTel trace → Jaeger),与 Java 侧 Micrometer + SLF4J 逐项对应。

你可能会懵的地方

  • self-contained 和 framework-dependent 怎么选? 前者把运行时打进产物(80MB+,宿主零依赖);后者小但要镜像版本匹配。容器化部署一律 framework-dependent + 官方 aspnet:8.0 镜像。

  • 镜像为什么不用 alpine? aspnet:8.0-alpine 存在,但 glibc/musl 差异会让依赖原生库的包出问题;判题 Worker 还要装编译器与沙箱工具,用 Debian 系(默认 aspnet:8.0)更省心。

  • EF 迁移什么时候执行?启动时自动跑吗? 生产惯例:迁移作为独立 Job 在部署管线执行,应用启动只校验不改动;启动时自动 Migrate() 多副本会竞争迁移锁,仅适合单人小项目。

  • GC/线程池要不要调参? 容器默认值就不错;高吞吐场景用 DOTNET_GCHeapHardLimit 限制堆避免 OOMKilled。对应 JVM 的 -Xmx 调优,但默认更省心。

  • 证书/TLS 在容器里怎么处理? Kestrel 容器内通常只听 HTTP,TLS 交给 ingress/Nginx 终结。需要端到端加密时挂载证书并配置 Kestrel__Certificates__* 环境变量。

错法对照

错法问题正解
单阶段 Dockerfile(SDK 当运行镜像)镜像 700MB+,攻击面大多阶段,运行阶段用 aspnet:8.0
镜像里打 appsettings.Production.json 密码密钥进仓库/进镜像层环境变量/K8s Secret 注入
容器里跑 root安全基线违规Dockerfile 末尾 USER 非 root 账户
日志用 $"提交{ id}完成" 插值结构丢失,无法按字段检索模板占位 logger.LogInformation("提交 {Id}...", id)
就绪探针指向依赖重启中的外部服务依赖抖动导致全量重启区分 liveness(进程)/readiness(依赖),依赖检查独立降级

卡点急救

  1. 容器时区不对(日志时间差 8 小时):设置 TZ=Asia/Shanghai 并在镜像里 apt-get install tzdata;或统一 UTC。
  2. dotnet restore 每次都全量拉包:Dockerfile 未按「先 csproj 后源码」分层,或 .dockerignore 缺失把 bin/obj 拷进去。
  3. OOMKilled(exit 137):内存限额低于实际需求。设 DOTNET_GCHeapHardLimit(十六进制字节数)略低于 limit,或调高 limit。

面试常问

  • 多阶段 Dockerfile 解决什么问题?.NET 侧关键细节? 分离构建环境与运行环境:SDK 层编译,运行层只拷贝产物,体积与攻击面骤降。关键细节:先 COPY csproj 再 restore 利用层缓存;/p:UseAppHost=false 免去生成可执行文件;USER 非 root;ASPNETCORE_URLS 统一监听端口。

  • 零停机发布的完整链路? 构建镜像 → 迁移 Job 先行(向后兼容)→ 滚动替换 Pod(readiness 拦截未就绪实例)→ rollout status 验证 → 异常 rollout undo 回滚。前提:接口向后兼容,删字段分两次发布——与 Spring 滚动发布约束一致。

  • 生产日志怎么设计才能高效排障? 结构化 JSON + traceId 贯穿全链路 + 统一采集(Loki/ELK);代码纪律是模板占位、分级克制、敏感信息脱敏。排障路径:traceId 串链路 → 指标定时段 → 堆栈定代码。

自测三问

  1. 为什么 Dockerfile 先 COPY csproj 再 COPY 源码?
  2. liveness 和 readiness 探针失败分别触发什么?
  3. ConnectionStrings__Default 里的双下划线是什么机制?

小结一句话

部署 = 多阶段镜像 + 环境变量配置注入 + CI/CD 一条龙;可观测 = ILogger 结构化日志 + OpenTelemetry 指标 + 健康探针——与 Spring Boot 的 fat jar/actuator/Micrometer 逐项同构,换的是命令与包名。

延伸

  • 官方文档:构建 .NET 的 Docker 镜像
  • 下一课:综合实战——把 12 课全部串成一个可运行的迷你在线判题平台。
加载本节练习…
← 上一课微服务与网关下一课 →综合练习