×

.NET 反向代理:用 YARP 构建网关,代码即配置

独孤求败 独孤求败 发表于2026-06-23 09:56:24 浏览24 评论0

抢沙发发表评论

网关反向代理,在 .NET 生态里不一定要用 Nginx 或第三方中间件。微软开源的 YARP 让你直接用 ASP.NET Core 代码搭一个,路由、负载均衡、动态配置全在 C# 层面解决,和现有认证中间件天然打通。这篇文章不讲空话,直接说怎么用、为什么用、什么场景别用。

YARP 是什么

YARP(Yet Another Reverse Proxy)是微软开源的一款 .NET 反向代理库。它不是一个开箱即用的独立网关服务,而是一个 NuGet 包——你把它装进 ASP.NET Core 项目里,你的项目就成了反向代理。

官网:


https://learn.microsoft.com/zh-cn/aspnet/core/fundamentals/servers/yarp/getting-started?view=aspnetcore-10.0
图片

 GitHub:


https://github.com/microsoft/reverse-proxy
图片

YARP 解决的核心问题是:在 .NET 体系内用代码而非配置文件来构建反向代理。传统上做反向代理你会想到 Nginx、HAProxy,它们用声明式配置做事;YARP 的路线是让你用 C# 代码来定义路由、负载均衡、中间件行为。这意味着你可以把代理逻辑和业务逻辑写在一起、动态调整路由、复用已有的认证授权中间件——这些东西用 Nginx 做起来会相当别扭。

图片

YARP 的核心机制包括:

  • 路由与集群:

     通过 Routes 定义匹配规则(路径、主机、Header),通过 Clusters 定义上游服务集群。
  • 负载均衡:

     内置 RoundRobin、Random、LeastRequests、PowerOfTwoChoices(默认)等策略。其中 PowerOfTwoChoices 是超高并发场景的优选,它随机选两个节点并从中挑选请求数最少的,在避免全局锁的同时近似达到 LeastRequests 的效果。
  • 请求转换:

     支持对转发前的请求进行路径重写、Header 增删改等转换操作。
  • 中间件管道:

     YARP 本身是 ASP.NET Core 中间件,可以跟认证、授权、日志、限流等中间件自由组合。
  • 可观测性:

     原生支持 OpenTelemetry 分布式追踪。

YARP 在 .NET 基础架构之上实现,可在 Windows、Linux 或 macOS 上运行。微软自家的 Azure 应用服务已经用上了 YARP——这算是官方背书级别的生产验证。

怎么引入

引入 YARP 只需要安装一个 NuGet 包。

安装包

dotnet add package Yarp.ReverseProxy
图片

安装对应 NuGet 包即可。

最小接入配置(Program.cs)

var builder = WebApplication.CreateBuilder(args);
 
 
// 必须项:注册反向代理服务,并从配置文件加载路由和集群
builder.Services.AddReverseProxy()
    .LoadFromConfig(builder.Configuration.GetSection("ReverseProxy"));
 
 
var app = builder.Build();
 
 
// 必须项:映射反向代理端点
app.MapReverseProxy();
 
app.Run();
 

这段代码做了两件事:AddReverseProxy() 把 YARP 的核心服务注册到依赖注入容器;MapReverseProxy() 把反向代理中间件挂到请求管道上。

配置文件(appsettings.json 最小示例)

{
  "ReverseProxy": {
    "Routes": {
      "route1": {
        "ClusterId": "cluster1",
        "Match": {
          "Path": "/api/{**catch-all}"
        }
      }
    },
    "Clusters": {
      "cluster1": {
        "Destinations": {
          "destination1": {
            "Address": "https://localhost:7001/"
          }
        }
      }
    }
  }
 
}
 

Routes 定义请求匹配规则,Clusters 定义转发目标。上面这个配置把所有 /api/ 开头的请求转发到 https://localhost:7001/

