資料
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 MVCbuilder.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 MVCbuilder.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反向代理ReverseProxybuilder.Services.AddReverseProxy().LoadFromConfig(builder.Configuration.GetSection("ReverseProxy"));var app = builder.Build();app.UseRouting();app.UseEndpoints(endpoints =>{ // 添加Yarp終端Endpointsendpoints.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?的集群中如下某個節點中的一個:
http://localhost:7721/api/ordering/orders
http://localhost:7722/api/ordering/orders
支持請求類型
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
完成上一節的練習后,還遺留了一個問題:
如何通過 YarpGateway 訪問內部服務的Swagger呢?
問題:無法訪問內部服務 Swagger
外部訪問 IdentityService 和 OrderService 是通過 網關:YarpGateway 訪問的,使用者這個并不知道這個兩個服務的具體地址,也就是不知道如何訪問它們的 Swagger,那么:
如何通過 YarpGateway 訪問這兩個服務的Swagger呢?
實現原理
使用網關內部服務的 Swagger 信息,其地址為:
http://ip:port/swagger/v1/swagger.json
例如,OrderService 服務的 Swagger 信息為:
http://localhost:7721/swagger/v1/swagger.json
在網關中使用內部服務的 Swagger 終點,再注冊 Swagger 終點。
訪問 OrderService 服務的 Swagger 信息地址:http://localhost:7711/swagger/v1/swagger.json
返回如下信息:(只列舉部分數據)
{"openapi": "3.0.1","info": {"title": "Identity Service","version": "v1"},"paths": {"/api/identity/users": {"get": {"tags": ["User"],"responses": {"200": {"description": "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": "http://localhost:7700" // 網關地址,支持網關的Yarp gatewary跨域請求}}
其中,這個地址http://localhost:7700?就是網關的地址。
支持跨域
修改?Program.cs
文件:
代碼清單:IdentityService/Program.cs
代碼清單:OrderService/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, description) => 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 static class YarpSwaggerUIBuilderExtensions{public static IApplicationBuilder 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"]);}}); return app;}
}
關鍵代碼:
options.SwaggerEndpoint($"{clusterGroup.Value.Address}/swagger/v1/swagger.json", $"{routeConfig.RouteId} API");
通過?IProxyConfigProvider?得到內部服務的信息,如下圖所示:
然后,拼接出內部服務的 Swagger 信息地址,
$"{clusterGroup.Value.Address}/swagger/v1/swagger.json"
最終得到兩個服務的Swagger信息地址:
IdentityServer 的 Swagger 信息地址:
http://localhost:7711/swagger/v1/swagger.json
OrderService 的 Swagger 信息地址:
http://localhost:7721/swagger/v1/swagger.json
最后,根據信息添加Swagger終點:
options.SwaggerEndpoint($"{clusterGroup.Value.Address}/swagger/v1/swagger.json", $"{routeConfig.RouteId} API");
其中,
routeConfig.RouteId
: Identity Service 或 Ordering Service
訪問網關 Swagger
訪問網關地址:http://localhost:7700
自動跳轉到其 Swagger首頁:http://localhost:7700/swagger/index.html
右上角有個下拉框,可以選擇不同的服務的Swagger,這里切換到 OrderService 的Swagger,如下圖所示:
在網關 Swagger 調用服務接口
可以在網關 Swagger 調用內部服務接口,如下圖所示:
返回: