歡迎光臨
每天分享高質量文章

NET Core微服務之路:實戰SkyWalking+Exceptionless體驗生產下追蹤系統

前言

當一個APM或一個日誌中心實際部署在生產環境中時,是有點力不從心的。

比如如下場景分析的問題:

  • 從APM上說,知道某個節點出現異常,或延遲過過高,卻不能及時知道日誌反饋情況,總不可能去相應的節點上一個一個的翻日誌檔案吧。

  • 從日誌中心上說(特別是Exceptionless,能及時反饋出異常信息),知道某個節點出現異常日誌,可不知道引起異常的源頭在哪;或者出現延遲過高日誌,卻不能及時知道節點問題,還是鏈路問題;就算諸上問題都能應付,那麼一行行的、一個個的日誌檔案和使用圖形化的表述形式,誰會更加直觀,當然,你說你可以一目十行,甚至百行來分析日誌,那我挺佩服你的。

本節內容較多,所以筆者特列舉瞭如下目錄。

一:準備

    1.SkyWalking和Exceptionless簡單回顧

    2.新建多個站點(物理節點)

    3.附加SkyApm-dotnet程式集到宿主

二:將SkyApm-dotnet的日誌輸出到Exceptionless

    4.SkyApm-dotnet的日誌入口

    5.繼承ILoggerFactory獲取全域性ILogger物件

    6.將Logger寫入到Exceptionless

三:運行

    7.SkyWalking和Exceptionless的結合分析

SkyWalking和Exceptionless簡單回顧

前兩篇就《NET Core微服務之路:SkyWalking+SkyApm-dotnet分佈式鏈路追蹤系統的分享》和《NET Core微服務之路:簡單談談對ELK,Splunk,Exceptionless統一日誌收集中心的心得體會》簡單的介紹了SkyApm-dotnet和三個日誌收集中心。為何最終會選擇SkyWalking和Exceptionless來作為生產實戰,很簡單:

1.SkyWalking和Exceptionless的儲存和檢索都是使用的ElasticSearch,ES的強大之處不用介紹:“you know, for search”

2.SkyWalking作為國人(吳晟)開發的一套開源追蹤系統,雖然比不上Pinpoint功能強大,但社區活躍且免費,相信開源的力量,會越來越完善,甚至更好。

3.Exceptionless作為.Net開源社區的新起之秀,目前也十分活躍,原生.Net語言支持,能做到日後無縫擴展。

新建多個站點(物理節點)

傳統單體應用(或站點)沒必須要做到APM追蹤,因為她毫無意義。只有在分佈式架構樣式下,例如SOA、微服務等架構才有意義,比如說,你在兩個地方分別部署了多個應用,當某個地方的應用出現了故障,你總不可能專門跑去一個一個檔案的查閱日誌吧,假如這個應用部署在火星呢(哈哈,開個玩笑)。

我們就SkyApm-dotnet中的

Sample https://github.com/SkyAPM/SkyAPM-dotnet/tree/master/sample

做一些二次修改和擴展,來模擬一個實際的分佈式系統。

先看看這個系統的網絡拓撲圖:

asp-net-core-*為系統主要節點,而localhost:50000為Exceptionless的日誌中心,114.215是資料庫,具體每個線條的顏色請查閱SkyWalking手冊。

asp-net-core-aspnetcore:我們可以把她理解為請求端,筆者在裡面做了一個單請求,和一個並行請求,嚴格意義上來說,代碼中不應該有try catch來進行重試,而是應該使用polly的Retry進行重試和異常處理,可以參考《NET Core微服務之路:彈性和瞬態故障處理庫Polly的介紹》,代碼參考如下:

[Route("api/[controller]")]
[ApiController]
public class ValuesController : ControllerBase
{
    [HttpGet]
    public async Task<string> Get()
    {
        var httpClient = new HttpClient();
        var values = await httpClient.GetStringAsync("http://localhost:5001/api/values");
        ExceptionlessClient.Default.SubmitLog(JsonConvert.SerializeObject(values), LogLevel.Debug);
        return values;
    }


