.NET Core 使用 Consul 服務(wù)注冊(cè)發(fā)現(xiàn)

Consul是一個(gè)用來(lái)實(shí)現(xiàn)分布式系統(tǒng)服務(wù)發(fā)現(xiàn)與配置的開(kāi)源工具。它內(nèi)置了服務(wù)注冊(cè)與發(fā)現(xiàn)框架、分布一致性協(xié)議實(shí)現(xiàn)、健康檢查、Key/Value存儲(chǔ)、多數(shù)據(jù)中心方案,不再需要依賴(lài)其他工具,使用起來(lái)也較為簡(jiǎn)單。
Consul官網(wǎng):https://www.consul.io開(kāi)源地址:https://github.com/hashicorp/consul、https://github.com/G-Research/consuldotnet
安裝
Consul支持各種平臺(tái)的安裝,安裝文檔:https://www.consul.io/downloads,為了快速使用,我這里選擇用docker方式安裝。
version:?"3"
services:
??service_1:
????image:?consul
????command:?agent?-server?-client=0.0.0.0?-bootstrap-expect=3?-node=service_1
????volumes:
??????-?/usr/local/docker/consul/data/service_1:/data
??service_2:
????image:?consul
????command:?agent?-server?-client=0.0.0.0?-retry-join=service_1?-node=service_2
????volumes:
??????-?/usr/local/docker/consul/data/service_2:/data
????depends_on:
??????-?service_1
??service_3:
????image:?consul
????command:?agent?-server?-client=0.0.0.0?-retry-join=service_1?-node=service_3
????volumes:
??????-?/usr/local/docker/consul/data/service_3:/data
????depends_on:
??????-?service_1
??client_1:
????image:?consul
????command:?agent?-client=0.0.0.0?-retry-join=service_1?-ui?-node=client_1
????ports:
??????-?8500:8500
????volumes:
??????-?/usr/local/docker/consul/data/client_1:/data
????depends_on:
??????-?service_2
??????-?service_3
提供一個(gè)docker-compose.yaml,使用docker-compose up編排腳本啟動(dòng)Consul,如果你不熟悉,可以選擇其它方式能運(yùn)行Consul即可。

這里使用 Docker 搭建 3個(gè) server 節(jié)點(diǎn) + 1 個(gè) client 節(jié)點(diǎn),API 服務(wù)通過(guò) client 節(jié)點(diǎn)進(jìn)行服務(wù)注冊(cè)和發(fā)現(xiàn)。
安裝完成啟動(dòng)Consul,打開(kāi)默認(rèn)地址 http://localhost:8500 可以看到Consului界面。

快速使用
添加兩個(gè)webapi服務(wù),ServiceA和ServiceB,一個(gè)webapi客戶(hù)端Client來(lái)調(diào)用服務(wù)。
dotnet?new?sln?-n?consul_demo
dotnet?new?webapi?-n?ServiceA
dotnet?sln?add?ServiceA/ServiceA.csproj
dotnet?new?webapi?-n?ServiceB
dotnet?sln?add?ServiceB/ServiceB.csproj
dotnet?new?webapi?-n?Client
dotnet?sln?add?Client/Client.csproj

