后端架构实战(八):落地——用 ASP.NET Core 搭一个微服务骨架

写在前面

前七篇全是道理:权衡、单体、拆分、分布式税、一致性、通信、可观测性。这一篇把理论落成代码——用 ASP.NET Core 搭一个最小可用的微服务骨架。

它不会是一个完整生产系统,但会覆盖微服务的"承重墙":项目结构、依赖注入、API 网关、健康检查、弹性(Polly)、可观测性(OpenTelemetry)。每一处都对应前面某一篇讲过的道理。

前置:本篇假设你读过我的《ASP.NET Core 学习笔记》系列(中间件管道、DI、认证授权)。基于 .NET 8 LTS


一、整体结构

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
一个最小微服务系统(4 个工程):

  Gateway/          API 网关(YARP,统一入口、路由、鉴权)
  OrderService/     订单服务(业务 A)
  InventoryService/ 库存服务(业务 A 的依赖)
  Shared/           共享内核(契约、公共类型)

  对应前文:
    拆分(第三篇)→ 按领域切 Order / Inventory
    通信(第六篇)→ 网关 + 服务间 HTTP
    分布式税(第四篇)→ 健康检查、Polly 弹性
    可观测性(第七篇)→ OpenTelemetry 串联
1
2
3
4
5
6
7
8
工程依赖方向(单向,严禁循环):
  Gateway ──→ Shared
  OrderService ──→ Shared
  InventoryService ──→ Shared
  OrderService ──调用──→ InventoryService(运行时 HTTP,非编译依赖)

  关键:服务间运行时依赖走网络,编译期互不引用
       → 这样才能独立编译、独立部署。

二、每个服务的内部分层

单个服务内部,按职责分层(不是按"被拆成服务"的技术层——区别于第三篇的反模式):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
OrderService/
├── OrderService.Api/          表现层(Controller / 最小 API)
│   └── 引用 Application、Domain
├── OrderService.Application/  应用层(用例、编排、接口定义)
│   └── 引用 Domain
├── OrderService.Domain/       领域层(实体、领域服务、规则)
│   └── 不依赖任何人(依赖倒置的核心)
└── OrderService.Infrastructure/ 基础设施(DB、外部调用、实现)
    └── 引用 Application(实现接口)

  依赖方向永远向内,Domain 在最中心、不依赖任何层。
  这是"整洁架构/洋葱架构"的思路,保证领域逻辑纯净。

三、依赖注入:组装一切

ASP.NET Core 的 DI 是一等公民(见我的 ASP.NET Core 笔记一)。微服务里,所有跨层依赖都通过 DI 注入,这样才好测试、好替换。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// OrderService.Application/Dependencies.cs
public static class Dependencies
{
    public static IServiceCollection AddOrderServices(this IServiceCollection services)
    {
        // 应用层服务
        services.AddScoped<IOrderService, OrderService>();
        // 端口适配器:接口在 Application,实现在 Infrastructure
        services.AddScoped<IInventoryClient, InventoryClient>();
        services.AddScoped<IOrderRepository, OrderRepository>();
        return services;
    }
}

// OrderService.Api/Program.cs
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddOrderServices();          // 业务服务
builder.Services.AddControllers();
builder.Services.AddHealthChecks();           // 健康检查(第五节)
builder.Services.AddHttpClientPolicies();     // Polly 弹性(第六节)

var app = builder.Build();
app.MapControllers();
app.MapHealthChecks("/health");               // 暴露健康检查端点
app.Run();

单元测试里可以替换 IInventoryClient 为 mock,这就是第四篇《.NET 单元测试:依赖注入与可测试性》的价值。


四、API 网关:YARP 统一入口

对外不直连各服务,用网关收口(第六篇)。.NET 生态用 YARP(Yet Another Reverse Proxy,微软出品,比 Ocelot 更现代)。

1
2
3
4
5
6
7
8
// Gateway/Program.cs
var builder = WebApplication.CreateBuilder();
builder.Services.AddReverseProxy()
    .LoadFromConfig(builder.Configuration.GetSection("ReverseProxy"));  // 路由放配置

