Consul- 服务注册发现
服务发现是什么
类似DNS服务器会根据我们的域名解析出一个Ip地址,然后去请求这个Ip来获取我们想要的数据,它可以让我们只需说我想要什么服务即可,而不必去关心服务提供者的具体网络位置(IP 地址、端口等)。
在传统单体架构中,由于应用不会频繁的更新和发布,也不会进行自动伸缩,我们通常将所有的服务地址都直接写在项目的配置文件中,发生变化时,手动修改。但是在微服务模式下,服务会更细的拆分解耦,微服务会被频繁的更新和发布、动态伸缩、动态迁移。服务发现也就成了微服务中的一个至关重要的环节。
客户端模式、服务端模式
目前,服务发现主要分为两种模式,客户端模式与服务端模式
在客户端模式下,首先要到服务注册中心获取服务列表,然后使用本地的负载均衡策略选择一个服务进行调用。
而在服务端模式下,客户端直接向服务注册中心发送请求,服务注册中心再通过自身负载均衡策略对微服务进行调用后返回给客户端。
客户端模式相对来说比较简单,也比较容易实现,本文就先来介绍一下基于Consul的客户端服务发现。
Consul简介
Consul是go语言开发的开源工具,用于实现分布式系统的服务发现与配置。比如:比如服务提供者(GoodsService)将自身注册到Consul中, 注册的信息是:ServiceName + ip/port,这样服务消费者只需要知道ServiceName就可以知道对应服务的ip+端口,从而进行访问,就好比DNS的功能。
Consul的安装包仅包含一个可执行文件,部署非常方便,直接从 官网) 下载即可。
同一个ServiceName下可以对应多个 ip+port,只要ServiceID不同即可,也就是说同一个项目多次注册到同一个ServiceName下,这样消费者通过ServiceName可以拿到其中一个或者多个,可以在消费者层次书实现负载均衡,即客户端的负载均衡。另外服务消费者不需要记住多个 ip+port 了,只需要记住一个ServiceName即可,即不需要关心SeviceName下的业务服务器是否增加,是否宕机的问题。
Consul下载地址:https://www.consul.io/downloads
consul客户端UI地址:http://127.0.0.1:8500
Consul案例源码:https://gitee.com/fan-microservices/consul
Consul主要做三件事:
提供服务到ip地址的注册;提供服务到ip地址列表的查询;对提供服务方的健康检查(HealthCheck)
启动Consul
开发环境
开发模式不会持久化数据,重启之后保存的配置信息就会丢失
consul.exe agent -dev
http://127.0.0.1:8500 -- Consul监控页面
生产环境
consul.exe agent -server -bootstrap-expect 1 -data-dir d:/consul/data
在D盘创建consul/data文件夹,用于持久化Consul。
Consul启动的各个参数
- agent:consul的核心命令,主要作用有维护成员信息、运行状态检测、声明服务以及处理请求等
- -server:就是代表server模式
- -bootstrap-expect:代表想要创建的集群数目,官方建议3或者5
- -data-dir:数据存储目录
- -client:是一个客户端服务注册的地址,可以和当前server的一致也可以是其他主机地址,提供HTTP、DNS、RPC等服务,系统默认是127.0.0.1,所以不对外提供服务,如果你要对外提供服务改成0.0.0.0
- -bind:集群通讯地址,集群内的所有节点到地址都必须是可达的,默认是0.0.0.0
- -node:代表当前node的名称,节点在集群中的名称,在一个集群中必须是唯一的,默认是该节点的主机名
- -ui:代表开启web 控制台
- -config-dir:配置文件目录,里面所有以.json结尾的文件都会被加载
服务注册
在服务启动的时候,进行服务注册
1、安装nuget:Consul
2、注册服务,在项目启动的时候调用:ConsulHelper.ConfulRegist(configuration)
/// <summary>
/// Consul服务注册
/// </summary>
/// <param name="configuration"></param>
public static void ConsulRegist(IConfiguration configuration, IHostApplicationLifetime applicationLifetime)
{
ConsulClient client = new ConsulClient(c =>
{
c.Address = new Uri("http://localhost:8500/");
c.Datacenter = "dc1";
});
string ip = string.IsNullOrWhiteSpace(configuration["ip"]) ? "127.0.0.1" : configuration["ip"];
int port = int.Parse(configuration["port"]);//命令行参数必须传入
int weight = string.IsNullOrWhiteSpace(configuration["weight"]) ? 1 : int.Parse(configuration["weight"]);
string serviceID = "service" + Guid.NewGuid();
client.Agent.ServiceRegister(new AgentServiceRegistration()
{
ID = serviceID,//服务编号,不能重复,用Guid 最简单
Name = "OrderService",//服务名
Address = ip,//服务提供者的能被消费者访问的ip
Port = port,// 服务提供者的能被消费者访问的端口
Tags = new string[] { weight.ToString() },//标签,可以配置一下额外的参数用于传递
Check = new AgentServiceCheck()
{
Interval = TimeSpan.FromSeconds(12),//健康检查时间间隔,或者称为心跳 间隔
HTTP = $"http://{ip}:{port}/",//健康检查地址
Timeout = TimeSpan.FromSeconds(5),//检测等待时间
DeregisterCriticalServiceAfter = TimeSpan.FromSeconds(120)//服务停止多久后注销
}
}).Wait();//Consult 客户端的所有方法几乎都是异步方法,但是都没按照规范加上 Async 后缀,所以容易误导。记得调用后要Wait()或者 await
//命令行参数获取
Console.WriteLine($"注册成功:{ip}:{port}--weight:{weight}");
//程序正常退出的时候从Consul 注销服务 ,要通过方法参数注入 IHostApplicationLifetime
applicationLifetime.ApplicationStopped.Register(() =>
{
Console.WriteLine("程序正常退出的时候从Consul 注销服务 ");
client.Agent.ServiceDeregister(serviceID).Wait();
});
}
3、启动项目
启动3个实例。ip、端口、权重通过命令行传入
dotnet run --urls=http://127.0.0.1:5001 --ip=127.0.0.1 --port=5001 --weight=5
dotnet run --urls=http://127.0.0.1:5002 --ip=127.0.0.1 --port=5002 --weight=3
dotnet run --urls=http://127.0.0.1:5003 --ip=127.0.0.1 --port=5003 --weight=2
4、查看consul控制台
点击OrderService服务,里面有三个实例
服务发现
通过Consule获取指定服务名对应的ip、端口,进行调用
/// <summary>
/// 获取服务真实地址
/// </summary>
/// <param name="url"></param>
/// <returns></returns>
public static string GetUrl(string url)
{
ConsulClient client = new ConsulClient(c =>
{
c.Address = new Uri("http://localhost:8500/");
c.Datacenter = "dc1";
});
var response = client.Agent.Services().Result.Response;//获取服务清单
Uri uri = new Uri(url);
string serviceName = uri.Host;
AgentService agentService = null;
var dictionary = response.Where(s => s.Value.Service.Equals(serviceName, StringComparison.OrdinalIgnoreCase)).ToArray();
//{
// //agentService = dictionary[0].Value;//写死第一个 死薅羊毛
//}
//{
// ////轮询策略 也是平均,但是太僵硬了
// //agentService = dictionary[iIndex++ % dictionary.Length].Value;
//}
//{
// //平均策略--随机获取索引--相对就平均
// //agentService = dictionary[new Random(iIndex++).Next(0, dictionary.Length)].Value;
//}
{
//权重:3个实例能力不同,承担的压力也要不同
List<KeyValuePair<string, AgentService>> pairsList = new List<KeyValuePair<string, AgentService>>();
foreach (var pair in dictionary)
{
int count = int.Parse(pair.Value.Tags?[0]);//1 5 10
for (int i = 0; i < count; i++)
{
pairsList.Add(pair);
}
}
//16个
agentService = pairsList.ToArray()[new Random(iIndex++).Next(0, pairsList.Count())].Value;
}
url = $"{uri.Scheme}://{agentService.Address}:{agentService.Port}{uri.PathAndQuery}";
return url;
}
HttpClient+Consul实现服务发现
string url = "http://OrderService/home/index";
var httpClient = new HttpClient(new ConsulDiscoveryDelegatingHandler(new HttpClientHandler()));
var responseMessage = httpClient.GetAsync(url).Result;
if (responseMessage.StatusCode==HttpStatusCode.OK)
{
result = responseMessage.Content.ReadAsStringAsync().Result;
}
public class ConsulDiscoveryDelegatingHandler : DelegatingHandler
{
public ConsulDiscoveryDelegatingHandler(HttpClientHandler innerHandler):base(innerHandler)
{ }
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
var current = request.RequestUri;
try
{
//调用的服务地址里的域名(主机名)传入发现的服务名称即可
request.RequestUri = new Uri($"{current.Scheme}://{LookupService(current.Host)}{current.PathAndQuery}");
return await base.SendAsync(request, cancellationToken).ConfigureAwait(false);
}
catch (Exception e)
{
throw;
}
finally
{
request.RequestUri = current;
}
}
private string LookupService(string serviceName)
{
using (ConsulClient consulClient = new ConsulClient(config => config.Address = new Uri("http://localhost:8500/")))
{
var services = consulClient.Catalog.Service(serviceName).Result.Response;
if (services != null && services.Any())
{
//模拟负载均衡算法(随机获取一个地址)
int index = new Random().Next(services.Count());
var service = services.ElementAt(index);
return $"{service.ServiceAddress}:{service.ServicePort}";
}
return null;
}
}
}