在分布式系統中,我們常常需要檢測某一個節點機器的狀態(是否可以正常工作)。常用的分布式部署會有兩種方式:中心化和去中心化。中心化架構類似於星型架構,比如HDFS的架構,集群中會有單獨一個主服務器(如HDFS中的NameNode)和多個從服務器(如HDFS中的DataNode)。一般這種架構,主服務器來管理所有從服務器的狀態,所有從服務器的狀態信息會在主服務器中保存,而從服務器之間一般不會知道彼此的存在。而去中心化的構架中,沒有主次之分,所有的服務器都是平等的,也就是說每個服務器都知道彼此的存在,一般每台服務器上都保存所有服務器的列表,如Facebook的分布式數據庫Cassandra。一般來說,不管是中心化還是去中心話,獲取其他服務器的狀態都是通過心跳包的形式來處理。雖然我們知道TCP是面向連接的,但是如果遇見拔網線,斷電等物理層的一些特殊情況,TCP還是沒有辦法快速的知道異常,所以心跳包的使用可以幫我們解決這些問題。
傳統的方法也是使用最廣的方法是采用心跳包閾值的方式,我們通常為這種算法設置一個閾值Ttimeout,啟用一個線程去定時查看目標服務器的最后心跳包的到達時間。如果當前時間Tnow - 上一次心跳時間Tlast >= Ttimeout的話,就斷定不目標服務器不能工作。那我們來看看這種方法弊端:
1. Ttimeout的設置依賴於經驗或者是一個估值。如果太小,當網絡負載加大,或心跳包發送或接收方本身計算負載增大處理時間增長而引起發送或處理心跳包的時間延遲,那么偏小的閾值Ttimeout就會導致判定異常的可能性增大。而太大的閾值則會影響診斷到異常的時間。所以這個閾值很難在長期運行,變化的分布式系統中保持可靠。
2. 不適用於某些心跳包發送的方式,比如Gossip協議。Gossip並不讓心跳包發送者按照固定的時間間隔向其他節點發送心跳包。
當然傳統心跳包檢測方式之所以使用廣泛,還是有一定的優勢:邏輯簡單,容易實現,加上高負載的,高性能的大型分布式系統畢竟少數所以該方式可以快速的解決大部分分布式系統的狀態管理。
下面介紹兩種基於概率密度函數的心跳檢測算法。這兩種算法都是采用了在一段時間內采集到得一些心跳間隔時間的樣本來進行概率計算,類似於一個隊列,有最新的心跳過來的時候會進入隊列作為算法樣本,樣本數值為當前過來的心跳於上一次心跳的時間間隔,隊列會有最大長度,滿了就會把對早的樣本踢出樣本隊列,所以隊列窗口保存了最近N個數據樣本。通過對樣本隊列中的樣本進行概率計算,最終得出不會再有心跳的概率。
稍后會在代碼中顯示。
基於正態分布的算法
這個算法應用於節點失效估算來源於這篇論文:The ϕ Accrual Failure Detector。論文認為,心跳時間的間隔是滿足正態分布的,正態分布的累積分布函數:
其中sigma,σ是標准偏差,miu,μ代表樣本隊列的平均值,x則代表隨即變量,我們可以把x認為是需要進行概率計算的輸入參數。那整個函數則表達了正態分布的幾何函數圖。函數圖以樣本數據為橫坐標,縱坐標為正態分布的函數值。
而正態分布的累積分布函數為:
最后,論文中提出了最后的函數:
在這篇文章中,對該算法做了詳細的介紹。
但我在用該算法測試效果的時候,感覺不是很理想,和文論效果相差很大,估計是我測試方法有問題。
基於指數分布的算法
在Facebook的分布式數據庫Cassandra中,雖然作者參考了The ϕ Accrual Failure Detector這篇論文,但是覺得效果沒有指數分布理想,也許是使用了Gossip協議的原因,所以使用了不同的概率密度函數,指數分布函數。
它使用的概率密度函數為:
累積分布函數為:
和上面的算法一樣,同樣采用滑動窗口采樣樣本數據的方式。x是隨機變量,在這里代表當前時間和最后一次心跳到達時間的間隔。λ是率參數我們這里取值 1/樣本平均數。
同樣的,按照The ϕ Accrual Failure Detector:
P_later(t) = 1 - F(t), F(t)是累計分布函數,上正態分布中使用正太累積分布函數,在這里使用的是指數分布累積函數。最終應該計算
-log10(P_later(t))的值,也就是-log10(1-(1-e^(-λx)))。通過計算也就簡化成
φ(x) = (xλ) / ln(10)。
最終的計算結果為節點失效的一個誤判概率值。值越大,誤判的幾率越小。Cassandra里面設置為8。
使用C#實現算法如下:
public double GetPhi(double timeDuration, double average, double standardDeviation)
{
return (-1) * Math.Log10(Math.Pow(Math.E, ((-1) * (timeDuration) / average)));
}
實現的滑動窗口如下:
public class ArrivalWindow
{
private BlockingCollection<double> _queue = new BlockingCollection<double>(1000);
private ICumulativeDistributionAlgorithm _algorithm = new NormalDistributionAlgorithm();
private object _locker = new object();
public void Add(double timeDuration)
{
lock (_locker)
{
if (_queue.Count == _queue.BoundedCapacity)
{
_queue.Take();
}
if (timeDuration <= 10000)
_queue.Add(timeDuration);
else
Console.WriteLine(string.Format("Ignoring interval time of {0}", timeDuration));
}
}
/// <summary>
/// 獲取方差
/// </summary>
/// <returns></returns>
public double GetVariance()
{
lock (_locker)
{
double average = _queue.Average();
double deviation = 0;
foreach (double item in _queue)
{
deviation += Math.Pow((item - average), 2);
}
return deviation / _queue.Count;
}
}
public double GetPhi(double timeDuration)
{
return _algorithm.GetPhi(timeDuration, _queue.Average(), GetVariance());
}
}
在我的測試中,指數分布的方式效果還是很好的。樣本數據越多效果越好。
完整實現代碼可以在這里下載。
參考資料: