.Net Core使用Consul+Ocelot搭建微服务项目

时代在变,技术也在更新迭代。从传统的单体应用架构到现在的分布式集群架构,在技术的学习上真的是一点都不能松懈。

网上关于微服务与Consul的话题太多了,我在这里不做过多描述。

其实就是在微服务中我们可以利用Consul可以实现服务的发现、治理、健康检查等...

用它先下载它:

我此番在windows下操作,打开下载的Consul所在文件夹,输入 consul.exe agent -dev

Consul的默认启动端口为8500,如果能正常显示页面则启动成功。

 新建解决方案,建立一个.Net Core MVC的项目和一个.Net Core WebApi的项目。 安装NuGet包Consul

首先Api端服务实例启动时需到Consul中进行服务注册,Web Client直接与Consul进行连接,从Consul中拿到服务实例并配置策略及发送http请求等。

如图所示:

 Consul每隔一段时间就会调用一次注册的服务实例进行健康检查。

在Api项目中新建一个IConfiguration的扩展方法:

public static void ConsulExtend(this IConfiguration configuration)
{
    ConsulClient client = new ConsulClient(m =>
    {
        m.Address = new Uri("http://localhost:8500/");
        m.Datacenter = "dc1";
    });
    //启动的时候在consul中注册实例服务
    //在consul中注册的ip,port
    string ip = configuration["ip"];
    int port = int.Parse(configuration["port"]);
    int weight = string.IsNullOrWhiteSpace(configuration["weight"]) ? 1 : int.Parse(configuration["weight"]);
    client.Agent.ServiceRegister(new AgentServiceRegistration()
    {
        ID = "service" + Guid.NewGuid(),//唯一的
        Name = "MicroserviceAttempt",//组(服务)名称
        Address = ip,
        Port = port,//不同的端口=>不同的实例
        Tags = new string[] { weight.ToString() },//标签
        Check = new AgentServiceCheck()//服务健康检查
        {
            Interval = TimeSpan.FromSeconds(12),//间隔12s一次 检查
            HTTP = $"http://{ip}:{port}/Api/Health/Index",
            Timeout = TimeSpan.FromSeconds(5),//检测等待时间
            DeregisterCriticalServiceAfter = TimeSpan.FromSeconds(20)//失败后多久移除
        }
    });
    Console.WriteLine($"{ip}:{port}--weight:{weight}");
}

心跳检查的接口:

[ApiController]
[Route("api/[controller]/[action]")]
public class HealthController : Controller
{
    readonly IConfiguration _configuration;
    public HealthController(IConfiguration configuration)
    {
        _configuration = configuration;
    }
    [HttpGet]
    public IActionResult Index()
    {
        //心跳,consul会每隔几秒调一次
        Console.WriteLine($"{ _configuration["port"]} Invoke");
        return Ok();
    }
}

在Startup类中的Configure方法加入:

//启动时注册,且注册一次
this.Configuration.ConsulExtend();

将api项目启动(三个端口)

dotnet ServicesInstances.dll --urls="http://*:5726" --ip="127.0.0.1" --port=5726
dotnet ServicesInstances.dll --urls="http://*:5727" --ip="127.0.0.1" --port=5727
dotnet ServicesInstances.dll --urls="http://*:5728" --ip="127.0.0.1" --port=5728

接着是web端,新建控制器:

