常见的区域层级有:
在RPC服务中,region是地区概念,如北京、上海、深圳等,不做流量分配,只做灾备。即在一个 region 不可用时,可以切换到另一个 region。
region层面不做流量分配的原因:一般跨region的访问延时很高,像北京到上海有30ms。正常情况下做流量分配会对服务性能造成比较大的影响。
region 不可用的定义:region 内某一服务的可用服务器数量小于一定比例。
在特定的region下,有不同的ip段,每个ip段对应概念是逻辑机房(idc),在同region下的idc,在访问速度上没有明显区别,可以忽略地理上的差异。
ldc 层面需要做流量分配,每个ldc需要定义各自承担的流量比例。
在RPC服务调用流程中,往往覆盖三块核心领域:
其中第二点和第三点往往是整合在一起的。
下面参考开源框架pigeon中的实现,可以基于以下配置来管理RPC服务的区域路由访问策略:
pigeon.regions.route.enable: 是否启用区域路由策略
pigeon.regions: 以ip网段划分区域的归属,配置示例如:region1:110.120,172.23;region2:192.168;region3:120.128
,注意所有的ip段都为xxx.xxx的形式,即只保留ip高16位
pigeon.regions.prefer.xxx,其中xxx是划分的区域名,如前面的region1,region2,region3,具体配置示例如:
pigeon.regions.prefer.region1=region1:3,region2:1,region3:0
pigeon.regions.prefer.region2=region2:10,region3:3,region1:1
pigeon.regions.prefer.region3=region3:3,region1:1,region2:0
上面配置了特定region的访问优先级策略(冒号后面为region权重,用于weight based policy)
基于不同的路由算法,服务调用方可以根据不同的需求特点来获取特定的服务提供方节点,来完成RPC服务调用。选择合适路由算法配置,可以提高RPC服务系统调用的整体可用性和调用性能。
参考pigeon框架,实现的3种调用算法包括:
按照region权重,随机选择特定region中的可用client连接。前面通过pigeon.regions.prefer.xxx配置了客户端所在区域的优先规则,假设客户端机器所在区域为region1,则依据配置:pigeon.regions.prefer.region1=region1:5,region2:3,region3:1
,会先对所有的客户端连接实例归类到3个区域。计算总权重为9,先初始化n=0到9的随机数,在regionSet中遍历region,判断n所属region权重区域,如region1=[0,5],region2=[6,8],region3=9,根据n的值,判断所属region,返回相应region的连接,以此实现基于权重的区域路由。
ForceRegionPolicy可以当作是AutoSwitchRegionPolicy的简化版,ForceRegionPolicy按照配置的region优先级,根据forceRegionConf切换规则判断是否使用优先region。切换规则参见AutoSwitchRegionPolicy。两个策略的区别是ForceRegionPolicy使用了同一路由优先级配置(pigeon.regions.force.config),不针对不同的区域有不同的优先级策略(pigeon.regions.prefer.xxx),且没有idc的细化路由策略。
对于集群的多个节点,我们通过应用相关的集群访问策略,来提高服务请求的可用性。
常见的集群访问算法包括:
轮询法:将请求按顺序轮流地分配到后端服务器上,它均衡地对待后端的每一台服务器,而不关心服务器实际的连接数和当前的系统负载。
随机法:通过系统的随机算法,根据后端服务器的列表大小值来随机选取其中的一台服务器进行访问。由概率统计理论可以得知,随着客户端调用服务端的次数增多,其实际效果越来越接近于平均分配调用量到后端的每一台服务器,也就是轮询的结果。
源地址哈希法:源地址哈希的思想是根据获取客户端的IP地址,通过哈希函数计算得到的一个数值,用该数值对服务器列表的大小进行取模运算,得到的结果便是客服端要访问服务器的序号。采用源地址哈希法进行负载均衡,同一IP地址的客户端,当后端服务器列表不变时,它每次都会映射到同一台后端服务器进行访问。
加权轮询法:不同的后端服务器可能机器的配置和当前系统的负载并不相同,因此它们的抗压能力也不相同。给配置高、负载低的机器配置更高的权重,让其处理更多的请;而配置低、负载高的机器,给其分配较低的权重,降低其系统负载,加权轮询能很好地处理这一问题,并将请求顺序且按照权重分配到后端。
加权随机法:与加权轮询法一样,加权随机法也根据后端机器的配置,系统的负载分配不同的权重。不同的是,它是按照权重随机请求后端服务器,而非顺序。
最小连接数法:最小连接数算法比较灵活和智能,由于后端服务器的配置不尽相同,对于请求的处理有快有慢,它是根据后端服务器当前的连接情况,动态地选取其中当前
基于权重的最小连接数法:在最小连接数算法基础上,进一步拓展考虑机器权重的影响
在上面的区域路由和负载均衡算法中,我们都使用到权重的概念。在通常应用中,权重是针对特定单元配置的一个值,比如在区域路由中,对应一个区域单元配置一个权重值,在负载均衡中,对应一个请求节点配置一个权重值。
对于此,这个权重值有个很大的缺点是配置死的,只能通过人工调控,而不能根据实际网络调用情况实行自动调节。另外还有一个问题是,在服务刚启动的时候,往往附带着大量的初始化工作,这个时候服务可能并不具备完整对外部提供服务的能力,因而我们往往也需要一个冷启动的过程。
基于此,在实际的RPC服务中,可以提供对权重的计算算法作两点优化支持:
下面我们主要来看一个源码示例实现:
private static class WeightFactorMaintainer implements Runnable, ServiceProviderChangeListener, ClusterListener {
private static int step = 1;
public WeightFactorMaintainer() {
// 初始因子
if (initialFactor < 0) {
initialFactor = 0;
}
// 定时间隔
if (interval < 0) {
interval = 1000;
}
if (initialFactor > defaultFactor || step < 0) {
throw new IllegalArgumentException("Invalid weight factor params");
}
}
@Override
public void run() {
while (!Thread.interrupted()) {
try {
// 默认为200ms
Thread.sleep(interval);
// 调整权重因子
adjustFactor();
} catch (InterruptedException e) {
} catch (RuntimeException e) {
logger.warn("error with weight factor maintainer:" + e.getMessage());
}
}
}
private void adjustFactor() {
// 遍历所有连接实例
for (Entry<String, WeightFactor> entry : weightFactors.entrySet()) {
WeightFactor weightFactor = entry.getValue();
// 默认权重因子defaultFactor=100,factor初始化为0,达到100后无需再调整
if (weightFactor.getFactor() < defaultFactor) {
// 最大权重权重100,如果没有到100,每次逐步+1
int factor = Math.min(defaultFactor, weightFactor.getFactor() + step);
weightFactor.setFactor(factor);
entry.setValue(weightFactor);
}
}
}
public static int getEffectiveWeight(String clientAddress) {
// 服务连接权重
Integer w = weights.get(clientAddress);
if (w == null) {
w = 1;
}
// 对应服务连接的权重因子
WeightFactor wf = weightFactors.get(clientAddress);
if (wf == null) {
return w * defaultFactor;
} else {
return w * wf.getFactor();
}
}
}
可以看到,权重因子factor从0到100逐步上涨,可以理解涨到100为恢复到正常权重值
我们可以定义以下请求质量模型:
private enum RequrlQuality {
// 质量优秀
REQURL_QUALITY_GOOD(1),
// 质量一般
REQURL_QUALITY_NORNAL(10),
// 质量较差
REQURL_QUALITY_BAD(100);
private int value;
RequrlQuality(int value) {
this.value = value;
}
public int getValue() {
return value;
}
}
模型中质量分为优秀、一般、较差,最后的权重值可以除以模型枚举值,以达到请求质量对权重的进一步调控:
weight /= quality.getQualityValue(); // int BAD 多数会归零
这里不关注请求数据统计实现,假设能够获取到请求总数、失败数等数据,可以参考以下方法实现请求质量判定:
// 获取失败率=失败数/总数
public float getFailedPercent() {
if (total.get() > 0) {
return failed.get() * 100 / total.get();
} else {
return 0;
}
}
// 根据请求总数和失败率计算质量
public RequrlQuality getQuality() {
// 请求总数需要大于配置阈值
if (getTotalValue() > reqQualityThresholdTotal) {
// 计算失败率
float failedRate = getFailedPercent();
if (failedRate < reqQualityFailedPercentGood) {
// 默认需要失败率低于1%
quality = RequrlQuality.REQURL_QUALITY_GOOD;
} else if (failedRate >= reqQualityFailedPercentGood && failedRate < reqQualityFailedPercentNormal) {
// 默认需要失败率低于5%
quality = RequrlQuality.REQURL_QUALITY_NORNAL;
} else if (failedRate >= reqQualityFailedPercentNormal) {
// 默认需要失败率高于5%
quality = RequrlQuality.REQURL_QUALITY_BAD;
}
}
return quality;
}