在項(xiàng)目中添加Consul組件包
Install-Package Consul
服務(wù)注冊(cè)
接下來(lái)在兩個(gè)服務(wù)中添加必要的代碼來(lái)實(shí)現(xiàn)將服務(wù)注冊(cè)到Consul中。
首先將Consul配置信息添加到appsettings.json
{
????"Consul":?{
????????"Address":?"http://host.docker.internal:8500",
????????"HealthCheck":?"/healthcheck",
????????"Name":?"ServiceA",
????????"Ip":?"host.docker.internal"
????}
}
因?yàn)槲覀円獙㈨?xiàng)目都運(yùn)行在docker中,所以這里的地址要用 host.docker.internal 代替,使用 localhost 無(wú)法正常啟動(dòng),如果不在 docker 中運(yùn)行,這里就配置層 localhost。
添加一個(gè)擴(kuò)展方法UseConul(this IApplicationBuilder app, IConfiguration configuration, IHostApplicationLifetime lifetime)。
using?System;
using?Consul;
using?Microsoft.AspNetCore.Builder;
using?Microsoft.Extensions.Configuration;
using?Microsoft.Extensions.Hosting;
namespace?ServiceA
{
????public?static?class?Extensions
????{
????????public?static?IApplicationBuilder?UseConul(this?IApplicationBuilder?app,?IConfiguration?configuration,?IHostApplicationLifetime?lifetime)
????????{
????????????var?client?=?new?ConsulClient(options?=>
????????????{
????????????????options.Address?=?new?Uri(configuration["Consul:Address"]);?//?Consul客戶(hù)端地址
????????????});
????????????var?registration?=?new?AgentServiceRegistration
????????????{
????????????????ID?=?Guid.NewGuid().ToString(),?//?唯一Id
????????????????Name?=?configuration["Consul:Name"],?//?服務(wù)名
????????????????Address?=?configuration["Consul:Ip"],?//?服務(wù)綁定IP
????????????????Port?=?Convert.ToInt32(configuration["Consul:Port"]),?//?服務(wù)綁定端口
????????????????Check?=?new?AgentServiceCheck
????????????????{
????????????????????DeregisterCriticalServiceAfter?=?TimeSpan.FromSeconds(5),?//?服務(wù)啟動(dòng)多久后注冊(cè)
????????????????????Interval?=?TimeSpan.FromSeconds(10),?//?健康檢查時(shí)間間隔
????????????????????HTTP?=?$"http://{configuration["Consul:Ip"]}:{configuration["Consul:Port"]}{configuration["Consul:HealthCheck"]}",?//?健康檢查地址
????????????????????Timeout?=?TimeSpan.FromSeconds(5)?//?超時(shí)時(shí)間
????????????????}
????????????};
????????????//?注冊(cè)服務(wù)
????????????client.Agent.ServiceRegister(registration).Wait();
????????????//?應(yīng)用程序終止時(shí),取消服務(wù)注冊(cè)
????????????lifetime.ApplicationStopping.Register(()?=>
????????????{
????????????????client.Agent.ServiceDeregister(registration.ID).Wait();
????????????});
????????????return?app;
????????}
????}
}
然后在Startup.cs中使用擴(kuò)展方法即可。
public?void?Configure(IApplicationBuilder?app,?IWebHostEnvironment?env,?IHostApplicationLifetime?lifetime)
{
????...
????app.UseConul(Configuration,?lifetime);
}
注意,這里將IConfiguration和IHostApplicationLifetime作為參數(shù)傳進(jìn)來(lái)的,根據(jù)實(shí)際開(kāi)發(fā)做對(duì)應(yīng)的修改就可以了。
分別在ServiceA和ServiceB都完成一遍上述操作,因?yàn)椴皇菍?shí)際項(xiàng)目,這里就產(chǎn)生的許多重復(fù)代碼,在真正的項(xiàng)目開(kāi)發(fā)過(guò)程中可以考慮放在一個(gè)單獨(dú)的項(xiàng)目中,ServiceA和ServiceB分別引用,調(diào)用。
接著去實(shí)現(xiàn)健康檢查接口。
//?ServiceA
using?Microsoft.AspNetCore.Mvc;
namespace?ServiceA.Controllers
{
????[Route("[controller]")]
????[ApiController]
????public?class?HealthCheckController?:?ControllerBase
????{
????????///?
????????///?健康檢查
????????///?
????????///?
????????[HttpGet]
????????public?IActionResult?api()
????????{
????????????return?Ok();
????????}
????}
}
//?ServiceB
using?Microsoft.AspNetCore.Mvc;
namespace?ServiceB.Controllers
{
????[Route("[controller]")]
????[ApiController]
????public?class?HealthCheckController?:?ControllerBase
????{
????????///?
????????///?健康檢查
????????///?
????????///?
????????[HttpGet]
????????public?IActionResult?Get()
????????{
????????????return?Ok();
????????}
????}
}
最后在ServiceA和ServiceB中都添加一個(gè)接口。
//?ServiceA
using?System;
using?Microsoft.AspNetCore.Mvc;
using?Microsoft.Extensions.Configuration;
namespace?ServiceA.Controllers
{
????[Route("api/[controller]")]
????[ApiController]
????public?class?ServiceAController?:?ControllerBase
????{
????????[HttpGet]
????????public?IActionResult?Get([FromServices]?IConfiguration?configuration)
????????{
????????????var?result?=?new
????????????{
????????????????msg?=?$"我是{nameof(ServiceA)},當(dāng)前時(shí)間:{DateTime.Now:G}",
????????????????ip?=?Request.HttpContext.Connection.LocalIpAddress.ToString(),
????????????????port?=?configuration["Consul:Port"]
????????????};
????????????return?Ok(result);
????????}
????}
}
//?ServiceB
using?System;
using?Microsoft.AspNetCore.Mvc;
using?Microsoft.Extensions.Configuration;
namespace?ServiceB.Controllers
{
????[Route("api/[controller]")]
????[ApiController]
????public?class?ServiceBController?:?ControllerBase
????{
????????[HttpGet]
????????public?IActionResult?Get([FromServices]?IConfiguration?configuration)
????????{
????????????var?result?=?new
????????????{
????????????????msg?=?$"我是{nameof(ServiceB)},當(dāng)前時(shí)間:{DateTime.Now:G}",
????????????????ip?=?Request.HttpContext.Connection.LocalIpAddress.ToString(),
????????????????port?=?configuration["Consul:Port"]
????????????};
????????????return?Ok(result);
????????}
????}
}
這樣我們寫(xiě)了兩個(gè)服務(wù),ServiceA和ServiceB。都添加了健康檢查接口和一個(gè)自己的服務(wù)接口,返回一段json。
我們現(xiàn)在來(lái)運(yùn)行看看效果,可以使用任何方式,只要能啟動(dòng)即可,我這里選擇在docker中運(yùn)行,直接在 Visual Studio中對(duì)著兩個(gè)解決方案右鍵添加,選擇Docker支持,默認(rèn)會(huì)幫我們自動(dòng)創(chuàng)建好Dockfile,非常方便。
生成的Dockfile文件內(nèi)容如下:
#?ServiceA
FROM?mcr.microsoft.com/dotnet/core/aspnet:3.1-buster-slim?AS?base
WORKDIR?/app
EXPOSE?80
EXPOSE?443
FROM?mcr.microsoft.com/dotnet/core/sdk:3.1-buster?AS?build
WORKDIR?/src
COPY?["ServiceA/ServiceA.csproj",?"ServiceA/"]
RUN?dotnet?restore?"ServiceA/ServiceA.csproj"
COPY?.?.
WORKDIR?"/src/ServiceA"
RUN?dotnet?build?"ServiceA.csproj"?-c?Release?-o?/app/build
FROM?build?AS?publish
RUN?dotnet?publish?"ServiceA.csproj"?-c?Release?-o?/app/publish
FROM?base?AS?final
WORKDIR?/app
COPY?--from=publish?/app/publish?.
ENTRYPOINT?["dotnet",?"ServiceA.dll"]
#?ServiceB
FROM?mcr.microsoft.com/dotnet/core/aspnet:3.1-buster-slim?AS?base
WORKDIR?/app
EXPOSE?80
EXPOSE?443
FROM?mcr.microsoft.com/dotnet/core/sdk:3.1-buster?AS?build
WORKDIR?/src
COPY?["ServiceB/ServiceB.csproj",?"ServiceB/"]
RUN?dotnet?restore?"ServiceB/ServiceB.csproj"
COPY?.?.
WORKDIR?"/src/ServiceB"
RUN?dotnet?build?"ServiceB.csproj"?-c?Release?-o?/app/build
FROM?build?AS?publish
RUN?dotnet?publish?"ServiceB.csproj"?-c?Release?-o?/app/publish
FROM?base?AS?final
WORKDIR?/app
COPY?--from=publish?/app/publish?.
ENTRYPOINT?["dotnet",?"ServiceB.dll"]
然后定位到項(xiàng)目根目錄,使用命令去編譯兩個(gè)鏡像,service_a和service_b
docker?build?-t?service_a:dev?-f?./ServiceA/Dockerfile?.
docker?build?-t?service_b:dev?-f?./ServiceB/Dockerfile?.