public class UserController : Controller
{readonly HttpSender _httpSender;
    public UserController(HttpSender httpSender)
    {
        _httpSender = httpSender;
    }
    //暂不考虑线程安全
    private static int index = 0;
    public async Task<IActionResult> Index()
    {
        #region nginx版 只知道nginx地址就行了
        //var str = await _httpSender.InvokeApi("http://localhost:8088/api/User/GetCustomerUser");
        #endregion

        #region consul
        //new一个consul实例
        ConsulClient client = new ConsulClient(m =>
        {
            new Uri("http://localhost:8500/");
            m.Datacenter = "dc1";
        });
        //与consul进行通信(连接),得到consul中所有的服务实例
        var response = client.Agent.Services().Result.Response;
        string url = "http://MicroserviceAttempt/api/User/GetCustomerUser";
        Uri uri = new Uri(url);
        string groupName = uri.Host;
        AgentService agentService = null;//服务实例
        var serviceDictionary = response.Where(m => m.Value.Service.Equals(groupName, StringComparison.OrdinalIgnoreCase)).ToArray();//找到的全部服务实例
        //{
        //    agentService = serviceDictionary[0].Value;
        //}
        {
            //轮询策略=>达到负载均衡的目的
            agentService = serviceDictionary[index++ % 3].Value;
        }
        {
            //平均策略(随机获取索引--相对平均)=>达到负载均衡的目的
            agentService = serviceDictionary[new Random(index++).Next(0, serviceDictionary.Length)].Value;
        }
        {
            //权重策略,给不同的实例分配不同的压力,注册时提供权重
            List<KeyValuePair<string, AgentService>> keyValuePairs = new List<KeyValuePair<string, AgentService>>();
            foreach (var item in keyValuePairs)
            {
                int count = int.Parse(item.Value.Tags?[0]);//在服务注册的时候给定权重数量
                for (int i = 0; i < count; i++)
                {
                    keyValuePairs.Add(item);
                }
            }
            agentService = keyValuePairs.ToArray()[new Random(index++).Next(0, keyValuePairs.Count())].Value;
        }
        url = $"{uri.Scheme}://{agentService.Address}:{agentService.Port}{uri.PathAndQuery}";
        string content = await _httpSender.InvokeApi(url);
        #endregion
        return View(JsonConvert.DeserializeObject<CustomerUser>(content));
    }
}
public class HttpSender
{
    public async Task<string> InvokeApi(string url)
    {
        using (HttpClient client = new HttpClient())
        {
            HttpRequestMessage message = new HttpRequestMessage();
            message.Method = HttpMethod.Get;
            message.RequestUri = new Uri(url);
            var result = client.SendAsync(message).Result;
            string content = result.Content.ReadAsStringAsync().Result;
            return content;
        }
    }
}

启动web项目,访问User-Index这个视图,会轮询不同的服务实例。

但是这样做不好,客户端都需要和Consul进行连接,拿到所有的服务实例,直接和服务实例进行交互,服务实例就暴露了--所以需要网关。

网关将服务实例与客户端进行隔离,是所有Api请求的入口。因此可以统一鉴权。当然微服务网关的作用有很多,大家可自行百度了解。

新建一个网关的项目,请求先到达网关,再由网关分发请求到不同的实例。如图:

 Consul整合GeteWay.引入NuGet包:Ocelot、Ocelot.Provider.Consul

修改Startup类:

public class Startup
{
    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public IConfiguration Configuration { get; }

    // This method gets called by the runtime. Use this method to add services to the container.
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddOcelot().AddConsul();
        //services.AddControllers();
    }

    // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        //将默认的请求管道全部丢掉
        app.UseOcelot();
        //if (env.IsDevelopment())
        //{
        //    app.UseDeveloperExceptionPage();
        //}

        //app.UseHttpsRedirection();

        //app.UseRouting();

        //app.UseAuthorization();

        //app.UseEndpoints(endpoints =>
        //{
        //    endpoints.MapControllers();
        //});
    }
}

修改Program类:

public class Program
{
    public static void Main(string[] args)
    {
        CreateHostBuilder(args).Build().Run();
    }

    public static IHostBuilder CreateHostBuilder(string[] args) =>
        Host.CreateDefaultBuilder(args)
        .ConfigureAppConfiguration(conf =>
        {
            conf.AddJsonFile("configuration.json", optional: false,
                reloadOnChange: true);
        })
            .ConfigureWebHostDefaults(webBuilder =>
            {
                webBuilder.UseStartup<Startup>();
            });
}

