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
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...