服务注册与发现作为微服务基础设施中的一部分,应该是低代码侵入,只需要通过简单的配置就可开箱即用,普通业务开发人员无需关注的功能,不然的话,服务较多,服务实例较多的情况下,服务的管理将牵扯开发人员很大的精力。
consul的服务注册有两种方式,一种是通过client agen结合配置文件进行主动注册,一种通过api的方式进行注册,个人倾向于api方式,可以免去增加或减少服务时候需要去修改配置文件。
Client agent方式这里就不讲了,大家可以参考
.NET Core微服务之基于Consul实现服务治理(续)
Consul 的主要接口是一个 RESTful HTTP API。该 API 可以对节点、服务、检查、配置等执行
基本的 CRUD 操作。
其中,服务注册的api是这个:/v1/agent/service/register
但是我们并不需要自己基于http请求去实现服务相关的操作,只需要引用两个开源的nuget包即可,里面已经基于官方提供的RESTful HTTP API封装好了consul相关操作。
下面是asp.net core接入consul服务发现具体步骤
新建一个 asp.net core web api应用
提供一个健康状态检查终结点
这里利用asp.net core自带的健康状态检查中间件,只需要在Startup类中简单配置即可
public void ConfigureServices(IServiceCollection services)
{
// 应用健康状态检查
services.AddHealthChecks();
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, IHostApplicationLifetime hostApplicationLifetime)
{
// 启用健康状态检查中间件
app.UseHealthChecks("/health");
}
启动应用后,通过 /health路径即可访问健康状态检查接口,接口内部其实就是返回一个字符串,只要接口能够正常返回即说明应用正常。
基于 Consul和Consul.AspNetCore实现自动注册
(1) 安装依赖的nuget包
install-package Consul
install-package Consul.AspNetCore
基于IServiceCollection写一个扩展,进行Consul注册相关的配置和注入,主要是为了简化配置和代码
public static class ConsulServiceCollectionExtensions
{
///
/// 向容器中添加Consul必要的依赖注入
///
///
///
///
public static IServiceCollection AddConsul(this IServiceCollection services, IConfiguration configuration)
{
// 配置consul服务注册信息
var option = configuration.GetSection("Consul").Get<ConsulOption>();
// 通过consul提供的注入方式注册consulClient
services.AddConsul(options => options.Address = new Uri($"http://{option.ConsulIP}:{option.ConsulPort}"));
// 通过consul提供的注入方式进行服务注册
var httpCheck = new AgentServiceCheck()
{
DeregisterCriticalServiceAfter = TimeSpan.FromSeconds(5),//服务启动多久后注册
Interval = TimeSpan.FromSeconds(10),//健康检查时间间隔,或者称为心跳间隔
HTTP = $"http://{option.IP}:{option.Port}/health",//健康检查地址
Timeout = TimeSpan.FromSeconds(5)
};
// Register service with consul
services.AddConsulServiceRegistration(options =>
{
options.Checks = new[] { httpCheck };
options.ID = Guid.NewGuid().ToString();
options.Name = option.ServiceName;
options.Address = option.IP;
options.Port = option.Port;
options.Meta = new Dictionary<string, string>() { { "Weight", option.Weight.HasValue ? option.Weight.Value.ToString() : "1" } };
options.Tags = new[] { $"urlprefix-/{option.ServiceName}" }; //添加
urlprefix-/servicename 格式的 tag 标签,以便 Fabio 识别
});
services.AddSingleton<IConsulServerManager, DefaultConsulServerManager>();
return services;
}
}
通过查看Consul包的源码,可以发现这里的AddConsulServiceRegistration就是添加一个主机服务,在应用启动的时候向consul进行注册,在应用关闭的时候结束注册.
而注册的时候就是调用consul提供的RESTful api接口
其中的ConsulOption类如下:
public class ConsulOption
{
///
/// 当前应用IP
///
public string IP { get; set; }
///
/// 当前应用端口
///
public int Port { get; set; }
///
/// 当前服务名称
///
public string ServiceName { get; set; }
///
/// Consul集群IP
///
public string ConsulIP { get; set; }
///
/// Consul集群端口
///
public int ConsulPort { get; set; }
///
/// 负载均衡策略
///
public string LBStrategy { get; set; }
///
/// 权重
///
public int? Weight { get; set; }
}
这里有几个注意点:
1)AgentServiceCheck
由于consul对服务状态可用状态检查是通过服务端轮询访问应用的,所以在服务注册时需要提供健康状态检查终结点,这也是step2中配置健康状态检查的原因
2)Meta中的权重和IConsulServerManager
这两者是为了实现服务间调用问题的,后面会细讲。
在startup中进行注入
public void ConfigureServices(IServiceCollection services)
{
// 应用健康状态检查
services.AddHealthChecks();
// 配置consul服务注册信息
services.AddConsul(Configuration);
}
在appsetting.json增加Consul配置节点
"Consul": {
"ConsulIP": "192.168.137.200",
"ConsulPort": "8500",
"ServiceName": "yyl.ClientService",
"Ip": "192.168.2.103",
"Port": "5000",
"Weight": 1,
"LBStrategy": "WeightRoundRobin",
"FloderName": "consulConfig", //这两个节点可暂时忽略,是为了使用consul配置中心而设置的
"FileName": "config" //这两个节点可暂时忽略,是为了使用consul配置中心而设置的
}
之后启动web api应用,在consul管理平台即可看到注册上去的服务
最基本的服务注册到此就已经完成了,我们可以通过服务注册管理页面清晰的看到现在我们有哪些服务,哪些服务是健康可用的。
在微服务架构中,很多时候需要调用很多服务才能完成一项功能。服务之间如何互相调用就变成微服务架构中的一个关键问题。微服务内部间的调用有多种方式,包括RPC、基于消息队列的事件驱动,还有最基本的RESTful Api。
对于RESTful Api,当一个服务需要调用另一个服务时,都是先获取调用服务的基本信息,如ip、端口等,需要知道当前有哪些服务,哪些服务是可用的,这时就需要服务发现,去获取注册中心中可用的服务。
Consul的服务发现通过API的方式提供,通过服务名称在web浏览器中就可以看到当前服务的相关信息。
当然,通过Consul包,我们在应用中也可以快捷的调用这些服务发现相关的接口,而不用自己去构建http请求。
这里最基本的用法是注入 IConsulClient 接口,通过其中的IHealthEndpoint获取健康可用的服务,这里新建了一个ServerController作为服务发现的测试。
[ApiController]
[Route("/api/[Controller]")]
public class ServerController : ControllerBase
{
private readonly IConsulClient _consulClient;
private readonly IConsulServerManager _consulServerManager;
public ServerController(IConsulServerManager consulServerManager, IConsulClient consulClient)
{
_consulServerManager = consulServerManager;
_consulClient = consulClient;
}
[HttpGet]
[Route("")]
public async Task<string> Get()
{
return await _consulServerManager.GetServerAsync("yyl.ClientService1");
}
public async Task GetServiceAsync()
{
var result = await _consulClient.Health.Service("yyl.ClientService1");
}
}
微服务内部各个服务的相互调用,我们公司在工作中的实践是不通过网关,各个服务之间自由调用,这里我自己写了一个IConsulServerManager接口及其实现,主要是处理服务间调用的负载均衡。一般情况下,相同的服务会启动多个实例,在我们需要调用一个服务的时候,应该调用哪个实例,这时负载均衡策略是很有必要的,如果没有负载均衡可能导致所有请求集中在一个实例,导致其压力过大,而其他实例闲置。
IConsulServerManager 及其实现类代码如下,这里实现了随机、轮询、加权随机、加权轮询等几种简单的方式
public interface IConsulServerManager
{
Task<string> GetServerAsync(string serviceName);
Task<AgentService> GetServerInfoAsync(string serviceName);
Task<IList<AgentService>> GetServerListAsync(string serviceName);
}
public class DefaultConsulServerManager : IConsulServerManager
{
private readonly ConsulOption _consulOption;
private readonly IConsulClient _consulClient;
private ConcurrentDictionary<string, int> ServerCalls = new ConcurrentDictionary<string, int>();
public DefaultConsulServerManager(IConsulClient consulClient, IOptions<ConsulOption> options)
{
_consulClient = consulClient;
_consulOption = options.Value;
}
///
/// 根据负载均衡策略获取服务地址
///
///
///
public async Task<string> GetServerAsync(string serviceName)
{
var service = await GetServerInfoAsync(serviceName);
return $"http://{ service.Address }:{ service.Port }";
}
///
/// 根据负载均衡策略获取服务信息
///
///
///
public async Task<AgentService> GetServerInfoAsync(string serviceName)
{
var services = await GetServerListAsync(serviceName);
// 安装负载均衡策略进行返回服务地址
return BalancingRoute(services, serviceName);
}
///
/// 获取服务列表
///
///
///
public async Task<IList<AgentService>> GetServerListAsync(string serviceName)
{
var result = await _consulClient.Health.Service(serviceName);
if (result.StatusCode != System.Net.HttpStatusCode.OK)
{
throw new ConsulRequestException("获取服务信息失败!", result.StatusCode);
}
return result.Response.Select(s => s.Service).ToList();
}
private AgentService BalancingRoute(IList<AgentService> services, string key)
{
if(services == null || !services.Any())
{
throw new ArgumentNullException(nameof(services), $"当前未找到{key}可用服务!");
}
switch (_consulOption.LBStrategy)
{
case "First":
return First(services);
case "Random":
return Random(services);
case "RoundRobin":
return RoundRobin(services, key);
case "WeightRandom":
return WeightRandom(services);
case "WeightRoundRobin":
return WeightRoundRobin(services, key);
default:
return RoundRobin(services, key);
}
}
///
/// 第一个
///
///
///
private AgentService First(IList<AgentService> services)
{
return services.First();
}
///
/// 随机
///
///
///
private AgentService Random(IList<AgentService> services)
{
return services.ElementAt(new Random().Next(0, services.Count()));
}
///
/// 轮询
///
///
///
private AgentService RoundRobin(IList<AgentService> services, string key)
{
var count = ServerCalls.GetOrAdd(key, 0);
var service = services.ElementAt(count++ % services.Count());
ServerCalls[key] = count;
return service;
}
///
/// 加权随机
///
///
///
public AgentService WeightRandom(IList<AgentService> services)
{
var pairs = services.SelectMany(s =>
{
var weight = 1;
if (s.Meta.ContainsKey("Weight") && int.TryParse(s.Meta["Weight"], out int w))
{
weight = w;
}
var result = new List<AgentService>();
for (int i = 0; i < weight; i++)
{
result.Add(s);
}
return result;
}).ToList();
return Random(pairs);
}
///
/// 加权轮询
///
///
///
///
public AgentService WeightRoundRobin(IList<AgentService> services, string key)
{
var pairs = services.SelectMany(s =>
{
var weight = 1;
if (s.Meta.ContainsKey("Weight") && int.TryParse(s.Meta["Weight"], out int w))
{
weight = w;
}
var result = new List<AgentService>();
for (int i = 0; i < weight; i++)
{
result.Add(s);
}
return result;
}).ToList();
return RoundRobin(pairs, key);
}
}
结合上面的依赖注入扩展和appsetting.json中的配置,可以实现内部服务调用间的负载均衡。
测试结果如下:
Consul包中还提供了其他通过Api方式对Consul服务端进行调用的封装,大家可以研究,或者在工作实践中去了解和使用。
Consul 服务注册与发现demo代码可在以下地址查看
生产环境实践中,可以将Consul相关操作独立成一个类库,打包成nuget包,这一部分是可以在不同服务中开箱即用的。
微服务系列文章:
上一篇:服务发现—Consul部署
下一篇: 服务间的通讯—工作实践