新建一个configuration.json的文件,用于配置策略等...

Routes中路由规则可以配置多个

{//*************************单地址多实例负载均衡+Consul*****************************
  "Routes": [
    {
      //GeteWay转发=>Downstream
      "DownstreamPathTemplate": "/api/{url}", //服务地址--url变量
      "DownstreamScheme": "http",
      //http://localhost:6299/T5/User/GetCustomerUser
      "UpstreamPathTemplate": "/T5/{url}", //网关地址--url变量 冲突的还可以加权重Priority
      "UpstreamHttpMethod": [ "Get", "Post" ],
      "UseServiceDiscovery": true, //使用服务发现
      "ServiceName": "MicroserviceAttempt", //Consul服务名称
      "LoadBalancerOptions": {
        "Type": "RoundRobin" //轮询  //"LeastConnection":最少连接数服务器   "NoloadBalance":不负载均衡     "CookieStickySession":会话粘滞
      }
    }
  ],
  "GlobalConfiguration": {
    "BaseUrl": "http://127.0.0.1:6299",
    "ServiceDiscoveryProvider": {
      "Host": "localhost",
      "Port": 8500,
      "Type": "Consul"//由Consul提供服务发现,每次请求去Consul
    }
    //"ServiceDiscoveryProvider": {
    //  "Host": "localhost",
    //  "Port": 8500,
    //  "Type": "PollConsul", //由Consul提供服务发现,每次请求去Consul
    //  "PollingInterval": 1000//轮询Consul,评率毫秒--down是不知道的
    //}
  }
  //*************************单地址多实例负载均衡+Consul*****************************
}

启动网关(项目)服务:

dotnet GateWay-Ocelot.dll --urls="http://*:6299" --ip="127.0.0.1" --port=6299

调用服务接口:

每请求一次就轮询不同的服务实例,达到负载均衡。

服务治理、熔断、降级

服务治理-缓存

引入NuGet包:Ocelot.Cache.Cache

修改ConfigureServices方法:

public void ConfigureServices(IServiceCollection services)
{
    services.AddOcelot().AddConsul()
        .AddCacheManager(m =>
        {
            m.WithDictionaryHandle();//默认字典存储
        });
    //services.AddControllers();
}

修改configuration.json文件

"Routes": [
    {
      //GeteWay转发=>Downstream
      "DownstreamPathTemplate": "/api/{url}", //服务地址--url变量
      "DownstreamScheme": "http",
      //http://localhost:6299/T5/User/GetCustomerUser
      "UpstreamPathTemplate": "/T5/{url}", //网关地址--url变量 冲突的还可以加权重Priority
      "UpstreamHttpMethod": [ "Get", "Post" ],
      "UseServiceDiscovery": true, //使用服务发现
      "ServiceName": "MicroserviceAttempt", //Consul服务名称
      "LoadBalancerOptions": {
        "Type": "RoundRobin" //轮询  //"LeastConnection":最少连接数服务器   "NoloadBalance":不负载均衡     "CookieStickySession":会话粘滞
      },
      //使用缓存
      "FileCacheOptions": {
        "TtlSeconds": 15,//过期时间
        "Region": "UserCache" //可以调用Api清理
      }
    }
  ]

再次调用会发现每隔15秒数据才会变.

自定义缓存

新建类CustomeCache

/// <summary>
/// 自定义Cache
/// </summary>
public class CustomeCache : IOcelotCache<CachedResponse>
{
    public class CacheDataModel
    {
        public CachedResponse CachedResponse { get; set; }
        public DateTime TimeOut { get; set; }
        public string Region { get; set; }
    }

    private static Dictionary<string, CacheDataModel> keyValuePairs = new Dictionary<string, CacheDataModel>();

