Net5 Consul、Ocelot、Polly

1 简单介绍

Consul 可以实现服务的注册,服务发现,治理,健康检查等。
Ocelot 作为一个网关应用,主要的功能有路由、请求聚合、服务发现、统一认证、统一鉴权、限流熔断、并内置了负载均衡器等的集成。
Polly 是一个 .NET 弹性和瞬态故障处理库,允许开发人员以线程安全的方式来实现重试、断路、超时、隔离和回退策略。

2 Consul

2.1 安装Consul

# 拉取consul镜像
docker pull consul
# 启动容器
docker run --name consul -d -p 8500:8500 consul

访问8500即可打开Consul Web界面
consul web 界面

2.2 编写注册Consul帮助类

引用Consul包

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <TargetFramework>net5.0</TargetFramework>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Consul" Version="1.6.10.4" />
    <PackageReference Include="Microsoft.AspNetCore.Http.Abstractions" Version="2.2.0" />
    <PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="6.0.0" />
    <PackageReference Include="Microsoft.Extensions.Options" Version="6.0.0" />
  </ItemGroup>

</Project>

新建注册信息相关类 ConsulOption.cs

    /// <summary>
    /// Consul 注册发现相关参数
    /// </summary>
    public class ConsulOption
    {
        /// <summary>
        /// 服务 必填
        /// </summary>
        public Service Service { get; set; }
        /// <summary>
        /// 服务健康检查地址 默认:/health
        /// </summary>
        public string ServiceHealthCheck { get; set; } = "/health";

        /// <summary>
        /// Consul 地址 默认:http://localhost:8500/
        /// </summary>
        public string ConsulAddress { get; set; } = "http://localhost:8500/";
    }

    /// <summary>
    /// 服务
    /// </summary>
    public class Service
    {
        /// <summary>
        /// http
        /// </summary>
        public string ServiceHead { get; set; } = "http";
        /// <summary>
        /// 服务名称
        /// </summary>
        public string ServiceName { get; set; }

        /// <summary>
        /// 服务IP
        /// </summary>
        public string ServiceIP { get; set; }

        /// <summary>
        /// 服务端口
        /// </summary>
        public int ServicePort { get; set; }
    }

编写Consul注册扩展方法,包括心跳检查 X22ConsulExtension.cs

    public static class X22ConsulExtension
    {
        /// <summary>
        /// 配置文件注册Consul
        /// </summary>
        /// <param name="app"></param>
        /// <param name="configuration"></param>
        /// <returns></returns>
        public static IApplicationBuilder RegisterConsul(this IApplicationBuilder app, IConfiguration configuration)
        {
            var serviceName = configuration["ServiceName"];
            var consulAddress = configuration["ConsulAddress"];
            var uri = new Uri(configuration["HealthUrl"]);

            var consulOption = new ConsulOption
            {
                Service = new Service
                {
                    ServiceName = serviceName,
                    ServiceHead = uri.Scheme,
                    ServiceIP = uri.Host,
                    ServicePort = uri.Port,
                },
                ServiceHealthCheck = uri.PathAndQuery,
                ConsulAddress = consulAddress
            };
            return RegisterConsul(app, consulOption);
        }
        /// <summary>
        /// 委托Consul
        /// </summary>
        /// <param name="app"></param>
        /// <param name="option"></param>
        /// <returns></returns>
        public static IApplicationBuilder RegisterConsul(this IApplicationBuilder app, Action<ConsulOption> option)
        {
            var consulOption = new ConsulOption();
            option.Invoke(consulOption);
            return RegisterConsul(app, consulOption);
        }
        /// <summary>
        /// 注册Consul
        /// </summary>
        /// <param name="app"></param>
        /// <param name="consulOption"></param>
        /// <returns></returns>
        private static IApplicationBuilder RegisterConsul(this IApplicationBuilder app, ConsulOption consulOption)
        {
            #region 检查配置项
            if (string.IsNullOrEmpty(consulOption.Service.ServiceName) || string.IsNullOrEmpty(consulOption.Service.ServiceIP))
            {
                throw new Exception("请检查配置项!");
            }

            if (consulOption.Service.ServiceHead != "http" && consulOption.Service.ServiceHead != "https")
            {
                throw new Exception("请检查配置项!");
            }

            if (!consulOption.ServiceHealthCheck.StartsWith("/"))
            {
                throw new Exception("健康检查url必须以 / 开头!");
            }
            #endregion

            var healthUrl = $"{consulOption.Service.ServiceHead}://{consulOption.Service.ServiceIP}:{consulOption.Service.ServicePort}{consulOption.ServiceHealthCheck}";
            Console.WriteLine($"健康检查地址:{healthUrl}");

            var consulClient = new ConsulClient(x =>
            {
                x.Address = new Uri(consulOption.ConsulAddress);
            });

            var registration = new AgentServiceRegistration()
            {
                ID = Guid.NewGuid().ToString(),
                Name = consulOption.Service.ServiceName,// 服务名
                Address = consulOption.Service.ServiceIP, // 服务绑定IP
                Port = consulOption.Service.ServicePort,//, // 服务绑定端口
                Check = new AgentServiceCheck()
                {
                    DeregisterCriticalServiceAfter = TimeSpan.FromSeconds(5),//服务启动多久后注册
                    Interval = TimeSpan.FromSeconds(10),//健康检查时间间隔
                    HTTP = healthUrl,//健康检查地址
                    Timeout = TimeSpan.FromSeconds(5)
                }
            };

            // 服务注册
            consulClient.Agent.ServiceRegister(registration).Wait();

            // 应用程序终止时,服务取消注册
            var lifetime = app.ApplicationServices.GetRequiredService<IHostApplicationLifetime>();
            lifetime.ApplicationStopping.Register(() =>
            {
                consulClient.Agent.ServiceDeregister(registration.ID).Wait();
            });

            //健康检查
            app.ServiceHealthCheck(consulOption.ServiceHealthCheck);
            return app;
        }

        /// <summary>
        /// 健康检查接口
        /// </summary>
        /// <param name="app"></param>
        /// <param name="healthUrl"></param>
        private static void ServiceHealthCheck(this IApplicationBuilder app,string healthUrl)
        {
            app.Map(healthUrl,
                app => app.Run(async context =>
                {
                    context.Response.StatusCode = (int)HttpStatusCode.OK;
                    await context.Response.WriteAsync("Hello ServiceHealthCheck OK");
                })
            );
        }
    }

