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來豐富你的算法庫,進一步提升準確率。