Yarp Gateway
资料
GitHub: https://github.com/microsoft/reverse-proxy
YARP 文档:https://microsoft.github.io/reverse-proxy/articles/getting-started.html
主动和被动健康检查 : https://microsoft.github.io/reverse-proxy/articles/dests-health-checks.html#active-health-check
gRpc:https://microsoft.github.io/reverse-proxy/articles/grpc.html
实战项目概览
Yarp Gateway 示意图
共享类库
创建一个 .Net6.0 的类库,项目名称:Artisan.Shared.Hosting.AspNetCore, 其它项目公用方法放在这个项目。
Serilog 日志
需要的包:
<PackageReference Include="Serilog.AspNetCore" Version="5.0.0" />
<PackageReference Include="Serilog.Sinks.Async" Version="1.5.0" />
<PackageReference Include="Serilog.Sinks.Console" Version="4.0.1" />
<PackageReference Include="Serilog.Sinks.File" Version="5.0.0" />
代码清单:Artisan.Shared.Hosting.AspNetCore/SerilogConfigurationHelper.cs
using Serilog;
using Serilog.Events;
namespace Artisan.Shared.Hosting.AspNetCore;
public static class SerilogConfigurationHelper
{
public static void Configure(string applicationName)
{
Log.Logger = new LoggerConfiguration()
#if DEBUG
.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 class Program
{
public static int Main(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, description) => 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();
return 0;
}
catch (Exception ex)
{
Log.Fatal(ex, $"{assemblyName} terminated unexpectedly!");
return 1;
}
finally
{
Log.CloseAndFlush();
}
}
}
其中:
SerilogConfigurationHelper.Configure(assemblyName);
是配置 Serilog 日志:引用上面创建的共享项目:【Artisan.Shared.Hosting.AspNetCore】
User 实体
代码清单:IdentityService/Models/User.cs
public class User
{
public int Id { get; set; }
public string Name { 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 class UserController : Controller
{
private readonly ILogger<UserController> _logger;
private static List<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;
}
[HttpGet]
public async Task<List<User>> GetAllAsync()
{
return await Task.Run(() =>
{
return Users;
});
}
[HttpGet]
[Route("{id}")]
public async Task<User> GetAsync(int id)
{
return await Task.Run(() =>
{
var entity = Users.FirstOrDefault(p => p.Id == id);
if (entity == null)
{
throw new Exception($"未找到用户:{id}");
}
return entity;
});
}
[HttpPost]
public async Task<User> CreateAsync(User user)
{
return await Task.Run(() =>
{
Users.Add(user);
return user;
});
}
[HttpPut]
[Route("{id}")]
public async Task<User> UpdateAsync(int id, User user)
{
return await Task.Run(() =>
{
var entity = Users.FirstOrDefault(p => p.Id == id);
if(entity == null)
{
throw new Exception($"未找到用户:{id}");
}
entity.Name = user.Name;
return entity;
});
}
[HttpDelete]
[Route("{id}")]
public async Task<User> DeleteAsync(int id)
{
return await Task.Run(() =>
{
var entity = Users.FirstOrDefault(p => p.Id == id);
if (entity == null)
{
throw new Exception($"未找到用户:{id}");
}
Users.Remove(entity);
return entity;
});
}
}
}
OrderService
创建一个【AspNetCore Web Api】项目,项目名称为:OrderService
Program
代码清单:OrderService/Program.cs
using Artisan.Shared.Hosting.AspNetCore;
using Microsoft.OpenApi.Models;
using Serilog;
namespace OrderService;
public class Program
{
public static int Main(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, description) => 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();
return 0;
}
catch (Exception ex)
{
Log.Fatal(ex, $"{assemblyName} terminated unexpectedly!");
return 1;
}
finally
{
Log.CloseAndFlush();
}
}
}
Order 实体
代码清单:OrderService/Models/Order.cs
public class Order
{
public string Id { get; set; }
public string Name { 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 class OrderController : Controller
{
private readonly ILogger<OrderController> _logger;
private static List<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;
}
[HttpGet]
public async Task<List<Order>> GetAllAsync()
{
return await Task.Run(() =>
{
return Orders;
});
}
[HttpGet]
[Route("{id}")]
public async Task<Order> GetAsync(string id)
{
return await Task.Run(() =>
{
var entity = Orders.FirstOrDefault(p => p.Id == id);
if (entity == null)
{
throw new Exception($"未找到订单:{id}");
}
return entity;
});
}
[HttpPost]
public async Task<Order> CreateAsync(Order order)
{
return await Task.Run(() =>
{
Orders.Add(order);
return order;
});
}
[HttpPut]
[Route("{id}")]
public async Task<Order> UpdateAsync(string id, Order Order)
{
return await Task.Run(() =>
{
var entity = Orders.FirstOrDefault(p => p.Id == id);
if (entity == null)
{
throw new Exception($"未找到订单:{id}");
}
entity.Name = Order.Name;
return entity;
});
}
[HttpDelete]
[Route("{id}")]
public async Task<Order> DeleteAsync(string id)
{
return await Task.Run(() =>
{
var entity = Orders.FirstOrDefault(p => p.Id == id);
if (entity == null)
{
throw new Exception($"未找到订单:{id}");
}
Orders.Remove(entity);
return entity;
});
}
}
}
创建网关
创建一个【AspNetCore 空】项目,项目名称为:YarpGateway
引用包
<PackageReference Include="Yarp.ReverseProxy" Version="1.1.0" />
添加 Yarp
代码清单:YarpGateway/Program.cs
using Artisan.Shared.Hosting.AspNetCore;
using Serilog;
using YarpGateway.Extensions;
namespace YarpGateway;
public class Program
{
public static int Main(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();
return 0;
}
catch (Exception ex)
{
Log.Fatal(ex, $"{assemblyName} terminated unexpectedly!");
return 1;
}
finally
{
Log.CloseAndFlush();
}
}
}
其中:
方法AddYarpJson() 是为了把 Yarp 的有关配置从appsetting.json
独立处理,避免配置文件很长很长,其代码如下:
代码清单:YarpGateway/Extensions/GatewayHostBuilderExtensions.cs
namespace YarpGateway.Extensions;
public static class GatewayHostBuilderExtensions
{
public const string AppYarpJsonPath = "yarp.json";
public static IHostBuilder AddYarpJson(
this IHostBuilder hostBuilder,
bool optional = true,
bool reloadOnChange = true,
string path = AppYarpJsonPath)
{
return hostBuilder.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": "http://localhost:7711"
}
}
},
"orderingCluster": {
"Destinations": {
"destination1": {
"Address": "http://localhost:7721"
}
"destination2": {
"Address": "http://localhost:7722"
}
}
}
}
}
}
运行
Yarp Gateway 示意图:
启动网关
在项目的bin/net6.0
目录下打开 CMD,执行如下命令启动网关:
dotnet YarpGateway.dll --urls "http://localhost:7700"
监听端口:7700
IdentityService
在项目的bin/net6.0
目录下打开 CMD,执行如下命令启动 Web API 服务:
dotnet IdentityService.dll --urls "http://localhost:7711"
监听端口:7711
OrderService
开启两个 OrderServcie 的进程,
在 bin/net6.0
目录下打开 CMD,执行如下命令启动 Web API 服务:
第一个监听端口:7721
dotnet OrderService.dll --urls "http://localhost:7721"
第二个监听端口:7722
dotnet OrderService.dll --urls "http://localhost:7722"
测试
路由功能
打开 PostMan,创建调用服务的各种请求。
IdentityService
创建 GET 请求调用网关: http://localhost:7700/api/identity/users
请求会被转发到 IdentityService的集群节点:http://localhost:7711/api/identity/users
OrderService
创建 GET 请求调用网关: http://localhost:7700/api/ordering/orders
请求会被转发到 OrderService 的集群中如下某个节点中的一个:
支持请求类型
Tips:
由于是两个服务,每个服务的进程都是独立的,数据也是独立,数据并没有共享,故测试结果可能不是你所预期的,比如:
第一步:增加数据,这次是由第一个服务处理的;
第二步:查询数据,如果这次查询是由第二个服务器处理的话,就会找不到刚才新增的数据。
当然在实际开发中,我们的数据都是从同一个数据库中读取,不会出现数据不一致的情况。
HTTP 1.0 / 2.0
创建 GET 请求: http://localhost:7700/api/ordering/orders/1
创建 POST 请求: http://localhost:7700/api/ordering/orders 参数:
{
"id":"10",
"name":"Order #100"
}
创建 PUT 请求: http://localhost:7700/api/ordering/orders/10 参数:
{
"id":"10",
"name":"Order #100-1"
}
创建 DELETE 请求: http://localhost:7700/api/ordering/orders/10
结论
上述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": "http://localhost:7721"
},
+ "destination2": {
+ "Address": "http://localhost:7722"
+ }
}
}
}
}
}
添加上述配置后,会看到如下日志信息:
14:51:11 DBG] Destination 'destination2' has been added.
[14:51:11 DBG] Existing client reused for cluster 'orderingCluster'.
结论
Yarp 会重新加载配置,使得新增的集群新服务节点生效。
删除集群服务节点
删除集群下的某个服务节点
- "destination2": {
- "Address": "http://localhost:7722"
- }
Yarp 会重新加载配置,该集群服务节点被删除。
[14:41:26 DBG] Destination 'destination2' has been removed.
[14:41:26 DBG] Existing client reused for cluster 'orderingCluster'.
结论
Yarp 会重新加载配置,使得被删除的集群服务节点配置失效。
某集群节点因故障离线
把监听7722端口的服务终止,请求还是会发送到这个端口程序上!!!
结论
Yarp 默认不会做健康检查
相关:
主动和被动健康检查 : https://microsoft.github.io/reverse-proxy/articles/dests-health-checks.html#active-health-check