首先在Startup的ConfigureServices方法中来配置IConsulClient到ASP.NET Core的依赖注入系统中:
services.AddSingleton<IConsulClient, ConsulClient>(p => new ConsulClient(consulConfig => { consulConfig.Address = new Uri("http://localhost:8500"); }));我们需要在服务启动的时候,将自身的地址等信息注册到Consul中,并在服务关闭的时候从Consul撤销。这种行为就非常适合使用 IHostedService 来实现。
1.启动时注册服务:
public async Task StartAsync(CancellationToken cancellationToken) { _cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); var features = _server.Features; var address = features.Get<IServerAddressesFeature>().Addresses.First(); var uri = new Uri(address); _serviceId = "Service-v1-" + Dns.GetHostName() + "-" + uri.Authority; var registration = new AgentServiceRegistration() { ID = _serviceId, Name = "Service", Address = uri.Host, Port = uri.Port, Tags = new[] { "api" } }; // 首先移除服务,避免重复注册 await _consulClient.Agent.ServiceDeregister(registration.ID, _cts.Token); await _consulClient.Agent.ServiceRegister(registration, _cts.Token); }这里要注意的是,我们需要保证_serviceId对于同一个实例的唯一,避免重复性的注册。
2.关闭时撤销服务:
public async Task StopAsync(CancellationToken cancellationToken) { _cts.Cancel(); await _consulClient.Agent.ServiceDeregister(_serviceId, cancellationToken); }我们可以复制一份ServiceA的代码,命名为ServiceB,修改一下端口,分别为5001和5002,运行后,打开Consul的管理UI :8500:
如果我们关闭其中一个服务的,会调用StopAsync方法,撤销其注册的服务,然后刷新浏览器,可以看到只剩下一个节点了。
Consul是支持健康检查,我们可以在注册服务的时候指定健康检查地址,修改上面AgentServiceRegistration中的信息如下:
var registration = new AgentServiceRegistration() { ID = _serviceId, Name = "Service", Address = uri.Host, Port = uri.Port, Tags = new[] { "api" } Check = new AgentServiceCheck() { // 心跳地址 HTTP = $"{uri.Scheme}://{uri.Host}:{uri.Port}/healthz", // 超时时间 Timeout = TimeSpan.FromSeconds(2), // 检查间隔 Interval = TimeSpan.FromSeconds(10) } };对于上面的healthz地址,我使用了ASP.NET Core 2.2中自带的健康检查,它需要在Startup中添加如下配置:
public void ConfigureServices(IServiceCollection services) { services.AddHealthChecks(); } public void Configure(IApplicationBuilder app) { app.UseHealthChecks("/healthz"); }关于健康检查更详细的介绍可以查看:ASP.NET Core 2.2.0-preview1: Healthchecks。
现在,我们重新运作这两个服务,等待注册成功后,使用任务管理器杀掉其中的一个进程(阻止StopAsync的执行),可以看到Consul会将其移动到不健康的节点,显示如下:
发现服务现在来看看服务消费者如何从Consul来获取可用的服务列表。
我们创建一个ConsoleApp,做为服务的调用端,添加ConsulNuget包,然后,创建一个ConsulServiceProvider类,实现如下:
public class ConsulServiceProvider : IServiceDiscoveryProvider { public async Task<List<string>> GetServicesAsync() { var consuleClient = new ConsulClient(consulConfig => { consulConfig.Address = new Uri("http://localhost:8500"); }); var queryResult = await consuleClient.Health.Service("Service", string.Empty, true); var result = new List<string>(); foreach (var serviceEntry in queryResult.Response) { result.Add(serviceEntry.Service.Address + ":" + serviceEntry.Service.Port); } return result; } }如上,我们创建一个ConsulClient实例,直接调用consuleClient.Health.Service就可以获取到可用的服务列表了,然后使用HttpClient就可以发起对服务的调用。
但我们需要思考一个问题,我们什么时候从Consul获取服务呢?
最为简单的便是在每次调用服务时,都先从Consul来获取一下服务列表,这样做的好处是我们得到的服务列表是最新的,能及时获取到新注册的服务以及过滤掉挂掉的服务。但是这样每次请求都增加了一次对Consul的调用,对性能有稍微的损耗,不过我们可以在每个调用端的机器上都部署一个Consul Agent,这样对性能的影响就微乎其微了。
另外一种方式,可以在调用端做服务列表的本地缓存,并定时与Consul同步,具体实现如下:
public class PollingConsulServiceProvider : IServiceDiscoveryProvider { private List<string> _services = new List<string>(); private bool _polling; public PollingConsulServiceProvider() { var _timer = new Timer(async _ => { if (_polling) return; _polling = true; await Poll(); _polling = false; }, null, 0, 1000); } public async Task<List<string>> GetServicesAsync() { if (_services.Count == 0) await Poll(); return _services; } private async Task Poll() { _services = await new ConsulServiceProvider().GetServicesAsync(); } }其实现也非常简单,通过一个Timer来定时从Consul拉取最新的服务列表。
现在我们获取到服务列表了,还需要设计一种负载均衡机制,来实现服务调用的最优化。
负载均衡