高可用实践之区域路由、集群访问和负载均衡策略(五)

文章目录

  • 机房和区域对比
  • RPC请求服务节点请求流程
  • 区域路由
    • 路由基础配置
    • 路由算法介绍
      • 自动切换区域路由策略
      • 基于权重区域路由
      • 强制区域路由
  • 集群访问
  • 负载均衡
    • 负载均衡算法
  • 权重算法优化
    • 冷启动优化
    • 请求质量动态调整
  • 参考

机房和区域对比

常见的区域层级有:

  1. 同机房(IDC)
  2. 同区域(region上海、北京等)

在RPC服务中,region是地区概念,如北京、上海、深圳等,不做流量分配,只做灾备。即在一个 region 不可用时,可以切换到另一个 region。

region层面不做流量分配的原因:一般跨region的访问延时很高,像北京到上海有30ms。正常情况下做流量分配会对服务性能造成比较大的影响。

region 不可用的定义:region 内某一服务的可用服务器数量小于一定比例。
在特定的region下,有不同的ip段,每个ip段对应概念是逻辑机房(idc),在同region下的idc,在访问速度上没有明显区别,可以忽略地理上的差异。

ldc 层面需要做流量分配,每个ldc需要定义各自承担的流量比例。

RPC请求服务节点请求流程

在RPC服务调用流程中,往往覆盖三块核心领域:

  1. 区域路由算法:根据特定的区域路由算法,选取指定区域的机器提供服务
  2. 集群访问策略:在选取特定区域后,得到服务节点集群,根据具体的集群访问策略来访问。
  3. 负载均衡选取:在选取特定区域后,得到服务节点集群后,依据负载均衡算法选择合适的节点请求

其中第二点和第三点往往是整合在一起的。

区域路由

路由基础配置

下面参考开源框架pigeon中的实现,可以基于以下配置来管理RPC服务的区域路由访问策略:

  1. pigeon.regions.route.enable: 是否启用区域路由策略

  2. pigeon.regions: 以ip网段划分区域的归属,配置示例如:region1:110.120,172.23;region2:192.168;region3:120.128,注意所有的ip段都为xxx.xxx的形式,即只保留ip高16位

  3. pigeon.regions.prefer.xxx,其中xxx是划分的区域名,如前面的region1,region2,region3,具体配置示例如:

    1. pigeon.regions.prefer.region1=region1:3,region2:1,region3:0
    2. pigeon.regions.prefer.region2=region2:10,region3:3,region1:1
    3. pigeon.regions.prefer.region3=region3:3,region1:1,region2:0

    上面配置了特定region的访问优先级策略(冒号后面为region权重,用于weight based policy)

路由算法介绍

基于不同的路由算法,服务调用方可以根据不同的需求特点来获取特定的服务提供方节点,来完成RPC服务调用。选择合适路由算法配置,可以提高RPC服务系统调用的整体可用性和调用性能。

参考pigeon框架,实现的3种调用算法包括:

  1. 自动切换区域路由 AutoSwitchRegionPolicy
  2. 基于权重区域路由 WeightBaseRegionPolicy
  3. 强制区域路由 ForceRegionPolicy

自动切换区域路由策略

  1. 按照优先级选择region中的可用client连接,当region可用率低于设置的切换阈值时,依次选择下一个优先级的region。切换阈值regionSwitchRatio默认为0.5f,即可用的client连接低于50%为region不可用。
  2. 在autoSwitch策略下,通过修改开关值isIdcFilterEnable,可以开启本region中的idc过滤,当同时满足以下3个条件会优先路由本地idc连接:
    1. 同一ip段活跃的连接数idcActive > 同一ip段总连接数idcTotal * 配置比例idcFilterThresHoldRatio
    2. idcActive > 最小阈值idcFilterThresholdLeast
    3. 同一ip段活跃的连接数idcActive > region可用连接数active * 配置比例idcFilterThresHoldRatio

基于权重区域路由

按照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的细化路由策略。

集群访问

对于集群的多个节点,我们通过应用相关的集群访问策略,来提高服务请求的可用性。

