构建智能灵活的 Ribbon 负载均衡策略
侧边栏壁纸
  • 累计撰写 307 篇文章
  • 累计阅读 104.3万

构建智能灵活的 Ribbon 负载均衡策略

TOTC
2024-12-13 / 77 阅读 / 正在检测是否收录...

分布式系统架构中,负载均衡是确保系统高可用性和性能的关键环节。以线上服务为例,某个服务原本部署了4个实例来应对大量的请求流量。然而,意外情况发生,其中一个实例所在的机房出现故障,导致其响应速度变得极为缓慢,但是仍然和Nacos注册中心保持着心跳。而当时所采用的负载均衡策略是轮询策略 RoundRobinRule,这一策略在正常情况下能够较为均匀地分配请求,但在面对这种异常情况时,却暴露出了明显的局限性。

由于轮询策略的特性,它不会根据实例的实际响应情况进行动态调整,这就使得故障实例上仍然会有大量请求持续堆积。随着时间的推移,发现该实例所在机器的 close_wait 连接数急剧增加,导致整个机器负载加重。

为了解决这一问题,调研了一些传统的应对策略:

其一,配置超时失败重试机制 ... httpclient: response-timeout: 30s。
故障实例响应慢时,自动失败路由到其他实例进行重试,从而使上游的请求最终能够成功。但故障服务实例的流量并没有得到有效的控制和调整。这意味着故障实例和所在机器仍然在承受着巨大的压力。

其二,采用熔断策略 Sentinel、Resilience4J、Hystrix。
在响应时间/出错百分比/线程数等达到阈值时进行降级、熔断,以保护其他服务实例不受影响。然而,在该场景中,由于还有 3/4 的实例处于正常可用状态,直接进行熔断操作显得过于激进。

其三,考虑使用权重轮询策略 WeightedResponseTimeRule。
根据服务实例的性能表现动态地分配权重,性能好的实例会被分配更多的请求,而性能差的实例则会逐渐减少请求分配。但该场景下,故障机器的响应时间与正常服务相比已经不在一个数量级,其 QPS 却依然很高。这就导致在权重轮询策略下,故障机器的服务权重会迅速降低,几乎不再接收请求。而且由于我们的配置是在网关层面,当故障机器恢复后,系统无法自动重新计算权重,使得分配到故障机器的流量很少,其权重也很难再次提升上去。

基于以上困境,决定对权重轮询策略进行二次开发,使其更加智能,以最大限度地减小请求端的影响。

首先增加过滤器RibbonResponseFilter。这个过滤器的主要作用是计算每个服务实例的响应时间,并将其记录到 ServerStats 中。同时,它还会记录请求的返回状态,如果返回状态不是 200,就将其转化为请求超时,并相应地减小该服务的权重。

@Component
@Slf4j
public class RibbonResponseFilter implements GlobalFilter, Ordered {
    @Autowired
    protected final SpringClientFactory springClientFactory;
    public static final String RQUEST_START_TIME = "RequestStartTime";
    public static final double TIME_WEIGHT = 30000;

    public RibbonResponseFilter(SpringClientFactory springClientFactory) {
        this.springClientFactory = springClientFactory;
    }

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        exchange.getAttributes().put(RQUEST_START_TIME, System.currentTimeMillis());
        return chain.filter(exchange).then(Mono.fromRunnable(() -> {
            URI requestUrl = exchange.getAttribute(ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR);
            Route route = exchange.getAttribute(ServerWebExchangeUtils.GATEWAY_ROUTE_ATTR);
            LoadBalancerContext loadBalancerContext = this.springClientFactory.getLoadBalancerContext(route.getUri().getHost());
            ServerStats stats = loadBalancerContext.getServerStats(new Server(requestUrl.getHost(), requestUrl.getPort()));
            long orgStartTime = exchange.getAttribute(RQUEST_START_TIME);
            long time = System.currentTimeMillis() - orgStartTime;
            // 响应时间超过 5s 或者服务异常时,减小权重
            if (exchange.getResponse().getStatusCode().value()!= 200 || time > 5000) {
                log.info("The abnormal response will lead to a decrease in weight : {} ", requestUrl.getHost());
                stats.noteResponseTime(TIME_WEIGHT);
            }
        }));
    }

    @Override
    public int getOrder() {
        return Ordered.LOWEST_PRECEDENCE;
    }
}

增加这个过滤器的原因在于,无论是使用自定义的负载均衡策略,还是内置的 WeightedResponseTimeRule,都无法自动获取到每个服务实例的总请求次数、异常请求次数以及响应时间等关键参数。通过这个过滤器,能够有效地收集这些信息,为后续的权重计算和调整提供有力的数据支持。

微信图片_20241213173510.png

在注册权重更新 Timer(默认 30s)的同时,同时注册了一个权重重置 Timer(5m)。这样一来,当故障服务实例恢复后,在 5 分钟内,它就能够重新参与到负载均衡的分配中。以下是相关的代码片段:

void resetWeight() {
    if (resetWeightTimer!= null) {
        resetWeightTimer.cancel();
    }
    resetWeightTimer = new Timer("NFLoadBalancer-AutoRobinRule-resetWeightTimer-"
            + name, true);
    resetWeightTimer.schedule(new ResetServerWeightTask(), 0,
            60 * 1000 * 5);
    ResetServerWeight rsw = new ResetServerWeight();
    rsw.maintainWeights();

    Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() {
        public void run() {
            logger.info("Stopping NFLoadBalancer-AutoRobinRule-ResetWeightTimer-" + name);
            resetWeightTimer.cancel();
        }
    }));
}

public void maintainWeights() {
    ILoadBalancer lb = getLoadBalancer();
    if (lb == null) {
        return;
    }

    if (!resetServerWeightAssignmentInProgress.compareAndSet(false, true)) {
        return;
    }

    try {
        logger.info("Reset weight job started");
        AbstractLoadBalancer nlb = (AbstractLoadBalancer) lb;
        LoadBalancerStats stats = nlb.getLoadBalancerStats();
        if (stats == null) {
            return;
        }

        Double weightSoFar = 0.0;
        List<Double> finalWeights = new ArrayList<Double>();
        for (Server server : nlb.getAllServers()) {
            finalWeights.add(weightSoFar);
        }
        setWeights(finalWeights);
    } catch (Exception e) {
        logger.error("Error reset server weights", e);
    } finally {
        resetServerWeightAssignmentInProgress.set(false);
    }
}

在采用此负载均衡策略时,若重置权重后服务仍未修复,由于配置了超时重试机制,请求端可毫无察觉。与此同时,该服务实例的权重会迅速在短时间内再次降至极低水平,如此循环,直至实例恢复正常。此策略有效地处理了线上服务可能遭遇的各类异常状况。

1

评论

博主关闭了所有页面的评论