• 第二节:Consul简介及服务注册、发现、健康检查


    一. 简介

    (需要补图的。。。。)

    1. 什么是Consul?

      Consul是一个用来实现分布式系统的服务发现与配置的开源工具,它的可以实现服务提供者 和 服务消费者的隔离,比如:比如服务提供者(GoodsService)将自身注册到Consul中, 注册的信息是:ServiceName + ip/port,这样服务消费者只需要知道ServiceName就可以知道对应服务的ip+端口,从而进行访问,就好比DNS的功能。

    PS. 和Consul同类的还有:zookeeper、etcd、Eureka。

    2. Consul的好处

      同一个ServiceName下可以对应多个 ip+port,只要ServiceID不同即可,也就是说同一个项目多次注册到同一个ServiceName下,这样消费者通过ServiceName可以拿到其中一个或者多个,可以在消费者层次书实现负载均衡,即客户端的负载均衡。另外服务消费者不需要记住多个 ip+port 了,只需要记住一个ServiceName即可,即不需要关心SeviceName下的业务服务器是否增加,是否宕机的问题。

    PS:当然客户端不能直接访问Consul了,后续会通过Ocelot转发。 本节这样的设置,最终消费者还是要访问业务服务器的,正常情况下业务服务器是不对外开放,而且也不利于做授权校验,这就需要后续章节Ocelot来解决了。

    3. Consul相关信息

     (1).consul的官网:https://www.consul.io/ 最新的Server下载地址:https://www.consul.io/downloads

     (2).consul客户端UI地址:http://127.0.0.1:8500

    4. Consul的启动

     (1).开发模式:【consul.exe agent -dev】,开发模式不会持久化数据,重启之后保存的配置信息就会丢失.

     (2).生产模式:【consul.exe agent -server -bootstrap-expect 1 -data-dir d:/consul/data 】

    PS:在D盘创建consul/data文件夹,用于持久化Consul。

     (3).客户端模式:通过.Net程序来启动

    补充Consul启动的各个参数:

     a. agent:consul的核心命令,主要作用有维护成员信息、运行状态检测、声明服务以及处理请求等

     b. -server:就是代表server模式

     c. -bootstrap-expect:代表想要创建的集群数目,官方建议3或者5

     d. -data-dir:数据存储目录

     e. -client:是一个客户端服务注册的地址,可以和当前server的一致也可以是其他主机地址,提供HTTP、DNS、RPC等服务,系统默认是127.0.0.1,所以不对外提供服务,如果你要对外提供服务改成0.0.0.0

     f. -bind:集群通讯地址,集群内的所有节点到地址都必须是可达的,默认是0.0.0.0

     g. -node:代表当前node的名称,节点在集群中的名称,在一个集群中必须是唯一的,默认是该节点的主机名

     h. -ui:代表开启web 控制台

     i. -config-dir:配置文件目录,里面所有以.json结尾的文件都会被加载

    二. 应用

    前提:下面所有的测试都是基于Consul开发模式启动的,注册或者消费者都需要Nuget安装consul程序包,目前最新:Consul 0.7.2.6

    1.服务注册

     (1).含义:通俗的来说就是将一个 Api业务服务(比如 OrderService 和 GoodService), 起一个ServiceName,然后对应启动地址即ip+port对应,注册到Consul中,供别人通过ServiceName来调用。 一个ServiceName下可以注册多个 ip+port,即同一个项目可以注册在一个ServiceName下, 只要ServiceId不同即可.

     (2).以Api项目为例:在Configure管道中进行注册,这里面动态获取ip和端口(需要添加 AddCommandLine支持),然后ServiceRegister进行注册,ServiceDeregister进行注销, 注销代码中写在 applicationLifetime.ApplicationStopped.Register里,即程序退出的时候注销Consul中的服务.

     详细代码如下:

        public void Configure(IApplicationBuilder app, IWebHostEnvironment env, IHostApplicationLifetime applicationLifetime)
        {   
                // 前面core本身的代码省略
                //---------------一以下是注册Consul代码------------------
    
                string ip = Configuration["ip"];
                int port = Convert.ToInt32(Configuration["port"]);
                string serviceName = "OrderService";
                string serviceId = serviceName + Guid.NewGuid();
                using (var client = new ConsulClient(ConsulConfig))
                {
                    //注册服务到 Consul 
                    client.Agent.ServiceRegister(new AgentServiceRegistration()
                    {
                        ID = serviceId,//服务编号,不能重复,用Guid 最简单 
                        Name = serviceName,//服务的名字 
                        Address = ip,//服务提供者的能被消费者访问的ip 地址(可以被其他应用访问的地址,本地测试可以用127.0.0.1,机房环境中一定要写自己的内网ip 地址) 
                        Port = port,// 服务提供者的能被消费者访问的端口 
                        //Tags      //可以配置一下额外的参数用于传递
                        Check = new AgentServiceCheck
                        {
                            DeregisterCriticalServiceAfter = TimeSpan.FromSeconds(5),//服务停止多久后注销
                            Interval = TimeSpan.FromSeconds(10),//健康检查时间间隔,或者称为心跳 间隔
                            HTTP = $"http://{ip}:{port}/api/health",//健康检查地址 
                            Timeout = TimeSpan.FromSeconds(3)   //超时时间
                        }
                    }).Wait();//Consult 客户端的所有方法几乎都是异步方法,但是都没按照规范加上       Async 后缀,所以容易误导。记得调用后要Wait()或者 await
                }
                //程序正常退出的时候从Consul 注销服务 ,要通过方法参数注入 IHostApplicationLifetime 
                applicationLifetime.ApplicationStopped.Register(() =>
                {
                    using (var client = new ConsulClient(ConsulConfig))
                    {
                        Console.WriteLine("程序正常退出的时候从Consul 注销服务 ");
                        client.Agent.ServiceDeregister(serviceId).Wait();
                    }
                });
       }
      private void ConsulConfig(ConsulClientConfiguration c)
      {
          c.Address = new Uri("http://127.0.0.1:8500");
      }        

     (3).UI页面:服务注册后,可以访问:http://127.0.0.1:8500, 来直观的查看注册情况.

    2.服务的健康检查

     (1).含义:Consul可以提供与给定服务相关的健康检查(Web服务器返回200 ok)或者本地节点(“内存利用率低于90%”),即心跳检测,如果服务不通,该服务在n秒内会自动从 Consul中注销。

     (2).如何配置:在注册的时候里面有个参数check,即配置AgentServiceCheck对象,比如参数如下:

      A.HTTP:健康检查的地址,这里仅支持http请求,接口返回ok

      B.DeregisterCriticalServiceAfter:代表服务停止多久后进行注销

      C.Interval:健康检查时间间隔,或者称为心跳 间隔

      D.Timeout:超时时间

    详见代码配置:

    Check = new AgentServiceCheck
    {
         DeregisterCriticalServiceAfter = TimeSpan.FromSeconds(5),//服务停止多久后注销
         Interval = TimeSpan.FromSeconds(10),//健康检查时间间隔,或者称为心跳 间隔
         HTTP = $"http://{ip}:{port}/api/health",//健康检查地址 
         Timeout = TimeSpan.FromSeconds(3)   //超时时间
    }

    健康检查的api接口(这里用的是Restful风格):

        [Route("api/[controller]")]
        [ApiController]
        public class HealthController : ControllerBase
        {
            [HttpGet]
            public IActionResult Get()
            {
                return Ok("ok");
            }
        }

    3.服务发现

    (1). 含义

      通俗的来说,就是通过消费者可以通过连接Consul获取到所有注册在线ServiceName或者指定的ServiceName,然后根据ServiceName拿到其对应的ip+port(1个或多个),从而进行访问。

    (2). 应用

     前提准备:将GoodsService项目注册在Consul中,服务名是GoodsService,注册三个端口分别是:7001 7002 7003

                将OrderService项目注册在Consul中,服务名是OrderService,注册三个端口分别是:7004 7005 7006

     A. 获取所有的服务

     B. 获取指定名称的服务,然后自己写一个随机算法,拿其中一个ip+port:

     C. 获取指定名称服务,轮训策略

     相关代码:

     using (var consulClient = new ConsulClient(c => c.Address = new Uri("http://127.0.0.1:8500")))
     {
    
                    #region 1.获取所有登记在策的consul集合
                    {
                        var services = consulClient.Agent.Services().Result.Response;
                        foreach (var service in services.Values)
                        {
                            Console.WriteLine($"id={service.ID},name={service.Service},ip={service.Address},port={service.Port} ");
                        }
                    }
                    #endregion
    
                    #region 2.随机找一个登记在册的服务进行连接(客户端负载均衡-随机策略)
                    {
                        var services = consulClient.Agent.Services().Result.Response.Values.Where(item => item.Service.Equals("OrderService", StringComparison.OrdinalIgnoreCase));
                        Random rand = new Random();
                        int index = rand.Next(services.Count());//[0,services.Count())
                        var s = services.ElementAt(index);
                        Console.WriteLine($"index={index},id={s.ID},service={s.Service},addr={s.Address},port={s.Port}");
                    }
    
                    #endregion
    
                    #region 3.轮训策略(这里只是演示,实际要考虑并发和溢出的问题)
                    {
                        Console.WriteLine(0 % 3);  //结果0
                        Console.WriteLine(1 % 3);  //结果1
                        Console.WriteLine(2 % 3);  //结果2
                        Console.WriteLine(3 % 3);  //结果1
                        Console.WriteLine(4 % 3);  //结果2
    
                        int Num = 0;
                        var services = consulClient.Agent.Services().Result.Response.Values.Where(item => item.Service.Equals("OrderService", StringComparison.OrdinalIgnoreCase));
                        for (int i = 1; i < 6; i++)
                        {
                            int index = Num++ % services.Count();
                            var s = services.ElementAt(index);
                            Console.WriteLine($"第{i}次的地址如下:");
                            Console.WriteLine($"index={index},id={s.ID},service={s.Service},addr={s.Address},port={s.Port}");
                        }
                    }
                    #endregion
    
                    #region 4.接口测试
                    {
                        var services = consulClient.Agent.Services().Result.Response.Values.Where(item => item.Service.Equals("OrderService", StringComparison.OrdinalIgnoreCase));
                        Random rand = new Random();
                        int index = rand.Next(services.Count());
                        var s = services.ElementAt(index);
    
                        var serviceProvider = new ServiceCollection().AddHttpClient().BuildServiceProvider();  //等价于以上三句话
                        IHttpClientFactory httpClientFactory = serviceProvider.GetService<IHttpClientFactory>();
                        string url2 = $"http://{s.Address}:{s.Port}/api/Buy/pOrder2";
                        var client = httpClientFactory.CreateClient();
                        var content = new StringContent("userId=001&goodId=123456&num=100", Encoding.UTF8, "application/x-www-form-urlencoded");
                        var response = client.PostAsync(url2, content).Result;
                        string result = "";
                        if (response.IsSuccessStatusCode)
                        {
                            result = response.Content.ReadAsStringAsync().Result;
                            Console.WriteLine(result);
                        }
                    }
                    #endregion
    
                }
                Console.ReadKey();
    }

     运行结果:

    !

    • 作       者 : Yaopengfei(姚鹏飞)
    • 博客地址 : http://www.cnblogs.com/yaopengfei/
    • 声     明1 : 如有错误,欢迎讨论,请勿谩骂^_^。
    • 声     明2 : 原创博客请在转载时保留原文链接或在文章开头加上本人博客地址,否则保留追究法律责任的权利。
     
  • 相关阅读:
    mysql 45讲 索引的使用 09-11
    mysql 45讲 相关锁的概念 06-08
    mysql 45讲 深入浅出索引04-05
    mysql 45讲 概览 01-03
    AQS源码解析第二回
    面试相关-怎么实现限流功能
    人工智能必备数学基础:线性代数基础(2)
    Elasticsearch问题总结和解决方法
    spring boot中打印所有日志
    Java中Stream流里面的findFirst()和findAny()区别
  • 原文地址:https://www.cnblogs.com/yaopengfei/p/12914690.html
Copyright © 2020-2023  润新知