使用 Yarp 做网关
资料
GitHub:
YARP 文档:
主动和被动健康检查 :
gRpc:
实战项目概览
Yarp Gateway 示意图
共享类库
创建一个 .Net6.0 的类库,项目名称:Artisan.Shared.Hosting.AspNetCore, 其它项目公用方法放在这个项目网关。
Serilog 日志
需要的包:
< PackageReferenceInclude= "Serilog.AspNetCore"Version= "5.0.0"/>
< PackageReferenceInclude= "Serilog.Sinks.Async"Version= "1.5.0"/>
< PackageReferenceInclude= "Serilog.Sinks.Console"Version= "4.0.1"/>
展开全文
< PackageReferenceInclude= "Serilog.Sinks.File"Version= "5.0.0"/>
代码清单:Artisan.Shared.Hosting.AspNetCore/SerilogConfigurationHelper.cs
using Serilog;
using Serilog.Events;
namespace Artisan.Shared.Hosting.AspNetCore;
public staticclassSerilogConfigurationHelper{
public staticvoidConfigure( stringapplicationName)
Log.Logger = new LoggerConfiguration # ifDEBUG
.MinimumLevel.Debug # else
.MinimumLevel.Information # endif
.MinimumLevel.Override( "Microsoft", LogEventLevel.Information)
.MinimumLevel.Override( "Microsoft.EntityFrameworkCore", LogEventLevel.Warning)
.Enrich.FromLogContext
.Enrich.WithProperty( "Application", $ "{applicationName}")
.WriteTo.Async(c => c.File($ "{AppDomain.CurrentDomain.BaseDirectory}/Logs/logs.txt"))
.WriteTo.Async(c => c.Console)
.CreateLogger;
} 创建服务 IdentityService
创建一个【AspNetCore Web Api】项目网关,项目名称为:IdentityService
Program
代码清单:IdentityService/Program.cs
using Artisan.Shared.Hosting.AspNetCore;
using Microsoft.OpenApi.Models;
using Serilog;
namespace IdentityService;
public classProgram{
public staticintMain( string[] args)
var assemblyName = typeof(Program).Assembly.GetName.Name;
SerilogConfigurationHelper.Configure(assemblyName);
try
Log.Information($ "Starting {assemblyName}.");
var builder = WebApplication.CreateBuilder(args);
builder.Host
.UseSerilog;
builder.Services.AddControllers; //Web MVC
builder.Services.AddSwaggerGen(options =>
options.SwaggerDoc( "v1", new OpenApiInfo { Title = "Identity Service", Version = "v1"});
options.DocInclusionPredicate((docName, deion) => true);
options.CustomSchemaIds(type => type.FullName);
var app = builder.Build; if(app.Environment.IsDevelopment)
app.UseDeveloperExceptionPage;
app.UseRouting;
app.UseSwagger;
app.UseSwaggerUI;
app.UseEndpoints(endpoints =>
endpoints.MapControllers; //Web MVC
app.Run; return0;
catch (Exception ex)
Log.Fatal(ex, $ "{assemblyName} terminated unexpectedly!"); return1;
finally
Log.CloseAndFlush;
其中:
SerilogConfigurationHelper.Configure(assemblyName);
是配置 Serilog日志:引用上面创建的共享项目:【Artisan.Shared.Hosting.AspNetCore】
User 实体
代码清单:IdentityService/Models/User.cs
public classUser
public intId { get; set; }
public stringName { get; set; }
} UserController
代码清单:IdentityService/Controlles/UserController.cs
using Microsoft.AspNetCore.Mvc;
using IdentityService.Models;
using System.Threading.Tasks;
namespace IdentityService.Controllers
[ApiController]
[Route( "/api/identity/users")]
public classUserController: Controller
private readonly ILogger<UserController> _logger;
private staticList<User> Users = new List<User>
new User{ Id = 1, Name = "Jack"},
new User{ Id = 2, Name = "Tom"},
new User{ Id = 3, Name = "Franck"},
new User{ Id = 4, Name = "Tony"},
public UserController(ILogger<UserController> logger)
_logger = logger;
[]
public async Task<List<User>> GetAllAsync
{ returnawait Task.Run( =>
returnUsers;
[]
[Route( "{id}")]
public async Task<User> GetAsync( intid)
{ returnawait Task.Run( =>
var entity = Users.FirstOrDefault(p => p.Id == id); if(entity == null)
throw new Exception($ "未找到用户:{id}");
} returnentity;
[]
public async Task<User> CreateAsync(User user)
{ returnawait Task.Run( =>
Users.Add(user); returnuser;
[]
[Route( "{id}")]
public async Task<User> UpdateAsync( intid, User user)
{ returnawait Task.Run( =>
var entity = Users.FirstOrDefault(p => p.Id == id); if(entity == null)
throw new Exception($ "未找到用户:{id}");
entity.Name = user.Name; returnentity;
[]
[Route( "{id}")]
public async Task<User> DeleteAsync( intid)
{ returnawait Task.Run( =>
var entity = Users.FirstOrDefault(p => p.Id == id); if(entity == null)
throw new Exception($ "未找到用户:{id}");
Users.Remove(entity); returnentity;
} OrderService
创建一个【AspNetCore Web Api】项目网关,项目名称为:OrderService
Program
代码清单:OrderService/Program.cs
using Artisan.Shared.Hosting.AspNetCore;
using Microsoft.OpenApi.Models;
using Serilog;
namespace OrderService;
public classProgram{
public staticintMain( string[] args)
var assemblyName = typeof(Program).Assembly.GetName.Name;
SerilogConfigurationHelper.Configure(assemblyName);
try
Log.Information($ "Starting {assemblyName}.");
var builder = WebApplication.CreateBuilder(args);
builder.Host
.UseSerilog;
builder.Services.AddControllers; //Web MVC
builder.Services.AddSwaggerGen(options =>
options.SwaggerDoc( "v1", new OpenApiInfo { Title = "Order Service", Version = "v1"});
options.DocInclusionPredicate((docName, deion) => true);
options.CustomSchemaIds(type => type.FullName);
var app = builder.Build; if(app.Environment.IsDevelopment)
app.UseDeveloperExceptionPage;
app.UseRouting;
app.UseSwagger;
app.UseSwaggerUI;
app.UseEndpoints(endpoints =>
endpoints.MapControllers; //Web MVC
app.Run; return0;
catch (Exception ex)
Log.Fatal(ex, $ "{assemblyName} terminated unexpectedly!"); return1;
finally
Log.CloseAndFlush;
} Order 实体
代码清单:OrderService/Models/Order.cs
public classOrder
public stringId { get; set; }
public stringName { get; set; }
} OrderController
代码清单:OrderService/Controlles/OrderController.cs
using Microsoft.AspNetCore.Mvc;
using OrderService.Models;
using System.Diagnostics;
namespace OrderService.Controllers
[ApiController]
[Route( "/api/ordering/orders")]
public classOrderController: Controller
private readonly ILogger<OrderController> _logger;
private staticList<Order> Orders = new List<Order>
new Order{ Id = "1", Name = "Order #1"},
new Order{ Id = "2", Name = "Order #2"},
new Order{ Id = "3", Name = "Order #3"},
new Order{ Id = "4", Name = "Order #4"},
public OrderController(ILogger<OrderController> logger)
_logger = logger;
[]
public async Task<List<Order>> GetAllAsync
{ returnawait Task.Run( =>
{ returnOrders;
[]
[Route( "{id}")]
public async Task<Order> GetAsync( stringid)
{ returnawait Task.Run( =>
var entity = Orders.FirstOrDefault(p => p.Id == id); if(entity == null)
throw new Exception($ "未找到订单:{id}");
} returnentity;
[]
public async Task<Order> CreateAsync(Order order)
{ returnawait Task.Run( =>
Orders.Add(order); returnorder;
[]
[Route( "{id}")]
public async Task<Order> UpdateAsync( stringid, Order Order)
{ returnawait Task.Run( =>
var entity = Orders.FirstOrDefault(p => p.Id == id); if(entity == null)
throw new Exception($ "未找到订单:{id}");
entity.Name = Order.Name; returnentity;
[]
[Route( "{id}")]
public async Task<Order> DeleteAsync( stringid)
{ returnawait Task.Run( =>
var entity = Orders.FirstOrDefault(p => p.Id == id); if(entity == null)
throw new Exception($ "未找到订单:{id}");
Orders.Remove(entity); returnentity;
} 创建网关
创建一个【AspNetCore 空】项目网关,项目名称为:YarpGateway
引用包 < PackageReferenceInclude= "Yarp.ReverseProxy"Version= "1.1.0"/> 添加 Yarp
代码清单:YarpGateway/Program.cs
using Artisan.Shared.Hosting.AspNetCore;
using Serilog;
using YarpGateway.Extensions;
namespace YarpGateway;
public classProgram{
public staticintMain( string[] args)
var assemblyName = typeof(Program).Assembly.GetName.Name;
SerilogConfigurationHelper.Configure(assemblyName);
try
Log.Information($ "Starting {assemblyName}.");
var builder = WebApplication.CreateBuilder(args);
builder.Host
.UseSerilog
.AddYarpJson; // 添加Yarp的配置文件
// 添加Yarp反向代理ReverseProxy
builder.Services.AddReverseProxy
.LoadFromConfig(builder.Configuration.GetSection( "ReverseProxy"));
var app = builder.Build;
app.UseRouting;
app.UseEndpoints(endpoints =>
{ // 添加Yarp终端Endpoints
endpoints.MapReverseProxy;
app.Run; return0;
catch (Exception ex)
Log.Fatal(ex, $ "{assemblyName} terminated unexpectedly!"); return1;
finally
Log.CloseAndFlush;
其中:
方法 AddYarpJson是为了把 Yarp 的有关配置从 appsetting.json 独立处理网关,避免配置文件很长很长,其代码如下:
代码清单:YarpGateway/Extensions/GatewayHostBuilderExtensions.cs
namespace YarpGateway.Extensions;
public staticclassGatewayHostBuilderExtensions{
public conststringAppYarpJsonPath = "yarp.json";
public staticIHostBuilder AddYarpJson(
this IHostBuilder hostBuilder, booloptional = true, boolreloadOnChange = true, stringpath = AppYarpJsonPath)
{ returnhostBuilder.ConfigureAppConfiguration((_, builder) =>
builder.AddJsonFile(
path: AppYarpJsonPath,
optional: optional,
reloadOnChange: reloadOnChange
.AddEnvironmentVariables;
其中:
reloadOnChange = true 保证配置文件修改时, Yarp 能重新读取配置文件网关。
添加 Yarp配置文件 : yarp.json
记得保证文件的属性:
复制到输出目录:如果内容较新则复制
生成操作:内容
代码清单:YarpGateway/yarp.json
{ "ReverseProxy": { "Routes": { "Identity Service": { "ClusterId": "identityCluster", "Match": { "Path": "/api/identity/{**everything}"
}, "Ordering Service": { "ClusterId": "orderingCluster", "Match": { "Path": "/api/ordering/{**everything}"
}, "Clusters": { "identityCluster": { "Destinations": { "destination1": { "Address": ""
}, "orderingCluster": { "Destinations": { "destination1": { "Address": ""
} "destination2": { "Address": ""
} 运行
Yarp Gateway 示意图:
启动网关
在项目的 bin/net6.0 目录下打开 CMD网关,执行如下命令启动网关:
dotnet YarpGateway.dll --urls""
监听端口:7700
IdentityService
在项目的 bin/net6.0 目录下打开 CMD网关,执行如下命令启动 Web API 服务:
dotnet IdentityService.dll --urls""
监听端口:7711
OrderService
开启两个 OrderServcie 的进程网关,
在 bin/net6.0 目录下打开 CMD网关,执行如下命令启动 Web API 服务:
第一个监听端口:7721
dotnet OrderService.dll --urls""
第二个监听端口:7722
dotnet OrderService.dll --urls""测试 路由功能
打开 PostMan,创建调用服务的各种请求网关。
IdentityService
创建 GET请求 调用网关:
请求会被 转发到 IdentityService的集群节点:
OrderService
创建 GET请求 调用网关:
请求会被 转发到 OrderService的集群中如下某个节点中的一个:
Tips:
由于是两个服务网关,每个服务的进程都是独立的,数据也是独立,数据并没有共享,故测试结果可能不是你所预期的,比如:
第一步:增加数据网关,这次是由第一个服务处理的;
第二步:查询数据,如果这次查询是由第二个服务器处理的话,就会找不到刚才新增的数据网关。
当然在实际开发中,我们的数据都是从同一个数据库中读取,不会出现数据不一致的情况网关。
Tips:
由于是两个服务网关,每个服务的进程都是独立的,数据也是独立,数据并没有共享,故测试结果可能不是你所预期的,比如:
第一步:增加数据网关,这次是由第一个服务处理的;
第二步:查询数据,如果这次查询是由第二个服务器处理的话,就会找不到刚才新增的数据网关。
当然在实际开发中,我们的数据都是从同一个数据库中读取,不会出现数据不一致的情况网关。
创建 GET请求:
创建 POST请求: 参数:
"id":"10",
"name":"Order #100"}
创建 PUT请求: 参数:
"id":"10",
"name":"Order #100-1"
创建 DELETE请求:
结论
上述4种 HTTP 请求都支持网关。
gRpc
待测试...
结论
支持 gRpc
新增集群服务节点
Yarp 支持动态添加服务集群服务节点网关,只要在配置文件 yarp.json, 添加新的服务配置,Yarp会自动加载新的服务节点:
代码清单:yarp.json
"ReverseProxy":{
"Routes":{
"Identity Service":{
"ClusterId":"identityCluster",
"Match":{
"Path":"/api/identity/{**everything}"
"Clusters":{
"orderingCluster":{
"Destinations":{
"destination1":{
"Address":""
+ "destination2":{
+ "Address":""
添加上述配置后网关,会看到如下日志信息:
14:51:11 DBG] Destination 'destination2' has been added.
[14:51:11 DBG] Existing client reused for cluster 'orderingCluster'. 结论
Yarp 会重新加载配置,使得新增的集群新服务节点生效网关。
删除集群服务节点
删除集群下的某个服务节点
- "destination2": {
- "Address": " }
Yarp 会重新加载配置,该集群服务节点被删除网关。
[ 14: 41: 26DBG] Destination 'destination2'has been removed.
[ 14: 41: 26DBG] Existing client reused forcluster 'orderingCluster'. 结论
Yarp 会重新加载配置,使得被删除的集群服务节点配置失效网关。
某集群节点因故障离线
把监听7722端口的服务终止网关,请求还是会发送到这个端口程序上!!!
结论
Yarp 默认不会做健康检查
相关:
主动和被动健康检查 :
完成上一节的练习后网关,还遗留了一个问题:
如何通过 YarpGateway 访问内部服务的Swagger呢网关?
问题:无法访问内部服务 Swagger
外部访问 IdentityService 和 OrderService 是通过 网关:YarpGateway 访问的网关,使用者这个并不知道这个两个服务的具体地址,也就是不知道如何访问它们的 Swagger,那么:
如何通过 YarpGateway 访问这两个服务的Swagger呢网关?
实现原理
使用网关内部服务的 Swagger 信息网关,其地址为:
例如网关,OrderService 服务的 Swagger 信息为:
在网关中使用内部服务的 Swagger 终点,再注册 Swagger 终点网关。
访问 OrderService 服务的 Swagger 信息地址:
返回如下信息:(只列举部分数据)
"openapi":"3.0.1",
"info":{
"title":"Identity Service",
"version":"v1"
"paths":{
"/api/identity/users":{
"get":{
"tags":[
"User"
"responses":{
"200":{
"deion":"Success",
"content":{
"text/plain":{
"schema":{
"type":"array",
"items":{
"$ref":"#/components/schemas/IdentityService.Models.User"
"application/json":{
"schema":{
"type":"array",
"items":{
"$ref":"#/components/schemas/IdentityService.Models.User"
"text/json":{
"schema":{
"type":"array",
"items":{
"$ref":"#/components/schemas/IdentityService.Models.User"
访问 OrderService 服务的 Swagger 信息地址:
返回如下信息:(只列举部分数据)
"openapi":"3.0.1",
"info":{
"title":"Identity Service",
"version":"v1"
"paths":{
"/api/identity/users":{
"get":{
"tags":[
"User"
"responses":{
"200":{
"deion":"Success",
"content":{
"text/plain":{
"schema":{
"type":"array",
"items":{
"$ref":"#/components/schemas/IdentityService.Models.User"
"application/json":{
"schema":{
"type":"array",
"items":{
"$ref":"#/components/schemas/IdentityService.Models.User"
"text/json":{
"schema":{
"type":"array",
"items":{
"$ref":"#/components/schemas/IdentityService.Models.User"
网关要请求内部服务的Swagger 信息,这是跨域请求,所以要求两个服务支持对网关的跨域请求网关。
在 IdentityService和 OrderService项目中都做如下修改:
添加跨域配置
在 appsettins.json 文件中添加跨域配置:
"App":{
"CorsOrigins":"跨域请求 其中,这个地址 就是 网关的地址
网关。 支持跨域
修改 Program.cs 文件:
IConfiguration configuration = builder.Configuration;
builder.Services.AddCors(options =>
options.AddDefaultPolicy(builder =>
builder
.WithOrigins(
configuration[ "App:CorsOrigins"]
.Split( ",", StringSplitOptions.RemoveEmptyEntries)
.ToArray
.SetIsOriginAllowedToAllowWildcardSubdomains
.AllowAnyHeader
.AllowAnyMethod
.AllowCredentials;
app.UseRouting;
+ app.UseCors; // 添加跨域支持
app.UseSwagger;
app.UseSwaggerUI;
..... 网关添加 Swagger
在网关项目【YarpGateway】中做如下修改:
代码清单:YarpGateway/Program.cs
builder.Services.AddControllers; //Web MVC
builder.Services.AddSwaggerGen(options =>
options.SwaggerDoc( "v1", new OpenApiInfo
Title = "Gateway", Version = "v1"
options.DocInclusionPredicate((docName, deion) => true);
options.CustomSchemaIds(type => type.FullName);
...... // 添加内部服务的Swagger终点
app.UseSwaggerUIWithYarp; //访问网关地址
网关,自动跳转到 /swagger 的首页 app.UseRewriter(new RewriteOptions
// Regex for "", "/" and "" (whitespace)
.AddRedirect( "^(|\\|\\s+)$", "/swagger"));
app.UseRouting;
其中
网关,调用方法 app.UseSwaggerUIWithYarp;的目的是:添加内部服务的Swagger终点,其代码如下: 代码清单:YarpGateway/Extensions/YarpSwaggerUIBuilderExtensions.cs
using Yarp.ReverseProxy.Configuration;
namespace YarpGateway.Extensions;
public staticclassYarpSwaggerUIBuilderExtensions{
public staticIApplicationBuilder UseSwaggerUIWithYarp(this IApplicationBuilder app)
var serviceProvider = app.ApplicationServices;
app.UseSwagger;
app.UseSwaggerUI(options =>
var configuration = serviceProvider.GetRequiredService<IConfiguration>;
var logger = serviceProvider.GetRequiredService<ILogger<Program>>;
var proxyConfigProvider = serviceProvider.GetRequiredService<IProxyConfigProvider>;
var yarpConfig = proxyConfigProvider.GetConfig;
var routedClusters = yarpConfig.Clusters
.SelectMany(t => t.Destinations,
(clusterId, destination) => new { clusterId.ClusterId, destination.Value });
var groupedClusters = routedClusters
.GroupBy(q => q.Value.Address)
.Select(t => t.First)
.Distinct
.ToList;
foreach (var clusterGroup in groupedClusters)
var routeConfig = yarpConfig.Routes.FirstOrDefault(q =>
q.ClusterId == clusterGroup.ClusterId); if(routeConfig == null)
logger.LogWarning($ "Swagger UI: Couldn't find route configuration for {clusterGroup.ClusterId}..."); continue;
options.SwaggerEndpoint($ "{clusterGroup.Value.Address}/swagger/v1/swagger.json", $ "{routeConfig.RouteId} API");
options.OAuthClientId(configuration[ "AuthServer:SwaggerClientId"]);
options.OAuthClientSecret(configuration[ "AuthServer:SwaggerClientSecret"]);
}); returnapp;
关键代码:
options.SwaggerEndpoint($ "{clusterGroup.Value.Address}/swagger/v1/swagger.json", $ "{routeConfig.RouteId} API");
通过 IProxyConfigProvider得到内部服务的信息
网关,如下图所示:
然后
,拼接出内部服务的 Swagger 信息地址, $ "{clusterGroup.Value.Address}/swagger/v1/swagger.json"
最终得到两个服务的Swagger信息地址:
IdentityServer 的 Swagger 信息地址:
OrderService 的 Swagger 信息地址:
最后,根据信息添加Swagger终点:
options.SwaggerEndpoint(
$ "{clusterGroup.Value.Address}/swagger/v1/swagger.json",
$ "{routeConfig.RouteId} API");
其中
网关, routeConfig.RouteId : Identity Service 或 Ordering Service
访问网关 Swagger
访问网关地址:
自动跳转到其 Swagger首页:
右上角有个下拉框
,可以选择不同的服务的Swagger,这里切换到 OrderService 的Swagger,如下图所示:
在网关 Swagger 调用服务接口
可以在网关 Swagger 调用内部服务接口
网关,如下图所示:
返回:
评论