    [HttpGet("getall")]
    public string GetAll()
    {
        var list = new List<int>();
        var listValue = new List<string>();
        for (var i = 1; i <= 50; i++)
        {
            list.Add(i);
        }


        Parallel.ForEach(list, (i, state) =>
        {
            try
            {
                using (var httpClient = new HttpClient())
                {
                    listValue.Add(httpClient.GetStringAsync($"http://localhost:5001/api/values/{i}/other").Result);
                }
            }
            catch (Exception)
            {
                // ignored
            }
        });
        ExceptionlessClient.Default.SubmitLog(JsonConvert.SerializeObject(listValue), LogLevel.Debug);
        return JsonConvert.SerializeObject(listValue);
    }
}

asp-net-core-frontend:我們可以把她理解為一個網關,一個中繼,或者一個權限驗證等等,筆者沒做太多處理,就單純做了一個switch的引數選擇橋接,參考代碼如下:

[Route("api/[controller]")]
[ApiController]
public class ValuesController : ControllerBase
{
    [HttpGet]
    public async Task<string> Get()
    {
        var httpClient = new HttpClient();
        var values = await httpClient.GetStringAsync("http://localhost:5002/api/values");
        return values;
    }


    [HttpGet("{id:int}/other")]
    public async Task<string> Get(int id)
    {
        var httpClient = new HttpClient();
        var values = "";
        switch (id)
        {
            case 1:
                values = await httpClient.GetStringAsync("http://localhost:5002/api/delay/100");
                break;
            case 2:
                values = await httpClient.GetStringAsync("http://localhost:5002/api/Error");
                break;
            case 3:
                values = await httpClient.GetStringAsync("http://localhost:5002/api/Values");
                break;
            case 4:
                values = await httpClient.GetStringAsync("http://localhost:5002/api/Apps");
                break;
            case 5:
            {
                var userClient = new User.UserClient(new Channel("127.0.0.1:5050", ChannelCredentials.Insecure));
                var response = await userClient.GetListAsync(new GetListRequest());
                if (response.Code == 1000)
                {
                    return JsonConvert.SerializeObject(response.Data);
                }


                break;
            }
        }


        return values;
    }
}

asp-net-core-backend:我們可以把她理解為一個節點,筆者還創建了一個Grpc的服務節點,不知是因為目前SkyApm-dotnet探針沒做Grpc的適配,還是筆者這邊配置錯誤,目前並未實現Grpc的追蹤,代碼較多,就不一一的貼上來了,做個截圖即可,原始碼在文章最後

附加SkyApm-dotnet程式集到宿主

An IHostingStartup (hosting startup) implementation adds enhancements to an app at startup from an external assembly. For example, an external library can use a hosting startup implementation to provide additional configuration providers or services to an app. IHostingStartup is available in ASP.NET Core 2.0 or later.

通過追加外部程式集來增強宿主功能,例如,可以在外部程式集中提供額外的服務或配置,此項功能支持NET Core 2.0+。

當然,能加載也就能禁用 ,使用ASPNETCORE_PREVENTHOSTINGSTARTUP便可實現。除以上通過set的方式配置環境引數以外,還可以通過代碼的方式來指定ASPNETCORE_HOSTINGSTARTUPASSEMBLIES啟動擴展程式集。

Environment.SetEnvironmentVariable(“ASPNETCORE_HOSTINGSTARTUPASSEMBLIES”, “SkyAPM.Agent.AspNetCore”);

對了,在WebHosting的環境變數定義中,預設提供瞭如下環境變數,有興趣的朋友可深入研究。

SkyApm-dotnet的日誌入口

在SkyApm-dotnet的配置檔案中,預設是開啟了本地日誌的,像這樣

"Logging": {
  "Level": "Information",
  "FilePath": "logs/skyapm-{Date}.log"
},

如果部署了多個SkyApm-dotnet探針到節點,那是不是要在多個節點上來查閱日誌呢?答案肯定是拒絕的,如果這樣下來,那麼我們的日誌收集中心就沒有任何存在的意義了。所以,為了實現這個功能,找到了SkyApm.Logging.ILoggerFactory的接口,使用再次註入的方式,替換了原來預設的DefaultLoggerFactory(當然,如果有更好的方式,或者已經提供了接口,麻煩大家告知一下),這是預設日誌註入的原始碼:

可以看到,SkyApm-dotnet的日誌預設通過ServiceCollection進行註入,我們只需要實現ILoggerFactory便可實現自定義的日誌處理方式。

