在大型系统开发调试中,跨系统之间联调开始变得很差使了。莫名其妙一个错误爆出来了,日志虽然有记录,但究竟是哪里出问题了呢? 前端
是Ios端参数传的不对?仍是A系统或B系统提供的接口致使?相信有很多人遇到这种状况,大多数问题每每不大,但排查起来比较费劲。程序员
下面介绍下怎么经过上下文跟踪的方法,最快定位到其问题。web
简单介绍就是,经过一个TraceId把整个业务请求逻辑相关联起来,根据时间顺序造成一个完整的调用链。sql
这样不管任何地方报错,只要拿TraceId去日志系统简查下,根据上下文的顺序就知道是哪一步、哪一个函数、哪一个参数出错了,能以最快速度定位处理BUG。mongodb
如图以博客园为例。当博客园收到一个请求后,自动为生产个惟一ID 1000,以后全部处理工做都是用这个1000。数据库
每一个处理模块都维持一个上下文ID自增,rpcid++。安全
其处理模块能够是函数级,逻辑层级,服务器级等均可以。服务器
一旦发现有异常后,自动将TraceId发给博客园。这样程序员们,就能根据TraceId最快定位问题了。网络
关于各类环境下具体的代码实现:多线程
定义跟踪日志须要的参数,进行上下文传递。
public class LogBody { /// <summary> /// 跟踪ID /// </summary> public string TraceId { get; set; } /// <summary> /// 上下文ID /// </summary> public int RpcId { get; set; } /// <summary> /// 处理时间 /// </summary> public DateTime LastTime { get; set; } }
在global.asax全局Application_BeginRequest函数中,使用HttpContext.Current上下文,开始进行埋点(跟踪),设置rpc 0。
void Application_BeginRequest(object sender, EventArgs e) { var lb = new LogBody(); lb.TraceId = Guid.NewGuid().ToString("N"); lb.RpcId=0; lb.LastTime = DateTime.Now; HttpContext.Current.Response.AppendHeader("traceID", lb.TraceId); HttpContext.Current.Items.Add(lb.TraceId, lb); //记录日志,例:用户请求参数,userAgent等。 }
在default页开始业务逻辑,设置rpc 1。
protected void Page_Load(object sender, EventArgs e) { var traceID = HttpContext.Current.Response.Headers["traceID"]; LogBody logbody = HttpContext.Current.Items[traceID] as LogBody; logbody.RpcId++; logbody.LastTime = DateTime.Now; //业务逻辑。 //记录日志。。。 }
如上就完成上下文的传递。
Application_BeginRequest 中在实际使用中,只须要对有用的页面(例:aspx,ashx)进行埋点。
日志记录的时候,能够把logbody都存储起来。
存储到Headers可让前端经过JS也能拿到TraceId,方便去排查问题。
LastTime这个字段,能够与上一次的相减,这样就得出中间逻辑处理所花费的时间了。
在web程序中能够用httpcontext的上下文传递。
在单线程的程序中,按照线性顺序便可。
多线程中利用用threadlocal传递。
public static ThreadLocal<LogBody> Body = new ThreadLocal<LogBody>(); static void Main(string[] args) { var t1 = new Thread(() => { Body.Value = new LogBody() { LastTime = DateTime.Now, RpcId = 0, TraceId = Guid.NewGuid().ToString("N") }; //业务1 Console.WriteLine("Thread1 log record:" + Body.Value.TraceId + "-" + Body.Value.RpcId + "-" + Body.Value.LastTime); Thread.Sleep(5000); Body.Value.RpcId++; Body.Value.LastTime = DateTime.Now; //业务2 Console.WriteLine("Thread1 log record:" + Body.Value.TraceId + "-" + Body.Value.RpcId + "-" + Body.Value.LastTime); }); t1.Start(); var t2 = new Thread(() => { Body.Value = new LogBody() { LastTime = DateTime.Now, RpcId = 0, TraceId = Guid.NewGuid().ToString("N") }; //业务1 Console.WriteLine("Thread2 log record:" + Body.Value.TraceId + "-" + Body.Value.RpcId + "-" + Body.Value.LastTime); Thread.Sleep(5000); Body.Value.RpcId++; Body.Value.LastTime = DateTime.Now; //业务2 Console.WriteLine("Thread2 log record:" + Body.Value.TraceId + "-" + Body.Value.RpcId + "-" + Body.Value.LastTime); }); t2.Start(); }
运行以下:
每每在生产环境中,会有大量的异步操做。若是有异步行为的话,打乱上下文怎么办?这时候须要引入另一个概念,父节点Id。
这样异步操做的行为就父节点之下,最终在日志后台展现的是一个倒着的树形结构。
如图能够看到业务2异步派生出来的子节点。
把上下文rpcid修改为double类型。
static void Main(string[] args) { var t2 = new Thread(() => { Body.Value = new LogBody() { LastTime = DateTime.Now, RpcId = 1, TraceId = Guid.NewGuid().ToString("N") }; var t1 = new Thread((lb) => { var temp = lb as LogBody; Body.Value = new LogBody() { LastTime = DateTime.Now, RpcId = temp.RpcId, TraceId = temp.TraceId }; Body.Value.RpcId += 0.1; //业务x Console.WriteLine("async Thread:" + Body.Value.TraceId + "-" + Body.Value.RpcId + "-" + Body.Value.LastTime ); Thread.Sleep(5000); Body.Value.RpcId+=0.1; Body.Value.LastTime = DateTime.Now; //业务y Console.WriteLine("async Thread:" + Body.Value.TraceId + "-" + Body.Value.RpcId + "-" + Body.Value.LastTime); }); t1.Start(Body.Value); //业务1 Console.WriteLine("sync Thread:" + Body.Value.TraceId + "-" + Body.Value.RpcId + "-" + Body.Value.LastTime); Thread.Sleep(2000); Body.Value.RpcId+=1; Body.Value.LastTime = DateTime.Now; //业务2 Console.WriteLine("sync Thread:" + Body.Value.TraceId + "-" + Body.Value.RpcId + "-" + Body.Value.LastTime); }); t2.Start(); }
代码中用参数传递给了异步线程中,运行以下:
从代码中能够看出,这种方式对程序性能影响能够忽略不计。
须要注意是:若是在生产环境跑的话,不管是写文件,仍是数据库,或写统一日志平台。都会致使大量IO读写,网络资源消耗。
若是服务器都消耗这上面,就得不偿失了。
能够用内存队列+队列+批量push或pull的方式,而且注意设置阀值。
大量的请求,其实多数是无效的。这里引入采样率的概念。 例如按求余取,或者按地区,时间等。也能够单独写采样规则。
日志能够只记录error以上的级别,只有在排查生产环境的时候才开启debug,info级别信息。
存储这块,能够根据实际须要选择sql server,mongodb,hbase hdfs。
若是有敏感数据,可根据安全级别进行加密。
本文是基于Google dapper论文的思路展开,基于此进行不少扩展。
示例中采用的是手动记录,在实际使用中,能够简化调用,封装成自动构建的,有兴趣的能够看前2篇自动注入的相关介绍。
Google dapper论文
淘宝EagleEye系统