Consul
是一個用來實現分佈式系統服務發現與配置的開源工具。它內置了服務註冊與發現框架、分佈一致性協議實現、健康檢查、Key/Value存儲、多數據中心方案,再也不須要依賴其餘工具,使用起來也較爲簡單。html
Consul
官網:https://www.consul.ioConsul
支持各類平臺的安裝,安裝文檔:https://www.consul.io/downloads,爲了快速使用,我這裏選擇用docker方式安裝。node
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
提供一個docker-compose.yaml
,使用docker-compose up
編排腳本啓動Consul
,若是你不熟悉,能夠選擇其它方式能運行Consul
便可。git
這裏使用 Docker 搭建 3個 server 節點 + 1 個 client 節點,API 服務經過 client 節點進行服務註冊和發現。github
安裝完成啓動Consul
,打開默認地址 http://localhost:8500 能夠看到Consul
ui界面。web
添加兩個webapi服務,ServiceA和ServiceB,一個webapi客戶端Client來調用服務。docker
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
在項目中添加Consul
組件包shell
Install-Package Consul
接下來在兩個服務中添加必要的代碼來實現將服務註冊到Consul
中。json
首先將Consul
配置信息添加到appsettings.json
bootstrap
{ "Consul": { "Address": "http://host.docker.internal:8500", "HealthCheck": "/healthcheck", "Name": "ServiceA", "Ip": "host.docker.internal" } }
由於咱們要將項目都運行在docker中,因此這裏的地址要用 host.docker.internal 代替,使用 localhost 沒法正常啓動,若是不在 docker 中運行,這裏就配置層 localhost。api
添加一個擴展方法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(), // 惟一Id Name = configuration["Consul:Name"], // 服務名 Address = configuration["Consul:Ip"], // 服務綁定IP Port = 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 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"]
而後定位到項目根目錄,使用命令去編譯兩個鏡像,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。
大功告成,服務註冊與發現,如今就算之中的某個節點掛掉,服務也能夠照常運行。