ASP.NET CORE 使用Consul實現服務治理與健康檢查(2)——源碼篇

題外話

筆者有個習慣,就是在接觸新的東西時,必定要先搞清楚新事物的基本概念和背景,對之有個相對全面的瞭解以後再開始進入實際的編碼,這樣作最主要的緣由是儘可能避免因爲對新事物的認知誤區致使更大的缺陷,Bug 一旦發生,將比普通的代碼缺陷帶來更加昂貴的修復成本。服務器

相信有了前一篇和園子裏其餘同窗的文章,你已經基本上掌握了使用 Consul 所須要具有的背景知識,那麼就讓咱們來看下,具體到 ASP.NET Core 中,如何更加優雅的編碼。app

Consul 在 ASP.NET CORE 中的使用

Consul服務在註冊時須要注意幾個問題:asp.net

  1. 那就是必須是在服務徹底啓動以後再進行註冊,不然可能致使服務在啓動過程當中已經註冊到 Consul Server,這時候咱們要利用 IApplicationLifetime 應用程序生命週期管理中的 ApplicationStarted 事件。
  2. 應用程序向 Consul 註冊時,應該在本地記錄應用 ID,以此解決每次重啓以後,都會向 Consul 註冊一個新實例的問題,便於管理。

具體代碼以下:async

注意:如下均爲根據排版要求所展現的示意代碼,並不是完整的代碼ide

1. 服務治理之服務註冊

  • 1.1 服務註冊擴展方法
public static IApplicationBuilder AgentServiceRegister(this IApplicationBuilder app,
    IApplicationLifetime lifetime,
    IConfiguration configuration,
    IConsulClient consulClient,
    ILogger logger)
{
    try
    {
        var urlsConfig = configuration["server.urls"];
        ArgumentCheck.NotNullOrWhiteSpace(urlsConfig, "未找到配置文件中關於 server.urls 相關配置!");

        var urls = urlsConfig.Split(';');
        var port =  urls.First().Substring(httpUrl.LastIndexOf(":") + 1);
        var ip = GetPrimaryIPAddress(logger);
        var registrationId = GetRegistrationId(logger);

        var serviceName = configuration["Apollo:AppId"];
        ArgumentCheck.NotNullOrWhiteSpace(serviceName, "未找到配置文件中 Apollo:AppId 對應的配置項!");

        //程序啓動以後註冊
        lifetime.ApplicationStarted.Register(() =>
        {
            var healthCheck = new AgentServiceCheck
            {
                DeregisterCriticalServiceAfter = TimeSpan.FromSeconds(5),
                Interval = 5,
                HTTP = $"http://{ip}:{port}/health",
                Timeout = TimeSpan.FromSeconds(5),
                TLSSkipVerify = true
            };

            var registration = new AgentServiceRegistration
            {
                Checks = new[] { healthCheck },
                ID = registrationId,
                Name = serviceName.ToLower(),
                Address = ip,
                Port = int.Parse(port),
                Tags = ""//手動高亮
            };

            consulClient.Agent.ServiceRegister(registration).Wait();
            logger.LogInformation($"服務註冊成功! 註冊地址:{((ConsulClient)consulClient).Config.Address}, 註冊信息:{registration.ToJson()}");
        });

        //優雅的退出
        lifetime.ApplicationStopping.Register(() =>
        {
            consulClient.Agent.ServiceDeregister(registrationId).Wait();
        });

        return app;
    }
    catch (Exception ex)
    {
        logger?.LogSpider(LogLevel.Error, "服務發現註冊失敗!", ex);
        throw ex;
    }
}

private static string GetPrimaryIPAddress(ILogger logger)
{
    string output = GetLocalIPAddress();
    logger?.LogInformation(LogLevel.Information, "獲取本地網卡地址結果:{0}", output);

    if (output.Length > 0)
    {
        var ips = output.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
        if (ips.Length == 1) return ips[0];
        else
        {
            var localIPs = ips.Where(w => w.StartsWith("10"));//內網網段
            if (localIPs.Count() > 0) return localIPs.First();
            else return ips[0];
        }
    }
    else
    {
        logger?.LogSpider(LogLevel.Error, "沒有獲取到有效的IP地址,沒法註冊服務到服務中心!");
        throw new Exception("獲取本機IP地址出錯,沒法註冊服務到註冊中心!");
    }
}