繼承ILoggerFactory獲取全域性ILogger物件

通過F12我們可以定位接口的具體原始碼定義,可以看到SkyApm.Logging中,定義了一個ILoggerFactory的接口定義,內部需實現一個Ilogger的創建,代碼原始碼截圖如下:

我們可以實現這個接口,定義為我們自己實現的處理方式。但是,其實我們可以將原始碼拷貝過來,因為我們仍然需要將日誌儲存在本地作為副本,而不是單純將日誌發送到日誌中心,所以需要另起一個實現的名字,我這裡取名叫SkyApmExtensionsLoggerFactory,原始碼如下:

namespace SkyApmExceptionless
{
    public class SkyApmExtensionsLoggerFactory : SkyApm.Logging.ILoggerFactory
    {
        private const string OutputTemplate =
            @"{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{ServiceName}] [{Level}] {SourceContext} : {Message}{NewLine}{Exception}";


        private readonly LoggerFactory _loggerFactory;


        public SkyApm.Logging.ILogger CreateLogger(Type type)
        {
            return new SkyApmExtensionsLogger(_loggerFactory.CreateLogger(type));
        }


        public SkyApmExtensionsLoggerFactory(IConfigAccessor configAccessor)
        {
            _loggerFactory = new LoggerFactory();


            var loggingConfig = configAccessor.Get();
            var instrumentationConfig = configAccessor.Get();
            var level = EventLevel(loggingConfig.Level);


            _loggerFactory.AddSerilog(new LoggerConfiguration().MinimumLevel.Verbose().Enrich
                .WithProperty("SourceContext", null).Enrich
                .WithProperty(nameof(instrumentationConfig.ServiceName),
                    instrumentationConfig.ServiceName ?? instrumentationConfig.ApplicationCode).Enrich
                .FromLogContext().WriteTo.RollingFile(loggingConfig.FilePath,
                    level,
                    OutputTemplate,
                    null,
                    1073741824,
                    31,
                    null,
                    false,
                    false,
                    TimeSpan.FromMilliseconds(500)).CreateLogger());
        }


        private static LogEventLevel EventLevel(string level)
        {
            return Enum.TryParse(level, out var logEventLevel)
                ? logEventLevel
                : LogEventLevel.Error;
        }
    }
}

從上面的代碼加粗的代碼中可以看到,通過ILoggerFactory創建了一個SkyApm.Logging.ILogger的實現SkyApmExtensionsLogger,這樣,我們便拿到的SkyApm.Logging.ILoggerFactory的ILogger接口,接下來便是將ILogger的具體實現功能寫到Exceptionless。

將Logger寫入到Exceptionless

先看看SkyApm.Logging.ILogger的接口定義,原始碼截圖如下:

超級簡單,跟NLog,Log4net等等日誌組件的接口定義大同小異,幾乎可以說是一樣的,包含Debug, Information, Warning, Error, Trace,接下來該怎麼做,就變得十分簡單了,不過,在寫入這個日誌前,先簡單瞭解一下Exceptionless的用法。

1.創建一個日誌。原始碼定義為Source,我覺得叫組比較容易理解,她就像一個分類器,指定她的名稱是SkyApmExtensionsLogger,其次,可以提交不同的日誌型別,Exceptionless定義瞭如下幾種日誌等級,其實有部分我們用不著。

ExceptionlessClient.Default.CreateLog(nameof(SkyApmExtensionsLogger), "Create logging started.", Exceptionless.Logging.LogLevel.Info).Submit();

2.創建一個會話Session。ession會話的作用在Exceptionless算是一個特殊功能的存在了,她可以自動發送會話開始,會話心跳和會話結束事件,使用非常簡單,後面會截圖介紹這個功能的作用。

ExceptionlessClient.Default.Configuration.UseSessions();

OK,Exceptionless就介紹這麼點用法(詳細更多用法可參考官網),已經可以滿足日誌的寫入(或收集)了,接下來看看完整的原始碼:

using System;
using Exceptionless;
using Microsoft.Extensions.Logging;

namespace SkyApmExceptionless
{
    internal class SkyApmExtensionsLogger : SkyApm.Logging.ILogger
    {
        private readonly ILogger _readLogger;