看到 Successfully 就成功了,通過(guò)docker image ls可以看到我們打包的兩個(gè)鏡像。

這里順便提一句,已經(jīng)可以看到我們編譯的鏡像,service_a和service_b了,但是還有許多名稱(chēng)為的鏡像,這些鏡像可以不用管它,這種叫做虛懸鏡像,既沒(méi)有倉(cāng)庫(kù)名,也沒(méi)有標(biāo)簽。是因?yàn)?code style="margin-right: 2px;margin-left: 2px;padding: 2px 4px;font-size: 14px;border-radius: 4px;background-color: rgba(27, 31, 35, 0.05);font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;word-break: break-all;color: rgb(71, 193, 168);">docker build導(dǎo)致的這種現(xiàn)象。由于新舊鏡像同名,舊鏡像名稱(chēng)被取消,從而出現(xiàn)倉(cāng)庫(kù)名、標(biāo)簽均為??的鏡像。
一般來(lái)說(shuō),虛懸鏡像已經(jīng)失去了存在的價(jià)值,是可以隨意刪除的,可以docker image prune命令刪除,這樣鏡像列表就干凈多了。
最后將兩個(gè)鏡像service_a和service_b,分別運(yùn)行三個(gè)實(shí)例。
docker run -d -p 5050:80 --name service_a1 service_a:dev --Consul:Port="5050"
docker run -d -p 5051:80 --name service_a2 service_a:dev --Consul:Port="5051"
docker run -d -p 5052:80 --name service_a3 service_a:dev --Consul:Port="5052"
docker run -d -p 5060:80 --name service_b1 service_b:dev --Consul:Port="5060"
docker run -d -p 5061:80 --name service_b2 service_b:dev --Consul:Port="5061"
docker run -d -p 5062:80 --name service_b3 service_b:dev --Consul:Port="5062"