public static string GetLocalIPAddress()
{
    if (!string.IsNullOrWhiteSpace(_localIPAddress)) return _localIPAddress;

    string output = "";
    try
    {
        foreach (NetworkInterface item in NetworkInterface.GetAllNetworkInterfaces())
        {
            if (item.OperationalStatus != OperationalStatus.Up) continue;

            var adapterProperties = item.GetIPProperties();
            if (adapterProperties.GatewayAddresses.Count == 0) continue;

            foreach (UnicastIPAddressInformation address in adapterProperties.UnicastAddresses)
            {
                if (address.Address.AddressFamily != AddressFamily.InterNetwork) continue;
                if (IPAddress.IsLoopback(address.Address)) continue;

                output = output += address.Address.ToString() + ",";
            }
        }
    }
    catch (Exception e)
    {
        Console.WriteLine("獲取本機IP地址失敗!");
        throw e;
    }

    if (output.Length > 0)
        _localIPAddress = output.TrimEnd(',');
    else
        _localIPAddress = "Unknown";

    return _localIPAddress;
}

private static string GetRegistrationId(ILogger logger)
{
    try
    {
        var basePath = Directory.GetCurrentDirectory();
        var folderPath = Path.Combine(basePath, "registrationid");
        if (!Directory.Exists(folderPath))
            Directory.CreateDirectory(folderPath);

        var path = Path.Combine(basePath, "registrationid", ".id");
        if (File.Exists(path))
        {
            var lines = File.ReadAllLines(path, Encoding.UTF8);
            if (lines.Count() > 0 && !string.IsNullOrEmpty(lines[0]))
                return lines[0];
        }

        var id = Guid.NewGuid().ToString();
        File.AppendAllLines(path, new[] { id });
        return id;
    }
    catch (Exception e)
    {
        logger?.LogWarning(e, "獲取 Registration Id 錯誤");
        return Guid.NewGuid().ToString();
    }
}
  • 1.2 健康檢查中間件

既然健康檢查是經過http請求來實現的,那麼咱們能夠經過 HealthMiddleware 中間件來實現:oop

public static void UseHealth(this IApplicationBuilder app)
{
    app.UseMiddleware<HealthMiddleware>();
}

public class HealthMiddleware
{
    private readonly RequestDelegate _next;
    private readonly string _healthPath = "/health";

    public HealthMiddleware(RequestDelegate next, IConfiguration configuration)
    {
        this._next = next;
        var healthPath = configuration["Consul:HealthPath"];
        if (!string.IsNullOrEmpty(healthPath))
        {
            this._healthPath = healthPath;
        }
    }

    //監控檢查能夠返回更多的信息,例如服務器資源信息
    public async Task Invoke(HttpContext httpContext)
    {
        if (httpContext.Request.Path == this._healthPath)
        {
            httpContext.Response.StatusCode = (int)HttpStatusCode.OK;
            await httpContext.Response.WriteAsync("I'm OK!");
        }
        else
            await this._next(httpContext);
    }
}
  • 1.3 Startup 配置
public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
    //手動高亮
    services.AddSingleton<IConsulClient>(sp =>
    {
        ArgumentCheck.NotNullOrWhiteSpace(this.Configuration["Consul:Address"], "未找到配置中Consul:Address對應的配置");
        return new ConsulClient(c => { c.Address = new Uri(this.Configuration["Consul:Address"]); });
    });
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env, IApplicationLifetime lifetime, IConsulClient consulClient, ILogger<Startup> logger)
{
    ...
    app.UseHealth();//手動高亮
    app.UseMvc();
    app.AgentServiceRegister(lifetime, this.Configuration, consulClient, logger);//手動高亮
}

2. 服務治理之服務發現

public class ServiceManager : IServiceManager
{
    private readonly IHttpClientFactory _httpClientFactory;
    private readonly ILogger _logger;
    private readonly IConsulClient _consulClient;
    private readonly IList<Func<StrategyDelegate, StrategyDelegate>> _components;
    private StrategyDelegate _strategy;

    public ServiceManager(IHttpClientFactory httpClientFactory,
        IConsulClient consulClient,
        ILogger<ServiceManager> logger)
    {
        this._cancellationTokenSource = new CancellationTokenSource();
        this._components = new List<Func<StrategyDelegate, StrategyDelegate>>();

        this._httpClientFactory = httpClientFactory;
        this._optionsConsulConfig = optionsConsulConfig;
        this._logger = logger;
        this._consulClient = consulClient;
    }