    /// <summary>
    /// 添加
    /// </summary>
    /// <param name="key"></param>
    /// <param name="value"></param>
    /// <param name="ttl"></param>
    /// <param name="region"></param>
    public void Add(string key, CachedResponse value, TimeSpan ttl, string region)
    {
        Console.WriteLine($"调用了{nameof(CustomeCache)}--{nameof(Add)}");
        keyValuePairs[key] = new CacheDataModel()
        {
            CachedResponse = value,
            Region = region,
            TimeOut = DateTime.Now.Add(ttl)
        };
    }

    /// <summary>
    /// 覆盖
    /// </summary>
    /// <param name="key"></param>
    /// <param name="value"></param>
    /// <param name="ttl"></param>
    /// <param name="region"></param>
    public void AddAndDelete(string key, CachedResponse value, TimeSpan ttl, string region)
    {
        Console.WriteLine($"调用了{nameof(CustomeCache)}--{nameof(AddAndDelete)}");
        keyValuePairs[key] = new CacheDataModel()
        {
            CachedResponse = value,
            Region = region,
            TimeOut = DateTime.Now.Add(ttl)
        };
    }

    /// <summary>
    /// 清除
    /// </summary>
    /// <param name="region"></param>
    public void ClearRegion(string region)
    {
        Console.WriteLine($"调用了{nameof(CustomeCache)}--{nameof(ClearRegion)}");
        var keyList = keyValuePairs.Where(m => m.Value.Region.Equals(region)).Select(e => e.Key);
        foreach (var item in keyList)
        {
            keyValuePairs.Remove(item);
        }
    }

    /// <summary>
    /// 获取
    /// </summary>
    /// <param name="key"></param>
    /// <param name="region"></param>
    /// <returns></returns>
    public CachedResponse Get(string key, string region)
    {
        Console.WriteLine($"调用了{nameof(CustomeCache)}--{nameof(Get)}");
        if (keyValuePairs.ContainsKey(key) && keyValuePairs[key] != null && keyValuePairs[key].TimeOut > DateTime.Now && keyValuePairs[key].Region.Equals(region))
            return keyValuePairs[key].CachedResponse;
        else
            return null;
    }
}

在ConfigureServices方法中加入:

public void ConfigureServices(IServiceCollection services)
{
    services.AddOcelot().AddConsul()
        .AddCacheManager(m =>
        {
            m.WithDictionaryHandle();//默认字典存储
        });
    //services.AddControllers();
    //这里的IOcelotCache<CachedResponse>是默认缓存的约束--准备替换成自定义的
    services.AddSingleton<IOcelotCache<CachedResponse>, CustomeCache>();
}

调用,15秒刷新一次。

 雪崩效应:微服务架构下,单个服务的故障而引发系列服务故障。

解决:1.超时:调用服务的操作可以配置为执行超时,如果服务未能在这个给定时间内响应,将回复一个失败的消息。

           2.熔断:使用断路器来检测故障是否已得到解决,防止请求反复尝试执行一个可能会失败的操作,从而减少等待纠正故障的时间。

安装NuGet包:Ocelot.Provider.Polly

修改ConfigureServices方法:

services.AddOcelot().AddConsul()
    .AddCacheManager(m =>
    {
        m.WithDictionaryHandle();//默认字典存储
    })
    .AddPolly();
//services.AddControllers();

//这里的IOcelotCache<CachedResponse>是默认缓存的约束--准备替换成自定义的
services.AddSingleton<IOcelotCache<CachedResponse>, CustomeCache>();

