Ocelot网关实战:.NET微服务路由与请求聚合配置详解

发布时间:2026/8/3 9:03:46
Ocelot网关实战:.NET微服务路由与请求聚合配置详解 1. 从单体到微服务为什么需要一个“交通警察”如果你正在构建或重构一个 .Net 应用并且开始考虑微服务架构那么“网关”这个词你肯定绕不过去。想象一下你原本是一个大商场单体应用所有顾客客户端请求都从一个大门进出内部导购业务逻辑自己处理一切。现在你决定把商场拆分成一个个独立的精品店微服务比如服装店、餐饮店、数码店。这时候问题来了顾客怎么知道该去哪个店每个店都有自己的入口和安保端口和认证顾客难道要记住几十个地址吗显然不现实。这就是 API 网关API Gateway登场的时候。它就像这个新商业综合体的总服务台和唯一主入口。所有外部请求都先到达网关由网关根据请求的“意图”比如是买衣服还是吃饭将其精准地路由到后面对应的精品店微服务。Ocelot就是 .Net 生态中一个非常流行、轻量且功能强大的开源 API 网关库。它不是一款需要独立部署的软件而是一个可以集成到你 ASP.NET Core 应用程序中的中间件这意味着你可以完全掌控它并用熟悉的 C# 来扩展它。我最初接触 Ocelot 时觉得它不过就是个“转发请求”的配置工具。但真正在微服务实践中用起来才发现它的价值远不止于此。除了最核心的路由功能它还能帮你做请求聚合把对多个微服务的多次调用合并成一次返回、负载均衡、服务发现、认证授权、限流熔断等一系列横切关注点。今天我们就聚焦在它的两个基石功能上路由和请求聚合。理解了这两点你就能搭建起微服务对外的统一门户这是微服务架构走向实用的关键一步。2. Ocelot 基础环境搭建与核心配置解析在开始配置炫酷的功能之前我们得先把 Ocelot 跑起来。整个过程非常“ .Net Core”如果你熟悉 Startup 或 Program 的配置那几乎就是几分钟的事。2.1 项目创建与包引用首先你需要一个 ASP.NET Core 项目作为你的网关。通常这是一个空的 Web API 项目。dotnet new webapi -n MyProject.Gateway cd MyProject.Gateway然后通过 NuGet 安装 Ocelot 包。截至我写这篇文章时稳定版本是 22.0.0 系列它支持 .NET 6/7/8。dotnet add package Ocelot2.2 核心配置文件ocelot.jsonOcelot 的强大和灵活很大程度上源于它的配置文件。默认情况下它会寻找一个名为ocelot.json的文件。这个文件是网关的“大脑”所有路由、聚合、安全等规则都在这里定义。让我们先创建一个最基础的、仅包含一个路由规则的配置。在项目根目录创建ocelot.json并确保其“复制到输出目录”属性设置为“始终复制”或“如果较新则复制”。{ Routes: [ { DownstreamPathTemplate: /api/orders/{everything}, DownstreamScheme: http, DownstreamHostAndPorts: [ { Host: localhost, Port: 5001 } ], UpstreamPathTemplate: /orders/{everything}, UpstreamHttpMethod: [ GET, POST, PUT, DELETE ] } ], GlobalConfiguration: { BaseUrl: https://api.mycompany.com } }我来拆解一下这几个关键配置项理解它们是你玩转 Ocelot 的前提Routes 这是核心数组定义了所有的路由规则。每个路由对象都是一条独立的“交通指示”。DownstreamPathTemplate “下游”路径模板。指的是目标微服务你的订单服务的 API 端点路径。{everything}是一个占位符表示匹配此部分之后的所有路径段并将其捕获为变量。例如/api/orders/123和/api/orders/123/items都能匹配。DownstreamScheme/Host/Port 定义了目标微服务的访问协议、主机名和端口。这就是网关最终要把请求转发到的地址http://localhost:5001。UpstreamPathTemplate “上游”路径模板。指的是客户端访问网关时使用的 URL 路径。在这个例子里客户端访问https://api.mycompany.com/orders/123。UpstreamHttpMethod 指定这个路由规则对哪些 HTTP 方法生效。通常我们会配置成[ “Get”, “Post”, “Put”, “Delete” ]来支持 RESTful 操作。GlobalConfiguration.BaseUrl 这是网关对外暴露的基础 URL。它主要用于生成聚合请求时的正确链接或者在某些中间件中构造完整的 URL。注意它不会改变网关实际的监听地址那是由Kestrel或IIS配置决定的。2.3 在 Program.cs 中集成 Ocelot配置好文件后需要在应用启动时加载它并启用 Ocelot 中间件。.NET 6 之后推荐使用最小托管模型在Program.cs中操作using Ocelot.DependencyInjection; using Ocelot.Middleware; var builder WebApplication.CreateBuilder(args); // 1. 添加 Ocelot 服务 builder.Services.AddOcelot(); // 2. 将 ocelot.json 配置文件与环境变量如 appsettings.json合并 // 这样你可以在 appsettings.Development.json 中覆盖生产环境的下游服务地址 builder.Configuration.AddJsonFile(“ocelot.json”, optional: false, reloadOnChange: true); var app builder.Build(); // 3. 使用 Ocelot 中间件 app.UseOcelot().Wait(); // 注意UseOcelot() 是异步的在 Main 方法中需要 Wait app.Run();注意app.UseOcelot().Wait();这行代码在同步上下文中是常见的写法。但在实际应用中如果你的启动逻辑更复杂或者使用async Main则应使用await app.UseOcelot();。另外确保ocelot.json文件被正确复制到输出目录如bin/Debug/net8.0否则 Ocelot 会因找不到配置文件而启动失败。现在启动你的网关项目假设运行在http://localhost:5000当你访问http://localhost:5000/orders/123时Ocelot 会拦截这个请求根据规则将其转发到http://localhost:5001/api/orders/123并将订单服务运行在 5001 端口的响应原样返回给客户端。对于客户端来说它只和网关5000端口打交道完全不知道背后订单服务5001端口的存在。这就是路由最直观的效果。3. 路由配置的进阶技巧与实战陷阱掌握了基础路由后你会发现真实场景要复杂得多。不同的服务可能有不同的路径结构你需要处理认证、限流还可能遇到各种诡异的错误。下面这些进阶配置和踩坑经验是我在多个项目中总结出来的。3.1 路由优先级与模糊匹配当你的Routes数组里有多个规则时Ocelot 会按照它们在配置文件中出现的顺序进行匹配第一个匹配成功的路由会被使用。这个特性非常重要意味着你需要把最具体的路由放在前面把最通用或兜底的路由放在后面。假设你有两个服务用户服务http://localhost:5002/api/users/{id}订单服务http://localhost:5001/api/orders/{id}同时你希望所有以/internal/开头的请求都去到一个内部管理服务。如果你的配置顺序是{ Routes: [ // 规则1通用内部路由 { DownstreamPathTemplate: /api/admin/{everything}, UpstreamPathTemplate: /internal/{everything}, ... }, // 规则2具体用户路由 { DownstreamPathTemplate: /api/users/{id}, UpstreamPathTemplate: /users/{id}, ... }, // 规则3具体订单路由 { DownstreamPathTemplate: /api/orders/{id}, UpstreamPathTemplate: /orders/{id}, ... } ] }那么当你访问/users/100时它会被规则1意外捕获因为{everything}匹配了users/100而规则1排在前面。正确的做法是把具体路由放前面{ Routes: [ // 规则1具体用户路由优先 { DownstreamPathTemplate: /api/users/{id}, UpstreamPathTemplate: /users/{id}, ... }, // 规则2具体订单路由 { DownstreamPathTemplate: /api/orders/{id}, UpstreamPathTemplate: /orders/{id}, ... }, // 规则3通用内部路由兜底 { DownstreamPathTemplate: /api/admin/{everything}, UpstreamPathTemplate: /internal/{everything}, ... } ] }3.2 占位符与查询字符串的处理Ocelot 的路径模板支持多种占位符除了{everything}最常用的是{id}这类命名占位符。它们不仅用于路径匹配还可以自动映射到下游路径。{ DownstreamPathTemplate: /api/products/{productId}, UpstreamPathTemplate: /catalog/products/{productId}, ... }当访问/catalog/products/abc123时网关会自动将请求转发到/api/products/abc123。对于查询字符串Query StringOcelot 默认会将其原封不动地传递给下游服务。你不需要在模板中声明它们。例如客户端请求/users?page1size20网关会将其转发到下游服务的/api/users?page1size20。这一点非常省心。3.3 集成服务发现从硬编码到动态寻址在之前的例子中DownstreamHostAndPorts是硬编码的。这在开发或小型系统中可行但在真正的微服务环境中服务实例的地址IP和端口是动态变化的尤其是使用 Docker 或 Kubernetes 时。这时就需要集成服务发现Service Discovery中心如 Consul、Eureka 或 .NET 原生的IConfiguration。以 Consul 为例你需要先安装Ocelot.Provider.Consul包。dotnet add package Ocelot.Provider.Consul然后在Program.cs中注册 Consulbuilder.Services.AddOcelot() .AddConsul();接着修改ocelot.json中的路由配置用ServiceName替代硬编码的Host和Port{ Routes: [ { DownstreamPathTemplate: /api/orders/{everything}, DownstreamScheme: http, UpstreamPathTemplate: /orders/{everything}, UpstreamHttpMethod: [ GET, POST ], ServiceName: order-service, // 关键指定服务名 LoadBalancerOptions: { Type: RoundRobin // 指定负载均衡策略 } } ], GlobalConfiguration: { ServiceDiscoveryProvider: { Host: localhost, Port: 8500, Type: Consul } } }现在Ocelot 会去 Consul默认运行在 8500 端口查询名为order-service的所有健康实例并使用RoundRobin轮询策略将请求分发到其中一个实例上。这样你的网关配置就与具体的服务部署细节解耦了实现了真正的动态路由。3.4 常见路由故障排查404 Not Found检查点首先确认你的网关应用本身是否启动成功能否访问其根地址。然后核对UpstreamPathTemplate是否与你的请求 URL 完全匹配包括大小写和路径分隔符。最后手动拼接DownstreamScheme、Host、Port和DownstreamPathTemplate用 Postman 直接访问下游服务看是否能通。这是最直接的隔离问题的方法。502 Bad Gateway或503 Service Unavailable检查点这通常表示网关能匹配到路由但无法连接到下游服务或下游服务返回了错误。检查下游服务是否正在运行且健康。如果使用了服务发现检查 Consul 中该服务是否有健康实例注册。查看网关应用的日志Ocelot 会记录详细的请求和错误信息这是定位问题的金钥匙。配置热重载不生效Ocelot 支持配置文件变更时自动重载但需要确保reloadOnChange: true已设置并且你的文件系统通知机制正常工作在 Docker 或某些虚拟化环境中可能需要额外配置。一个稳妥的办法是在开发环境你可以通过发送一个 HTTP 请求到网关的管理端点如果配置了来触发重载或者直接重启网关应用。4. 请求聚合将多次 API 调用合并为一次响应请求聚合Request Aggregation是 Ocelot 提供的一个非常实用的功能。设想一个移动端页面需要加载用户基本信息、最近的订单和消息通知。如果没有聚合客户端需要分别调用/users/me、/orders/recent、/notifications三个接口产生三次 HTTP 请求不仅延迟高而且增加了客户端的复杂性。通过 Ocelot 的聚合功能我们可以定义一个聚合路由如/dashboard当客户端访问它时网关会在后端并行地调用这三个微服务然后将结果组装成一个统一的 JSON 响应返回给客户端。客户端只需一次请求就能拿到所有数据。4.1 基础聚合配置在ocelot.json的Routes中你需要定义一个特殊的聚合路由并在Aggregates数组中定义被聚合的子路由。{ Routes: [ // 子路由1用户信息 { Key: User, // 必须聚合路由通过 Key 来引用它 DownstreamPathTemplate: /api/users/{userId}, UpstreamPathTemplate: /users/{userId}, UpstreamHttpMethod: [ GET ] // ... 其他下游配置 }, // 子路由2订单列表 { Key: Orders, DownstreamPathTemplate: /api/orders, UpstreamPathTemplate: /orders, UpstreamHttpMethod: [ GET ], AddQueriesToRequest: [ { Key: userId, Value: {userId} // 从聚合路由的 upstream 模板中获取 userId 变量 } ] }, // 聚合路由 { DownstreamPathTemplate: /api/aggregated/dashboard, UpstreamPathTemplate: /dashboard/{userId}, UpstreamHttpMethod: [ GET ], Aggregator: DashboardAggregator // 指定聚合器名称 } ], Aggregates: [ { RouteKeys: [ User, Orders ], // 要聚合的子路由 Key UpstreamPathTemplate: /dashboard/{userId}, // 聚合路由的访问路径 Aggregator: DashboardAggregator // 与聚合路由中的 Aggregator 对应 } ] }关键点解析Key 每个需要被聚合的子路由必须有一个唯一的Key。Aggregates 这是一个独立的配置节。每个聚合定义通过RouteKeys数组指明要聚合哪些子路由并通过UpstreamPathTemplate定义聚合路由的访问路径。Aggregator 这是聚合器的名称需要同时在聚合路由和Aggregates配置中指定且两者必须一致。Ocelot 会根据这个名称去寻找对应的聚合器实现类。参数传递 注意子路由Orders的配置中使用了AddQueriesToRequest。这是因为订单列表接口可能需要userId作为查询参数。{userId}的值来自于聚合路由的路径参数/dashboard/{userId}。Ocelot 支持这种上下文变量的传递。4.2 实现自定义聚合器Ocelot 不会自动帮你拼接 JSON。你需要实现IDefinedAggregator接口告诉它如何将各个子请求的响应合并成一个。创建一个类DashboardAggregator.csusing Ocelot.Middleware; using Ocelot.Responses; using System.Dynamic; using System.Net; namespace MyProject.Gateway.Aggregators { public class DashboardAggregator : IDefinedAggregator { public async TaskDownstreamResponse Aggregate(ListHttpContext responses) { // 1. 准备动态对象作为聚合结果 dynamic aggregatedResult new ExpandoObject(); var resultDict aggregatedResult as IDictionarystring, object; // 2. 遍历每个子请求的响应 foreach (var context in responses) { // 获取 Ocelot 存储在 HttpContext.Items 中的路由信息 var routeKey context.Items.DownstreamRoute().Key; // 读取响应体 var responseContent await context.Items.DownstreamResponse().Content.ReadAsStringAsync(); // 假设下游返回的是 JSON这里简单反序列化。生产环境应用更健壮的解析。 var responseObject System.Text.Json.JsonSerializer.DeserializeExpandoObject(responseContent); // 3. 根据子路由的 Key将结果放入聚合对象的不同字段 if (routeKey “User”) { resultDict[“userProfile”] responseObject; } else if (routeKey “Orders”) { resultDict[“recentOrders”] responseObject; } // 可以继续添加其他子路由... } // 4. 构造聚合后的响应 var aggregatedJson System.Text.Json.JsonSerializer.Serialize(aggregatedResult); var stringContent new StringContent(aggregatedJson, System.Text.Encoding.UTF8, “application/json”); return new DownstreamResponse(stringContent, HttpStatusCode.OK, new ListKeyValuePairstring, IEnumerablestring(), “OK”); } } }然后在Program.cs中注册这个聚合器using MyProject.Gateway.Aggregators; using Ocelot.DependencyInjection; var builder WebApplication.CreateBuilder(args); builder.Services.AddOcelot() .AddSingletonDefinedAggregatorDashboardAggregator(); // 注册聚合器 var app builder.Build(); app.UseOcelot().Wait(); app.Run();现在当客户端访问GET /dashboard/123时Ocelot 会并行调用User和Orders两个子路由对应的下游服务获取到用户信息和订单列表然后交给DashboardAggregator处理。聚合器将两个 JSON 对象合并成一个如{ “userProfile”: {…}, “recentOrders”: […] }的新 JSON最后返回给客户端。4.3 聚合功能的局限性与实践建议请求聚合听起来很美好但它并非银弹有以下几个重要的局限性并行与超时 聚合请求是并行发往下游服务的。整个聚合请求的耗时取决于最慢的那个下游服务。你需要为聚合路由和每个子路由合理设置超时QoSOptions避免一个慢服务拖垮整个聚合接口。错误处理 如果某个子请求失败了如返回 4xx 或 5xx聚合器会收到一个包含错误状态的HttpContext。你需要在聚合器实现中决定如何处理部分失败——是返回一个包含错误信息的聚合结果还是直接让整个聚合请求失败这需要根据业务逻辑仔细设计。数据格式耦合 聚合器强依赖于下游服务返回的 JSON 结构。一旦下游服务的 API 发生变更即使是字段名修改聚合器就可能出错。建议为聚合器编写单元测试并在下游服务变更时同步更新。性能考量 对于极其复杂的聚合逻辑如数据转换、计算在网关层处理可能会成为性能瓶颈。此时可以考虑在后台专门创建一个用于聚合的“组合服务”Composition Service或者使用 GraphQL 这类更专业的 API 查询语言来替代。个人经验 请求聚合最适合用在数据关联性弱、可并行获取、且对前端性能提升明显的场景比如首页仪表盘、商品详情页聚合商品信息、库存、评论。对于强事务性或存在复杂业务逻辑链路的调用应优先考虑在服务间通过 RPC 或消息队列进行编排而不是在网关做简单的 HTTP 聚合。5. 生产环境部署与性能调优考量当你的 Ocelot 网关准备从开发环境走向生产环境时有一些关键的配置和考量点需要提前规划。5.1 配置管理环境分离与安全绝对不要将包含内部服务地址和敏感信息的ocelot.json提交到代码仓库。标准的做法是基础配置ocelot.json中只包含路由结构、上游模板等不敏感的逻辑配置。DownstreamHostAndPorts可以留空或使用占位符。环境覆盖 利用 .NET Core 的配置系统通过appsettings.Production.json或环境变量来注入不同环境的下游服务地址、Consul 主机名等。// appsettings.Production.json { “Ocelot”: { “Routes”: [ { “DownstreamHostAndPorts”: [ { “Host”: “order-service.prod.svc.cluster.local”, // K8s 服务名 “Port”: 80 } ] } ], “GlobalConfiguration”: { “ServiceDiscoveryProvider”: { “Host”: “consul-server.prod.svc.cluster.local”, “Port”: 8500 } } } }在Program.cs中按顺序加载配置builder.Configuration .AddJsonFile(“ocelot.json”, optional: false, reloadOnChange: true) .AddJsonFile($“ocelot.{builder.Environment.EnvironmentName}.json”, optional: true, reloadOnChange: true) // 环境特定配置 .AddEnvironmentVariables(); // 允许通过环境变量覆盖这样在开发、测试、生产环境中你可以轻松切换配置而无需修改代码。5.2 性能、可观测性与高可用缓存 Ocelot 支持对下游响应进行缓存可以显著降低后端压力和提高响应速度。你可以在路由级别配置FileCacheOptions指定缓存的 TTL生存时间。对于不经常变化的静态数据或查询结果启用缓存是立竿见影的优化手段。限流与熔断 在Route配置中可以通过RateLimitOptions和QoSOptions来设置限流每秒最多请求数和熔断失败次数阈值、超时时间。这是保护下游微服务不被突发流量或自身故障拖垮的关键屏障。务必根据服务的实际承载能力进行配置。日志与追踪 确保 Ocelot 的日志级别如LogLevel.Information或Debug在开发调试时足够详细在生产环境调整为Warning或Error以减少噪音。同时集成分布式追踪系统如 OpenTelemetry Jaeger为每个经过网关的请求生成唯一的追踪 ID并传递到下游所有微服务。这在排查复杂的跨服务调用链问题时不可或缺。高可用部署 网关本身不能是单点故障。你需要部署至少两个网关实例并通过一个负载均衡器如 Nginx、HAProxy 或云负载均衡服务对外暴露。所有网关实例共享同一份配置可以从数据库、Consul KV 或配置中心拉取并指向同一个服务发现中心。5.3 与现有基础设施的集成Ocelot 是一个库这给了它极大的灵活性。你可以轻松地将它与其他 .NET 中间件集成认证/授权 在app.UseOcelot()之前加入app.UseAuthentication()和app.UseAuthorization()。Ocelot 支持 JWT Bearer Token 等标准方案你可以在路由配置中设置AuthenticationOptions要求特定路由必须携带有效的 Token网关会先验签再将用户声明Claims以请求头如X-Claim-Sub的形式传递给下游服务。健康检查 为网关应用添加 ASP.NET Core 的健康检查端点/health。这可以让你的容器编排平台如 Kubernetes或监控系统感知网关实例的健康状态。指标收集 集成Prometheus或Application Insights暴露网关的请求量、延迟、错误率等指标为容量规划和故障预警提供数据支持。经过以上步骤你的 Ocelot 网关就不再是一个简单的转发器而是一个具备路由、聚合、安全、可观测性和韧性的成熟 API 网关能够稳健地支撑起整个微服务架构的南北向流量。