• 使用 Yarp 做网关 (一)


    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 的集群中如下某个节点中的一个:

    1. http://localhost:7721/api/ordering/orders
    2. 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

  • 相关阅读:
    数据结构
    SpringBoot实战
    基于Redis的分布式资源锁
    计算机网络知识
    Dubbo学习使用
    css设置图片适配:显示中间部分(居中显示)
    document.ready和onload的区别
    JS的事件委托(事件代理)
    在vue-cli项目中使用echarts
    addEventListener的第三个参数
  • 原文地址:https://www.cnblogs.com/easy5weikai/p/16313790.html
Copyright © 2020-2023  润新知