運(yùn)行成功,接下來(lái)就是見(jiàn)證奇跡的時(shí)刻,去到Consul看看。



成功將兩個(gè)服務(wù)注冊(cè)到Consul,并且每個(gè)服務(wù)都有多個(gè)實(shí)例。
訪(fǎng)問(wèn)一下接口試試吧,看看能不能成功出現(xiàn)結(jié)果。

因?yàn)榻K端編碼問(wèn)題,導(dǎo)致顯示亂碼,這個(gè)不影響,ok,至此服務(wù)注冊(cè)大功告成。
服務(wù)發(fā)現(xiàn)
搞定了服務(wù)注冊(cè),接下來(lái)演示一下如何服務(wù)發(fā)現(xiàn),在Client項(xiàng)目中先將Consul地址配置到appsettings.json中。
{
????"Consul":?{
????????"Address":?"http://host.docker.internal:8500"
????}
}
然后添加一個(gè)接口,IService.cs,添加三個(gè)方法,分別獲取兩個(gè)服務(wù)的返回結(jié)果以及初始化服務(wù)的方法。
using?System.Threading.Tasks;
namespace?Client
{
????public?interface?IService
????{
????????///?
????????///?獲取?ServiceA?返回?cái)?shù)據(jù)
????????///?
????????///?
????????Task<string>?GetServiceA();
????????///?
????????///?獲取?ServiceB?返回?cái)?shù)據(jù)
????????///?
????????///?
????????Task<string>?GetServiceB();
????????///?
????????///?初始化服務(wù)
????????///?
????????void?InitServices();
????}
}
實(shí)現(xiàn)類(lèi):Service.cs
using?System;
using?System.Collections.Concurrent;
using?System.Linq;
using?System.Net.Http;
using?System.Threading.Tasks;
using?Consul;
using?Microsoft.Extensions.Configuration;
namespace?Client
{
????public?class?Service?:?IService
????{
????????private?readonly?IConfiguration?_configuration;
????????private?readonly?ConsulClient?_consulClient;
????????private?ConcurrentBag<string>?_serviceAUrls;
????????private?ConcurrentBag<string>?_serviceBUrls;
????????private?IHttpClientFactory?_httpClient;
????????public?Service(IConfiguration?configuration,?IHttpClientFactory?httpClient)
????????{
????????????_configuration?=?configuration;
????????????_consulClient?=?new?ConsulClient(options?=>
????????????{
????????????????options.Address?=?new?Uri(_configuration["Consul:Address"]);
????????????});
????????????_httpClient?=?httpClient;
????????}
????????public?async?Task<string>?GetServiceA()
????????{
????????????if?(_serviceAUrls?==?null)
????????????????return?await?Task.FromResult("ServiceA正在初始化...");
????????????using?var?httpClient?=?_httpClient.CreateClient();
????????????var?serviceUrl?=?_serviceAUrls.ElementAt(new?Random().Next(_serviceAUrls.Count()));
????????????Console.WriteLine("ServiceA:"?+?serviceUrl);
????????????var?result?=?await?httpClient.GetStringAsync($"{serviceUrl}/api/servicea");
????????????return?result;
????????}
????????public?async?Task<string>?GetServiceB()
????????{
????????????if?(_serviceBUrls?==?null)
????????????????return?await?Task.FromResult("ServiceB正在初始化...");
????????????using?var?httpClient?=?_httpClient.CreateClient();
????????????var?serviceUrl?=?_serviceBUrls.ElementAt(new?Random().Next(_serviceBUrls.Count()));
????????????Console.WriteLine("ServiceB:"?+?serviceUrl);
????????????var?result?=?await?httpClient.GetStringAsync($"{serviceUrl}/api/serviceb");
????????????return?result;
????????}
????????public?void?InitServices()
????????{
????????????var?serviceNames?=?new?string[]?{?"ServiceA",?"ServiceB"?};
????????????foreach?(var?item?in?serviceNames)
????????????{
????????????????Task.Run(async?()?=>
????????????????{
????????????????????var?queryOptions?=?new?QueryOptions
????????????????????{
????????????????????????WaitTime?=?TimeSpan.FromMinutes(5)
????????????????????};
????????????????????while?(true)
????????????????????{
????????????????????????await?InitServicesAsync(queryOptions,?item);
????????????????????}
????????????????});
????????????}
????????????async?Task?InitServicesAsync(QueryOptions?queryOptions,?string?serviceName)
????????????{
????????????????var?result?=?await?_consulClient.Health.Service(serviceName,?null,?true,?queryOptions);
????????????????if?(queryOptions.WaitIndex?!=?result.LastIndex)
????????????????{
????????????????????queryOptions.WaitIndex?=?result.LastIndex;
????????????????????var?services?=?result.Response.Select(x?=>?$"http://{x.Service.Address}:{x.Service.Port}");
????????????????????if?(serviceName?==?"ServiceA")
????????????????????{
????????????????????????_serviceAUrls?=?new?ConcurrentBag<string>(services);
????????????????????}
????????????????????else?if?(serviceName?==?"ServiceB")
????????????????????{
????????????????????????_serviceBUrls?=?new?ConcurrentBag<string>(services);
????????????????????}
????????????????}
????????????}
????????}
????}
}
代碼就不解釋了,相信都可以看懂,使用了Random類(lèi)隨機(jī)獲取一個(gè)服務(wù),關(guān)于這點(diǎn)可以選擇更合適的負(fù)載均衡方式。
在Startup.cs中添加接口依賴(lài)注入、使用初始化服務(wù)等代碼。
using?Microsoft.AspNetCore.Builder;
using?Microsoft.AspNetCore.Hosting;
using?Microsoft.Extensions.Configuration;
using?Microsoft.Extensions.DependencyInjection;
using?Microsoft.Extensions.Hosting;
namespace?Client
{
????public?class?Startup
????{
????????public?Startup(IConfiguration?configuration)
????????{
????????????Configuration?=?configuration;
????????}
????????public?IConfiguration?Configuration?{?get;?}
????????public?void?ConfigureServices(IServiceCollection?services)
????????{
????????????services.AddControllers();
????????????services.AddHttpClient();
????????????services.AddSingleton();
????????}
????????public?void?Configure(IApplicationBuilder?app,?IWebHostEnvironment?env,?IService?service)
????????{
????????????if?(env.IsDevelopment())
????????????{
????????????????app.UseDeveloperExceptionPage();
????????????}
????????????app.UseHttpsRedirection();
????????????app.UseRouting();
????????????app.UseAuthorization();
????????????app.UseEndpoints(endpoints?=>
????????????{
????????????????endpoints.MapControllers();
????????????});
????????????service.InitServices();
????????}
????}
}
一切就緒,添加api訪(fǎng)問(wèn)我們的兩個(gè)服務(wù)。
using?System.Threading.Tasks;
using?Microsoft.AspNetCore.Mvc;
namespace?Client.Controllers
{
????[Route("api")]
????[ApiController]
????public?class?HomeController?:?ControllerBase
????{
????????[HttpGet]
????????[Route("service_result")]
????????public?async?Task?GetService([FromServices]?IService?service)
????????{
????????????return?Ok(new
????????????{
????????????????serviceA?=?await?service.GetServiceA(),
????????????????serviceB?=?await?service.GetServiceB()
????????????});
????????}
????}
}
直接在Visual Studio中運(yùn)行Client項(xiàng)目,在瀏覽器訪(fǎng)問(wèn)api。

