我為什麼要設計自己的流量排程演算法?

haolujun發表於2017-09-22

背景

公司使用阿里的雲主機部署計算型的服務,就是特別耗cpu的那種。使用過程中有一件事情很苦惱,那就是雲主機的效能是不一致的,機器間的效能可相差30%,更嚴重的是由於是共享雲主機,經常在晚上8點鐘左右(各大網站的高峰期)有某些機器的系統cpu突然飆高(原因是一次系統呼叫消耗突然增加,系統cpu能飆到90%,機器基本不可用)。這個問題其實很好解釋:阿里雲在同一個物理機上虛擬好多雲主機,並且雲主機之間隔離做的不好,一臺雲主機可能會影響同一物理機上的另一臺雲主機,導致你的雲主機效能有問題的可能是另外一家公司使用的雲主機。多次向阿里提出這個問題,阿里並沒給啥解釋,就是推薦我們換一臺雲主機試試(這是玩我呢麼?)。而我要在這麼不靠譜的機器上要做到服務穩定,最需要解決的問題就是如何動態的根據機器的效能變化調整分配給每臺機器的流量。

方案一:嘗試阿里的slb(不能解決問題)

阿里的slb提供了幾種流量排程演算法:輪詢排程、加權輪詢排程、最小連線數優先排程。

  • 輪詢排程:顧名思義就是順序的分配流量,它保證每臺機器接受的請求量是相同的。這個演算法不能解決問題,因為我們現在就是不能讓每臺機器接收的請求量相同,我們的要求是效能好的機器接收的多,效能差的機器接收的少,故pass。
  • 加權輪詢:這個方法給每臺機器加了一個權重,比如預設可以都設定成100,這樣所有服務接收等量的請求。也可以設定成100,100,80,這樣如果有3臺機器和280個請求,三臺機器分別接收100,100,80個請求。看起來很美好,但是現實很骨感。它沒法動態的調整權重,特別對於突發情況,必須人為手動去更改slb配置。更嚴重的是,對slb的操作會偶爾持續失敗(阿里的slb也可能正在上線、維護),這對於及時處理問題可有不小的麻煩,故pass。
  • 最小連線數優先:乍看起來可能覺得不錯,其實和輪詢排程一樣的問題,它總嘗試把所有機器的連線數壓成一致,也就是說盡量保證每個機器的請求量一致,這就會使得效能差的機器cpu一直被壓滿。我們要的就是流量不均衡的排程,而不是這種非得保證流量均衡的排程,故pass。

所以,最終我放棄了。看來必須自己解決這個問題,那麼就順手寫個流量排程演算法吧。

方案二:客戶端流量自適應演算法

設計思路

首先要明確我們想要解決的問題是根據機器的效能分配流量,面對的第一個難題就是如何實時的獲取機器效能資訊呢?大多數小夥伴的思路是實時獲取機器的cpu資訊,但是我認為這太複雜,還需要開發一套實時收集cpu資訊的輔助系統,不划算。我只利用一個資訊:某段時間內呼叫某個機器上服務的失敗次數。

第二個問題就是客戶端自適應演算法不是全域性感知。我說的全域性感知是:由統一的服務收集並記錄客戶端呼叫每個服務的失敗次數,客戶端根據這個集中統計資料來分配流量。我們不採用全域性感知,就意味著每個客戶端根據自己當前得到的資訊進行區域性的流量分配,這可行嗎?可行!如果客戶端數量足夠多,根據統計學意義,所有區域性感知的結果彙總和整體感知的效果基本相當。當客戶端數目不夠多的時候呢?反證法:如果存在一個服務接收過量的請求,那麼推測肯定有某一客戶端呼叫該服務會失敗,那麼該客戶端就會主動減少對這臺後端的服務的呼叫,從而減少這個有問題服務接收的請求數。接下來說說說演算法設計思路:初始時設定每臺後端機器的權重w=10,當超時10次的時間間隔(最後一次超時的時間戳與第一次超時的時間戳相差)小於10s時,則對這臺機器的權重減1,從而減少10%的流量。

