Consul服務發現的使用方法:
1. 在每臺電腦上都以Client Mode的方式運行一個Consul代理, 這個代理只負責與Consul Cluster高效地交換最新註冊信息(不參與Leader的選舉)
2. 每臺電腦上的服務Service都向本機的consul代理註冊 服務名稱和提供服務的url
3. 當Computer1上部署的程序ServiceA須要調用服務ServiceB時, 程序ServiceA直接從本機的Consul Agent經過服務名稱獲取ServiceB的訪問地址, 而後直接向ServiceB的url發出請求服務器
本文的重點不是描述上述過程, 只是準備分享一下本身編寫的服務註冊代碼, 雖然網上已經有很多相似的文章, 我的以爲本身的代碼要智能一點
其餘文章通常要求在參數中提供 服務訪問的外部地址, 但我想能夠直接從 IWebHostBuilder.UseUrls(params string[] urls)獲取, 這樣就能夠少輸入一個參數.
可是在實現的時候才發現, 問題不少
1. urls能夠輸入多個, 要有一個種最佳匹配的方式, 從裏面選取一個註冊到consul裏
這裏假設該服務會被不一樣服務器的程序調用, 那麼localhost(127.0.0.1)首先要排除掉, 剩下urls隨便選取一個供其餘程序調用, 固然用戶也能夠指定一個ip
2. urls可使用 "0.0.0.0"、"[::]", 表示綁定全部網卡地址的80端口, 而物理服務器每每有多個網卡
假如服務器確實只有一個IP, 直接使用便可; 假若有多個IP, 仍是必須讓用戶傳入經過參數指定一個IP
3. urls還可使用 "192.168.1.2:0"這種動態端口, asp.net core會隨機選擇一個端口讓外部訪問, 但這要服務器徹底啓動後才能得知是哪一個端口
使用IApplicationLifetime.ApplicationStarted 發起註冊請求
4. IPv6地址表示方式解析起來很是麻煩
System.Uri這個類已經支持IPv6, 能夠直接app
下面就是實現代碼, 須要nuget Consulasp.net
using Consul; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Hosting.Server.Features; using Microsoft.Extensions.Configuration; using System; using System.Collections.Generic; using System.Linq; public static class RegisterCansulExtension { public static void RegisterToConsul(this IApplicationBuilder app, IConfiguration configuration, IApplicationLifetime lifetime) { lifetime.ApplicationStarted.Register(() => { string serviceName = configuration.GetValue<string>("serviceName"); string serviceIP = configuration.GetValue<string>("serviceIP"); string consulClientUrl = configuration.GetValue<string>("consulClientUrl"); string healthCheckRelativeUrl = configuration.GetValue<string>("healthCheckRelativeUrl"); int healthCheckIntervalInSecond = configuration.GetValue<int>("healthCheckIntervalInSecond"); ICollection<string> listenUrls = app.ServerFeatures.Get<IServerAddressesFeature>().Addresses; if (string.IsNullOrWhiteSpace(serviceName)) { throw new Exception("Please use --serviceName=yourServiceName to set serviceName"); } if (string.IsNullOrEmpty(consulClientUrl)) { consulClientUrl = "http://127.0.0.1:8500"; } if (string.IsNullOrWhiteSpace(healthCheckRelativeUrl)) { healthCheckRelativeUrl = "health"; } healthCheckRelativeUrl = healthCheckRelativeUrl.TrimStart('/'); if (healthCheckIntervalInSecond <= 0) { healthCheckIntervalInSecond = 1; } string protocol; int servicePort = 0; if (!TryGetServiceUrl(listenUrls, out protocol, ref serviceIP, out servicePort, out var errorMsg)) { throw new Exception(errorMsg); } var consulClient = new ConsulClient(ConsulClientConfiguration => ConsulClientConfiguration.Address = new Uri(consulClientUrl)); var httpCheck = new AgentServiceCheck() { DeregisterCriticalServiceAfter = TimeSpan.FromSeconds(10),//服務啓動多久後註冊 Interval = TimeSpan.FromSeconds(healthCheckIntervalInSecond), HTTP = $"{protocol}://{serviceIP}:{servicePort}/{healthCheckRelativeUrl}", Timeout = TimeSpan.FromSeconds(2) }; // 生成註冊請求 var registration = new AgentServiceRegistration() { Checks = new[] { httpCheck }, ID = Guid.NewGuid().ToString(), Name = serviceName, Address = serviceIP, Port = servicePort, Meta = new Dictionary<string, string>() { ["Protocol"] = protocol }, Tags = new[] { $"{protocol}" } }; consulClient.Agent.ServiceRegister(registration).Wait(); //服務中止時, 主動發出註銷 lifetime.ApplicationStopping.Register(() => { try { consulClient.Agent.ServiceDeregister(registration.ID).Wait(); } catch { } }); }); } private static bool TryGetServiceUrl(ICollection<string> listenUrls, out string protocol, ref string serviceIP, out int port, out string errorMsg) { protocol = null; port = 0; errorMsg = null; if (!string.IsNullOrWhiteSpace(serviceIP)) // 若是提供了對外服務的IP, 只須要檢測是否在listenUrls裏面便可 { foreach (var listenUrl in listenUrls) { Uri uri = new Uri(listenUrl); protocol = uri.Scheme; var ipAddress = uri.Host; port = uri.Port; if (ipAddress == serviceIP || ipAddress == "0.0.0.0" || ipAddress == "[::]") { return true; } } errorMsg = $"The serviceIP that you provide is not in urls={string.Join(',', listenUrls)}"; return false; } else // 沒有提供對外服務的IP, 須要查找本機全部的可用IP, 看看有沒有在 listenUrls 裏面的 { var allIPAddressOfCurrentMachine = System.Net.NetworkInformation.NetworkInterface.GetAllNetworkInterfaces() .Select(p => p.GetIPProperties()) .SelectMany(p => p.UnicastAddresses) // 這裏排除了 127.0.0.1 loopback 地址 .Where(p => p.Address.AddressFamily == System.Net.Sockets.AddressFamily.InterNetwork && !System.Net.IPAddress.IsLoopback(p.Address)) .Select(p => p.Address.ToString()).ToArray(); var uris = listenUrls.Select(listenUrl => new Uri(listenUrl)).ToArray(); // 本機全部可用IP與listenUrls進行匹配, 若是listenUrl是"0.0.0.0"或"[::]", 則任意IP都符合匹配 var matches = allIPAddressOfCurrentMachine.SelectMany(ip => uris.Where(uri => ip == uri.Host || uri.Host == "0.0.0.0" || uri.Host == "[::]") .Select(uri => new { Protocol = uri.Scheme, ServiceIP = ip, Port = uri.Port }) ).ToList(); if (matches.Count == 0) { errorMsg = $"This machine has IP address=[{string.Join(',', allIPAddressOfCurrentMachine)}], urls={string.Join(',', listenUrls)}, none match."; return false; } else if (matches.Count == 1) { protocol = matches[0].Protocol; serviceIP = matches[0].ServiceIP; port = matches[0].Port; return true; } else { errorMsg = $"Please use --serviceIP=yourChosenIP to specify one IP, which one provide service: {string.Join(",", matches)}."; return false; } } } }
使用時能夠提供5個參數, 只有serviceName是必需要由參數提供的, serviceIP會自動儘量匹配出來, consulClientUrl默認是http://127.0.0.1:8500, healthCheckRelativeUrl默認是 /health, healthCheckIntervalInSecond默認是1秒運維
使用方法
上面代碼裏 健康檢查 使用了asp.net core的HealthChecks中間件, 須要在Startup.ConfigureServices(IServiceCollection services)中增長
services.AddHealthChecks();
在Startup.Configure也要增長UseHealthChecks()ide
public class Startup { public Startup(IConfiguration configuration) { Configuration = configuration; } public IConfiguration Configuration { get; } public void ConfigureServices(IServiceCollection services) { services.AddHealthChecks(); services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2); } public void Configure(IApplicationBuilder app, IHostingEnvironment env, IApplicationLifetime lifetime) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } // consul的http check只須要返回是200, 就認爲服務可用; 這裏必須把Degraded改成503 app.UseHealthChecks("/Health", new HealthCheckOptions() { ResultStatusCodes = { [Microsoft.Extensions.Diagnostics.HealthChecks.HealthStatus.Healthy] = StatusCodes.Status200OK, [Microsoft.Extensions.Diagnostics.HealthChecks.HealthStatus.Degraded] = StatusCodes.Status503ServiceUnavailable, [Microsoft.Extensions.Diagnostics.HealthChecks.HealthStatus.Unhealthy] = StatusCodes.Status503ServiceUnavailable } }); app.UseMvc(); app.RegisterToConsul(Configuration, lifetime); } }
調試的時候, 也不能使用默認的 http://localhost:5000了微服務
後記: 這段代碼也是好久之前寫的了, 後來發現Ocelot是個功能很豐富的網關, 幫咱們作了很大部分工做, 實施微服務的基礎功能就再也不寫了.oop
後來又學習了Kubernetes, 感受這纔是微服務的正道, 但Kubernetes更復雜, 對運維有更高的要求, 幸虧各大雲服務商都提供了Kubernetes的基礎設施, 這樣只須要開發人員提高開發方式便可學習
如今Service Mesh不斷髮展, 但還沒造成統一的解決方案, 其中Consul也支持Mesh, 有時間再寫吧ui
Endthis