这次接触ReBucket算法是在实际需求中需要完成一个落地的相关功能模块,也算是少有的复现论文到落地项目的功能模块的机会了,这里做一下算法本身的总结,有关的实现后续会放在另一篇文章中
PDM :位置相关模型(Position Dependent Model)并查集 :一种树型的数据结构 ,用于处理一些不交集 (Disjoint Sets)的合并及查询问题层次聚类方法 :一种自底向上的聚类方法(类似并查集,从每个元素都属于自己的集群开始)WER :Windows Error Reporting,微软部署的一套用于及时告警的分布式系统Grid Search :一项模型超参数(需要人工选择的参数)优化技术,常用于优化三个或者更少数量的超参数,本质是一种穷举法尽管在日常的开发工作中,开发团队已经在发布产品前花费大量资源和精力进行软件测试,但实际上,已发布的软件仍然有一些错误,而这些错误往往表现为release版本运行时崩溃
针对这个现象,微软部署了一套分布式系统WER (Windows Error Reporting)用于自动从崩溃现场收集崩溃信息、聚类到各个Bucket,当某个Bucket内的崩溃次数达到某个阈值后,WER 会自动为其生成错误报告,并将其提供给开发人员;WER 已经在运行的十几年内证明了其价值,收集了数十亿词崩溃报告,大大加快了开发人员的Debug流程,但另一方面,WER 也仍存在一些问题,如“第二个Bucket问题”以及“长尾巴”问题(见下图)
ReBucket 算法就是为了解决这些已知问题而产生的一种堆栈聚类算法
在ReBucket 算法中,对于一系列新到达的崩溃信息(一个时间片内到达的崩溃信息),首先我们对其进行预处理,以提取简化的堆栈信息。然后使用层次聚类方法 将崩溃报告聚类到相应的Bucket内;同时可以使用历史Bucket数据构建的训练模型训练在PDM中使用到的参数,下图为ReBucket算法的总流程图
在计算PDM并进行聚类前我们需要从原始堆栈信息中删除以下几种函数(方法),以提取我们真正需要的堆栈信息:
白名单函数 :白名单函数指那些被认为在软件崩溃时被视为不可能发生错误 的函数,通常包括那些非常简单的函数,或者已经成功运行了很长时间的函数递归函数 :递归函数经常出现在堆栈信息中,并且大多数递归函数并不包含有效信息,而且会影响到相似度的度量,尤其是当递归函数的数量比较大时。因此这里我们使用一种去除递归函数的算法来去掉它在计算堆栈间相似度的过程中需要用到两个度量:
当前帧到顶部帧的距离 对齐偏移:两个堆栈中匹配的函数到顶部帧的距离的偏移量(差的绝对值) 
以上图中的两个堆栈为例,对于两个堆栈C 1 C_1 C 1  C 2 C_2 C 2  C 1 C_1 C 1  f 0 f_0 f 0  f 1 f_1 f 1  f 4 f_4 f 4  C 2 C_2 C 2  f 0 f_0 f 0  f 2 f_2 f 2  f 6 f_6 f 6  f 4 f_4 f 4  f 6 f_6 f 6  
在ReBucket算法中,我们使用PDM来衡量两个堆栈之间的相似度,而PDM是基于以下两个观点的:
应该放更大的权重在离顶部帧近的帧上,因为bug的根因更容易出现在离顶部帧近的帧上 两个相似的堆栈中的匹配函数之间的对齐偏移应该很小 基于这两个观点,两个堆栈C 1 C_1 C 1  C 2 C_2 C 2  
定义L L L C 1 C_1 C 1  C 2 C_2 C 2  L i L_i L i  S i , 1 , S i , 2 , … S i , k S_{i, 1}, S_{i, 2},\ldots S_{i, k} S i , 1  , S i , 2  , … S i , k  L i L_i L i  
L = { L 1 , L s , L 3 … } L i = { S i , 1 , S i , 2 , S i , 3 , … S i , k … } L=\left\{L_{1}, L_{s}, L_{3} \ldots\right\} \quad L_{i}=\left\{S_{i, 1}, S_{i, 2}, S_{i, 3}, \ldots S_{i, k} \ldots\right\} L = { L 1  , L s  , L 3  … } L i  = { S i , 1  , S i , 2  , S i , 3  , … S i , k  … } 
定义P O S ( C q , S i , k ) POS(C_q, S_{i,k}) P O S ( C q  , S i , k  ) S i , k S_{i,k} S i , k  C q C_q C q  l l l C 1 C_1 C 1  C 2 C_2 C 2  ( 1 ) (1) ( 1 ) C 1 C_1 C 1  C 2 C_2 C 2  
{ sim  ( C 1 , C 2 ) = max  L i ∈ L [ Q ( L i ) ] ∑ j = 0 l e − c j Q ( L i ) = ∑ s i , k ∈ L i e − c min  ( Pos  ( C 1 , s i , k ) , Pos  ( C 2 , s i , k ) ) e − o ∣ Pos  ( C 1 , s i , k ) − Pos  ( C 2 , s i , k ) ∣ ( 1 ) \left\{ \begin{array}{lr} \operatorname{sim}\left(C_{1}, C_{2}\right)=\frac{\max_{L_{i}\in L} \left[ Q\left(L_{i}\right) \right] }{\sum_{j=0}^{l} e^{-cj}} \\\\ Q\left(L_{i}\right)=\sum_{s_{i, k} \in L_{i}}e^{-c \min \left( \operatorname{Pos}\left(C_{1}, s_{i, k}\right), \operatorname{Pos}\left(C_{2}, s_{i, k}\right) \right)}e^{-o \mid \operatorname{Pos}\left(C_{1}, s_{i, k}\right)-\operatorname{Pos}\left(C_{2}, s_{i, k}\right) \mid} \end{array} \right. \quad (1) ⎩ ⎪ ⎨ ⎪ ⎧  s i m ( C 1  , C 2  ) = ∑ j = 0 l  e − c j max L i  ∈ L  [ Q ( L i  ) ]  Q ( L i  ) = ∑ s i , k  ∈ L i   e − c min ( P o s ( C 1  , s i , k  ) , P o s ( C 2  , s i , k  ) ) e − o ∣ P o s ( C 1  , s i , k  ) − P o s ( C 2  , s i , k  ) ∣  ( 1 ) 
其中,c c c o o o c c c o o o Q ( L i ) Q\left(L_{i}\right) Q ( L i  ) L i L_i L i  Q ( L i ) Q\left( L_i \right) Q ( L i  ) 
从公式( 1 ) (1) ( 1 ) Q ( L i ) Q\left(L_{i}\right) Q ( L i  ) Q ( L i ) Q\left(L_{i}\right) Q ( L i  ) 
定义一个相似度矩阵M [ i , j ] M\left[i,j\right] M [ i , j ] M i , j M_{i,j} M i , j  C 1 C_1 C 1  i i i C 2 C_2 C 2  j j j  根据相似度矩阵M [ i , j ] M\left[i,j\right] M [ i , j ] M m , n M_{m,n} M m , n  m m m C 1 C_1 C 1  n n n C 2 C_2 C 2  ( 4 ) (4) ( 4 )  M [ i , j ] M\left[i,j\right] M [ i , j ] ( 1 ) (1) ( 1 ) ( 2 ) (2) ( 2 ) ( 3 ) (3) ( 3 ) M i , j = max  { M i − 1 , j − 1 + cost  ( i , j ) M i − 1 , j M i , j − 1 ( 2 ) cost  ( i , j ) = { e − c ∗ min  ( i , j ) e − o ∗ ∣ i − j ∣ if  i t h  frame of  C 1 = = j t h  frame of  C 2 0  otherwise  ( 3 ) sim  ( C 1 , C 2 ) = M m , n ∑ j = 0 l e − c j ( 4 ) \begin{array}{lr} M_{i, j}=\max \left\{\begin{array}{lr} M_{i-1, j-1} + \operatorname{cost}(i, j) \\ M_{i-1, j} \\ M_{i, j-1} \end{array}\right. &(2) \\\\ \operatorname{cost}(i, j)= \left\{\begin{array}{lr} e^{-c^{*} \min (i, j)} e^{-o^{*} \left| i-j \right|} & \text {if $ith$ frame of } C_1==\text {$jth$ frame of } C_2 \\ 0 & \text { otherwise } \end{array}\right. &(3) \\\\ \operatorname{sim}\left(C_{1}, C_{2}\right)=\frac{M_{m, n}}{\sum_{j=0}^{l} e^{-c j}} &(4) \end{array} M i , j  = max ⎩ ⎨ ⎧  M i − 1 , j − 1  + c o s t ( i , j ) M i − 1 , j  M i , j − 1   c o s t ( i , j ) = { e − c ∗ min ( i , j ) e − o ∗ ∣ i − j ∣ 0  if  i t h  frame of  C 1  = = j t h  frame of  C 2   otherwise   s i m ( C 1  , C 2  ) = ∑ j = 0 l  e − c j M m , n    ( 2 ) ( 3 ) ( 4 )  
通过PDM 我们就可以衡量任意两个堆栈的相似度,这也是下面对堆栈进行聚类操作的前提和依据
对堆栈的聚类基于前面通过PDM 计算的堆栈相似性度量,如果堆栈之间非常相似,则相关的崩溃报告会被分到相同的Bucket内
对堆栈聚类这里采用层次聚类方法 (一种自底向上的聚类方法),其流程大致为:在聚类的开始,每个堆栈都属于其自己的集群;每一次迭代选择**“最近”**的集群并进行合并
为了确定某个集群在一次迭代中应该和哪个集群合并,我们需要定义集群之间的距离度量 ,这里我们将这个度量定义为两个集群中所有堆栈之间的最大距离(见公式( 5 ) (5) ( 5 ) ( 6 ) (6) ( 6 ) C l i Cl_i C l i  C l j Cl_j C l j  C 1 C_1 C 1  C 2 C_2 C 2  C l i Cl_i C l i  C l j Cl_j C l j  
distance  ( C l i , C l j ) = max  C 1 ∈ C l i , C 2 ∈ C l j dist  ( C 1 , C 2 ) ( 5 ) dist  ( C 1 , C 2 ) = 1 − sim  ( C 1 , C 2 ) ( 6 ) \begin{array}{lr} \operatorname{distance}\left({Cl}_{i}, {Cl}_{j}\right)=\max_{C_{1} \in {Cl}_{i}, C_{2} \in {Cl}_{j}} \operatorname{ dist }\left(C_{1}, C_{2}\right) & (5) \\ \operatorname{ dist }\left(C_{1}, C_{2}\right)=1-\operatorname{sim}\left(C_{1}, C_{2}\right) & (6) \end{array} d i s t a n c e ( C l i  , C l j  ) = max C 1  ∈ C l i  , C 2  ∈ C l j   d i s t ( C 1  , C 2  ) d i s t ( C 1  , C 2  ) = 1 − s i m ( C 1  , C 2  )  ( 5 ) ( 6 )  
在一次聚类过程中,考虑最坏的情况,所有堆栈都合并为一个集群,则层次聚类方法 的时间复杂度为O ( n 3 ) O(n^3) O ( n 3 ) 距离阈值 d d d d d d 距离阈值 d d d 
PDM中用到的两个参数:
c c c o o o 分层聚类方法中的距离阈值d d d 
虽然这些参数都可以手动设置,但因为项目的不同,合适的参数也会不同,所以还是需要一个训练过程来学习这些参数的最优值
首先我们需要根据历史Bucket内的数据和相应的崩溃报告构建数据集,从同一Bucket 中提取由开发人员确认的由相同错误引起的崩溃报告作为聚类正确的数据,从不同(不相似)的Bucket 中提取由不同错误引起的崩溃报告作为聚类错误的数据(由于异类崩溃报告的数量通常很大,这里提取异类崩溃报告的过程是通过随机采样 进行的)。基于获得的重复的和不相似的崩溃报告,收集成对的相似和不相似的堆栈,构建成数据集
对于需要训练的三个参数,它们的值独立变化,不同的参数直接导致不同的聚类性能,所以这里采用一种基于搜索的算法(类似Grid Search),在三个参数的有限集合的笛卡尔积内穷举搜索最优参数组合,流程如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 type  trainStackPair struct  {	 	similarity       float64  	predictIsSimilar bool  	isSimilar        bool  } func  trainParams ()  (bestParamC float64 , bestParamO float64 , bestParamD float64 ) 	var  ( 		trainData    []trainStackPair 		paramC       float64  		paramO       float64  		paramD       float64  		bestFMeasure float64  	) 	for  paramC <= 2  { 		paramO = float64 (0 ) 		for  paramO <= 2  { 			paramD = float64 (0 ) 			for  paramD <= 1  { 				for  _, pair := range  trainData { 					pair.getSimilarity() 					if  pair.similarity > float64 (1  - paramD) { 						pair.predictIsSimilar = true  					} else  { 						pair.predictIsSimilar = false   					} 				} 				if  newFMeasure := getFMeasure(trainData); newFMeasure > bestFMeasure { 					bestParamC, bestParamO, bestParamD = paramC, paramO, paramD 					bestFMeasure = newFMeasure 				} 				paramD += 0.01   			} 			paramO += 0.1   		} 		paramC += 0.1   	} 	return  } func  (*trainStackPair)  getSimilarity ()  (similarity float64 ) 	 	return  } func  getFMeasure (stackPairs []trainStackPair)  (FMeasure float64 ) 	length := len (stackPairs) 	TP, FN, FP, TN := 0 , 0 , 0 , 0  	for  _, pair := range  stackPairs { 		if  pair.isSimilar { 			if  pair.predictIsSimilar { 				TP += 1  			} else  { 				FN += 1  			} 		} else  { 			if  pair.predictIsSimilar { 				FP += 1  			} else  { 				TN += 1  			} 		} 	} 	FMeasure = 2  * float64 (TP) / float64 (length+TP-TN) 	return  } 
其中c c c o o o d d d c m a x = 2 , o m a x = 2 , d m a x = 1 c_{max}=2,o_{max}=2,d_{max}=1 c m a x  = 2 , o m a x  = 2 , d m a x  = 1 
总的来说ReBucket算法可以分为四个模块:
堆栈预处理(白名单,递归函数等) PDM(二维动态规划) Clustering(类似并查集,只是Find函数需要改一下) 参数训练(二分类模型,基于F值的Grid-Search) 具体实现见下一篇文章
应该放更大的权重在离顶部帧近的帧上,因为bug的根因更容易出现在离顶部帧近的帧上这一观点在实际工程环境 中并不对,实际上顶部帧存在许多系统调用 / sdk调用 / hook,所以顶部帧并不一定是bug的根因,这里可能可以利用堆栈预处理中的静态 / 动态白名单 机制来解决(动态白名单:当一个方法被review并列入白名单后,若其发生变更,则移出白名单)在oom,deadlock等崩溃报告中可能会有多个堆栈,计算其相似性度量时不能是简单的累加关系,而是应该赋予不同的权重 ;如oom中应该按各个调用所用的内存来分配权重、deadlock中应该按lock关键字等来分配权重。所以PDM模型应该按照崩溃类型异化