链路追踪在ERP系统中的应用实践
源寶導讀:隨著ERP的部署架構越來越復雜,對運維監(jiān)控、問題排查等工作增加了難度,本文將介紹通過引入鏈路追蹤技術,提高ERP系統(tǒng)問題排查效率,支撐更全面監(jiān)控系統(tǒng)運行情況的實踐過程。
一、導讀
? ? 隨著ERP的部署架構越來越復雜,微應用分布式部署架構在給用戶帶來高性能高穩(wěn)定的同時,給運維監(jiān)控、問題排查帶來了一定難度,特別是后端服務的內部調用由于缺少日志,難以快速定位問題根因。借助鏈路追蹤并結合異常日志所記錄的鏈路id,可以方便定位整個異常鏈路,更快找到問題的原因。鏈路追蹤還有一個好處,針對性能慢的頁面,原來很難快速定位到慢的具體點,通過借助鏈路追蹤,能快速掌握每個請求執(zhí)行了哪些操作,每個操作消耗了多長時間,精準定位性能問題。
二、鏈路追蹤介紹
? ? 分布式系統(tǒng)變得日趨復雜,越來越多的組件開始走向分布式化,如微服務、分布式數據庫、分布式緩存等,使得后臺服務構成了一種復雜的分布式網絡。
? ? 在服務能力提升的同時,復雜的網絡結構也使問題定位更加困難。在一個請求在經過諸多服務過程中,出現了某一個調用失敗的情況,查詢具體的異常由哪一個服務引起的就變得十分抓狂,問題定位和處理效率是也會非常低。
? ? 分布式鏈路追蹤就是將一次分布式請求還原成調用鏈路,將一次分布式請求的調用情況集中展示,比如各個服務節(jié)點上的耗時、請求具體到達哪臺機器上、每個服務節(jié)點的請求狀態(tài)等等。
? ? 目前業(yè)界的鏈路追蹤系統(tǒng),如Twitter的Zipkin,Uber的Jaeger,國內比較流行的SkyWalking。
三、ERP中鏈路追蹤的實現
什么是 Diagnostics
? ? 在.NET Core中實現鏈路追蹤非常簡單,因為在 .NET Core 中 .NET 團隊設計了一個全新的 DiagnosticSource,新的 DiagnosticSource 非常的簡單,它允許你在生產環(huán)境記錄豐富的 payload 數據,然后你可以在另外一個消費者中消費感興趣的記錄。
? ? ERP因為是.NET Framework實現鏈路追蹤復雜一些,對此為了實現鏈路追蹤,我們將Diagnostics應用到ERP中,手動創(chuàng)建生產者,然后在具體的鏈路追蹤中創(chuàng)建對應消費者采集數據。
? ? Diagnostics 就是提供一組功能使我們能夠很方便的可以記錄在應用程序運行期間發(fā)生的關鍵性操作以及他們的執(zhí)行時間等,使管理員可以查找特別是生產環(huán)境中出現問題所在的根本原因。
? ? 在應用程序出現問題的時候,特別是出現可用性或者性能問題的時候,開發(fā)人員或者IT人員經常會對這些問題花費大量的時間來進行診斷,很多時候生產環(huán)境的問題都無法復現,這可能會對業(yè)務造成很大的影響。
? ? 目前平臺實現了兩種對接SkyWalking和Jaeger,可以方便的在配置中心啟用,接入自建服務或云服務都是支持。
SkyWalking對接
? ? SkyWalking目前對于.NET Core有著很好的支持,但是對于.NET Framework因為本身Diagnostics的原因,只有一個很簡單的ASP.NET請求客戶端。
? ? 其它的客戶端我們都做了一些改造,但有一些我們也盡量標準,如SqlClient、HttpClient。以下介紹一下Redis的支持,完全自己實現客戶端。
? ? 首先要創(chuàng)建生產者DiagnosticListener,然后在Redis的相關操作里執(zhí)行具體事件。
? ? DiagnosticListener需要引入System.Diagnostics.DiagnosticSource這個dll,以下為簡單實現執(zhí)行前事件:
internal static class RedisDiagnosticListenerExtensions {/// <summary>/// 定義監(jiān)聽的名稱/// </summary>public const string DiagnosticListenerName = "RedisDataDiagnosticListener";/// <summary>/// 前綴/// </summary>public const string CacheDataPrefix = "Mysoft.Map6.Cache.";/// <summary>/// 執(zhí)行前事件名稱/// </summary>public const string CacheBeforeExecuteName = CacheDataPrefix + nameof(RedisExecuteBefore);public static Guid RedisExecuteBefore(this DiagnosticListener @this, string operation,string endpoint, string cacheKey, long? cacheLength = null){if (@this.IsEnabled(CacheBeforeExecuteName)){Guid operationId = Guid.NewGuid();@this.Write(CacheBeforeExecuteName,new{OperationId = operationId,Operation = operation,Endpoint = endpoint,CacheKey = cacheKey,CacheLength = cacheLength,Timestamp = Stopwatch.GetTimestamp()});return operationId;}elsereturn Guid.Empty;} }? ? 然后在具體行為里面執(zhí)行事件,以下寫入Redis緩存為例:
private bool WriteToRedis(string key, byte[] value, TimeSpan expires) {var operationId = s_diagnosticListener.RedisExecuteBefore(nameof(WriteToRedis), _redisProvider.GetCurrentEndPoint().ToString(), key, cacheLength: value?.Length);try{var result = RetryExecute(() => _redisProvider.GetRedis().GetDatabase().StringSet(key, value, expires));s_diagnosticListener.RedisExecuteAfter(operationId, nameof(WriteToRedis), _redisProvider.GetCurrentEndPoint().ToString(), key);return result;}catch (Exception ex){s_diagnosticListener.RedisExecuteError(operationId, ex, endpoint: _redisProvider.GetCurrentEndPoint().ToString(), cacheKey: key, cacheLength: value?.Length);throw;} }? ? 在寫入Redis緩存前執(zhí)行RedisExecuteBefore傳入基本信息,然后寫入Redis緩存完成再執(zhí)行RedisExecuteAfter,如果寫入Redis緩存發(fā)生異常就會執(zhí)行RedisExecuteError。
? ? 最終在消費實現的時候,根據對應的信息構造鏈路跟蹤信息,在After和Error中寫入鏈路信息到隊列。具體邏輯如下:
internal class RedisDiagnosticProcessor : ITracingDiagnosticProcessor{public static string ComponentName = "StackExchange.Redis";private readonly ITracingContext _tracingContext;private readonly IExitSegmentContextAccessor _contextAccessor;public string ListenerName => RedisDiagnosticStrings.DiagnosticListenerName;public RedisDiagnosticProcessor(ITracingContext tracingContext,IExitSegmentContextAccessor contextAccessor){_tracingContext = tracingContext;_contextAccessor = contextAccessor;}private static string ResolveOperationName(string operation){return $"{RedisDiagnosticStrings.CacheDataPrefix} {operation}";}[DiagnosticName(RedisDiagnosticStrings.CacheBeforeExecuteName)]public void CacheExecuteBefore([Property(Name = "Endpoint")] string endpoint,[Property(Name = "CacheKey")] string cacheKey, [Property(Name = "CacheLength")]long? cacheLength, [Property(Name = "Operation")]string operation){var context = _tracingContext.CreateExitSegmentContext(ResolveOperationName(operation), endpoint);context.Span.SpanLayer = SpanLayer.CACHE;context.Span.Component = new StringOrIntValue(ComponentName);context.Span.AddTag(CacheTags.CACHEKEY, cacheKey);if (cacheLength != null){context.Span.AddTag(CacheTags.CACHELENGTH, cacheLength.Value);}context.Span.AddTag(CacheTags.OPERATION, operation);}[DiagnosticName(RedisDiagnosticStrings.CacheAfterExecuteName)]public void CacheExecuteAfter([Property(Name = "CacheLength")]long? cacheLength){var context = _contextAccessor.Context;if (context != null){if (cacheLength != null){context.Span.AddTag(CacheTags.CACHELENGTH, cacheLength.Value);}_tracingContext.Release(context);}}[DiagnosticName(RedisDiagnosticStrings.CacheErrorExecuteName)]public void CacheExecuteError([Property(Name = "Exception")] Exception ex){var context = _contextAccessor.Context;if (context != null){context.Span.ErrorOccurred(ex);_tracingContext.Release(context);}}}? ListenerName 就是上面RedisDiagnosticListener定義的名稱。因為CacheExecuteBefore、CacheExecuteAfter這些方法都是通用的,同樣會應用于讀取Redis緩存和移除Redis緩存。
? ? SkyWalking會構造一個 SegmentContext,在CacheExecuteBefore的時候構造Context信息,然后CacheExecuteAfter或CacheExecuteError發(fā)布Context 信息。
? ? 同時在SkyWalking的.NET客戶端里會維護一個隊列,將鏈路Context信息緩存在其中,例如當滿足一定條件:如達到1000條或5秒鐘等條件,就會推送至SkyWalking服務端。這個在客戶端初始化的時候是可以配置的。
Jaeger對接
? ?Jaeger的對接,在客戶端上稍微做了一些改動。由于其中一個庫OpenTracing.Contrib沒有Framework版,平臺單獨編譯的一個Framework版本。
? ? 然后Jaeger客戶端是完美支持OpenTracing標準,平臺采集數據的是標準OpenTracing格式,對接其它客戶端也是會非常容易。
? ? OpenTracing 是與后臺無關的一套接口,被跟蹤的服務只需要調用這套接口,就可以被任何實現這套接口的跟蹤后臺(比如Zipkin, Jaeger等等)支持,而作為一個跟蹤后臺,只要實現了個這套接口,就可以跟蹤到任何調用這套接口的服務。
標準化了對跟蹤最小單位Span的管理:定義了開始Span,結束Span和記錄Span耗時的API。Span的定義可以參照開源分布式跟蹤系統(tǒng)Zipkin介紹(架構篇)
標準化了進程間跟蹤數據傳遞的方式:定義了一套API方便跟蹤數據的傳遞
標準化了進程內當前Span的管理:定義了存儲和獲取當前Span的API
? ? 以下是Redis對應監(jiān)聽的實現:
internal class RedisDiagnosticObserver : DiagnosticListenerObserver {public static string ComponentName = "StackExchange.Redis";private readonly ITracer _tracer;/// <inheritdoc />public RedisDiagnosticObserver(ILoggerFactory loggerFactory, ITracer tracer,IOptions<GenericEventOptions> genericEventOptions) : base(loggerFactory, tracer, genericEventOptions.Value){_tracer = tracer;}/// <inheritdoc />protected override string GetListenerName() => RedisDiagnosticStrings.DiagnosticListenerName;/// <inheritdoc />protected override void OnNext(string eventName, object untypedArg){switch (eventName){case RedisDiagnosticStrings.CacheBeforeExecuteName:RedisExecuteBefore(untypedArg);break;case RedisDiagnosticStrings.CacheAfterExecuteName:RedisExecuteAfter(untypedArg);break;case RedisDiagnosticStrings.CacheErrorExecuteName:RedisExecuteError(untypedArg);break;}} }? ? 以上對應的具體方法省略,跟SkyWalking類似,其中主要構建的是Scope,然后內部是Span,這些內容都是OpenTracing中的對象,最終再引入Jaeger的客戶端即可完成接入。
? ? ERP整個鏈路數據流轉如下圖:
四、與ERP結合
? ? 由于ERP基于.NET Framework,有很多場景是需要自己調整。具體在ERP中如何接入調整的,下面可以看看,目前初始化使用的動態(tài)HttpModule注入,然后在HttpModule中進行初始化。
? ? 動態(tài)注入,利用System.Web.PreApplicationStartMethod特性在程序啟動時執(zhí)行方法,然后使用DynamicModuleUtility中RegisterModule方法注冊HttpModule。要使用RegisterModule方法,需要引入Microsoft.Web.Infrastructure類庫。
[assembly: System.Web.PreApplicationStartMethod(typeof(InstrumentModuleFactory), nameof(InstrumentModuleFactory.Create))] namespace Mysoft.Map6.OpenTracing.Startup {public class InstrumentModuleFactory{public static void Create(){DynamicModuleUtility.RegisterModule(typeof(InstrumentModule));}} }? ? 然后HttpModule 的初始化,整體流程如下:
? ? 首先HttpModule的Init方法會在初始化的時候執(zhí)行,但是在ASP.NET的請求中,會多次初始HttpModule,這樣會導致鏈路追蹤的方法多次初始化,同時內部的ServiceProvider多次build這是不合理。
? ? 所以在此基礎上做了調整,使用雙重鎖,確保初始化只執(zhí)行一次,同時在開啟鏈路追蹤的時候才初始化。
? ? 鏈路追蹤目前的設計,只支持開啟其中的一種,這個可以在配置中心進行配置。
? ? 在HttpModule初始化的時候,綁定BeginRequest和EndRequest事件,對應的實現是追蹤ASP.NET的請求數據,在EndRequest中往鏈路追蹤寫入數據。這樣對于ERP整個請求可以實現完整的鏈路。
五、應用效果
? ?最終接入鏈路追蹤的效果,以SkyWaking為例,SkyWaking UI比較全,可以看到整個服務以及對應鏈路的詳細信息。
服務信息:
鏈路信息:
? ? 以上可以看到整個ERP系統(tǒng)服務的拓補圖。ERP在開啟鏈路追蹤以后,每個請求都會有對應TraceId,利用TraceId可以到鏈路詳細信息中查詢對應信息、時間等數據,對于后續(xù)性能分析及異常分析都提供良好的條件。
? ? ERP接入鏈路追蹤以后可以方便定位請求性能,同時對應異常、性能日志中也會記錄TraceId,為排查問題提供方便支持。
------ END ------
作者簡介
張同學:?研發(fā)工程師,目前在ERP建模平臺團隊負責開發(fā)工作。
也許您還想看
【復雜系統(tǒng)遷移 .NET Core平臺系列】之靜態(tài)文件
【復雜系統(tǒng)遷移 .NET Core平臺系列】之遷移項目工程
【復雜系統(tǒng)遷移 .NET Core平臺系列】之界面層
.NET Core MVC擴展實踐
總結
以上是生活随笔為你收集整理的链路追踪在ERP系统中的应用实践的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 用long类型让我出了次生产事故,写代码
- 下一篇: 谈谈登录密码传输这件小事