但是現在這個排程演算法並不完善,有如下幾個問題要解決。

  • 集中失敗問題 觀察實際情況發現,當一個服務效能突然下降時,會有大量的請求同時失敗。由於我們的演算法是記錄連續失敗次數的時間段,這就可能導致機器的權重瞬間降至0(如果瞬間100次超時,就會出現這種情況),從而該服務獲取不到流量。解決這個問題的方式是在降權一次後忽略接下來一個時間段內的失敗次數(比如設定5秒或者10秒),這樣把這些瞬間失敗歸為同一撥。
  • 升權問題 阿里雲的機器突發這種系統cpu飆高的情況持續的時間不一定,有的時候持續幾分鐘,有的時候持續幾小時甚至幾天,當這種情況恢復正常時,排程演算法能夠逐漸恢復該機器的請求數量。所以,可以在合適的時機試探性的增加流量。我的做法是:在一定的權值基礎上,如果演算法正常工作超過10分鐘(10分鐘內沒有再發生降權的情況),可以嘗試把權重+1,這樣就多分配10%的流量,流量增加10%後還可能會由於效能不夠導致降權。
  • 比率問題 假如w=10時,服務在10秒內接收到100個請求,其中失敗了10次,演算法降權後w=9;w=9時,服務在10秒內接收了90個請求,失敗了9次,這個時候應不應該降權呢?要降!因為w=9之所以失敗次數少,是因為呼叫次數少,但是這同樣說明服務的效能是有問題的。所以,在一開始我們的演算法思路說的不完全準確,不是固定時間固定的失敗次數,而是需要按照權重不斷調整失敗次數。w=10時,10s內失敗10次就要降權;w=9時,10s內失敗9次就要降權;以此類推。
  • 保底策略 由於我們是統計失敗10次所用的時間長度,可能在某些情況下(比如上線的時候,演算法配置時間段,失敗次數,不合理)會造成對某些機器一直降權,而真實情況是後端機器效能並沒有差到哪去,這會造成機器資源浪費並且還會有更多的請求拒絕。所以演算法需配置一個機器權重下限,比如設定成6,就能保證最起碼有該機器正常情況下流量的60%會傳送到該機器上。

具體實現

  • 快速選擇機器 每個服務對應一個呼叫次數:request_cnt,演算法每次選擇該服務時就對其累加+1。我們設定初始權重為w,當前權重為cur_w,那麼當request_cnt % w < cur_w時,流量分配給該服務,否則跳過,這樣就保證了安裝權重分配流量。
  • 解耦 為了解耦排程演算法與具體環境,我們抽象一套演算法介面。

 

  • SelectAlgorithm:演算法介面
  • SimpleSelectAlgorithm:普通輪詢排程演算法
  • WeightSelectAlgorithm:客戶端動態自適應演算法
  • add_addr:為演算法新增後端服務
  • remove_addr:從演算法中移除後端服務
  • failed:當請求後端服務失敗後呼叫,比如統計失敗次數,最近10次失敗的間隔,降權等。
  • succeed:當請求後端服務成功時呼叫,如果最近一段時間內沒有呼叫失敗,嘗試升高權重。
  • next:返回下一次呼叫時可用的後端服務

如何使用我們的演算法呢,例項如下:

Address addr;
Algorithm *algorithm = new WeightSelectAlgorithm(...);
algorithm->next(addr);  //選擇一個服務

int ret = request(addr);

if(ret != 0) {
  algorithm->failed(addr.host, addr.port); //請求失敗
} else {
  algorithm->succeed(addr.host, addr.port);  //請求成功
}

我這裡只給大家提供了這個設計思路,具體程式碼實現我相信難不倒各位小夥伴,這裡就不貼了。演算法初始化時需接受一些配置引數,一定要保證這些引數可以手動調整,直到達到滿意的效果位置。這個演算法上線後的效果還是挺好的,請求失敗的次數能夠降一個量級,由於客戶端有重試策略,最終失敗的呼叫次數可以忽略不計。

相關文章