源宝导读:随着ERP的部署架构越来越复杂,对运维监控、问题排查等工作增加了难度,本文将介绍通过引入链路追踪技术,提高ERP系统问题排查效率,支撑更全面监控系统运行情况的实践过程。

一、导读

随着ERP的部署架构越来越复杂,微应用分布式部署架构在给用户带来高性能高稳定的同时,给运维监控、问题排查带来了一定难度,特别是后端服务的内部调用由于缺少日志,难以快速定位问题根因。借助链路追踪并结合异常日志所记录的链路id,可以方便定位整个异常链路,更快找到问题的原因。链路追踪还有一个好处,针对性能慢的页面,原来很难快速定位到慢的具体点,通过借助链路追踪,能快速掌握每个请求执行了哪些操作,每个操作消耗了多长时间,精准定位性能问题。

二、链路追踪介绍

分布式系统变得日趋复杂,越来越多的组件开始走向分布式化,如微服务、分布式数据库、分布式缓存等,使得后台服务构成了一种复杂的分布式网络。

在服务能力提升的同时,复杂的网络结构也使问题定位更加困难。在一个请求在经过诸多服务过程中,出现了某一个调用失败的情况,查询具体的异常由哪一个服务引起的就变得十分抓狂,问题定位和处理效率是也会非常低。

分布式链路追踪就是将一次分布式请求还原成调用链路,将一次分布式请求的调用情况集中展示,比如各个服务节点上的耗时、请求具体到达哪台机器上、每个服务节点的请求状态等等。

目前业界的链路追踪系统,如Twitter的Zipkin,Uber的Jaeger,国内比较流行的SkyWalking。

三、ERP中链路追踪的实现

什么是 Diagnostics

在.NET Core中实现链路追踪非常简单,因为在 .NET Core 中 .NET 团队设计了一个全新的 DiagnosticSource,新的 DiagnosticSource 非常的简单,它允许你在生产环境记录丰富的 payload 数据,然后你可以在另外一个消费者中消费感兴趣的记录。

ERP因为是.NET Framework实现链路追踪复杂一些,对此为了实现链路追踪,我们将Diagnostics应用到ERP中,手动创建生产者,然后在具体的链路追踪中创建对应消费者采集数据。

Diagnostics 就是提供一组功能使我们能够很方便的可以记录在应用程序运行期间发生的关键性操作以及他们的执行时间等,使管理员可以查找特别是生产环境中出现问题所在的根本原因。

在应用程序出现问题的时候,特别是出现可用性或者性能问题的时候,开发人员或者IT人员经常会对这些问题花费大量的时间来进行诊断,很多时候生产环境的问题都无法复现,这可能会对业务造成很大的影响。

目前平台实现了两种对接SkyWalking和Jaeger,可以方便的在配置中心启用,接入自建服务或云服务都是支持。

SkyWalking对接

SkyWalking目前对于.NET Core有着很好的支持,但是对于.NET Framework因为本身Diagnostics的原因,只有一个很简单的ASP.NET请求客户端。

其它的客户端我们都做了一些改造,但有一些我们也尽量标准,如SqlClient、HttpClient。以下介绍一下Redis的支持,完全自己实现客户端。

首先要创建生产者DiagnosticListener,然后在Redis的相关操作里执行具体事件。

DiagnosticListener需要引入System.Diagnostics.DiagnosticSource这个dll,以下为简单实现执行前事件:

internal static class RedisDiagnosticListenerExtensions
{/// <summary>/// 定义监听的名称/// </summary>public const string DiagnosticListenerName = "RedisDataDiagnosticListener";/// <summary>/// 前缀/// </summary>public const string CacheDataPrefix = "Mysoft.Map6.Cache.";/// <summary>/// 执行前事件名称/// </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;}
}

然后在具体行为里面执行事件,以下写入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缓存前执行RedisExecuteBefore传入基本信息,然后写入Redis缓存完成再执行RedisExecuteAfter,如果写入Redis缓存发生异常就会执行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发布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的定义可以参照开源分布式跟踪系统Zipkin介绍(架构篇)

  • 标准化了进程间跟踪数据传递的方式:定义了一套API方便跟踪数据的传递

  • 标准化了进程内当前Span的管理:定义了存储和获取当前Span的API

以下是Redis对应监听的实现:

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中如何接入调整的,下面可以看看,目前初始化使用的动态HttpModule注入,然后在HttpModule中进行初始化。

动态注入,利用System.Web.PreApplicationStartMethod特性在程序启动时执行方法,然后使用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方法会在初始化的时候执行,但是在ASP.NET的请求中,会多次初始HttpModule,这样会导致链路追踪的方法多次初始化,同时内部的ServiceProvider多次build这是不合理。

所以在此基础上做了调整,使用双重锁,确保初始化只执行一次,同时在开启链路追踪的时候才初始化。

链路追踪目前的设计,只支持开启其中的一种,这个可以在配置中心进行配置。

在HttpModule初始化的时候,绑定BeginRequest和EndRequest事件,对应的实现是追踪ASP.NET的请求数据,在EndRequest中往链路追踪写入数据。这样对于ERP整个请求可以实现完整的链路。

五、应用效果

最终接入链路追踪的效果,以SkyWaking为例,SkyWaking UI比较全,可以看到整个服务以及对应链路的详细信息。

服务信息:

链路信息:

以上可以看到整个ERP系统服务的拓补图。ERP在开启链路追踪以后,每个请求都会有对应TraceId,利用TraceId可以到链路详细信息中查询对应信息、时间等数据,对于后续性能分析及异常分析都提供良好的条件。

