調整敏感度

目錄

目錄

一個功能

Netty高低水位

Java8的Map

調整敏感度

應用

 


一個功能

最近在做一個功能,消息隊列的消費端負載均衡,像kafka、RocketMQ等都可以做到全局消費的負載均衡,但是他們都需要一箇中心節點來做統一分配與調整。我們做到了不用集中節點的全局負載均衡,並且模仿kafka的StickAssignor做到了粘性分配,即每次變動最少(純屬爲了炫耀,與本文無關)。

迴歸正題,每次對消費端做負載均衡是一個代價很大的操作,因爲要停止受影響消費端的消費,收集消費offset,重新分配,所以這個操作觸發不能過於頻繁。但是觸發重新負載均衡的條件有很多,所以我們要對此做限制。

最初的想法,對觸發操作增加一個最小時間間隔限制,每次實際執行前,要判斷上一次調整距離當前的時間間隔是否大於最小時間間隔。這保證了在最小時間間隔內,不會連續觸發負載均衡。

public void rebalanceAll() {
    if (lastRebalanceTimeStamp + minRebalanceInterval < System.currentTimeMillis()) {
    	logger.info("rebalance too frequently . last rebalance time {}, min interval {}", lastRebalanceTimeStamp, minRebalanceInterval);
    	return;
    }
    lastRebalanceTimeStamp = System.currentTimeMillis();
    executorService.execute(() -> doRebalanceAll());
}

但是,觸發操作來源於不同線程,這就會有併發問題。首先想到的就是加鎖,對該方法進行加鎖,保證了線程安全。當然了,無論是用jvm的synchronized或者JUC的lock,都可以。

public synchronized void rebalanceAll() {
    if (lastRebalanceTimeStamp + minRebalanceInterval < System.currentTimeMillis()) {
    	logger.info("rebalance too frequently . last rebalance time {}, min interval {}", lastRebalanceTimeStamp, minRebalanceInterval);
    	return;
    }
    lastRebalanceTimeStamp = System.currentTimeMillis();
    executorService.execute(() -> doRebalanceAll());
}

對於這種場景,加鎖就可以滿足要求,因爲該操作不需要考慮性能問題。拋開具體場景,我們還可以繼續優化,利用CAS的方式,做到無鎖化。我們增加一個原子變量,用來表示當前是否已經有任務在執行,在任務執行結束後,重置該變量。

AtomicBoolean processing = new AtomicBoolean(false);
public void rebalanceAll() {
    if (processing.compareAndSet(false,true)) {
		if (lastRebalanceTimeStamp + minRebalanceInterval < System.currentTimeMillis()) {
			logger.info("rebalance too frequently . last rebalance time {}, min interval {}", lastRebalanceTimeStamp, minRebalanceInterval);
			return;
		}
		lastRebalanceTimeStamp = System.currentTimeMillis();
		executorService.execute(() -> doRebalanceAll());	
	}	
}

再擴展一下,如果要支持同一時間內可以運行N個任務,用Boolean的原子變量就無法實現了。我們繼續可以用一個int的原子變量來表示當前時間段正在運行的任務數量,當任務數量<N時,可以添加任務,當任務數量>N時,拋棄任務。

AtomicInteger processing = new AtomicInteger(0);
public void rebalanceAll(int serverOffset) {
    if (processing.getAndIncrement() > N) {
	    processing.decrementAndGet();
	    logger.info("rebalance too frequently . last rebalance time {}, min interval {}", lastRebalanceTimeStamp, minRebalanceInterval);
	    return;
    }
    lastRebalanceTimeStamp = System.currentTimeMillis();
    executorService.execute(() -> doRebalanceAll(serverOffset));
}

再擴展一下,如果系統同一時間內最多可以支持運行N個任務,如果到達瓶頸N時,爲了保護系統,需要等待任務消耗到一定閾值M後,纔可以繼續執行任務,否則全部拋棄。這是一個很常見的場景,比如系統最多並行處理任務10個,此時系統負載已經跑滿,如果消耗掉一個任務後,新任務立即可添加,系統負載又會跑滿,會導致系統始終處於一個滿負荷狀態,並且是否允許添加任務的狀態會連續更改。