可选项

  • 需要负载均衡:在 Clusters 下配置 LoadBalancingPolicy
  • 需要路径重写:在 Routes 下配置 Transforms
  • 需要代码级动态配置:实现 IProxyConfigProvider 替代 LoadFromConfig

验证接入是否成功

启动项目后,访问一个匹配路由的路径(如 /api/xxx),如果请求被正确转发到目标地址并返回响应,说明接入成功。也可以查看日志输出,YARP 会记录路由匹配和转发信息。

快速上手

示例一:最小可用反向代理

这就是上面“怎么引入”里的最小配置——一个空 ASP.NET Core 项目加 YARP,把所有请求转发到同一个上游服务。这是最基础的用法,5 分钟跑通。

启动一个上游服务(比如另一个 WebAPI 项目跑在 https://localhost:7001),再启动 YARP 代理项目(跑在 https://localhost:5000)。访问 https://localhost:5000/api/xxx,实际会转发到 https://localhost:7001/api/xxx

示例二:多路由分发(微服务网关场景)

配置多个路由,把不同路径的请求分发到不同的后端服务:

{
  "ReverseProxy": {
    "Routes": {
      "order-route": {
        "ClusterId": "order-cluster",
        "Match": { "Path": "/order/{**catch-all}" }
      },
      "user-route": {
        "ClusterId": "user-cluster",
        "Match": { "Path": "/user/{**catch-all}" }
      }
    },
    "Clusters": {
      "order-cluster": {
        "Destinations": {
          "dest1": { "Address": "https://localhost:7101/" }
        }
      },
      "user-cluster": {
        "Destinations": {
          "dest1": { "Address": "https://localhost:7201/" }
        }
      }
    }
  }
 
}
 

/order/ 开头的请求走订单服务,/user/ 开头的走用户服务。这就是一个最基础的微服务网关雏形。

示例三:负载均衡(多实例部署)

在一个集群下配置多个目标地址,并指定负载均衡策略:

{
  "ReverseProxy": {
    "Clusters": {
      "order-cluster": {
        "LoadBalancingPolicy": "RoundRobin",
        "Destinations": {
          "dest1": { "Address": "https://localhost:7101/" },
          "dest2": { "Address": "https://localhost:7102/" }
        }
      }
    }
  }
 
}
 

可用策略包括 RoundRobinRandomLeastRequestsPowerOfTwoChoices。生产环境如果后端实例性能差异较大,可以考虑 LeastRequests,它倾向于把请求分给当前活跃请求数最少的节点。

示例四:路径重写(Transforms)

很多时候后端服务的路由前缀和网关暴露的不一致,需要用 Transforms 做路径转换。比如网关暴露 /api/order/create,但后端真正的路径是 /order-service/v1/create

{
  "ReverseProxy": {
    "Routes": {
      "order-rewrite": {
        "ClusterId": "order-cluster",
        "Match": { "Path": "/api/order/{**remainder}" },
        "Transforms": [
          { "PathRouteValuesPattern": "/order-service/v1/{**remainder}" }
        ]
      }
    }
  }
 
}
 

{**remainder} 会捕获通配符部分,然后拼接到新的路径模板里。注意 Transforms 中用于重写路径的键名是 PathRouteValuesPattern,而非 PathPattern

边界提醒: 当用户请求为 /api/order/(末尾带斜杠)时,{**remainder} 捕获的值为空字符串,拼接后目标路径为 /order-service/v1/。后端服务能否正确处理这个路径取决于其路由设计。如果后端不兼容空后缀,可能返回 404。生产环境可以考虑使用 PathRemovePrefix 转换器做更稳定的前缀裁剪,或者在目标路径中显式处理斜杠边界。Transforms 还支持修改 Header、查询字符串、主机名等,能满足多数网关的 URL 重写需求。

示例五:代码级动态路由(IProxyConfigProvider)

如果路由信息存储在数据库或配置中心,需要在运行时动态更新,可以实现 IProxyConfigProvider 接口。YARP 会自动检测并使用容器中已注册的 IProxyConfigProvider,无需额外调用 LoadFromMemory 等不存在的扩展方法。

下面是一个完整的、可直接编译的实现,包含 IProxyConfig 的显式实现和 CancellationChangeToken 信号触发机制:

using Microsoft.Extensions.Primitives;
 
using Yarp.ReverseProxy.Configuration;
 
 
public class DynamicProxyConfigProvider : IProxyConfigProvider
{
    private TypeSafeProxyConfig _config;
    private CancellationTokenSource _cts = new();
 
    public DynamicProxyConfigProvider()
    {
        // 初始化时加载配置
        _config = new TypeSafeProxyConfig(LoadRoutes(), LoadClusters(), new CancellationChangeToken(_cts.Token));
    }
 
    public IProxyConfig GetConfig() => _config;
 
    public void Refresh()
    {
        var oldCts = _cts;
        _cts = new CancellationTokenSource(); // 创建新的 Token 供下次使用
 
        // 切换新配置
        _config = new TypeSafeProxyConfig(LoadRoutes(), LoadClusters(), new CancellationChangeToken(_cts.Token));
 
        // 触发旧 Token 的信号,通知 YARP 重新调用 GetConfig()
        oldCts.Cancel();
        oldCts.Dispose();
    }
 
    private IReadOnlyList<RouteConfig> LoadRoutes()
    {
        // 生产环境应从数据库或配置中心加载。
        // 警告:此处若返回空列表,YARP 将无法匹配任何请求,所有访问都会返回 404。
        // 建议至少配置一条测试路由用于验证动态加载是否生效。
        // 示例占位数据(仅供演示):
        // return new List<RouteConfig>
        // {
        //     new RouteConfig
        //     {
        //         RouteId = "dynamic-route",
        //         ClusterId = "dynamic-cluster",
        //         Match = new RouteMatch { Path = "/test/{**catch-all}" }
        //     }
        // };
        return new List<RouteConfig>();
    }
 
    private IReadOnlyList<ClusterConfig> LoadClusters()
    {
        // 生产环境应从数据库或配置中心加载。
        // 警告:此处若返回空列表,YARP 将无法匹配任何集群,所有访问都会返回 404。
        return new List<ClusterConfig>();
    }
 
    // 显式实现 IProxyConfig 接口的内部类
    private class TypeSafeProxyConfig : IProxyConfig
    {
        public IReadOnlyList<RouteConfig> Routes { get; }
        public IReadOnlyList<ClusterConfig> Clusters { get; }
        public IChangeToken ChangeToken { get; }
 
        public TypeSafeProxyConfig(             IReadOnlyList<RouteConfig> routes,             IReadOnlyList<ClusterConfig> clusters,             IChangeToken changeToken)
        {
            Routes = routes;
            Clusters = clusters;
            ChangeToken = changeToken;
        }
    }
}
 
 
// 在 Program.cs 中注册
builder.Services.AddSingleton<IProxyConfigProvider, DynamicProxyConfigProvider>();
builder.Services.AddReverseProxy(); // YARP 会自动使用容器中的 IProxyConfigProvider
 

这种方式让你的网关具备动态配置能力,无需重启即可调整路由或上下线后端节点。需要注意:每次刷新时必须利用新的 CancellationTokenSource 生成 CancellationChangeToken 并赋给 IProxyConfig.ChangeToken,YARP 才能感知配置变更。

常见报错排查

1. 404 Not Found

  • 触发条件:请求路径没有匹配到任何 Route。
  • 报错信息:No route match found for the current request.
  • 修复方式:检查 Route 的 Match.Path 是否写对,注意 {**catch-all} 通配符能匹配到子路径,不带通配符的精确匹配则不会匹配子路径。如果使用动态配置,确认 LoadRoutes() 返回了有效的路由列表。

2. 连接失败(Connection refused)

  • 触发条件:Cluster 中配置的 Destination 地址不可达(服务未启动或端口错误)。
  • 报错信息:System.Net.Http.HttpRequestException: Connection refused
  • 修复方式:确认上游服务是否正常运行,检查 Address 的协议(http/https)和端口是否一致。本地调试时特别注意自签名证书问题,可能需要配置 HttpClient 跳过证书验证(仅在非生产环境使用)。

3. 502 Bad Gateway(超时)

  • 触发条件:上游服务响应时间超过 YARP 默认的 100 秒超时限制,或后端处理时间过长。
  • 报错信息:The request timed out.
  • 修复方式:在 Cluster 配置中调整 HttpClient.Timeout 属性,或者优化上游接口性能。

适用场景

微服务网关是 YARP 最典型的应用场景。开发团队规模中等以上、拆分出多个业务域服务时,用 YARP 做统一的流量入口可以集中处理认证鉴权、跨域、限流、日志聚合等横切关注点。由于 YARP 本身就运行在 ASP.NET Core 管道上,你可以直接复用标准的 JWT 认证中间件,并通过在路由配置中指定 "AuthorizationPolicy" 来对接 ASP.NET Core 的策略授权,比在 Nginx 里写 Lua 脚本要自然得多。

API 聚合与协议转换也是合适的场景。YARP 支持修改请求和响应,虽然它本身不是专门的聚合层框架,但结合 .NET 的 HttpClientFactory 和自定义中间件,可以实现轻量级的请求合并和适配。

A/B 测试与灰度发布用得着它。通过代码动态路由可以根据 Header、Cookie 甚至用户 ID 将流量按比例或规则分配到不同版本的服务集群,配置在代码层做比静态配置文件灵活不少。

不太适合的场景包括:纯静态文件反向代理(Nginx 效率更高)、四层 TCP/UDP 代理(YARP 只支持 HTTP/HTTPS 和 WebSocket)、极低延迟要求的转发(YARP 的 .NET 托管开销会比 Nginx 的 C 实现稍高)。另外,如果你团队没有 .NET 开发能力,或者运维体系完全依赖 K8s Ingress 标准,引入 YARP 会增加维护成本,这种情况用云原生方案更省事。

实战建议

高频坑位一:转发请求头丢失

YARP 默认会转发大部分标准请求头,但自定义 Header 可能被过滤或未正确传递。如果后端依赖特定自定义头(如 X-User-Id),建议在 Route 的 Transforms 中显式设置或通过自定义中间件强制添加,不要依赖默认转发行为。

触发条件:使用了自定义 Header 且未在 YARP 端显式声明。
 错误表现:后端接收不到网关透传的用户上下文信息。
 修复策略:在 AddReverseProxy() 时自定义 IHttpClientFactory 或在 Transforms 中配置 RequestHeader 操作。

高频坑位二:负载均衡状态下某个实例异常仍继续被转发

YARP 的被动健康检查默认只标记失败,不会快速熔断异常实例。如果某个上游实例挂掉,YARP 不会立即将其剔除出负载均衡池,导致部分请求持续报 502。

触发条件:上游实例崩溃但进程仍在(如端口监听但请求挂起),或实例下线但未从 Destination 列表中移除。
 错误表现:请求频繁出现超时或 5xx 错误,且不均衡。
 修复策略:配置主动健康检查,定期探测后端实例的健康状态,异常时自动从可用列表摘除。配置示例(注意必须指定 Policy,通常使用 ConsecutiveFailures):

{
  "Clusters": {
    "order-cluster": {
      "HealthCheck": {
        "Active": {
          "Enabled": true,
          "Interval": "00:00:10",
          "Timeout": "00:00:03",
          "Policy": "ConsecutiveFailures",
          "Path": "/health"
        }
      }
    }
  }
 
}
 

高频坑位三:动态配置变更引发内存泄漏

使用自定义 IProxyConfigProvider 时,如果频繁触发配置刷新,但未妥善管理 CancellationToken 的生命周期,或者每次刷新都全量重建大量规则对象,会导致 HttpContext 管道频繁重建,引起内存碎片和 GC 压力增加。

触发条件:高频率调用 ChangeToken 的 Signal 触发刷新,但旧的 CancellationTokenSource 未被及时 Dispose;或者每次刷新都创建全新的路由/集群对象且未复用不变的部分。
 错误表现:内存持续上涨,GC 频繁,甚至 OOM。
 修复策略:确保每次刷新时,利用新的 CancellationTokenSource 生成 CancellationChangeToken 并赋给 IProxyConfig.ChangeToken,同时及时 Dispose 旧的 CancellationTokenSource(如示例五所示)。降低刷新频率,仅在配置真正变更时触发信号。同时监控 GC 指标,必要时手动调用 GC.Collect() 仅用于排查,生产环境不建议。

其他推荐实践

  • 集成 OpenTelemetry:

     YARP 内置了对 Distributed Tracing 的支持,开启后可以在 Jaeger 或 Zipkin 中看到完整的代理链路,排查问题事半功倍。
  • 合理设置超时:

     Clusters 级别可配置 HttpClient.Timeout,建议结合上游平均响应时间设置,不要太长(避免线程堆积)也不要太短(避免误判超时)。
  • 与限流中间件配合:

     YARP 本身不内置限流,但可以搭配 .NET 8 的 RateLimiting 中间件使用,在 YARP 路由匹配之前或之后生效都可以。

选型建议

什么场景相对更适合选 YARP:

  • 你所在团队已经是 .NET 技术栈,不想额外引入 Nginx 或 Traefik 等外部组件。
  • 需要频繁变更路由规则,且希望用代码逻辑控制流量分发(如基于灰度标签、租户 ID)。
  • 网关层需要和现有 .NET 认证授权(如 IdentityServer、JWT)深度集成,不想在两层代理中维护两套配置。
  • 并发量在每秒数千到数万级别,且后续可用 .NET 性能优化手段(如 Kestrel 调优、HTTP/3)做垂直扩展。

什么场景可能不太适合:

  • 纯静态资源服务或超高并发(单机 10 万+ QPS)场景,Nginx/OpenResty 或云厂商的 CDN 网关会更有优势。
  • 团队运维能力有限,完全依赖 K8s Ingress Controller 且不愿额外维护 .NET 网关进程。
  • 主要需求是四层代理或 UDP 透传,YARP 不支持这类协议。
  • 上线周期紧迫,团队对 .NET 8 新特性尚不熟悉,建议先从更成熟的 Ocelot 过渡,或直接用云服务商的全托管网关。

综合判断:如果你已经决定用 .NET 8 作为主技术栈,且需要构建微服务网关或边车代理,YARP 值得优先考虑。它比 Ocelot 更轻量、性能更好(基于原生 HttpClient 和 Kestrel),且有微软持续维护。如果并发预估在 2 万 QPS 以上,务必提前做压测,根据结果调整 System.Threading.ThreadPool 参数和连接池限制,同时做好水平扩展的多节点部署。

总结收尾

你不需要在微服务网关选型上反复纠结。如果团队已经在 .NET 生态里,YARP 是构建反向代理最顺手的那把刀——配置比 Nginx 灵活,性能比 Ocelot 强,还能跟你的业务代码长在一起。从最小可用配置起步,先把流量转起来,再逐步把负载均衡、健康检查、动态路由加进去。不要一上来就追求全量动态配置,先把文件配置跑稳定,哪天发现配置文件改不动了,再把 IProxyConfigProvider 拿出来。建议今天拿一个空闲的 WebAPI 项目试一下 YARP 的转发效果,确认它能跑通再考虑替换现有的网关组件。


群贤毕至

访客