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

来源:https://www.cnblogs.com/xboo/archive/2019/12/15/12030951.html
-Advertisement-
Play Games

題外話 筆者有個習慣,就是在接觸新的東西時,一定要先搞清楚新事物的基本概念和背景,對之有個相對全面的瞭解之後再開始進入實際的編碼,這樣做最主要的原因是儘量避免由於對新事物的認知誤區導致更大的缺陷,Bug 一旦發生,將比普通的代碼缺陷帶來更加昂貴的修複成本。 相信有了前一篇和園子里其他同學的文章,你已 ...


題外話

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

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

Consul 在 ASP.NET CORE 中的使用

Consul服務在註冊時需要註意幾個問題:

  1. 那就是必須是在服務完全啟動之後再進行註冊,否則可能導致服務在啟動過程中已經註冊到 Consul Server,這時候我們要利用 IApplicationLifetime 應用程式生命周期管理中的 ApplicationStarted 事件。
  2. 應用程式向 Consul 註冊時,應該在本地記錄應用 ID,以此解決每次重啟之後,都會向 Consul 註冊一個新實例的問題,便於管理。

具體代碼如下:

註意:以下均為根據排版要求所展示的示意代碼,並非完整的代碼

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 中間件來實現:

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 的方式,並且筆者認為這個設計非常值得借鑒,並參考了部分源碼實現,使用方式也基本相同。

源碼實現如下:

//策略委托
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);
    }
}

