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节点等等可以实现这样的动态增删多好。实际项目中我们很多的监控组件可获取到平均到达率、平均服务率这些参数,也有自动部署的各类平台,可自动部署服务节点。目前不太了解实际项目中有没有大公司这样做,或者有更好的算法,脑子一热感觉排队论好神奇,可以解决这类问题,哈哈哈哈。。。