大功告成,服務(wù)注冊(cè)與發(fā)現(xiàn),現(xiàn)在就算之中的某個(gè)節(jié)點(diǎn)掛掉,服務(wù)也可以照常運(yùn)行。
【推薦】.NET Core開(kāi)發(fā)實(shí)戰(zhàn)視頻課程?★★★
.NET Core實(shí)戰(zhàn)項(xiàng)目之CMS 第一章 入門(mén)篇-開(kāi)篇及總體規(guī)劃
【.NET Core微服務(wù)實(shí)戰(zhàn)-統(tǒng)一身份認(rèn)證】開(kāi)篇及目錄索引
Redis基本使用及百億數(shù)據(jù)量中的使用技巧分享(附視頻地址及觀看指南)
.NET Core中的一個(gè)接口多種實(shí)現(xiàn)的依賴(lài)注入與動(dòng)態(tài)選擇看這篇就夠了
10個(gè)小技巧助您寫(xiě)出高性能的ASP.NET Core代碼
用abp vNext快速開(kāi)發(fā)Quartz.NET定時(shí)任務(wù)管理界面
在ASP.NET Core中創(chuàng)建基于Quartz.NET托管服務(wù)輕松實(shí)現(xiàn)作業(yè)調(diào)度
現(xiàn)身說(shuō)法:實(shí)際業(yè)務(wù)出發(fā)分析百億數(shù)據(jù)量下的多表查詢(xún)優(yōu)化