2.3 编写WebService Api

新建net5 web api 项目
在Startup.cs 注册Consul(通过上述扩展方法X22ConsulExtension)

#region 注册Consul
////dotnet run --urls="http://*:5728" --ip=192.168.0.111 --port=5728
//var ip = Configuration["ip"];
//var port = Convert.ToInt32(Configuration["port"]);
//var name = "X22ServiceTest";
//app.RegisterConsul(x => x.Service = new Service { ServiceIP = ip, ServicePort = port, ServiceName = name });

//dotnet run --urls="http://*:5728" --ip=192.168.0.111 --port=5728 --HealthUrl="http://192.168.0.111:5728/check" --ServiceName=X22ServiceTest --ConsulAddress=http://192.168.152.128:8500
app.RegisterConsul(Configuration);
#endregion

编写api

//WeatherForecastController.cs

[ApiController]
[Route("[controller]/[action]")]
public class WeatherForecastController : ControllerBase
{
    private readonly IConfiguration _configuration;

    public WeatherForecastController(IConfiguration configuration)
    {
        _configuration = configuration;
    }
    [HttpGet]
    public string Test(int time = 0)
    {
        //time 用于测试超时熔断
        Thread.Sleep(time * 1000);
        return $"{_configuration["ip"]}:{_configuration["port"]}";
    }
}

分别运行启动项目 ip 端口 心跳检测地址 服务名称 Consul地址

dotnet run --urls="http://*:5001" --ip=192.168.0.111 --port=5001 --HealthUrl="http://192.168.0.111:5001/check" --ServiceName=X22ServiceTest --ConsulAddress=http://192.168.152.128:8500

dotnet run --urls="http://*:5002" --ip=192.168.0.111 --port=5002 --HealthUrl="http://192.168.0.111:5002/check" --ServiceName=X22ServiceTest --ConsulAddress=http://192.168.152.128:8500

dotnet run --urls="http://*:5003" --ip=192.168.0.111 --port=5003 --HealthUrl="http://192.168.0.111:5003/check" --ServiceName=X22ServiceTest --ConsulAddress=http://192.168.152.128:8500

浏览器可访问,均正确返回信息
http://192.168.0.111:5001/WeatherForecast/Test?time=0
http://192.168.0.111:5001/WeatherForecast/Test?time=0
http://192.168.0.111:5001/WeatherForecast/Test?time=0
eg.运行界面
此时访问consul 8500 地址
eg.consul web 界面

3 Ocelot、Polly

