写在前面
前七篇全是道理:权衡、单体、拆分、分布式税、一致性、通信、可观测性。这一篇把理论落成代码——用 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:把理论变成可跑的代码
架构没有终点。系统会演进,约束会变化,今天的最优解是明天的技术债。保持权衡的思维、演进的视角、记录决策的习惯——这才是这套系列想留给你的,比任何具体技术都重要的东西。