常见的集群访问算法包括:

  1. failfast:如果使用failfast策略,会在调用失败后抛出异常。可以同时配置重试timeoutRetry和retries属性,如果配置了timeoutRetry=true(超时重试),会在因为捕获到超时异常失败后,重试调用最多retries次
  2. failover:调用服务的一个节点失败后会尝试调用另外的一个节点,另外一个节点的选取也是通过负载均衡算法从集群中选取,也可以同时配置重试timeoutRetry和retries属性,failover和failfast的实现区别是如果配置了超时失败重试,在超时失败后进行重试时,failover选取服务端连接会排除掉之前失败的链接,而failfast不会。
  3. failsafe:调用服务的一个节点失败后不会抛出异常,返回null或默认值
  4. forking:同时调用服务的所有可用节点,返回调用最快的节点结果数据,在forking策略中,可通过修改forkingSize配置,指定最多调用的节点数,不进行forkingSize配置,则forkingSize的默认值为2,也即会调用2个可用的服务节点。如果forkingSize大于当前所有连接实例数len,则调用所有连接实例。
    具体的选取算法实现是生成一个0到len-1的整型随机数,然后从随机数开始,赠序选取数组中的连接,如果超过了数组的长度,则从头取起。
  5. HedgedCluster:发起调用后,超过hedgedDelay时间后未返回结果,会再次向其他服务节点发送一个请求,以最先返回的结果为结果返回,主要用在解决服务调用长尾问题

负载均衡

负载均衡算法

  1. 轮询法:将请求按顺序轮流地分配到后端服务器上,它均衡地对待后端的每一台服务器,而不关心服务器实际的连接数和当前的系统负载。

  2. 随机法:通过系统的随机算法,根据后端服务器的列表大小值来随机选取其中的一台服务器进行访问。由概率统计理论可以得知,随着客户端调用服务端的次数增多,其实际效果越来越接近于平均分配调用量到后端的每一台服务器,也就是轮询的结果。

  3. 源地址哈希法:源地址哈希的思想是根据获取客户端的IP地址,通过哈希函数计算得到的一个数值,用该数值对服务器列表的大小进行取模运算,得到的结果便是客服端要访问服务器的序号。采用源地址哈希法进行负载均衡,同一IP地址的客户端,当后端服务器列表不变时,它每次都会映射到同一台后端服务器进行访问。

  4. 加权轮询法:不同的后端服务器可能机器的配置和当前系统的负载并不相同,因此它们的抗压能力也不相同。给配置高、负载低的机器配置更高的权重,让其处理更多的请;而配置低、负载高的机器,给其分配较低的权重,降低其系统负载,加权轮询能很好地处理这一问题,并将请求顺序且按照权重分配到后端。

  5. 加权随机法:与加权轮询法一样,加权随机法也根据后端机器的配置,系统的负载分配不同的权重。不同的是,它是按照权重随机请求后端服务器,而非顺序。

  6. 最小连接数法:最小连接数算法比较灵活和智能,由于后端服务器的配置不尽相同,对于请求的处理有快有慢,它是根据后端服务器当前的连接情况,动态地选取其中当前

  7. 基于权重的最小连接数法:在最小连接数算法基础上,进一步拓展考虑机器权重的影响

权重算法优化

在上面的区域路由和负载均衡算法中,我们都使用到权重的概念。在通常应用中,权重是针对特定单元配置的一个值,比如在区域路由中,对应一个区域单元配置一个权重值,在负载均衡中,对应一个请求节点配置一个权重值。

对于此,这个权重值有个很大的缺点是配置死的,只能通过人工调控,而不能根据实际网络调用情况实行自动调节。另外还有一个问题是,在服务刚启动的时候,往往附带着大量的初始化工作,这个时候服务可能并不具备完整对外部提供服务的能力,因而我们往往也需要一个冷启动的过程。

基于此,在实际的RPC服务中,可以提供对权重的计算算法作两点优化支持:

  1. 在客户端监听服务方节点注册时,通过定时器,对权重进行逐步累加,在冷启动定义时间内逐步恢复到配置权重值。
  2. 加入每个配置单元的一个请求质量统计,并根据请求质量调整权重

冷启动优化

下面我们主要来看一个源码示例实现:

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;
}

参考

  1. https://mp.csdn.net/mdeditor/86737607#
  2. https://blog.csdn.net/qwe6112071/article/details/86737655
  3. https://blog.csdn.net/qwe6112071/article/details/86737655
  4. https://blog.csdn.net/qwe6112071/article/details/86737593

你可能感兴趣的:(分布式)