Asp.net core 向Consul 注冊服務


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, 可以直接

 

下面就是實現代碼, 需要nuget Consul

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;
            }
        }
    }
}
View Code

使用時可以提供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()

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是個功能很豐富的網關, 幫我們做了很大部分工作, 實施微服務的基礎功能就不再寫了.

后來又學習了Kubernetes, 感覺這才是微服務的正道, 但Kubernetes更復雜, 對運維有更高的要求, 幸好各大雲服務商都提供了Kubernetes的基礎設施, 這樣只需要開發人員提升開發方式即可

現在Service Mesh不斷發展, 但還沒形成統一的解決方案, 其中Consul也支持Mesh, 有時間再寫吧

 

 

 

End

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM