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综合练习

微服务与网关

Ocelot、YARP、Dapr、Orleans、.NET Aspire。

35 分钟

微服务与跨服务通信(对照 Spring Cloud)

这一课你将能做到

  1. 把判题平台按「题库 / 判题 / 用户」拆出服务边界,说清拆与不拆的判断标准
  2. 用 gRPC 定义判题服务内部接口,用类型化 HttpClient + 标准弹性策略调用下游
  3. 用 YARP 搭 API 网关,配健康检查,对照 Spring Cloud Gateway/Nacos/OpenFeign 完成映射

为什么要学

单体够用前不要拆微服务。「拆」的时机一到,需要回答四个问题:服务间怎么调用(同步 REST/gRPC)、怎么扛失败(超时/重试/熔断)、流量怎么进来(网关)、实例怎么找到彼此(服务发现)。Spring Cloud 全家桶(Gateway/OpenFeign/Resilience4j/Nacos)在 .NET 侧对应:YARP/HttpClientFactory+Resilience/gRPC/K8s DNS——理念相同,组件更「原生」。

人话:微服务的本质不是技术栈,是「团队与部署的边界」。判题要独立伸缩(CPU 密集且要沙箱安全)、题库读多写少、用户服务全局共享——这三块天然是三个部署单元。

先建立图像

                    ┌───────────────┐
 浏览器 ──HTTPS──→  │  YARP 网关     │   /api/problems/* → 题库服务
                    │ (路由/认证/限流) │   /api/submissions/* → 提交服务
                    └───────┬───────┘
                            │ gRPC(内部, 低延迟强类型)
        ┌───────────────────┼────────────────────┐
   题库服务              提交服务               判题服务(扩缩容)
   (EF+MySQL)          (API+队列发布)         (沙箱, BackgroundService)
Spring CloudASP.NET Core 生态职责
Spring Cloud GatewayYARP(基于 .NET 的反向代理)网关/路由/限流
OpenFeign类型化 HttpClient(IHttpClientFactory)声明式客户端
Resilience4jMicrosoft.Extensions.Http.Resilience / Polly重试/熔断/超时
Eureka / NacosK8s DNS / Consul服务发现
Spring Boot Actuator健康检查中间件 + AspNetCore.HealthChecks健康探测

跟着做

步骤 1:gRPC 定义判题接口(内部通信首选)

judge.proto(共享给服务端与客户端):

syntax = "proto3";
option csharp_namespace = "Oj.Contracts";

service Judge {
  rpc Submit (JudgeRequest) returns (JudgeReply);
}

message JudgeRequest {
  int64 submission_id = 1;
  string language = 2;
  string code = 3;
  int32 time_limit_ms = 4;
}

message JudgeReply {
  string status = 1;
  int32 time_ms = 2;
  int32 memory_kb = 3;
}

服务端实现:

dotnet add package Grpc.AspNetCore
public class JudgeGrpcService(OjDbContext db) : Judge.JudgeBase
{
    public override async Task<JudgeReply> Submit(JudgeRequest req, ServerCallContext ctx)
    {
        var result = await SandboxRunner.RunAsync(req.Code, req.Language, req.TimeLimitMs, ctx.CancellationToken);
        return new JudgeReply { Status = result.Status, TimeMs = result.TimeMs, MemoryKb = result.MemoryKb };
    }
}

// Program.cs
builder.Services.AddGrpc();
app.MapGrpcService<JudgeGrpcService>();

客户端(提交服务):

dotnet add package Grpc.Net.ClientFactory
builder.Services
    .AddGrpcClient<Judge.JudgeClient>(o => o.Address = new Uri("http://judge-service:5100"))
    .AddStandardResilienceHandler();   // 挂弹性策略

var reply = await judgeClient.SubmitAsync(new JudgeRequest
{
    SubmissionId = id, Language = "cpp", Code = code, TimeLimitMs = 2000
});

步骤 2:REST 调用(对外/低频场景,对应 OpenFeign)

public record UserInfoDto(Guid Id, string DisplayName);

public class UserApiClient(HttpClient http)   // 类型化客户端 ≙ Feign 接口
{
    public async Task<UserInfoDto?> GetAsync(Guid id, CancellationToken ct) =>
        await http.GetFromJsonAsync<UserInfoDto>($"api/users/{id}", ct);
}

builder.Services.AddHttpClient<UserApiClient>(c => c.BaseAddress = new Uri("http://user-service:5200"))
    .AddStandardResilienceHandler();

IHttpClientFactory 解决两件事与 Java 相同:避免 socket 耗尽、集中配置弹性策略。

步骤 3:弹性策略(.NET 8 内置 resilience ≙ Resilience4j)

builder.Services.AddHttpClient<UserApiClient>(c => c.BaseAddress = new Uri("http://user-service:5200"))
    .AddResilienceHandler("user-pipeline", b =>
    {
        b.AddTimeout(TimeSpan.FromSeconds(3));                    // 总超时
        b.AddRetry(new HttpRetryStrategyOptions
        {
            MaxRetryAttempts = 3,
            BackoffType = DelayBackoffType.Exponential,
            UseJitter = true,                                     // 抖动防重试风暴
        });
        b.AddCircuitBreaker(new HttpCircuitBreakerStrategyOptions
        {
            FailureRatio = 0.5,                                   // 50% 失败率熔断
            MinimumThroughput = 20,
            BreakDuration = TimeSpan.FromSeconds(30),
        });
    });

.AddStandardResilienceHandler() 是这四件(超时/重试/熔断/舱壁)的官方推荐预设,非定制场景直接用。

步骤 4:YARP 网关

dotnet add package Yarp.ReverseProxy

appsettings.json:

{
  "ReverseProxy": {
    "Routes": {
      "problems": {
        "ClusterId": "problem-cluster",
        "Match": { "Path": "/api/problems/{**catch-all}" }
      },
      "submissions": {
        "ClusterId": "submission-cluster",
        "Match": { "Path": "/api/submissions/{**catch-all}" },
        "AuthorizationPolicy": "Authenticated"
      }
    },
    "Clusters": {
      "problem-cluster": {
        "Destinations": {
          "d1": { "Address": "http://problem-service:5001/" }
        },
        "HealthCheck": { "Active": { "Enabled": true, "Path": "/healthz" } }
      }
    }
  }
}
builder.Services.AddReverseProxy().LoadFromConfig();
app.UseAuthentication();
app.UseAuthorization();
app.MapReverseProxy();     // 网关还兼任 JWT 校验与全局限流(AddRateLimiter)

负载均衡、主动健康检查、灰度分流都是配置项(Route + Cluster + Transforms)。

步骤 5:健康检查(K8s 探针的数据源)

dotnet add package AspNetCore.HealthChecks.NpgSql
dotnet add package AspNetCore.HealthChecks.Redis
builder.Services.AddHealthChecks()
    .AddNpgSql(connString, name: "db")
    .AddRedis(redisConn, name: "redis");

app.MapHealthChecks("/healthz/live");                       // 存活:进程在就行
app.MapHealthChecks("/healthz/ready", new HealthCheckOptions
{
    Predicate = r => r.Tags.Contains("ready")               // 就绪:依赖可达
});

K8s 里 livenessProbe → /healthz/live、readinessProbe → /healthz/ready,与 actuator 的 /actuator/health/liveness|readiness 同一设计。

步骤 6:服务发现(K8s 环境最简)

// K8s 内:服务名即 DNS,无需注册中心
// http://judge-service.default.svc.cluster.local:5100
// 本地开发:appsettings 按环境覆盖 BaseAddress

讲清楚要点

  1. 拆分边界按业务能力与伸缩特征,不按技术分层:题库(读多缓存重)/提交(写多事务重)/判题(CPU 密集 + 安全隔离)各自可独立部署扩缩。早期先「服务内模块化」,流量与团队到位再物理拆——同 Spring modular monolith 主张。
  2. 内部通信 gRPC、外部 REST:gRPC 基于 HTTP/2 + Protobuf,低延迟、契约强类型,适合服务间高频调用;对浏览器/第三方仍暴露 REST + JSON。Java 侧 grpc-spring-boot-starter 同角色。
  3. 每一次跨网络调用都要假设会失败:超时(比下游更早放弃)→ 重试(只重试幂等请求,带抖动)→ 熔断(阻止雪崩式重试)→ 降级(兜底响应)。顺序不能反:没有超时的重试是灾难放大器。
  4. 数据一致性放弃强一致:跨服务不搞分布式事务,用 Saga(判题失败发补偿事件回滚提交状态)或事件驱动最终一致。与 Spring 社区 Saga/Outbox 实践同源。

你可能会懵的地方

  • 服务发现用不用 Nacos/Eureka? K8s 部署不需要:Service 名就是稳定 DNS,探针做摘流。虚拟机/混合部署才需要 Consul。Spring 的 Nacos 兼管配置中心,.NET 侧用环境变量 + 挂载文件。

  • gRPC 和消息队列怎么分工? 同步问答(判题要立刻拿返回值)走 gRPC;异步事实广播(通知多订阅方)走 MQ。把需要「等回复」的流程塞进消息队列是常见设计事故。

  • 每个服务要独立的数据库吗? 是。共享库会让服务因表结构耦合,发布互相牵制。跨服务取数走 API/gRPC 或订阅事件落本地副本——Java 微服务训诫相同。

  • 链路追踪怎么做? OpenTelemetry + OTLP 导出器一行接入,trace id 自动跨 gRPC/HTTP/MQ 传播,后端接 Jaeger/Tempo——对应 Spring 的 Micrometer Tracing/SkyWalking。

错法对照

错法问题正解
重试不带超时/不带幂等判断慢下游 + 重复副作用,雪上加霜先超时、再重试(幂等接口)、带 jitter
网关里写业务逻辑网关变成单体,发布风险扩散网关只做路由与横切,聚合进 BFF/服务
两个服务直连对方数据库隐式耦合,schema 一改全线崩API/事件交互,数据自治
内部调用不做熔断一个服务抖动拖垮整条调用链StandardResilienceHandler 全局默认挂载

卡点急救

  1. gRPC 客户端报 HTTP/2 不支持:gRPC 必须 HTTP/2。K8s 内部直连没问题;经 Nginx 要开 grpc_pass 与 h2c。
  2. proto 改了客户端字段对不上:Protobuf 兼容规则——字段号不可复用/修改,只新增;两端用同一 Contracts 包发版,弃用字段标 reserved。
  3. YARP 路由不生效:检查 MapReverseProxy() 是否在 UseAuthentication/UseAuthorization 之后;路径转换 {**catch-all} 写法。

面试常问

  • 单体什么时候该拆微服务?判题平台怎么拆? 信号:模块部署节奏冲突(判题要频繁发布/扩容)、资源特征差异、团队并行度受限。判题平台拆题库、提交、判题三服务 + 网关。反信号:只有性能诉求没组织诉求时,单体 + 模块化更划算。

  • gRPC 相比 REST/JSON 的取舍? 得:Protobuf 小而快、HTTP/2 多路复用、契约生成强类型双端代码、双向流。失:浏览器直连受限(需 gRPC-Web)、调试成本高。实践:内部服务间 gRPC,对外 API 仍 REST。

  • 跨服务的数据一致性怎么设计? 放弃 2PC,走 Saga/最终一致:判题失败发补偿事件回滚提交状态;关键链路用 Outbox 保证「本地事务 + 消息投递」原子;对账兜底。每步补偿操作的设计是 Saga 落地核心。

自测三问

  1. 网关与服务的职责边界怎么划?
  2. 标准弹性管线的四个策略和挂载顺序?
  3. 为什么「重试只用于幂等操作」?给出两个非幂等例子。

小结一句话

微服务 = 边界判断(该不该拆)+ 通信选型(gRPC 内 / REST 外 / MQ 异步)+ 弹性四件套(超时/重试/熔断/降级)+ 网关与发现;.NET 侧组件名与 Spring Cloud 不同,决策框架逐字相同。

延伸

  • 官方文档:ASP.NET Core 上的 gRPC
  • 下一课:打包部署与可观测——Docker 多阶段构建、CI/CD、日志与指标,对照 Spring DevOps 实践。
加载本节练习…
← 上一课消息队列下一课 →日志、任务与工具链