常见的AIOps应用路径为:对监控的各种关键性能指标(KPI)进行实时异常检测;对多维指标进行根源分析,快速下钻到异常维度和元素;基于应用拓扑和实时Trace,实现根因定位;结合CMDB、关联等、构建异常根因上下文,帮助快速修复问题。
作为KPI指标, 往往包含了很多维度和元素,最显而易见的则是对每一个维度的元素都进行实时异常检测。 对于维度组合笛卡尔集数量很长的场景, 该方案的成本则有点难以承受。 此外,还有很多业务场景更多的是关心业务总体指标,总体指标出现问题了才会关心具体哪个维度下有问题。 这俩类问题都可以通过多维下钻分析得到很好的解决。 本文主要是介绍多维分析中最常用的Attributor算法,以及基于该算法的实践。
1. 背景知识
KL散度 & JS散度导入
1.KL散度
用来衡量两个分布之间的差异,等于一个交叉熵减去一个信息熵(交叉熵损失函数的由来)
1.1 KL散度的性质
-
非负性(用Jenson‘s inequality 证明)
-
不对称性,即KL(P||Q)≠KL(Q||P)
1.2 KL散度的问题即JS散度的引出
正是由于KL散度的不对称性问题使得在训练过程中可能存在一些问题,为了解决这个问题,我们在KL散度基础上引入了JS散度
JS如下:
-
JS(Jenson’s Shannon)散度
一般地,JS散度是对称的,其取值是 0 到 1 之间。如果两个分布 P,Q 离得很远,完全没有重叠的时候,那么KL散度值是没有意义的,而JS散度值是一个常数。这在学习算法中是比较致命的,这就意味这这一点的梯度为 0。梯度消失了。
2. 算法解析
背景
论文:https://www.usenix.org/system/files/conference/nsdi14/nsdi14-paper-bhagwan.pdf
本文主要针对论文《Adtributor: Revenue Debugging in Advertising Systems》做简单介绍。这篇论文主要介绍如何定位造成广告收入波动的根本因素。
问题定义
问题:某个月的预测广告总收入为100万,实际广告总收入为50万
数据:如果每个月的广告总收入可以从数据中心,广告商,终端设备这3个维度分别进行统计。其收入分布情况具体如下图所示:
广告收入分布图
需求:如何定位出造成这次收入突降的根本因素,即找到对广告收入波动影响最大的因素(某个维度下的某个具体因素)
论文关键思路
(1)定义
度量指标: 论文中用广告收入
维度: 论文中主要包含数据中心、广告商、终端设备3个维度
维度下具体因素: 数据中心这个维度主要包含2个因素,X和Y
后续公式中会使用的标识具体如下图:
(2)核心思路
第一步:计算某维度下某因素的真实值和预测值占总收入的差异性----Surprise(惊奇性)
A : 计算 。其中 表示i维度i下j因素对应的预测收入占总预测收入的比率,具体公式为:
其中m是某种度量指标如广告收入,i表示维度,j表维度下具体因素。F(m)表示预测总收入如100万, 表示维度i下j因素对应的预测收入,如数据中心X对的预测收入为94万,则对应的 ; A(m)表示实际总收入50万,则数据中心X的实际收入为47万,则对应的
B: 计算 的差异度,论文中利用js散度进行计算, 直接的差异越大,则js散度值越大
第二步:计算某维度下某因素波动占总体波动的比率----Explanatory power(解释力)
其中波动表示真实值和异常值的差异性,具体公式入下:
如终端设备PC的EP值为 ,Tablet的EP值为
(3)率值指标处理
率值KPI的计算方式不同于量值KPI,因为率值KPI不能像量值KPI一样在不同维度和元素上进行加减操作,率值KPI的波动变化也不能通过加减反映在各个维度和元素上。此时,偏导数和有限差分便派上用场了。在离散场景下, 衡量对于这种形式函数的波动变化情况的偏导数可以简化为:
最后文中推导出的EP值如下:
根据相对熵理论,对于需要首先计算f(.)和g(.)函数的联合概率分布,然后计算联合概率分布函数的相对熵,计算十分复杂。论文作出近似假设,认为f和g函数之间相互独立,则的联合概率分布相对熵就是f(.)和g(.)的概率分布函数相对熵之和。因此,率值KPI的S值等于组成率值KPI定义公式的分子KPI和分母KPI的S值之和。省略下标ij,S值计算公式如下
S = sum(Sf, Sg)
具体的算法如下:
总结
Adtributor假设所有根因都是一维的提出了解释力(Explanatory power)和惊奇性(Surprise)来量化根因的定义。通过计算维度的惊奇性(维度内所有元素惊奇性之和)对维度进行排序,确定根因所在的维度(例如省份)。在维度内部计算每个元素的解释力,当元素的解释力之和(例如北京+上海)超过阈值时,这些元素就被认为是根因。
(1) 指标类型的泛化:对于基本类型的KPI的计算公式(例如PV、交易量),直接走正常的S值和EP值计算公式, 对于率值类指标(例如成功率,失败率),则需要结合偏导数和有限差分, 推导出对应的率值公式。
(2)维度之间的关系:将根因限定在一维的假设不太符合我们的实际场景,同时用解释性和惊奇性的大小来衡量根因也不完全合理。因为其没有考虑到维度之间的相互影响以及「外部根因」的可能。
(3)对于参考值的选型:Adtributor的根因分析严重依赖于整体KPI的变化情况,对于整体变化不大,但是内部波动较为剧烈的数据表现不好
优化建议
-
异常检测,标注正常和异常范围, 为提供正常和异常对比提供依据
-
率值指标转化为量值指标进行,避免复杂公式推导。
-
多维分析先进行维度和元素剪枝,降低规模。
-
可参考腾讯哈勃系统多维分析设计: https://cloud.tencent.com/developer/article/1644348
3. 算法实践
Attributor算法构建:
import numpy as np
import pandas as pd
import scipy.stats
def js_divergence(p, q):
p = np.array(p)
q = np.array(q)
m = (p + q) / 2
js = 0.5 * np.sum(p * np.log(p / m)) + 0.5 * np.sum(q * np.log(q / m))
return round(float(js), 6)
def get_exp(pv_pred, pv_actual, pred_sum, actual_sum):
if actual_sum - pred_sum == 0:
return pv_actual - pv_pred
else:
return (pv_actual - pv_pred) / (actual_sum - pred_sum)
def root_cause_analysis_for_single_dimension(dframe,dimension_2_check):
EP_threshold = 0.1
pv_sum = dframe.sum(numeric_only=True)
# 先计算isp维度的
group_isp = dframe.groupby(dimension_2_check).sum()
group_isp = group_isp.reset_index()
group_isp['pred_sum'] = pv_sum['pv_pred']
group_isp['actual_sum'] = pv_sum['pv_actual']
group_isp['p'] = group_isp['pv_pred'] / group_isp['pred_sum']
group_isp['q'] = group_isp['pv_actual'] / group_isp['actual_sum']
# 第一步:计算当前维度下Surprise
group_isp['surprise'] = group_isp[['p', 'q']].apply(lambda x: js_divergence(x['p'], x['q']), axis=1)
# 第二步:计算当前维度下每个元素值的EP值
group_isp['EP'] = group_isp[['pv_pred', 'pv_actual', 'pred_sum', 'actual_sum']].apply(
lambda x: get_exp(x['pv_pred'], x['pv_actual'] , x['pred_sum'], x['actual_sum']), axis=1)
# 第三步:计算每个维度下元素的变化占比:
group_isp['change'] = group_isp[['pv_pred', 'pv_actual', 'pred_sum', 'actual_sum']].apply(
lambda x: (x['pv_actual'] - x['pv_pred']) / (x['actual_sum'] ), axis=1)
isp_surprise = group_isp['surprise'].sum()
group_isp = group_isp[group_isp['EP'] > EP_threshold]
return [dimension_2_check, isp_surprise, group_isp]
单维度分析主函数
import pandas as pd
import numpy as np
from attributor import root_cause_analysis_for_single_dimension
def data_from_KPI_to_pd(df, axis, anomaly_start_time, anomaly_end_time, normal_start_time, normal_end_time):
data = df[axis]
axis_list = []
pred_list = []
actual_list = []
for key, item in data.items():
pred = np.mean(item[normal_start_time:normal_end_time])
actual = np.mean(item[anomaly_start_time:anomaly_end_time])
actual_list.append(actual)
pred_list.append(pred)
axis_list.append(key)
result = pd.DataFrame({"elements": axis_list,
"pv_pred": pred_list,
"pv_actual": actual_list, })
return result
# 单维度分析
def single_axis_analysis(df, anomaly_start_time, anomaly_end_time, normal_start_time, normal_end_time, axis_2_check):
"""
进行指标数据的单维度下钻分析
params:
df: 包含总指标、各维度/元素指标
axis_2_check: 分析的维度
anomaly_start_time: 总指标的异常开始
anomaly_end_time: 总指标的异常结束
normal_start_time: 总指标的正常开始
normal_end_time:总指标的正常结束
"""
check_list = []
df_total = []
dimension_list = []
axis_after_check = []
print("we will check these axis: ", axis_2_check)
for a_to_c in axis_2_check:
result = data_from_KPI_to_pd(df, a_to_c, anomaly_start_time, anomaly_end_time, normal_start_time, normal_end_time)
result.columns = ['elements', 'pv_pred', 'pv_actual']
# None值处理
result = result.fillna(1)
res = root_cause_analysis_for_single_dimension(result, ['elements'])
# print(res)
group_isp = res[2]
group_isp['axis'] = a_to_c
group_isp['suprise_sum'] = res[1]
df_total.append(res[2])
check_list.extend(list(group_isp['elements']))
if res[1] > 0:
axis_after_check.append(a_to_c)
dimension_list.append([a_to_c, res[1]])
df_final = pd.concat(df_total, axis=0)
df_final = df_final.sort_values(by=['change'], ascending=False)
dimension_list = sorted(dimension_list, key=lambda x: x[1], reverse=True)
dimension_list = [i[0] for i in dimension_list]
print("these axis exists problems ", dimension_list)
return {"axis_error":dimension_list,
"elements":df_final['elements'].to_list()}
测试数据
# -*- coding: utf-8 -*
# 功能说明: ## 测试通过http方式去获取异常排序结果
import json
import requests
import numpy as np
import pandas as pd
import random
def make_data():
# 构造总的KPI
KPI = []
for i in range(97):
tmp = random.randint(5, 10)
KPI.append(tmp)
KPI.extend([100, 200, 100])
# 构造各个维度:
# 维度1: region,包含3个元素
KPI_region_1 = []
for i in range(100):
tmp = random.randint(1, 4)
KPI_region_1.append(tmp)
KPI_region_2 = []
for i in range(100):
tmp = random.randint(1, 4)
KPI_region_2.append(tmp)
KPI_region_3 = []
for i in range(100):
tmp = KPI[i] - KPI_region_1[i] - KPI_region_2[i]
KPI_region_3.append(tmp)
# 维度2: api,包含2个元素
KPI_api_1 = []
for i in range(97):
tmp = random.randint(1, 4)
KPI_api_1.append(tmp)
KPI_api_1.extend([20, 40, 20])
KPI_api_2 = []
for i in range(100):
tmp = KPI[i] - KPI_api_1[i]
KPI_api_2.append(tmp)
# 输入数据
# df为总指标和各维度/元素的下指标,组合成的dict类型。
# axis_2_check中的元素, 必须是df中的key,否则该元素无法进行分析。
# 异常时间段、正常时间段。 这个一般是和检测进行结合, 得到俩个段的开始和结束时间。
df = {
"metric": KPI,
"region": {"sh": KPI_region_1,
"sz": KPI_region_2,
"bj": KPI_region_3,
},
"api": {
"api1": KPI_api_1,
"api2": KPI_api_2,
},
}
axis_2_check = ['region', 'api']
anomaly_start_time = 97
anomaly_end_time = 100
normal_start_time = 0
normal_end_time = 90
return df, anomaly_start_time, anomaly_end_time, normal_start_time, normal_end_time, axis_2_check
if __name__ == '__main__':
df, anomaly_start_time, anomaly_end_time, normal_start_time, normal_end_time, axis_2_check = make_data()
result = single_axis_analysis(df, anomaly_start_time, anomaly_end_time, normal_start_time, normal_end_time, axis_2_check)
实验结果
Case1:突增
-
总数据
total为汇总的KPI,
包含了region和api俩个维度,各个维度的元素可见图
-
分析结果:
Case2:下跌
-
总数据
total为汇总的KPI,
包含了region和api俩个维度,各个维度的元素可见图
-
分析结果:
Reference
https://blog.csdn.net/weixin_44441131/article/details/105878383
https://zhuanlan.zhihu.com/p/89266916
论文地址:https://www.usenix.org/system/files/conference/nsdi14/nsdi14-paper-bhagwan.pdf