        public SkyApmExtensionsLogger(ILogger readLogger)
        {
            _readLogger = readLogger;
            ExceptionlessClient.Default.CreateLog(nameof(SkyApmExtensionsLogger), "Create logging started.", Exceptionless.Logging.LogLevel.Info).Submit();
            ExceptionlessClient.Default.Configuration.UseSessions();
            ExceptionlessClient.Default.Configuration.SetUserIdentity("SetUserIdentity", $"{nameof(SkyApmExtensionsLogger)} Groups");
        }


        public void Debug(string message)
        {
            _readLogger.LogDebug(message);
            ExceptionlessClient.Default
                .CreateLog(nameof(SkyApmExtensionsLogger), message, Exceptionless.Logging.LogLevel.Debug).Submit();
        }


        public void Information(string message)
        {
            _readLogger.LogInformation(message);
            ExceptionlessClient.Default
                .CreateLog(nameof(SkyApmExtensionsLogger), message, Exceptionless.Logging.LogLevel.Info).Submit();
        }


        public void Warning(string message)
        {
            _readLogger.LogWarning(message);
            ExceptionlessClient.Default
                .CreateLog(nameof(SkyApmExtensionsLogger), message, Exceptionless.Logging.LogLevel.Warn).Submit();
        }


        public void Error(string message, Exception exception)
        {
            _readLogger.LogError(message + Environment.NewLine + exception);
            ExceptionlessClient.Default
                .CreateLog(nameof(SkyApmExtensionsLogger), message + Environment.NewLine + exception,
                    Exceptionless.Logging.LogLevel.Error)
                .Submit();
        }


        public void Trace(string message)
        {
            _readLogger.LogTrace(message);
            ExceptionlessClient.Default
                .CreateLog(nameof(SkyApmExtensionsLogger), message, Exceptionless.Logging.LogLevel.Trace).Submit();
        }
    }
}

這樣,通過SkyApm-dotnet生成的日誌,將自動發送到Exceptionless日誌中心去,是不是非常簡單。當然,如果作者有更好的建議,歡迎分享和交流。

SkyWalking和Exceptionless的結合分析

通過上面的擴展和部署,我們已經可以開始跑起來玩一玩了,如果有小伙伴跑不通,或者懶得敲代碼(哎…),原始碼在文章結尾,但如何配置環境還請自行搜索,以免浪費篇幅。

萬惡的再來兩張截圖,哈哈,其實是先看看預設狀態下SkyWalking和Exceptionless的初始界面。

讓我們啟動這個專案。嗯,很好,發現日誌正在蹭蹭的上漲,再來一張萬惡的全屏截圖。

我們並沒運行任何一個接口,也並沒呼叫任何一個接口,這日誌是哪來的呢,對,就是SkyApm-dotnet的日誌,我們可以通過Session裡面查看到SkyApmExtensionsLogger正在不斷的追加日誌,這是因為SkyApm-Agent正在運行追蹤,這裡也清晰的解釋了Session事件在這個SkyApmExtensionsLogger中的作用(目前還在不斷的追加中)。

再看看SkyWalking,很好,出現了三個服務(節點)

運行一下,代碼在上面,萬惡的全屏截圖再來一張:

我們發現,在ListMode中有報錯的情況,這樣:

趕緊定位到日誌,搜索Api/Error

嗯,這正是剛纔掃清兩次所產生的錯誤結果,也是筆者故意丟擲的,查看一下詳情

確實由於5001上面接受到了遠程傳回404錯誤,因為這個接口實際就不存在。

反之,你也可以通過Exceptionless的exception模塊或其他日誌來反查SkyWalking詳情,但是這樣的效率不高。

萬惡的全屏截圖已結束,感謝!

總結

通過APM和日誌中心(例如SkyWalking和Exceptionless)進行整合分析的場景越來越被重視和使用,如果還是停留在單個日誌分析,或者單個APM分析,那麼隨著節點數的增加,服務的規模增加,那將無法及時確定問題所在的。還有更多的結合用法,歡迎小伙伴們共同交流。

原文地址:

https://www.cnblogs.com/SteveLee/p/SkyWalking_Exceptionless_Actual_Combat.html

赞(0)

分享創造快樂