var app = builder.Build();
app.MapReverseProxy();   // 网关入口
app.Run();
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// Gateway/appsettings.json —— 路由配置
{
  "ReverseProxy": {
    "Routes": {
      "order-route": {
        "ClusterId": "order-cluster",
        "Match": { "Path": "/api/orders/{**catch-all}" }
      },
      "inventory-route": {
        "ClusterId": "inventory-cluster",
        "Match": { "Path": "/api/inventory/{**catch-all}" }
      }
    },
    "Clusters": {
      "order-cluster": {
        "Destinations": {
          "d1": { "Address": "http://order-service:8080/" }
        }
      },
      "inventory-cluster": {
        "Destinations": {
          "d1": { "Address": "http://inventory-service:8080/" }
        }
      }
    }
  }
}
1
2
3
4
5
6
网关集中处理(不在每个服务里重复):
  - 路由(/api/orders → 订单服务)
  - 鉴权(统一验 token)
  - 限流、熔断
  - 协议转换
  生产环境再配多实例 + 负载均衡,保证网关本身高可用。

五、健康检查:编排的基础

K8s 和负载均衡器靠健康检查决定"流量往哪打"(第四篇分布式税)。ASP.NET Core 内置 HealthCheck。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// 自定义健康检查:探测下游依赖
public class InventoryHealthCheck : IHealthCheck
{
    private readonly IInventoryClient _client;
    public InventoryHealthCheck(IInventoryClient client) => _client = client;

    public async Task<HealthCheckResult> CheckHealthAsync(
        HealthCheckContext context, CancellationToken ct = default)
    {
        try
        {
            var ok = await _client.PingAsync(ct);
            return ok ? HealthCheckResult.Healthy("inventory reachable")
                      : HealthCheckResult.Degraded("inventory slow");
        }
        catch
        {
            return HealthCheckResult.Unhealthy("inventory down");
        }
    }
}

// 注册
builder.Services.AddHealthChecks()
    .AddCheck<InventoryHealthCheck>("inventory", failureStatus: HealthStatus.Unhealthy);

// 分别暴露存活探针 / 就绪探针(K8s 标准用法)
app.MapHealthChecks("/health/live",  new() { Predicate = _ => false }); // 进程活着
app.MapHealthChecks("/health/ready", new() { Predicate = r => r.Tags.Contains("ready") });
1
2
3
4
5
K8s 里:
  livenessProbe  → /health/live  (进程活着吗)
  readinessProbe → /health/ready (依赖就绪、能接流量吗)
  就绪探针失败 → K8s 把该实例摘出流量,但不重启;
  存活探针失败 → K8s 重启容器。

六、弹性:Polly 应对分布式税

跨网络调用必加弹性(第四篇的超时/重试/熔断)。.NET 用 Polly,配合 HttpClientFactory

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// 订单服务调用库存服务,加满弹性策略
builder.Services.AddHttpClient<IInventoryClient, InventoryClient>(client =>
{
    client.BaseAddress = new("http://inventory-service:8080/");
    client.Timeout = TimeSpan.FromSeconds(3);          // ① 超时
})
.AddTransientHttpErrorPolicy(p =>                      // ② 重试(仅对临时错误)
    p.WaitAndRetryAsync(3, i => TimeSpan.FromMilliseconds(200 * Math.Pow(2, i))))
.AddTransientHttpErrorPolicy(p =>                      // ③ 熔断
    p.CircuitBreakerAsync(5, TimeSpan.FromSeconds(30)));
// AddTransientHttpErrorPolicy 默认对 5xx、408、网络错误生效
1
2
3
4
5
这几行代码对应第四篇的整笔账:
  超时  → 别傻等慢服务
  重试  → 应对网络抖动(配合幂等,库存扣减接口必须幂等)
  熔断  → 库存服务挂了,订单服务直接快速失败,不耗尽自己的线程池
        → 切断雪崩的放大效应

