1、背景
通常我們的服務器面對高併發時會採取一些限流、熔斷、降級的手段,條件允許的時候採取水平擴展提升系統的吞吐量。最近腦子一熱在想若是不考慮水平擴展的成本,我們是不是可以設計一款支持動態擴展的系統,當併發量達到限流上限時自動增加服務節點,當併發量很小時自動銷燬空閒節點。
其實想一想我們的服務模型,與如下的排隊模型很相似。
其實排隊論就是爲解決這類問題的,原理見參考
2、實現服務指標的計算
動態擴展的前提應該是能根據當前QPS和服務自身的處理能力,計算出性能指標,根據這些指標來衡量當前是否需要增刪節點。
這裏選用了比較簡單的M/M/S模型,該模型需要知道服務節點總數s、請求到達率和平均服務率來計算排隊長、隊長、平均等待時間、平均逗留時間、到達時必須等待的概率、服務空閒概率等。
服務空閒概率p0:
請求到來時必須等待的概率
平均排隊長 Lq 爲:
平均等待時間(Wq)和平均逗留時間(Ws):
java的實現:
public class MmsModel {
/**
* 計算M/M/S排隊模型的各項指標
*
* @param queueParamDto
* @return
*/
public static ServerEvaluateDto evaluateMmsModel(QueueParamDto queueParamDto) {
ServerEvaluateDto serverEvaluateDto = new ServerEvaluateDto();
double rho = queueParamDto.getLamda() / queueParamDto.getMu();
double rhoS = rho / queueParamDto.getServerNum();
int s = queueParamDto.getServerNum();
if (rhoS < 1) {
//計算空閒概率p(0)
double p0 = pn(rho, s, rhoS, 0);
serverEvaluateDto.setIdleProbability(p0);
//計算必須等待的概率
double mustWaitProp = Math.pow(rho, s) * p0 / factorial(s) / (1 - rhoS);
serverEvaluateDto.setMustWaitProp(mustWaitProp);
//計算平均排隊長
serverEvaluateDto.setAvgQueueLengthQ(mustWaitProp * rhoS / (1 - rhoS));
//計算平均隊長
serverEvaluateDto.setAvgQueueLength(serverEvaluateDto.getAvgQueueLengthQ() + rho);
//計算平均等待時間
serverEvaluateDto.setAvgWaitTime(serverEvaluateDto.getAvgQueueLengthQ() / queueParamDto.getLamda());
//計算平均逗留時間
serverEvaluateDto.setAvgStayTime(serverEvaluateDto.getAvgQueueLength() / queueParamDto.getLamda());
}
return serverEvaluateDto;
}
/**
* 計算階乘
*
* @param num
* @return
*/
private static Integer factorial(int num) {
if (num == 0) {
return 1;
}
List<BigInteger> list = new ArrayList<>();
list.add(BigInteger.valueOf(1));
for (int i = list.size(); i <= num; i++) {
BigInteger lastfact = list.get(i - 1);
BigInteger nextfact = lastfact.multiply(BigInteger.valueOf(i));
list.add(nextfact);
}
return list.get(num).intValue();
}
/**
* p(n)
*
* @param rho
* @param s-服務檯數
* @param rhoS
* @return
*/
private static double pn(double rho, int s, double rhoS, int n) {
if (n == 0) {
double result = 0;
for (int i = 0; i < s; i++) {
result += Math.pow(rho, i) / factorial(i);
}
result += Math.pow(rho, s) / factorial(s) / (1 - rhoS);
return 1.0 / result;
}
if (n <= s) {
return pn(rho, s, rhoS, 0) * Math.pow(rho, n) / factorial(n);
} else {
return pn(rho, s, rhoS, 0) * Math.pow(rho, n) / factorial(s) / Math.pow(s, n - s);
}
}
public static void main(String[] args) {
QueueParamDto queueParamDto = new QueueParamDto();
queueParamDto.setServerNum(3);
queueParamDto.setLamda(0.9);
queueParamDto.setMu(0.4);
System.out.println(MmsModel.evaluateMmsModel(queueParamDto));
}
}
3、模擬請求響應模型
使用這個例子模擬實現請求響應模型,改造監控部分代碼,當必須等待概率大於0.5時增加服務節點。
監控線程:
long current = System.currentTimeMillis();
//平均每秒接收的請求
double lamda = new Double(CustomerQuene.getTotal())/(current - serverStartTime)*1000.0;
//平均每秒服務處理的請求數
double mu = new Double(ServantThread.getCustomerNum())/(current - serverStartTime)*1000.0;
System.out.println("總達到顧客數:" + CustomerQuene.getTotal());
System.out.println("總服務顧客數:" + ServantThread.getCustomerNum());
System.out.println("平均服務時間: " + mu);
System.out.println("平均到達時間:" + lamda);
QueueParamDto queueParamDto = new QueueParamDto();
queueParamDto.setServerNum(servantNum);
queueParamDto.setLamda(lamda);
queueParamDto.setMu(mu);
ServerEvaluateDto serverEvaluateDto = MmsModel.evaluateMmsModel(queueParamDto);
System.out.println("服務評價指標:" + serverEvaluateDto);
if(serverEvaluateDto.getMustWaitProp()>0.5){
servantNum++;
System.out.println("水平擴容增加服務器一臺,當前服務器" + servantNum +"臺");
ServantThread threadAdd = new ServantThread("服務檯" + (servantNum-1));
threadAdd.start();
}
try {
TimeUnit.SECONDS.sleep(2);
} catch (Exception e) {
e.printStackTrace();
}
部分監控日誌:
平均服務時間: 4.7041346868036635
平均到達時間:17.908723281340265
服務評價指標:ServerEvaluateDto(idleProbability=0.004934283986573964, avgQueueLengthQ=17.658817680236186, avgQueueLength=21.465835224095837, avgWaitTime=0.9860455936931883, avgStayTime=1.1986245410616096, mustWaitProp=0.8951474400119712)
水平擴容增加服務器一臺,當前服務器5臺
服務檯3 服務顧客耗時:381ms 顧客等待:8744ms
服務檯4 服務顧客耗時:418ms 顧客等待:8789ms
服務檯2 服務顧客耗時:1420ms 顧客等待:8023ms
服務檯0 服務顧客耗時:1448ms 顧客等待:8224ms
服務檯1 服務顧客耗時:1367ms 顧客等待:8582ms
服務檯4 服務顧客耗時:774ms 顧客等待:9088ms
服務檯0 服務顧客耗時:757ms 顧客等待:9265ms
服務檯3 服務顧客耗時:1201ms 顧客等待:9056ms
服務檯2 服務顧客耗時:1227ms 顧客等待:9114ms
服務檯1 服務顧客耗時:856ms 顧客等待:9558ms
服務檯2 服務顧客耗時:309ms 顧客等待:9886ms
總達到顧客數:245
總服務顧客數:68
平均服務時間: 4.8138184907263195
平均到達時間:17.34390485629336
服務評價指標:ServerEvaluateDto(idleProbability=0.02271825440912265, avgQueueLengthQ=1.0609071142777542, avgQueueLength=4.6638482907483425, avgWaitTime=0.06116887304607165, avgStayTime=0.26890416716371873, mustWaitProp=0.41137214635259833)
可以看出當必須等待概率達到0.8951474400119712時,增加一個節點,立馬降到了0.41137214635259833。
4、總結
demo中僅使用到了到達時必須等待概率這一個性能指標,實際中可根據多個參數綜合考量是否需動態增刪節點。
幻想有一天我們的微服務架構中服務節點、redis集羣節點、hadoop節點等等可以實現這樣的動態增刪多好。實際項目中我們很多的監控組件可獲取到平均到達率、平均服務率這些參數,也有自動部署的各類平臺,可自動部署服務節點。目前不太瞭解實際項目中有沒有大公司這樣做,或者有更好的算法,腦子一熱感覺排隊論好神奇,可以解決這類問題,哈哈哈哈。。。