修改configuration.json文件

  "Routes": [
    {
      //GeteWay转发=>Downstream
      "DownstreamPathTemplate": "/api/{url}", //服务地址--url变量
      "DownstreamScheme": "http",
      //http://localhost:6299/T5/User/GetCustomerUser
      "UpstreamPathTemplate": "/T5/{url}", //网关地址--url变量 冲突的还可以加权重Priority
      "UpstreamHttpMethod": [ "Get", "Post" ],
      "UseServiceDiscovery": true, //使用服务发现
      "ServiceName": "MicroserviceAttempt", //Consul服务名称
      "LoadBalancerOptions": {
        "Type": "RoundRobin" //轮询  //"LeastConnection":最少连接数服务器   "NoloadBalance":不负载均衡     "CookieStickySession":会话粘滞
      },
      //使用缓存
      "FileCacheOptions": {
        "TtlSeconds": 15, //过期时间
        "Region": "UserCache" //可以调用Api清理
      },
      "QoSOptions": {
        "ExceptionsAllowedBeforeBreaking": 3, //熔断之前允许多少个异常请求
        "DurationOfBreak": 10000, //熔断的时间,单位为ms.超过这个时间可再请求
        "TimeoutValue": 4000 //如果下游请求的处理时间超过多少则将请求设置为超时  默认90秒
      }
    }
  ],

故意设置接口休眠5秒钟

调用:

 限流:限制单位时间内的请求数(保护机制),超过就返回指定信息。

修改configuration.json文件

  "Routes": [
    {
      //GeteWay转发=>Downstream
      "DownstreamPathTemplate": "/api/{url}", //服务地址--url变量
      "DownstreamScheme": "http",
      //http://localhost:6299/T5/User/GetCustomerUser
      "UpstreamPathTemplate": "/T5/{url}", //网关地址--url变量 冲突的还可以加权重Priority
      "UpstreamHttpMethod": [ "Get", "Post" ],
      "UseServiceDiscovery": true, //使用服务发现
      "ServiceName": "MicroserviceAttempt", //Consul服务名称
      "LoadBalancerOptions": {
        "Type": "RoundRobin" //轮询  //"LeastConnection":最少连接数服务器   "NoloadBalance":不负载均衡     "CookieStickySession":会话粘滞
      },
      //使用缓存
      "FileCacheOptions": {
        "TtlSeconds": 15, //过期时间
        "Region": "UserCache" //可以调用Api清理
      },
      //限流  张队长贡献的
      "RateLimitOptions": {
        "ClientWhitelist": ["Microservice","Attempt"],//白名单  ClientId区分大小写
        "EnableRateLimiting": true,
        "Period": "1s", //5m 1h 1d
        "PeriodTimespan": 30,//多少秒之后客户端可以重试
        "Limit": 5 //统计时间段内允许的最大请求数
      },
      "QoSOptions": {
        "ExceptionsAllowedBeforeBreaking": 3, //熔断之前允许多少个异常请求
        "DurationOfBreak": 10000, //熔断的时间,单位为ms.超过这个时间可再请求
        "TimeoutValue": 4000 //如果下游请求的处理时间超过多少则将请求设置为超时  默认90秒
      }
    }
  ],
  "GlobalConfiguration": {
    "BaseUrl": "http://127.0.0.1:6299",
    "ServiceDiscoveryProvider": {
      "Host": "localhost",
      "Port": 8500,
      "Type": "Consul" //由Consul提供服务发现,每次请求去Consul
    },
    "RateLimitOptions": {
      "QuotaExceededMessage": "Customize Tips!", //限流时返回的消息
      "HttpStatusCode": 999 //限流时返回的code
    }
    //"ServiceDiscoveryProvider": {
    //  "Host": "localhost",
    //  "Port": 8500,
    //  "Type": "PollConsul", //由Consul提供服务发现,每次请求去Consul
    //  "PollingInterval": 1000//轮询Consul,评率毫秒--down是不知道的
    //}
  }

调用接口,超过五次就会限流:

 当设置了白名单后,就对来访的请求就不做限流机制

到此就结尾了。如有不当的地方,请谅解,希望能帮到大家。

代码已上传至我的github:

Smiling Face with Smiling Eyes on Microsoft Windows 10 May 2019 Update

posted @ 2020-06-25 19:19  江北、  阅读(2138)  评论(2编辑  收藏  举报