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做一些二次修改和擴展,來模擬一個實際的分布式系統。
先看看這個系統的網絡拓撲圖:

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程序集到宿主

我們在啟動Aspnetcore應用的時候,可以通過Appsettings.*.json來配置應用的環境參數,比如ASPNETCORE_ENVIRONMENT,可以設置當前應用的環境是開發(Development)還是生產(Production),關於多環境的介紹,可以參考這篇文章 https://docs.microsoft.com/en-us/aspnet/core/fundamentals/environments?view=aspnetcore-2.0。而SkyApm-dotnet的最大優勢,就是達到了開箱即用,我們只需要通過ASPNETCORE_HOSTINGSTARTUPASSEMBLIES參數來指定SkyApm即可,當然,你可以使用set ASPNETCORE_HOSTINGSTARTUPASSEMBLIES=SkyApm.Agent.AspNetCore,也可以通過配置文件來啟用SkyApm.Agent.AspNetCore。ASPNETCORE_HOSTINGSTARTUPASSEMBLIES是個什么鬼,我們來查一查微軟官方的解釋(地址 https://docs.microsoft.com/en-us/aspnet/core/fundamentals/host/platform-specific-configuration?view=aspnetcore-2.0):
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<LoggingConfig>();
            var instrumentationConfig = configAccessor.Get<InstrumentConfig>();
            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<LogEventLevel>(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分析,那么隨着節點數的增加,服務的規模增加,那將無法及時確定問題所在的。還有更多的結合用法,歡迎小伙伴們共同交流。
 

參考

 
 
感謝閱讀!
 


免責聲明!

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



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