ERP接入链路追踪以后可以方便定位请求性能,同时对应异常、性能日志中也会记录TraceId,为排查问题提供方便支持。

------ END ------

作者简介

张同学: 研发工程师,目前在ERP建模平台团队负责开发工作。

也许您还想看

【复杂系统迁移 .NET Core平台系列】之静态文件

【复杂系统迁移 .NET Core平台系列】之迁移项目工程

【复杂系统迁移 .NET Core平台系列】之界面层

.NET Core MVC扩展实践

链路追踪在ERP系统中的应用实践相关推荐

  1. 浅析制造业物料编码在ERP系统中的实施

    1 概述 在ERP系统中,物料编码本身没有含义,但在系统内,是各种物料应用的唯一代号.不同的物料在编码时应区别对待,例如对于半成品,可以直接以图号作为编码进行编号,这样,在PDM.CAPP以及实际操作 ...

  2. 【转载】ERP系统中的存货计价过程

    ERP系统中的存货计价过程 本文主要以SAP为例,论述在ERP系统的物料移动过程中,移动平均法和标准价格法这两种存货计价方法的应用. 企业的各种存货,由于是分散购入或分批生产形成,同一次领用或发出的存 ...

  3. 如何发挥ERP系统中的财务监控职能?

    ERP系统的管理理念与特点 ERP,是整合了企业管理理念.业务流程.基础数据.人力物力.计算机硬件和软件于一体的企业资源管理系统.ERP系统运用信息技术将企业的资金流.物资流.信息流进行有效的集成,使 ...

  4. 专家答疑:在ERP系统中确保销售订单准确性

    若销售订单有错,如订单数量或者订单价格有问题的话,则会造成一系列的连锁反应.计划模块.采购模块.生产模块.库存模块等等都会受到牵连.而且最要命的是,很哪进行调整.所以,在ERP系统使用过程中,应该千方 ...

  5. SAP License:ERP系统中供应商管理怎么做?

    ERP系统中供应商管理怎么做?企业在日常运营过程中,难免会遇到主动或者被动地供应商变更情况,尤其是被动地更换供应商,对企业运作来说是一个很大的挑战.所以根据企业的采购需求寻求供应商,系统地梳理企业可能 ...

  6. ERP系统中的工作流和业务流

    首先解释两个概念: 工作流,将工作分解成几段不同的任务,然后通过一定的规则和过程来执行这些任务并对它们进行监控,达到提高工作效率,降低生产成本,提高企业竞争力等目的.它大多应用于办公自动化领域. 业务 ...

  7. 运输费用在ERP 系统中是如何处理

    今天我结合企业的具体需求,谈谈运输费用在ERP 系统中是如何处理的.为了叙述的简便,我这里就以进货的运输费用为例. [IT专家网独家]在ERP项目实施过程中,可能有些人对于运输费用并不关注.其实,运输 ...

  8. ERP系统中KPI指标的建立与管理

    摘自:http://www.360doc.com/content/09/0725/08/69678_4433715.shtml ERP 系统中 KPI 指标的建立与管理 ERP 系统的 KPI 应用现 ...

  9. 5.2.4 ERP系统中C#应用实例(1)

    5.2.4 ERP系统中C#应用实例(1) http://book.51cto.com  2010-09-14 20:47  梁立新/雷玉广  电子工业出版社  我要评论(0) 摘要:<项目实践 ...

最新文章

  1. nginx的gzip压缩功能
  2. Nature | 原核生物基因的生物地理学研究
  3. echarts line 去掉最外围方框_干货 | 关于射频芯片最详细解读
  4. smartgwt_SmartGWT入门,提供出色的GWT界面
  5. php c扩展的方式,php中使用C语言写扩展的方法
  6. 【Kafka】Kafka topic 的消费组 出现 CURRENT_OFFSET 为 unknown
  7. weblogic 调优参数
  8. vue电商网站后台管理系统模板
  9. 在电脑上怎么做报表新手_初学者在电脑上如何制作电子表格
  10. linux ext4 inode,Ext4文件系统中inode数量的限制
  11. 常见的 Android 性能指标获取方式:CPU、FPS、Memory、GPU 、I/O、Network
  12. 英语四级和计算机一级算多少学分,大学英语四级多少分算过
  13. NOIP提高组【JZOJ4816】label
  14. vue3 + js-cookie加密解密(普通版本/TS版本)
  15. 计算机走技术路线发展,硬件测试工程师发展前景_计算机硬件测试工程师_硬件测试工程师职责...
  16. jupyter 内核似乎挂掉了 它很快将自动重启---解决方案
  17. (翻译)箭头和省略号的使用方式
  18. 视频剪辑如何快速制作图文视频
  19. matlab logpolar,GitHub - luxinjin/polar-code: matlab simulation for polar code
  20. windows和linux下源码编译7-Zip(7za)

热门文章

  1. 零拷贝概念 -- linux内核
  2. javascript规范以及设计原则
  3. POJ1269 Intersecting Lines 计算几何 C语言
  4. 北京一公交车发生爆炸 疑为乘客携带药品起反应
  5. 如何在Android主屏幕上添加热点快捷方式
  6. NetCore2.0Web应用之Startup
  7. Microsoft POS for .NET v1.12 发布了
  8. ELKstack-Elasticsearch各类安装部署方法
  9. 【Mongodb】用户和认证 权限总结
  10. NSInteger,NSUInteger,NSNumber