    public async Task<HttpClient> GetHttpClientAsync(string serviceName, string errorIPAddress = null, string hashkey = null)
    {
        //重要:獲取全部健康的服務
        var resonse = (await this._consulClient.Health.Service(serviceName.ToLower(), this._cancellationTokenSource.Token)).Response;
        var filteredService = this.GetServiceNode(serviceName, resonse.ToArray(), hashkey);
        return this.CreateHttpClient(serviceName.ToLower(), filteredService.Service.Address, filteredService.Service.Port);
    }

    private ServiceEntry GetServiceNode(string serviceName, ServiceEntry[] services, string hashKey = null)
    {
        if (this._strategy == null)
        {
            lock (this) { if (this._strategy == null) this._strategy = this.Build(); }
        }

        //策略過濾
        var filterService = this._strategy(serviceName, services, hashKey);
        return filterService.FirstOrDefault();
    }

    private HttpClient CreateHttpClient(string serviceName, string address, int port)
    {
        var httpClient = this._httpClientFactory.CreateClient(serviceName);
        httpClient.BaseAddress = new System.Uri($"http://{address}:{port}");
        return httpClient;
    }
}

服務治理之——訪問策略

服務在註冊時,能夠經過配置或其餘手段給當前服務配置相應的 Tags ,一樣在服務獲取時,咱們也將同時獲取到該服務的 Tags, 這對於咱們實現策略訪問夯實了基礎。例如開發和測試共用一套服務註冊發現基礎設施(固然這實際不可能),咱們就能夠經過給每一個服務設置環境 Tag ,以此來實現環境隔離的訪問。這個 tag 維度是沒有限制的,開發人員徹底能夠根據本身的實際需求進行打標籤,這樣既能夠經過內置默認策略兜底,也容許開發人員在此基礎之上動態的定製訪問策略。測試

筆者所實現的訪問策略方式相似於 Asp.Net Core Middleware 的方式,而且筆者認爲這個設計很是值得借鑑,並參考了部分源碼實現,使用方式也基本相同。
ui

源碼實現以下:this

//策略委託
public delegate ServiceEntry[] StrategyDelegate(string serviceName, ServiceEntry[] services, string hashKey = null);

//服務管理
public class ServiceManager:IServiceManager
{
    private readonly IList<Func<StrategyDelegate, StrategyDelegate>> _components;
    private StrategyDelegate _strategy;//策略鏈

    public ServiceManager()
    {
        this._components = new List<Func<StrategyDelegate, StrategyDelegate>>();
    }

    //增長自定義策略
    public IServiceManager UseStrategy(Func<StrategyDelegate, StrategyDelegate> strategy)
    {
        _components.Add(strategy);
        return this;
    }

    //build 最終策略鏈
    private StrategyDelegate Build()
    {
        StrategyDelegate strategy = (sn, services, key) =>
        {
            return new DefaultStrategy().Invoke(null, sn, services, key);
        };

        foreach (var component in _components.Reverse())
        {
            strategy = component(strategy);
        }

        return strategy;
    }
}
public class DefaultStrategy : IStrategy
{
    private ushort _idx;
    public DefaultStrategy(){}

    public ServiceEntry[] Invoke(StrategyDelegate next, string serviceName, ServiceEntry[] services, string hashKey = null)
    {
        var service = services.Length == 1 ? services[0] : services[this._idx++ % services.Length];
        var result = new[] { service };
        return next != null ? next(serviceName, result, hashKey) : result;
    }
}

自定義策略擴展方法以及使用編碼

public static IApplicationBuilder UseStrategy(this IApplicationBuilder app)
{
    var serviceManager = app.ApplicationServices.GetRequiredService<IServiceManager>();
    var strategies = app.ApplicationServices.GetServices<IStrategy>();

    //註冊全部的策略
    foreach (var strategy in strategies)
    {
        serviceManager.UseStrategy(next =>
        {
            return (serviceName, services, hashKey) => strategy.Invoke(next, serviceName, services, hashKey);
        });
    }
    return app;
}

public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        ...
        services.AddSingleton<IStrategy, CustomStrategy>(); //自定義策略1
        services.AddSingleton<IStrategy, CustomStrategy2>(); //自定義測率2
    }

    public void Configure(IApplicationBuilder app, IHostingEnvironment env, IApplicationLifetime lifetime, IConsulClient consulClient, ILogger<Startup> logger)
    {
        app.UseStrategy(); //手動高亮
        app.AgentServiceRegister(lifetime, this.Configuration, consulClient, logger);
    }
}
相關文章
相關標籤/搜索