七、可观测性:OpenTelemetry 串联

第七篇的链路追踪,几行代码接入。

1
2
3
4
5
6
7
8
9
// 各服务统一接入 OpenTelemetry
builder.Services.AddOpenTelemetry()
    .WithTracing(tp => tp
        .AddAspNetCoreInstrumentation()      // 自动追踪 HTTP 入站
        .AddHttpClientInstrumentation()      // 自动追踪 HttpClient 出站
        .AddSqlClientInstrumentation()       // 自动追踪 DB 调用
        .AddOtlpExporter());                 // 导出到 OTel Collector

builder.Logging.AddOpenTelemetry();          // 日志自动带 traceId
1
2
3
4
5
效果:一次"下单"请求
  网关(span) → 订单服务(span) → 库存服务(span) → DB(span)
  自动连成一条完整 trace,traceId 自动透传、日志自动关联。
  Jaeger 里一眼看到哪一段慢。
  → 这就是第七篇说的"微服务神经系统",接入成本极低。

八、配置与外部化

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
每个服务的配置分环境(开发/测试/生产)外置:
  appsettings.json         —— 默认值
  appsettings.Production   —— 生产覆盖
  环境变量 / K8s ConfigMap / Secret —— 敏感和环境相关

  builder.Configuration
    .AddJsonFile("appsettings.json")
    .AddJsonFile($"appsettings.{env}.json", optional: true)
    .AddEnvironmentVariables();   // 环境变量优先级最高,适合容器

  分布式税里的"配置中心"(Nacos/Apollo)在规模上来后再引入,
  小系统先用环境变量 + ConfigMap 足够。

九、把前七篇串起来

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
这个骨架里,每一处都对应前面某一篇:

  工程结构、依赖方向     ← 第三篇:按领域切,单向依赖
  DI 注入一切            ← 可测试性(ASP.NET Core 笔记四)
  YARP 网关              ← 第六篇:统一入口、路由、鉴权
  健康检查               ← 第四篇:编排基础(K8s 探针)
  Polly 超时/重试/熔断    ← 第四篇:弹性,防雪崩
  OpenTelemetry          ← 第七篇:可观测性神经系统
  服务间 HTTP + 幂等     ← 第五、六篇:一致性 + 通信
  配置外置               ← 第四篇:分布式税中的配置

  这就是一个"承重墙齐全"的微服务起点。
  业务往上长,基础设施已就位。

十、回到第一篇:要不要这么做

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
最后,回到系列第一篇的"权衡":

  这个骨架是"当你要做微服务时"的标准起手式。
  但你要不要做微服务,回到那几个问题:

    - 团队是不是大到需要拆分协作?
    - 是不是有独立扩容 / 独立发布 / 故障隔离的真实需求?
    - 有没有 K8s + CI/CD + 可观测性的运维能力?

  如果没有 → 第二篇的"模块化单体"才是你的答案。
            把这套骨架的"DI + 分层 + 健康检查 + 可观测性"
            用在单体内部,一样成立,还省了网络和分布式税。

  记住:技术选型服务于问题和约束,不是反过来。

十一、系列小结

八篇下来,一条主线:

  • (一)架构是权衡:没有银弹,一切看约束
  • (二)单体被低估:模块化单体是大多数团队的起点
  • (三)拆分看领域:按业务切,不按技术层切
  • (四)分布式要收税:网络、一致性、可观测、运维,样样要自建
  • (五)一致性靠模式:核心收敛强一致,周边用最终一致 + Saga + Outbox
  • (六)通信看同步异步:REST/gRPC 与消息队列各擅其场
  • (七)可观测性是神经:日志 + 指标 + 链路,traceId 串联
  • (八)落地 ASP.NET Core:把理论变成可跑的代码

架构没有终点。系统会演进,约束会变化,今天的最优解是明天的技术债。保持权衡的思维、演进的视角、记录决策的习惯——这才是这套系列想留给你的,比任何具体技术都重要的东西。