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