您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 前言 本文的文字及圖片來源於網路,僅供學習、交流使用,不具有任何商業用途,版權歸原作者所有,如有問題請及時聯繫我們以作處理。作者:Botreechan 1.進入地址我們可以發現,頁面有著非常整齊的目錄,那麼網頁源代碼中肯定也有非常規律的目錄,進去看看吧。如果你看不懂,建議先去小編的Python交流. ...
  • 本項目實現的是一個微riscv處理器核(tinyriscv),用verilog語言編寫,只求以最簡單、最通俗易懂的方式實現riscv指令的功能,因此沒有特意去對代碼做任何的優化,因此你會看到裡面寫的代碼有很多冗餘的地方。tinyriscv處理器核有以下特點: 1)實現了RV32I指令集,通過risc ...
  • 數據挖掘作業,要實現決策樹,現記錄學習過程 win10系統,Python 3.7.0 構建一個決策樹,在鳶尾花數據集上訓練一個DecisionTreeClassifier: from sklearn.datasets import load_iris from sklearn.tree import ...
  • web應用簡介 Web 應用在我們的生活中無處不在。看看我們日常使用的各個應用程式,它們要 麽是 Web 應用,要麼是移動 App 這類 Web 應用的變種。無論哪一種編程語言,只要 它能夠開發出與人類交互的軟體,它就必然會支持 Web 應用開發。對一門嶄新的編程 語言來說,它的開發者首先要做的一件 ...
  • 有時候我們想要修改xadmin詳情頁欄位的顯示方式,比如django預設的ImageField在後臺顯示的是image的url,我們更希望看到image的縮略圖;再比如django將多對多欄位顯示為多選的下拉框或者左右選擇欄的方式,向圖片展示的這兩種: 如果我想要上面這種帶搜索功能並且只占一行的效果 ...
  • 相信很多初次接觸java的同學,在遇見主函數的時候,聽到最多的就是主函數的格式是固定的,不能進行修改等等。這就讓人疑惑了,主函數為啥就那麼特殊呢?接下來博主會為大家解釋主函數。大家先看看下麵這個程式: 1 public static void main(String[ ] args){ 2 Syst ...
  • 一種可以實現" 先進先出 "的存儲結構 分類: 1. 鏈式隊列:用鏈表實現 2. 靜態隊列:用數組實現,靜態隊列通常都必須是 迴圈隊列 迴圈隊列的講解: 1. 靜態隊列為什麼是迴圈隊列 減少對記憶體的浪費 2. 迴圈隊列需要幾個參數來確定 兩個參數, frant 、rear 但這2個參數不同場合有不同 ...
  • 大家好,我是Dotnet9小編,一個從事dotnet開發8年+的程式員。我最近在寫dotnet分享文章,希望能讓更多人看到dotnet的發展,瞭解更多dotnet技術,幫助dotnet程式員應用dotnet技術更好的運用於工作和學習中去。 文章閱讀導航 一、寫在前面的話 二、HZHControls介 ...
一周排行
    -Advertisement-
    Play Games
  • 示例項目結構 在 Visual Studio 中創建一個 WinForms 應用程式後,項目結構如下所示: MyWinFormsApp/ │ ├───Properties/ │ └───Settings.settings │ ├───bin/ │ ├───Debug/ │ └───Release/ ...
  • [STAThread] 特性用於需要與 COM 組件交互的應用程式,尤其是依賴單線程模型(如 Windows Forms 應用程式)的組件。在 STA 模式下,線程擁有自己的消息迴圈,這對於處理用戶界面和某些 COM 組件是必要的。 [STAThread] static void Main(stri ...
  • 在WinForm中使用全局異常捕獲處理 在WinForm應用程式中,全局異常捕獲是確保程式穩定性的關鍵。通過在Program類的Main方法中設置全局異常處理,可以有效地捕獲並處理未預見的異常,從而避免程式崩潰。 註冊全局異常事件 [STAThread] static void Main() { / ...
  • 前言 給大家推薦一款開源的 Winform 控制項庫,可以幫助我們開發更加美觀、漂亮的 WinForm 界面。 項目介紹 SunnyUI.NET 是一個基於 .NET Framework 4.0+、.NET 6、.NET 7 和 .NET 8 的 WinForm 開源控制項庫,同時也提供了工具類庫、擴展 ...
  • 說明 該文章是屬於OverallAuth2.0系列文章,每周更新一篇該系列文章(從0到1完成系統開發)。 該系統文章,我會儘量說的非常詳細,做到不管新手、老手都能看懂。 說明:OverallAuth2.0 是一個簡單、易懂、功能強大的許可權+可視化流程管理系統。 有興趣的朋友,請關註我吧(*^▽^*) ...
  • 一、下載安裝 1.下載git 必須先下載並安裝git,再TortoiseGit下載安裝 git安裝參考教程:https://blog.csdn.net/mukes/article/details/115693833 2.TortoiseGit下載與安裝 TortoiseGit,Git客戶端,32/6 ...
  • 前言 在項目開發過程中,理解數據結構和演算法如同掌握蓋房子的秘訣。演算法不僅能幫助我們編寫高效、優質的代碼,還能解決項目中遇到的各種難題。 給大家推薦一個支持C#的開源免費、新手友好的數據結構與演算法入門教程:Hello演算法。 項目介紹 《Hello Algo》是一本開源免費、新手友好的數據結構與演算法入門 ...
  • 1.生成單個Proto.bat內容 @rem Copyright 2016, Google Inc. @rem All rights reserved. @rem @rem Redistribution and use in source and binary forms, with or with ...
  • 一:背景 1. 講故事 前段時間有位朋友找到我,說他的窗體程式在客戶這邊出現了卡死,讓我幫忙看下怎麼回事?dump也生成了,既然有dump了那就上 windbg 分析吧。 二:WinDbg 分析 1. 為什麼會卡死 窗體程式的卡死,入口門檻很低,後續往下分析就不一定了,不管怎麼說先用 !clrsta ...
  • 前言 人工智慧時代,人臉識別技術已成為安全驗證、身份識別和用戶交互的關鍵工具。 給大家推薦一款.NET 開源提供了強大的人臉識別 API,工具不僅易於集成,還具備高效處理能力。 本文將介紹一款如何利用這些API,為我們的項目添加智能識別的亮點。 項目介紹 GitHub 上擁有 1.2k 星標的 C# ...