十行代码,让日志存储成本降低80%( 三 )


四、堆栈倒打
本文的重点来啦,这个设计就是开头提到的奇思妙想 。堆栈倒打源于我在排查另一个系统问题过程中感受到的几个痛点,首先来看一个堆栈示例 。

十行代码,让日志存储成本降低80%

文章插图
这么长的堆栈,这密密麻麻的字母,即使是天天跟它打交道的开发,相信第一眼看上去也会头皮发麻 。回想一下我们看堆栈 , 真正想得到的是什么信息 。
所以我感受到的痛点核心有两个 。
第一个是,SLS上搜出来的日志,默认是折叠的 。对于堆栈,我们应该都知道,传统异常堆栈的特征是 , 最顶层的异常,是最接近流量入口的异常,这种异常我们一般情况下不太关心 。最底层的异常,才是引起系列错误的源头,我们日常排查问题的时候,往往最关心的是错误源头 。所以对于堆栈日志,我们无法通过摘要一眼看出问题出在哪行代码,必须点开,拉到最下面,看最后一个堆栈才能确定源头 。
我写了一个错误示例来说明这个问题 。常规的堆栈结构其实分两部分,我称之为 , 异常原因栈,和错误堆栈 。
十行代码,让日志存储成本降低80%

文章插图
【十行代码,让日志存储成本降低80%】如上,一个堆栈包含有三组异常,每一个RuntimeException是一个异常,这三个异常连起来 , 我们称为一个异常原因栈 。
每一个RuntimeException内部的堆栈,我们称为错误堆栈 。
说明一下,这两个名词是我杜撰的,没有看到有人对二者做区分 , 我们一般都统称为堆栈 。读者能理解我想表达的就行,不用太纠结名词 。
第二个痛点是,这种堆栈存储成本太高 , 有效信息承载率很低 。老实说这一点可能大多数一线开发并没有太强烈的体感,但在这个降本增效的大环境下,我们每个人应该把这点作为自己的OKR去践行,变被动为主动 , 否则在机器成本和人力成本之间,公司只好做选择题了 。
现在目标很明确了,那我们就开始对症下药 。核心思路有两个 。
针对堆栈折叠的问题,采用堆栈倒打 。倒打之后,最底层的异常放在了最上面 , 甚至不用点开,瞟一眼就能知道原因 。
十行代码,让日志存储成本降低80%

文章插图
同时我们也支持异常原因栈层数配置化,以及错误堆栈的层数配置化 。解这个问题,本质上就是这样一个简单的算法题:倒序打印堆栈的最后N个元素 。核心代码如下 。
/**
* 递归逆向打印堆栈及cause(即从最底层的异常开始往上打)
* @param t 原始异常
* @param causeDepth 需要递归打印的cause的最大深度
* @param counter 当前打印的cause的深度计数器(这里必须用引用类型,如果用基本数据类型,你对计数器的修改只能对当前栈帧可见 , 但是这个计数器,又必须在所有栈帧中可见,所以只能用引用类型)
* @param stackDepth 每一个异常栈的打印深度
* @param sb 字符串构造器
*/
public static void recursiveReversePrintStackCause(Throwable t, int causeDepth, ForwardCounter counter, int stackDepth, StringBuilder sb){
if(t == null){
return;
}
if (t.getCause() != null){
recursiveReversePrintStackCause(t.getCause(), causeDepth, counter, stackDepth, sb);
}
if(counter.i++ < causeDepth){
doPrintStack(t, stackDepth, sb);
}
}
要降低存储成本,同时也要确保信息不失真 , 我们考虑对堆栈行下手,把全限定类名简化为类名全打,包路径只打第一个字母,行号保留 。如:c.a.u.m.s.LogAspect#log:88 。核心代码如下 。
public static void doPrintStack(Throwable t, int stackDepth, StringBuilder sb){
StackTraceElement[] stackTraceElements = t.getStackTrace();
if(sb.lastIndexOf("t") > -1){
sb.deleteCharAt(sb.length()-1);
sb.append("Caused: ");
}
sb.append(t.getClass().getName()).append(": ").append(t.getMessage()).append("nt");
for(int i=0; i < stackDepth; ++i){
if(i >= stackTraceElements.length){
break;
}
StackTraceElement element = stackTraceElements[i];
sb.append(reduceClassName(element.getClassName()))
.append("#")
.append(element.getMethodName())
.append(":")
.append(element.getLineNumber())
.append("nt");
}
}
最终的效果大概长这样 。我们随机挑了一个堆栈做对比,统计字符数量,在同等信息量的情况下,压缩比达到88% 。


推荐阅读