.NET Core 使用 Consul 服务注册发现
Consul是一個用來實現分布式系統服務發現與配置的開源工具。它內置了服務注冊與發現框架、分布一致性協議實現、健康檢查、Key/Value存儲、多數據中心方案,不再需要依賴其他工具,使用起來也較為簡單。
Consul官網:https://www.consul.io
開源地址:https://github.com/hashicorp/consul、https://github.com/G-Research/consuldotnet
安裝
Consul支持各種平臺的安裝,安裝文檔: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提供一個docker-compose.yaml,使用docker-compose up編排腳本啟動Consul,如果你不熟悉,可以選擇其它方式能運行Consul即可。
這里使用 Docker 搭建 3個 server 節點 + 1 個 client 節點,API 服務通過 client 節點進行服務注冊和發現。
安裝完成啟動Consul,打開默認地址 http://localhost:8500 可以看到Consului界面。
快速使用
添加兩個webapi服務,ServiceA和ServiceB,一個webapi客戶端Client來調用服務。
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在項目中添加Consul組件包
Install-Package Consul服務注冊
接下來在兩個服務中添加必要的代碼來實現將服務注冊到Consul中。
首先將Consul配置信息添加到appsettings.json
{"Consul":?{"Address":?"http://host.docker.internal:8500","HealthCheck":?"/healthcheck","Name":?"ServiceA","Ip":?"host.docker.internal"} }因為我們要將項目都運行在docker中,所以這里的地址要用 host.docker.internal 代替,使用 localhost 無法正常啟動,如果不在 docker 中運行,這里就配置層 localhost。
添加一個擴展方法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客戶端地址});var?registration?=?new?AgentServiceRegistration{ID?=?Guid.NewGuid().ToString(),?//?唯一IdName?=?configuration["Consul:Name"],?//?服務名Address?=?configuration["Consul:Ip"],?//?服務綁定IPPort?=?Convert.ToInt32(configuration["Consul:Port"]),?//?服務綁定端口Check?=?new?AgentServiceCheck{DeregisterCriticalServiceAfter?=?TimeSpan.FromSeconds(5),?//?服務啟動多久后注冊Interval?=?TimeSpan.FromSeconds(10),?//?健康檢查時間間隔HTTP?=?$"http://{configuration["Consul:Ip"]}:{configuration["Consul:Port"]}{configuration["Consul:HealthCheck"]}",?//?健康檢查地址Timeout?=?TimeSpan.FromSeconds(5)?//?超時時間}};//?注冊服務client.Agent.ServiceRegister(registration).Wait();//?應用程序終止時,取消服務注冊lifetime.ApplicationStopping.Register(()?=>{client.Agent.ServiceDeregister(registration.ID).Wait();});return?app;}} }然后在Startup.cs中使用擴展方法即可。
public?void?Configure(IApplicationBuilder?app,?IWebHostEnvironment?env,?IHostApplicationLifetime?lifetime) {...app.UseConul(Configuration,?lifetime); }注意,這里將IConfiguration和IHostApplicationLifetime作為參數傳進來的,根據實際開發做對應的修改就可以了。
分別在ServiceA和ServiceB都完成一遍上述操作,因為不是實際項目,這里就產生的許多重復代碼,在真正的項目開發過程中可以考慮放在一個單獨的項目中,ServiceA和ServiceB分別引用,調用。
接著去實現健康檢查接口。
//?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中都添加一個接口。
//?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)},當前時間:{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)},當前時間:{DateTime.Now:G}",ip?=?Request.HttpContext.Connection.LocalIpAddress.ToString(),port?=?configuration["Consul:Port"]};return?Ok(result);}} }這樣我們寫了兩個服務,ServiceA和ServiceB。都添加了健康檢查接口和一個自己的服務接口,返回一段json。
我們現在來運行看看效果,可以使用任何方式,只要能啟動即可,我這里選擇在docker中運行,直接在 Visual Studio中對著兩個解決方案右鍵添加,選擇Docker支持,默認會幫我們自動創建好Dockfile,非常方便。
生成的Dockfile文件內容如下:
#?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"]然后定位到項目根目錄,使用命令去編譯兩個鏡像,service_a和service_b
docker?build?-t?service_a:dev?-f?./ServiceA/Dockerfile?.docker?build?-t?service_b:dev?-f?./ServiceB/Dockerfile?.看到 Successfully 就成功了,通過docker image ls可以看到我們打包的兩個鏡像。
這里順便提一句,已經可以看到我們編譯的鏡像,service_a和service_b了,但是還有許多名稱為<none>的鏡像,這些鏡像可以不用管它,這種叫做虛懸鏡像,既沒有倉庫名,也沒有標簽。是因為docker build導致的這種現象。由于新舊鏡像同名,舊鏡像名稱被取消,從而出現倉庫名、標簽均為 <none> 的鏡像。
一般來說,虛懸鏡像已經失去了存在的價值,是可以隨意刪除的,可以docker image prune命令刪除,這樣鏡像列表就干凈多了。
最后將兩個鏡像service_a和service_b,分別運行三個實例。
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"運行成功,接下來就是見證奇跡的時刻,去到Consul看看。
成功將兩個服務注冊到Consul,并且每個服務都有多個實例。
訪問一下接口試試吧,看看能不能成功出現結果。
因為終端編碼問題,導致顯示亂碼,這個不影響,ok,至此服務注冊大功告成。
服務發現
搞定了服務注冊,接下來演示一下如何服務發現,在Client項目中先將Consul地址配置到appsettings.json中。
{"Consul":?{"Address":?"http://host.docker.internal:8500"} }然后添加一個接口,IService.cs,添加三個方法,分別獲取兩個服務的返回結果以及初始化服務的方法。
using?System.Threading.Tasks;namespace?Client {public?interface?IService{///?<summary>///?獲取?ServiceA?返回數據///?</summary>///?<returns></returns>Task<string>?GetServiceA();///?<summary>///?獲取?ServiceB?返回數據///?</summary>///?<returns></returns>Task<string>?GetServiceB();///?<summary>///?初始化服務///?</summary>void?InitServices();} }實現類: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類隨機獲取一個服務,關于這點可以選擇更合適的負載均衡方式。
在Startup.cs中添加接口依賴注入、使用初始化服務等代碼。
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訪問我們的兩個服務。
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中運行Client項目,在瀏覽器訪問api。
大功告成,服務注冊與發現,現在就算之中的某個節點掛掉,服務也可以照常運行。
總結
以上是生活随笔為你收集整理的.NET Core 使用 Consul 服务注册发现的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: RabbitMq如何确保消息不丢失
- 下一篇: 《ASP.NET Core 真机拆解》