ADDOPS团队籍鑫璞 360云计算 算法
该文章出自于ADDOPS团队,是一篇关于机器学习在监控报警的阀值设置方面的实战性文章。该文章针对咱们平常监控报警中(必须设置)的静态阀值的弊端,利用机器学习设置动态阀值的思路,极大的解放了运维的人力负担,可以很是有效的下降运维的误报率。因此但愿能给你们有所启发。
PS:丰富的一线技术、多元化的表现形式,尽在“HULK一线技术杂谈”,点关注哦!网络
传统的异常检测系统经过设置一个固定的阈值来保证监控项处于正常水平,一旦超过设定的阈值,就会触发报警来提醒人们的注意。app
静态阈值法适用于在必定范围内波动的监控项,好比磁盘使用率,CPU使用率等,可是若是遇到网络流量这种不具备明显上限,波动比较剧烈的状况,单纯利用静态阈值法若是设置的阈值比较小,会出现不少误报的状况,增长人工成本;而若是将阈值设置的比较大,又会出现漏报的状况。运维
因此咱们提出了一个应对复杂场景的异常检测算法,它不只考虑同期数据的状况,也会将周期性归入考虑范围,经过设置动态阈值的方法来对异常数据进行检测。机器学习
对于时间序列(是指将同一统计指标的数值按其发生的时间前后顺序排列而成的数列)来讲,T时刻的数值对于T-1时刻有很强的依赖性。好比某个游乐园人数在8:00不少,在8:01时刻的人数不少的几率是很大的,可是若是07:01时刻人数对于8:01时刻影响不是很大。ide
针对最近时间窗口内的数据遵循某种趋势的现象,咱们使用一条曲线对该趋势进行拟合,若是新的数据打破了这种趋势,使曲线变得不平滑,则该点就出现了异常。函数
曲线拟合的方法有不少,好比回归、moving average……。在这篇文章中,咱们使用EWMA,即指数权重移动平均方法来拟合曲线。EWMA的递推公式是:学习
EWMA(1) = p(1) // 有时也会取前若干值的平均值。α越小时EWMA(1)的取值越重要。 EWMA(i) = α * p(i) + (1-α) * EWMA(i – 1) //α是一个0-1间的小数,称为smoothing factor.
能够从上面公式得出,下一点的平均值是由上一点的平均值,加上当前点的实际值修正而来。对于每个EWMA值,每一个数据的权重是不同的,最近的数据将拥有越高的权重。云计算
有了平均值以后,咱们就可使用3-sigma理论来判断新的input是否超过了容忍范围。比较实际的值是否超出了这个范围就能够知道是否能够告警了。超出了上界,多是流量忽然增长了;低于下界,多是流量忽然下降了,这两种状况都须要告警。code
咱们可使用pandas库中的ewma函数来实现咱们上面的计算过程,代码以下:
def EWMA(timeseries): flag = "" series = pandas.Series([x[1] for x in timeseries]) expAverage = pandas.stats.moments.ewma(series, com=50) stdDev = pandas.stats.moments.ewmstd(series, com=50) if abs(series.iget(-1) - expAverage.iget(-1)) > 3 * stdDev.iget(-1) and series.iget(-1) - expAverage.iget(-1) > 0: flag = "突增" if abs(series.iget(-1) - expAverage.iget(-1)) > 3 * stdDev.iget(-1) and series.iget(-1) - expAverage.iget(-1) > 0: flag = "突减" return flag
EWMA因为其时效性被普遍应用在时间序列的预测,
它的优点在于:
而劣势则是
因此咱们须要引入周期性的检测方法,来针对性处理具备周期性趋势的曲线。
不少监控项都具备必定的周期性,其中以一天为周期的状况比较常见,好比VIP流量在早上4点最低,而在晚上11点最高。为了将监控项的周期性考虑进去,咱们选取了某个监控项过去14天的数据。对于某个时刻,将获得14个点能够做为参考值,咱们记为xi,其中i=1,...,14。
咱们先考虑静态阈值的方法来判断input是否异常(突增和突减)。若是input比过去14天同一时刻的最小值乘以一个阈值还小,就会认为该输入为异常点(突减);而若是input 比过去14天同一时刻的最大值乘以一个阈值还大,就会认为该输入为异常点(突增)。咱们将上面的计算过程用代码来表示:
def simultaneous(data, min_threshold, max_threshold): last_time = data.index[-1] last_time = datetime.datetime.strptime(last_time, "%Y-%m-%d %H:%M:%S") simultaneous_data =[] for i in range(1, days_num+1): before_time = last_time + datetime.timedelta(days=int("-%s" %i)) before_time_index = before_time.strftime("%Y-%m-%d %H:%M:%S") if before_time_index in data.keys(): simultaneous_data.append(int(data[before_time_index])) if int(data[-1]) < min(simultaneous_data) * min_threshold: return "突降" if int(data[-1]) > max(simultaneous_data) * max_threshold: return "突增"
静态阈值的方法是根据历史经验得出的值,实际中如何给max_threshold和min_threshold是一个须要讨论的话题。根据目前动态阈值的经验规则来讲,取平均值是一个比较好的思路。
上面咱们把基于同期数据的检测方法进行了说明,下面咱们讨论一下它的优缺点:
优势:
检测方法二遇到下图的现象就不能检测出异常。好比今天是11.18日,过去14天的历史曲线必然会比今天的曲线低不少。那么今天出了一个小故障,曲线下跌了,相对于过去14天的曲线仍然是高不少的。这样的故障使用方法二就检测不出来,那么咱们将如何改进咱们的方法呢?一个直觉的说法是,两个曲线虽然不同高,可是“长得差很少”。那么怎么利用这种“长得差很少”呢?那就是振幅了。
怎么计算t时刻的振幅呢? 咱们使用x(t) – x(t-1) 再除以 x(t-1)来表示振幅。举个例子,例如t时刻的在线900人,t-1时刻的在线是1000人,那么能够计算出掉线人数是10%。若是参考过去14天的数据,咱们会获得14个振幅值。使用14个振幅的绝对值做为标准,若是m时刻的振幅({m(t) – m(t-1)]/m(t-1))大于amplitudethreshold而且m时刻的振幅大于0,则咱们认为该时刻发生突增,而若是m时刻的振幅大于amplitudethreshold而且m时刻的振幅小于0,则认为该时刻发生突减。
咱们将上面的计算过程用一段代码表示:
def amplitude_max(data, threshold): #最后一个值和倒数第二个值的振幅 last_amplitude = 0.0 last_time = data.index[-1] last_time = datetime.datetime.strptime(last_time, "%Y-%m-%d %H:%M:%S") last_amplitude_time = last_time + datetime.timedelta(minutes=int(-1)) last_amplitude_time_index = last_amplitude_time.strftime("%Y-%m-%d %H:%M:%S") if last_amplitude_time_index in data.keys(): last_amplitude = float((float(data.values[-1])-float(data[last_amplitude_time_index]))/float(data.values[-1])) last_time = last_time + datetime.timedelta(days=int(-1)) #同期的振幅 amplitude_data = [] for i in range(0, days_num): now_time = last_time prior_time = last_time + datetime.timedelta(minutes=int(-1)) prior_time_index = prior_time.strftime("%Y-%m-%d %H:%M:%S") now_time_index = now_time.strftime("%Y-%m-%d %H:%M:%S") if now_time_index in data.keys() and prior_time_index in data.keys(): tmp = float((float(data[now_time_index])-float(data[prior_time_index]))/float(data[now_time_index])) amplitude_data.append(abs(round(tmp, 2))) last_time = last_time + datetime.timedelta(days=int(-1)) if abs(last_amplitude) > max(amplitude_data) * threshold and last_amplitude > 0: return "突增" if abs(last_amplitude) > max(amplitude_data) * threshold and last_amplitude < 0: return "突减"
一样,咱们也讨论一下该方法的优缺点,先说优势:
上面介绍了三种检测的方法,每一个方法都有每一个方法的优缺点,也有每一个方法能检测和不能检测的范围。所以单纯依靠一种方法并不能达到咱们预期的效果,并且具备很高的误报率。咱们借鉴了投票制度中“少数服从多数”的原则,即三个方法中有两个或者两个以上符合,咱们才认为符合。
若是你以为上面的三种方法还不够精确,你甚至可使用开源项目skyline来丰富你的算法库,进一步提升准确率。