前言
前段時間我們在升級 Pulsar 版本的時候發現升級後最後一個節點始終沒有流量。
雖然對業務使用沒有任何影響,但負載不均會導致資源的浪費。
和同事溝通後得知之前的升級也會出現這樣的情況,最終還是人工呼叫 Pulsar 的 admin API
完成的負載均衡。
這個問題我嘗試在 Google 和 Pulsar 社群都沒有找到類似的,不知道是大家都沒碰到還是很少升級叢集。
我之前所在的公司就是一個版本走到黑?
Pulsar 負載均衡原理
當一個叢集可以水平擴充套件後負載均衡就顯得非常重要,根本目的是為了讓每個提供服務的節點都能均勻的處理請求,不然擴容就沒有意義了。
在分析這個問題的原因之前我們先看看 Pulsar 負載均衡的實現方案。
# Enable load balancer
loadBalancerEnabled=true
我們可以透過這個 broker 的這個配置來控制負載均衡器的開關,預設是開啟。
但具體使用哪個實現類來作為負載均衡器也可以在配置檔案中指定:
# Name of load manager to use
loadManagerClassName=org.apache.pulsar.broker.loadbalance.impl.ModularLoadManagerImpl
預設使用的是 ModularLoadManagerImpl
。
static LoadManager create(final PulsarService pulsar) {
try {
final ServiceConfiguration conf = pulsar.getConfiguration();
// Assume there is a constructor with one argument of PulsarService.
final Object loadManagerInstance = Reflections.createInstance(conf.getLoadManagerClassName(),
Thread.currentThread().getContextClassLoader());
if (loadManagerInstance instanceof LoadManager) {
final LoadManager casted = (LoadManager) loadManagerInstance;
casted.initialize(pulsar);
return casted;
} else if (loadManagerInstance instanceof ModularLoadManager) {
final LoadManager casted = new ModularLoadManagerWrapper((ModularLoadManager) loadManagerInstance);
casted.initialize(pulsar);
return casted;
}
} catch (Exception e) {
LOG.warn("Error when trying to create load manager: ", e);
}
// If we failed to create a load manager, default to SimpleLoadManagerImpl.
return new SimpleLoadManagerImpl(pulsar);
}
當 broker
啟動時會從配置檔案中讀取配置進行載入,如果載入失敗會使用 SimpleLoadManagerImpl
作為兜底策略。
當 broker 是一個叢集時,只有 leader 節點的 broker 才會執行負載均衡器的邏輯。
Leader 選舉是透過 Zookeeper 實現的。
默然情況下成為 Leader 節點的 broker 會每分鐘讀取各個 broker 的資料來判斷是否有節點負載過高需要做重平衡。
而是否重平衡的判斷依據是由 org.apache.pulsar.broker.loadbalance.LoadSheddingStrategy
介面提供的,它其實只有一個函式:
public interface LoadSheddingStrategy {
/**
* Recommend that all of the returned bundles be unloaded.
* @return A map from all selected bundles to the brokers on which they reside.
*/
Multimap<String, String> findBundlesForUnloading(LoadData loadData, ServiceConfiguration conf);
}
根據所有 broker 的負載資訊計算出一個需要被 unload 的 broker 以及 bundle。
這裡解釋下 unload 和 bundle 的概念:
bundle
是一批topic
的抽象,將bundle
和broker
進行關聯後客戶端才能知道應當連線哪個 broker;而不是直接將 topic 與broker
繫結,這樣才能實現海量 topic 的管理。- unload 則是將已經與 broker 繫結的 bundle 手動解綁,從而觸發負載均衡器選擇一臺合適的 broker 重新進行繫結;通常是整個叢集負載不均的時候觸發。
ThresholdShedder 原理
LoadSheddingStrategy
介面目前有三個實現,這裡以官方預設的 ThresholdShedder
為例:
它的實現演算法是根據頻寬、記憶體、流量等各個指標的權重算出每個節點的負載值,之後為整個叢集計算出一個平均負載值。
# 閾值
loadBalancerBrokerThresholdShedderPercentage=10
當叢集中有某個節點的負載值超過平均負載值達到一定程度(可配置的閾值)時,就會觸發 unload,以上圖為例就會將最左邊節點中紅色部分的 bundle 解除安裝掉,然後再重新計算一個合適的 broker 進行繫結。
閾值存在的目的是為了避免頻繁的 unload,從而影響客戶端的連線。
問題原因
當某些 topic 的流量突然爆增的時候這種負載策略確實可以處理的很好,但在我們叢集升級的情況就不一定了。
假設我這裡有三個節點:
- broker0
- broker1
- broker2
叢集升級時會從 broker2->0
進行映象替換重啟,假設在升級前每個 broker 的負載值都是 10。
- 重啟 broker2 時,它所繫結的 bundle 被 broker0/1 接管。
- 升級 broker1 時,它所繫結的 bundle 又被 broker0/2 接管。
- 最後升級 broker0, 它所繫結的 bundle 會被broker1/2 接管。
只要在這之後沒有發生流量激增到觸發負載的閾值,那麼當前的負載情況就會一直保留下去,也就是 broker0
會一直沒有流量。
經過我反覆測試,現象也確實如此。
./pulsar-perf monitor-brokers --connect-string pulsar-test-zookeeper:2181
透過這個工具也可以檢視各個節點的負載情況
最佳化方案
這種場景是當前 ThresholdShedder
所沒有考慮到的,於是我在我們所使用的版本 2.10.3 的基礎上做了簡單的最佳化:
- 當原有邏輯走完之後也沒有獲取需要需要解除安裝的 bundle,同時也存在一個負載極低的 broker 時(
emptyBundle
),再觸發一次 bundle 查詢。 - 按照 broker 所繫結的數量排序,選擇一個數量最多的 broker 的 第一個 bundle 進行解除安裝。
修改後打包釋出,再走一遍升級流程後整個叢集負載就是均衡的了。
但其實這個方案並不嚴謹,第二步選擇的重點是篩選出負載最高的叢集中負載最高的 bundle;這裡只是簡單的根據數量來判斷,並不夠準確。
正當我準備持續最佳化時,鬼使神差的我想看看 master 上有人修復這個問題沒,結果一看還真有人修復了;只是還沒正式發版。
https://github.com/apache/pulsar/pull/17456
整體思路是類似的,只是篩選負載需要解除安裝 bundle 時是根據 bundle 自身的流量來的,這樣會更加精準。
總結
不過看社群的進度等這個最佳化最終能用還不知道得多久,於是我們就自己參考這個思路在管理臺做了類似的功能,當升級後出現負載不均衡時人工觸發一個邏輯:
- 系統根據各個節點的負載情況計算出一個負載最高的節點和 bundle 在頁面上展示。
- 人工二次確認是否要解除安裝,確認無誤後進行解除安裝。
本質上只是將上述最佳化的自動負載流程改為人工處理了,經過測試效果是一樣的。
Pulsar 整個專案其實非常龐大,有著幾十上百個模組,哪怕每次我只改動一行程式碼準備釋出測試時都得經過漫長的編譯+ Docker映象打包+上傳私服這些流程,通常需要1~2個小時;但總的來說收穫還是很大的,最近也在提一些 issue 和 PR,希望後面能更深入的參與進社群。