.NET Core 使用 Consul 服务注册发现
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:?consulcommand:?agent?-server?-client=0.0.0.0?-bootstrap-expect=3?-node=service_1volumes:-?/usr/local/docker/consul/data/service_1:/dataservice_2:image:?consulcommand:?agent?-server?-client=0.0.0.0?-retry-join=service_1?-node=service_2volumes:-?/usr/local/docker/consul/data/service_2:/datadepends_on:-?service_1service_3:image:?consulcommand:?agent?-server?-client=0.0.0.0?-retry-join=service_1?-node=service_3volumes:-?/usr/local/docker/consul/data/service_3:/datadepends_on:-?service_1client_1:image:?consulcommand:?agent?-client=0.0.0.0?-retry-join=service_1?-ui?-node=client_1ports:-?8500:8500volumes:-?/usr/local/docker/consul/data/client_1:/datadepends_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_demodotnet?new?webapi?-n?ServiceA dotnet?sln?add?ServiceA/ServiceA.csprojdotnet?new?webapi?-n?ServiceB dotnet?sln?add?ServiceB/ServiceB.csprojdotnet?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(),?//?唯一IdName?=?configuration["Consul:Name"],?//?服務(wù)名Address?=?configuration["Consul:Ip"],?//?服務(wù)綁定IPPort?=?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{///?<summary>///?健康檢查///?</summary>///?<returns></returns>[HttpGet]public?IActionResult?api(){return?Ok();}} } //?ServiceB using?Microsoft.AspNetCore.Mvc;namespace?ServiceB.Controllers {[Route("[controller]")][ApiController]public?class?HealthCheckController?:?ControllerBase{///?<summary>///?健康檢查///?</summary>///?<returns></returns>[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?443FROM?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/buildFROM?build?AS?publish RUN?dotnet?publish?"ServiceA.csproj"?-c?Release?-o?/app/publishFROM?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?443FROM?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/buildFROM?build?AS?publish RUN?dotnet?publish?"ServiceB.csproj"?-c?Release?-o?/app/publishFROM?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)為<none>的鏡像,這些鏡像可以不用管它,這種叫做虛懸鏡像,既沒(méi)有倉(cāng)庫(kù)名,也沒(méi)有標(biāo)簽。是因?yàn)閐ocker build導(dǎo)致的這種現(xiàn)象。由于新舊鏡像同名,舊鏡像名稱(chēng)被取消,從而出現(xiàn)倉(cāng)庫(kù)名、標(biāo)簽均為 <none> 的鏡像。
一般來(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í)例。
訪問(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{///?<summary>///?獲取?ServiceA?返回?cái)?shù)據(jù)///?</summary>///?<returns></returns>Task<string>?GetServiceA();///?<summary>///?獲取?ServiceB?返回?cái)?shù)據(jù)///?</summary>///?<returns></returns>Task<string>?GetServiceB();///?<summary>///?初始化服務(wù)///?</summary>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<IService,?Service>();}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訪問(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<IActionResult>?GetService([FromServices]?IService?service){return?Ok(new{serviceA?=?await?service.GetServiceA(),serviceB?=?await?service.GetServiceB()});}} }直接在Visual Studio中運(yùn)行Client項(xiàng)目,在瀏覽器訪問(wèn)api。
大功告成,服務(wù)注冊(cè)與發(fā)現(xiàn),現(xiàn)在就算之中的某個(gè)節(jié)點(diǎn)掛掉,服務(wù)也可以照常運(yùn)行。
總結(jié)
以上是生活随笔為你收集整理的.NET Core 使用 Consul 服务注册发现的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: RabbitMq如何确保消息不丢失
- 下一篇: 《ASP.NET Core 真机拆解》