我們繼續修改,再增加一個標記,用來表示當前是否到達了最高閾值,如果是,在恢復到最低閾值前,全部拋棄任務。

int processingCount = 0;
volatile boolean rejectAll = false;
public synchronized void rebalanceAll() {
    if (processingCount > N) {
    	rejectAll = true;
    	logger.info("rebalance too frequently . last rebalance time {}, min interval {}", lastRebalanceTimeStamp, minRebalanceInterval);
    	return;
    }
	
    if(processingCount < M){
    	rejectAll = false;
    }
    if(processingCount > M && rejectAll){
    	logger.info("rebalance too frequently . last rebalance time {}, min interval {}", lastRebalanceTimeStamp, minRebalanceInterval);
    	return;	
    }    
    processingCount++; 
    lastRebalanceTimeStamp = System.currentTimeMillis();
    executorService.execute(() -> doRebalanceAll());
}

這個小功能算是擴展結束了,有一種很熟悉的感覺,N是不是就叫做高水位,M是不是就叫做低水位。

Netty高低水位

在Netty裏我們經常會設置兩個參數:

WRITE_BUFFER_HIGH_WATER_MARK:高水位
WRITE_BUFFER_LOW_WATER_MARK:低水位

netty中使用高低水位用來做流控,當你的channel寫緩衝區WriteRequestQueue的數據到達了高水位後,就會設置channel爲不可寫狀態,只有當數據堆積量降低到低水位以下後,纔會重新設置channel爲可寫狀態。

這種目的其實也是爲了防止寫緩衝長期處於滿負荷狀態,保護服務穩定。並且減少頻繁更改channel的可寫狀態。

 

Java8的Map

在JDK8中,HashMap的hash衝突採用的拉鍊法,做了一些優化,當鏈表長度超過一定閾值後,會把鏈表轉換爲紅黑樹,目的就是爲了防止鏈表過長,查找效率降低。當鏈長度縮減到一定閾值後,會把紅黑樹重新恢復爲鏈表,目的是在查找效率相近時,降低插入效率。

這就會有一個問題,如果連續的insert---delete---insert---delete操作,並且key都落在一個hash槽上,就會導致連續的鏈表與紅黑樹轉換。

JDK是怎麼解決的呢?

其實很簡單,鏈表轉紅黑樹用了一個閾值N,紅黑樹恢復鏈表用了另外一個閾值M,其中N>M。也就是說,當鏈表長度>=N時,把鏈表轉換爲紅黑樹。當紅黑樹的節點數量<=M時,把紅黑樹恢復爲鏈表。其中M與N之間的差值,就是用來做緩衝的,防止連續變化。

 

調整敏感度

以上的三個例子,其實都是一個問題,即調整敏感度。對於一個狀態的設置與恢復,如果過於頻繁,可能會影響效率或者影響系統的穩定性,此時就需要降低調整的敏感度。

一般的做法,都是採用高低水位的方式來控制,利用高低水位間的差值來做緩衝。當到達高水位時,設置狀態,並且只有在恢復到低水位後,纔可以恢復狀態。

那麼我們可以抽象出這類問題,當某個狀態影響了系統的穩定性或者性能,我們減低該狀態調整的敏感度,一般方法爲高低水位控制法,利用高低水位間的距離來做調整緩衝。

應用

在實際中,其實我們經常會用到。比如上面說的HashMap的鏈表與紅黑樹互轉,netty的高低水位。像我工作中也有很多地方用到,比如前面說的任務併發度控制、限流控制、緩衝隊列長度的內存保護等等。

在生活中也會有這種場景,比如地鐵放行,當地鐵進站人數到達一定程度之後,管理人員會做人流控制,禁止排隊乘客再過安檢。只有當地鐵進站人數少於一定量後,纔會繼續放行安檢。

 

個人公衆號,期待大家關注:

 

 

 

 

 

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章