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

一. 简介

本节架构图:

(PS:该图仅服务于本节,完整版的微服务架构图见后最后章节)

 

 

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 1.6.1.1】

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 : 原创博客请在转载时保留原文链接或在文章开头加上本人博客地址,否则保留追究法律责任的权利。
 

 

posted @ 2020-05-19 07:36  Yaopengfei  阅读(5337)  评论(7编辑  收藏  举报