3.1 新建网关项目 GetWay ,并引用相关包

<Project Sdk="Microsoft.NET.Sdk.Web">

  <PropertyGroup>
    <TargetFramework>net5.0</TargetFramework>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Ocelot" Version="17.0.1" />
    <PackageReference Include="Ocelot.Provider.Consul" Version="17.0.1" />
    <PackageReference Include="Ocelot.Provider.Polly" Version="17.0.1" />
    <PackageReference Include="Swashbuckle.AspNetCore" Version="5.6.3" />
  </ItemGroup>

</Project>

3.2 注册并使用Ocelot、Consul、Polly

//Startup.cs

public void ConfigureServices(IServiceCollection services)
{
    services.AddOcelot().AddConsul().AddPolly();
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    app.UseOcelot();
}
//Program.cs

public static IHostBuilder CreateHostBuilder(string[] args) =>
    Host.CreateDefaultBuilder(args)
        .ConfigureAppConfiguration(c =>
        {
            //加载ocelot配置文件
            c.AddJsonFile("ocelot.json", optional: false, reloadOnChange: true);
        })
        .ConfigureWebHostDefaults(webBuilder =>
        {
            webBuilder.UseStartup<Startup>();
        });

3.3 添加 ocelot.json 配置文件

{
  "Routes": [
    {
      //转发到下游服务地址--url变量
      "DownstreamPathTemplate": "/{url}",
      //下游http协议
      "DownstreamScheme": "http",
      //负载方式,
      "LoadBalancerOptions": {
        "Type": "RoundRobin" // 轮询
        //"RoundRobin:轮询机制,循环找到可以用的服务" 
        //"LeastConnection:最少连接数,跟踪发现现在有最少请求或处理的可用服务",
        //"NoLoadBalancer:不使用负载均衡,直接访问config配置或者服务发现的第一个可用服务"
      },
      //"DownstreamHostAndPorts": [
      //  {
      //    "Host": "192.168.0.111",
      //    "Port": 5001 //服务端口
      //  }, //可以多个,自行负载均衡
      //  {
      //    "Host": "192.168.0.111",
      //    "Port": 5002 //服务端口
      //  },
      //  {
      //    "Host": "192.168.0.111",
      //    "Port": 5003 //服务端口
      //  }
      //],
      //上游地址
      "UpstreamPathTemplate": "/X22/{url}", //网关地址--url变量   //冲突的还可以加权重Priority
      "UpstreamHttpMethod": [ "GET", "POST", "DELETE", "PUT" ],
      "UseServiceDisConvery": true, //使用服务发现
      "ServiceName": "X22ServiceTest", //Consul服务名称
      ////限流配置
      //"RateLimitOptions": {
      //  "ClientWhitelist": [ "admin" ], // 白名单
      //  "EnableRateLimiting": true, // 是否启用限流
      //  "Period": "10s", // 统计时间段:1s, 5m, 1h, 1d
      //  "PeriodTimespan": 10, // 多少秒之后客户端可以重试
      //  "Limit": 5 // 在统计时间段内允许的最大请求数量
      //}, 
      //熔断设置
      "QoSOptions": {
        "ExceptionsAllowedBeforeBreaking": 3, //允许多少个异常请求
        "DurationOfBreak": 5000, // 熔断的时间5s,单位为ms
        "TimeoutValue": 5000 //单位ms,如果下游请求的处理时间超过多少则自如将请求设置为超时 默认90秒
      }
    }
  ],
  "GlobalConfiguration": {
    //Ocelot应用地址
    "BaseUrl": "http://192.168.0.111:5000",
    "ServiceDiscoveryProvider": {
      //Consul地址
      "Host": "192.168.152.128",
      //Consul端口
      "Port": 8500,
      "Type": "Consul" //由Consul提供服务发现,每次请求Consul
    } //,
    ////限流
    //"RateLimitOptions": {
    //  "QuotaExceededMessage": "errr:request fast!", //限流后响应内容
    //  "HttpStatusCode": 666, //http状态码可以自定义
    //  "ClientIdHeader": "client_id" // 用来识别客户端的请求头,默认是 ClientId
    //}
  }
}

3.4 Over

浏览器访问 http://localhost:5000/X22/WeatherForecast/Test?time=0
会自动转发到相应路由地址
eg.consul web 界面

4 源码下载

posted @   Boring246  阅读(455)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· Manus爆火,是硬核还是营销?
· 终于写完轮子一部分:tcp代理 了,记录一下
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
点击右上角即可分享
微信分享提示