在這篇文章中,咱們將快速瞭解什麼是服務發現,使用consul實現一個基本的服務基礎設施;使用asp.net核心mvc框架,並使用dns client.net實現基於dns的客戶端服務發現。html
在現代微服務體系結構中,服務能夠在容器中運行,而且能夠動態地啓動、中止和擴展。這將致使一個很是動態的託管環境,其中可能有數百個實際的端點,沒法手動配置或找到正確的端點。git
儘管如此,我相信服務發現不只僅是針對生活在容器中的細粒度微服務。它能夠被任何須要訪問其餘資源的應用程序使用。資源能夠是數據庫、其餘web服務,也能夠只是託管在其餘地方的網站的一部分。服務發現有助於刪除特定於環境的配置文件!web
服務發現能夠用來解決這個問題,可是和往常同樣,有不少不一樣的方法來實現它數據庫
客戶端服務發現json
一種解決方案是有一箇中心服務註冊中心,全部服務實例都在這裏註冊。客戶機必須實現邏輯來查詢他們須要的服務,最終驗證端點是否仍然存在,並可能將請求分發到多個端點。api
服務器端/負載平衡服務器
全部流量都經過一個負載平衡器,它知道全部實際的、動態變化的端點,並相應地重定向全部請求網絡
consul是一個服務註冊中心,可用於實現客戶端服務發現。mvc
除了使用這種方法的許多優勢和特性外,它的缺點是每一個客戶機應用程序都須要實現一些邏輯來使用這個中央註冊表。這種邏輯可能會變得很是具體,由於consun和任何其餘技術都有自定義的api和工做原理。app
負載平衡也可能不會自動完成。客戶機能夠查詢服務的全部可用/註冊的端點,而後決定選擇哪一個端點。
好消息是consul不只提供了一個rest api來查詢服務註冊中心。它還提供返回標準srv和txt記錄的dns端點。
DNS終結點確實關心服務運行情況,由於它不會返回不正常的服務實例。它還經過以交替順序返回記錄來實現負載平衡!此外,它可能會賦予服務更高的優先級,使其更接近客戶端。
consul是hashicorp開發的一個軟件,它不只進行服務發現(如上所述),還進行「健康檢查」,並提供一個分佈式的「密鑰值存儲」。
consul應該在一個集羣中運行,其中至少有三個實例處理集羣和宿主環境中每一個節點上的「代理」的協調。應用程序老是隻與本地代理通訊,這使得通訊速度很是快,並將網絡延遲降至最低。
不過,對於本地開發,您能夠在--dev模式下運行consul,而不是設置完整的集羣。但請記住,爲了生產使用,須要作一些工做來正確設置consul。
下載地址:https://www.consul.io/downloads.html
用代理——dev參數運行consul。這將在本地服務模式下引導consul,無需配置文件,而且只能在本地主機上訪問。
http://localhost:8500,打開consul ui。
下面開始
目標
經過appsettings.json配置服務名
主機和端口不該硬編碼
使用microsoft.extensions.configuration和options正確配置全部須要的內容
將註冊設置爲啓動管道的一部分
集成Identity Server4到Identity api
添加Ocelot網關並集成identity server4認證
.Ocelot集成Consul服務發現
新建項目User.Api
添加UserController
[Route("api/[controller]")] [ApiController] public class UserController : BaseController { private readonly UserDbContext _userContext; private readonly ILogger<UserController> _logger; public UserController(UserDbContext userContext, ILogger<UserController> logger) { _userContext = userContext; _logger = logger; } /// <summary> /// 檢查或者建立用戶 但其那手機號碼不存在的時候建立 /// </summary> /// <returns></returns> [HttpPost("check-or-create")] public async Task<ActionResult> CheckOrCreate([FromForm]string phone) { var user = await _userContext.Users.SingleOrDefaultAsync(s => s.Phone == phone); if (user == null) { user = new Users() { Phone = phone }; await _userContext.Users.AddAsync(user); await _userContext.SaveChangesAsync(); } return Ok(user.Id); } [HttpGet] public async Task<IActionResult> Get() { var user = await _userContext.Users.AsNoTracking(). Include(u => u.Properties). SingleOrDefaultAsync(s => s.Id == UserIdentity.UserId); if (user == null) throw new UserOperationException($"錯誤的用戶上下文id:{UserIdentity.UserId}"); return Json(user); } [HttpPatch] public async Task<IActionResult> Patch(JsonPatchDocument<Users> patch) { var user = await _userContext.Users.SingleOrDefaultAsync(u => u.Id == UserIdentity.UserId); if (user == null) throw new UserOperationException($"錯誤的用戶上下文id:{UserIdentity.UserId}"); patch.ApplyTo(user); var originProperties = await _userContext.UserProperty.AsNoTracking().Where(s => s.UserId == user.Id).ToListAsync(); var allProperties = originProperties.Distinct(); if (user.Properties != null) { allProperties = originProperties.Union(user.Properties).Distinct(); } var removeProperties = allProperties; var newProperties = allProperties.Except(originProperties); if (removeProperties != null) { _userContext.UserProperty.RemoveRange(removeProperties); } if (newProperties != null) { foreach (var item in newProperties) { item.UserId = user.Id; } await _userContext.AddRangeAsync(newProperties); } _userContext.Users.Update(user); await _userContext.SaveChangesAsync(); return Json(user); } }
appsettings.json 添加下面配置
"ServiceDiscovery": { "ServiceName": "userapi", "Consul": { "HttpEndpoint": "http://127.0.0.1:8500", "DnsEndpoint": { "Address": "127.0.0.1", "Port": 8600 } }
添加poco,映射類
public class ServiceDisvoveryOptions { public string ServiceName { get; set; } public ConsulOptions Consul { get; set; } } public class ConsulOptions { public string HttpEndpoint { get; set; } public DnsEndpoint DnsEndpoint { get; set; } } public class DnsEndpoint { public string Address { get; set; } public int Port { get; set; } public IPEndPoint ToIPEndPoint() { return new IPEndPoint(IPAddress.Parse(Address), Port); } }
而後在Startup對其進行配置。ConfigureServices
services.Configure<ServiceDisvoveryOptions>(Configuration.GetSection("ServiceDiscovery"));
//使用此配置設置consul客戶端:
services.AddSingleton<IConsulClient>(p => new ConsulClient(cfg =>
{
var serviceConfiguration = p.GetRequiredService<IOptions<ServiceDisvoveryOptions>>().Value;
if (!string.IsNullOrEmpty(serviceConfiguration.Consul.HttpEndpoint))
{
// 若是未配置,客戶端將使用默認值「127.0.0.1:8500」
cfg.Address = new Uri(serviceConfiguration.Consul.HttpEndpoint);
}
}));
//consulclient不必定須要配置,若是沒有指定任何內容,它將返回到默認值(localhost:8500)。
動態服務註冊
只要kestrel用於在某個端口上託管服務,app.properties[「server.features」]就能夠用來肯定服務的託管位置。如上所述,若是使用了iis集成或任何其餘反向代理,則此解決方案將再也不工做,而且必須使用服務可訪問的實際端點在consul中註冊服務。但在啓動過程當中沒法獲取這些信息。
若是要將IIS集成與服務發現結合使用,請不要使用如下代碼。相反,能夠經過配置配置端點,或者手動註冊服務。
不管如何,對於紅隼,咱們能夠執行如下操做:獲取承載服務的uri紅隼(這不適用於useurls(「*:5000」)之類的通配符,而後遍歷地址以在consul中註冊全部地址:這裏默認使用 UseUrls("http://localhost:92")
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory, IApplicationLifetime applicationLifetime,
IOptions<ServiceDisvoveryOptions> serviceOptions, IConsulClient consul)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseHsts();
}
app.UseSwagger();
//啓用中間件服務對swagger-ui,指定Swagger JSON終結點
app.UseSwaggerUI(c =>
{
c.SwaggerEndpoint("/swagger/v1/swagger.json", "My API V1");
});
//app.UseHttpsRedirection();
//啓動的時候註冊服務
applicationLifetime.ApplicationStarted.Register(() =>
{
RegisterService(app, serviceOptions, consul);
});
//中止的時候移除服務
applicationLifetime.ApplicationStopped.Register(() =>
{
RegisterService(app, serviceOptions, consul);
});
app.UseMvc();
UserContextSeed.SeedAsync(app, loggerFactory).Wait();
// InitDataBase(app);
}
private void RegisterService(IApplicationBuilder app, IOptions<ServiceDisvoveryOptions> serviceOptions, IConsulClient consul)
{
var features = app.Properties["server.Features"] as FeatureCollection;
var addresses = features.Get<IServerAddressesFeature>()
.Addresses
.Select(p => new Uri(p));
foreach (var address in addresses)
{
var serviceId = $"{serviceOptions.Value.ServiceName}_{address.Host}:{address.Port}";
//serviceid必須是惟一的,以便之後再次找到服務的特定實例,以便取消註冊。這裏使用主機和端口以及實際的服務名
var httpCheck = new AgentServiceCheck()
{
DeregisterCriticalServiceAfter = TimeSpan.FromMinutes(1),
Interval = TimeSpan.FromSeconds(30),
HTTP = new Uri(address, "HealthCheck").OriginalString
};
var registration = new AgentServiceRegistration()
{
Checks = new[] { httpCheck },
Address =address.Host,
ID = serviceId,
Name = serviceOptions.Value.ServiceName,
Port = address.Port
};
consul.Agent.ServiceRegister(registration).GetAwaiter().GetResult();
}
}
private void DeRegisterSWervice(IApplicationBuilder app, IOptions<ServiceDisvoveryOptions> serviceOptions, IConsulClient consul)
{
var features = app.Properties["server.Features"] as FeatureCollection;
var addresses = features.Get<IServerAddressesFeature>().
Addresses.Select(p => new Uri(p));
foreach (var address in addresses)
{
var serviceId = $"{serviceOptions.Value.ServiceName}_{address.Host}:{address.Port}";
consul.Agent.ServiceDeregister(serviceId).GetAwaiter().GetResult();
}
}
添加健康檢查接口
[Route("HealthCheck")] [ApiController] public class HealthCheckController : ControllerBase { [HttpGet] [HttpHead] public IActionResult Get() { return Ok(); } }
添加項目Cateway.Api:添加端口爲91
添加Ocelot.json
{ "ReRoutes": [ { //暴露出去的地址 "UpstreamPathTemplate": "/{controller}", "UpstreamHttpMethod": [ "Get" ], //轉發到下面這個地址 "DownstreamPathTemplate": "/api/{controller}", "DownstreamScheme": "http", //資源服務器列表 "DownstreamHostAndPorts": [ { "host": "localhost", "port": 92 } ], "AuthenticationOptions": { "AuthenticationProviderKey": "finbook", "AllowedScopes": [] } }, { //暴露出去的地址 "UpstreamPathTemplate": "/connect/token", "UpstreamHttpMethod": [ "Post" ], //轉發到下面這個地址 "DownstreamPathTemplate": "/connect/token", "DownstreamScheme": "http", //資源服務器列表 "DownstreamHostAndPorts": [ { "host": "localhost", "port": 93 } ] } ], //對外暴露的訪問地址 也就是Ocelot所在的服務器地址 "GlobalConfiguration": { "BaseUrl": "http://localhost:91" } }
program 修改
public static IWebHostBuilder CreateWebHostBuilder(string[] args) => WebHost.CreateDefaultBuilder(args). ConfigureAppConfiguration((hostingContext, builder) => { builder .SetBasePath(hostingContext.HostingEnvironment.ContentRootPath) .AddJsonFile("Ocelot.json"); }) .UseUrls("http://+:91") .UseStartup<Startup>(); }
Startup
public class Startup { // This method gets called by the runtime. Use this method to add services to the container. // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940 public void ConfigureServices(IServiceCollection services) { var authenticationProviderKey = "finbook"; services.AddAuthentication(). AddIdentityServerAuthentication(authenticationProviderKey, options => { options.Authority = "http://localhost:93";//.Identity服務 配置 options.ApiName = "gateway_api"; options.SupportedTokens = SupportedTokens.Both; options.ApiSecret = "secret"; options.RequireHttpsMetadata = false; }); services.AddOcelot(); } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. public void Configure(IApplicationBuilder app, IHostingEnvironment env) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } app.UseOcelot().Wait(); app.UseMvc(); } }
添加項目User.Identity idneity代碼省略
使用dnsclient
appsettings.json 添加配置
"ServiceDiscovery": { "UserServiceName": "userapi", "Consul": { "HttpEndpoint": "http://127.0.0.1:8500", "DnsEndpoint": { "Address": "127.0.0.1", "Port": 8600 } }
註冊dnslookup客戶端:
services.Configure<ServiceDisvoveryOptions>(Configuration.GetSection("ServiceDiscovery")); //di中註冊dns lookup客戶端 services.AddSingleton<IDnsQuery>(p => { var serviceConfiguration = p.GetRequiredService<IOptions<ServiceDisvoveryOptions>>().Value; return new LookupClient(serviceConfiguration.Consul.DnsEndpoint.ToIPEndPoint()); });
private readonly IDnsQuery _dns; private readonly IOptions<ServiceDisvoveryOptions> _options; public SomeController(IDnsQuery dns, IOptions<ServiceDisvoveryOptions> options) { _dns = dns ?? throw new ArgumentNullException(nameof(dns)); _options = options ?? throw new ArgumentNullException(nameof(options)); } [HttpGet("")] [HttpHead("")] public async Task<IActionResult> DoSomething() { var result = await _dns.ResolveServiceAsync("service.consul", _options.Value.ServiceName); ... }
dnsclient.net的resolveserviceasync執行dns srv查找,匹配cname記錄,併爲每一個包含主機名和端口(以及地址(若是使用)的條目返回一個對象。
如今,咱們能夠用一個簡單的httpclient調用(或生成的客戶端)來調用服務:
var address = result.First().AddressList.FirstOrDefault(); var port = result.First().Port; using (var client = new HttpClient()) { var serviceResult = await client.GetStringAsync($"http://{address}:{port}/Values"); }
啓動項目
打開http://localhost:8500,查看服務運行
測試identity 服務
請求userapi -api/user
git:https://gitee.com/LIAOKUI/user.api
參考:http://michaco.net/blog/ServiceDiscoveryAndHealthChecksInAspNetCoreWithConsul?tag=ASP.NET%20Core#service-discovery