Eureka高可用之Client重试机制:RetryableEurekaHttpClient

下面有几个疑问是我看源码时问自己的,先提出来,希望看这篇文章的人带着疑问去读,然后初步介绍下EurekaHttpClient体系,后面会详细讲RetryableEurekaHttpClient

1、Eureka Client如何向Eureka Server集群注册?如果我的Client端的ServiceUrl配置了多个Eureka Service地址,那么Client是否会向每一个Server发起注册?

2、Eureka Server具有复制行为,即向其他Eureka Server节点复制自身的实例信息,那么既然有复制行为,那么Eureka Client的ServiceUrl中只配置一个不就行了吗,我注册到Server上,Server自己去把我的信息复制为其他Eureka Server节点不就好了吗,是否就说明Eureka Client的ServiceUrl只配置一个就好?

3、如果Eureka Client的ServiceUrl配置了多个,那么Client会和那个Eureka Server保持通信(注册、续约心跳等)?是否是第一个,或者是随机的?

defaultZone: http://server3:50220/eureka,http://server1:50100/eureka,http://server2:50300/eureka

RetryableEurekaHttpClient继承自EurekaHttpClient装饰器EurekaHttpClientDecorator,它并不是真正发起http请求的HttpClient,它会间接的把请求委托给AbstractJerseyEurekaHttpClient,如下类图:

EurekaHttpClientDecorator这个类采用了模板方法模式,在register、cancel、sendHeartBeat等行为中,抽象出了execute方法,让子类自定义执行行为

//匿名接口,没有具体实现类
public interface RequestExecutor<R> {
    EurekaHttpResponse<R> execute(EurekaHttpClient delegate);
    RequestType getRequestType();
}
 
//采用模板方法设计模式,从register、cancel、sendHeartBeat等行为中抽象出execute行为,让子类自己去定义具体实现
protected abstract <R> EurekaHttpResponse<R> execute(RequestExecutor<R> requestExecutor);

下面只列出register方法 

@Override
public EurekaHttpResponse<Void> register(final InstanceInfo info) {
    //匿名接口的实现,调用子类的execute方法
    return execute(new RequestExecutor<Void>() {
        @Override
        public EurekaHttpResponse<Void> execute(EurekaHttpClient delegate) {
            //一步一步委托,最后委托给AbstractJerseyEurekaHttpClient
            return delegate.register(info);
        }
 
        @Override
        public RequestType getRequestType() {
            return RequestType.Register;
        }
    });
}

这篇文章主要介绍RetryableEurekaHttpClient,算是比较重要的,后面有时间再去写其他几个。

顾名思义,可重试的HttpClient,那么这个类中一定会有重试机制的实现,我们先来看看它的execute(RequestExecutor<R> requestExecutor)方法,我们看到这个方法里面有个for循环,并且在循环内发起了http请求,这个循环默认的重试次数为3(没有配置可以配置这个次数)。

这个循环里面有一个getHostCandidates()方法,获取所有可用的Eureka Server端点,然后通过endpointIdx++遍历Eureka Server端点发送http请求,如果请求过程中出现超时等异常,注意catch代码块中并没有抛出异常,而是记录日志,然后将这个超时的Eureka Server端点加入黑名单quarantineSet中,继续进行for循环。

异常处理是重试机制重要的一环,如果这个地方没有try catch或者直接抛出异常,那么比如有三个serviceUrl,某一时间在向server3发起请求的时候出现异常,即便后面两个server1和server2是可用的,也不会去请求了(抛出异常,后面for循环代码不会执行了)。

那么由于numberOfRetries等于3,也就是说,最多重试三次,如果都不成功,即便第四个serviceUrl是可用的,也不会去尝试了。

@Override
protected <R> EurekaHttpResponse<R> execute(RequestExecutor<R> requestExecutor) {
    List<EurekaEndpoint> candidateHosts = null;
    //候选Eureka ServerList的下标
    int endpointIdx = 0;
    //默认重试3次,DEFAULT_NUMBER_OF_RETRIES = 3
    for (int retry = 0; retry < numberOfRetries; retry++) {
        EurekaHttpClient currentHttpClient = delegate.get();
        EurekaEndpoint currentEndpoint = null;
        if (currentHttpClient == null) {
            if (candidateHosts == null) {
                //获取候选Eureka Server 的serviceUrlList
                candidateHosts = getHostCandidates();
                if (candidateHosts.isEmpty()) {
                    //如果出现这个异常,基本山可以肯定的是,没有配置serviceUrl和remoteRegion
                    throw new TransportException("There is no known eureka server; cluster server list is empty");
                }
            }
            if (endpointIdx >= candidateHosts.size()) {
                // 这个异常也很常见,这个方法里面的循环默认要执行三次,当你只配了一个ServiceUrl,
                // 并且是无效的,那么在第二次重试的时候,就会抛出这个异常
                throw new TransportException("Cannot execute request on any known server");
            }
            //获取serviceUrl信息
            currentEndpoint = candidateHosts.get(endpointIdx++);
            //根据新的serviceUrl信息构建新的httpClient
            currentHttpClient = clientFactory.newClient(currentEndpoint);
        }
 
        try {
            //向serviceUrl发起请求,register、heartBeat、Cancel、statusUpdate。
            EurekaHttpResponse<R> response = requestExecutor.execute(currentHttpClient);
            // serverStatusEvaluator为状态评估器,为每个请求类型(Register、SendHeartBeat、Cancel、GetDelta等)
            // 设定可接受的状态码,比如,当请求类型为Register,且response.getStatusCode()为404,那么此时也算可接受的
            // 不再去尝试下一个ServiceURl
            if (serverStatusEvaluator.accept(response.getStatusCode(), requestExecutor.getRequestType())) {
                delegate.set(currentHttpClient);
                if (retry > 0) {
                    logger.info("Request execution succeeded on retry #{}", retry);
                }
                return response;
            }
            logger.warn("Request execution failure with status code {}; retrying on another server if available", response.getStatusCode());
        } catch (Exception e) {
            //如果请求过程中,出现连接超时等异常,打印日志,更新currentHttpClient,更换下一个serviceUrl重新尝试
            logger.warn("Request execution failed with message: {}", e.getMessage());  // just log message as the underlying client should log the stacktrace
        }
 
        // Connection error or 5xx from the server that must be retried on another server
        delegate.compareAndSet(currentHttpClient, null);
        if (currentEndpoint != null) {
            //http请求失败,将当前尝试的Eureka Server端点放入黑名单。
            quarantineSet.add(currentEndpoint);
        }
    }
    //如果三次都没有请求成功,则放弃请求,如果serviceUrl中配置了4个Eureka地址,前三个都请求失败了,那么即便第四个serviceUrl可用,也不会去尝试
    throw new TransportException("Retry limit reached; giving up on completing the request");
}

那么从上面的代码可以得出结论:

1、Eureka Client在发送注册、心跳等请求时,会向Eureka Server集群节点serviceUrlList顺序逐个去尝试,如果有一个请求成功了,那么直接返回response ,不再去向其他节点请求,最多只重试3次,超过3次直接抛出异常。

2、如果按如下配置defaultZone那么请求的顺序是server3->server1->server2

3、defaultZone建议配置多个url,有多少配置多少,即便大于3,因为有的server可能被client拉黑了,不会被client请求,也就不会计入numberOfRetries次数

4、如果下面这个配置中server3永远可用,那么这个Client永远只向这一个server发送心跳等事件

5、Eureka Client的defaultZone不要都配置成一样的顺序,最好打乱配置,如果所有的Eureka Client都按以下的配置,那么这个server3的压力很大,既要负责接收所有client的心跳状态变更等,又要负责向其他server集群节点同步信息

defaultZone: http://server3:50220/eureka,http://server1:50100/eureka,http://server2:50300/eureka

getHostCandidates()这个方法是用来获取ServiceUrlList的,它内部有个黑名单机制,如果向一个Eureka Server端点发起请求异常失败,那么就会把这个Eureka Server端点放入quarantineSet(隔离集合)里面,下一次调用getHostCandidates()方法时候(在上面那个for循环里面,这个方法只会执行一次),会拿quarantineSet.size()和一个阈值做比较,如果小于这个阈值,那么就会对candidateHosts 进行过滤。

这个黑名单机制,其根本目的是为了让Eureka Client请求成功的概率更大,想象一下,如果上面那个server3永远挂了,而且还没好办法去动态改变Client的defaultZone的配置,那么每30秒发送一次心跳的时候,client都会去请求一下server3

private List<EurekaEndpoint> getHostCandidates() {
    //获取所有的Eureka Server集群节点
    List<EurekaEndpoint> candidateHosts = clusterResolver.getClusterEndpoints();
    //黑名单取交集,看看candidateHosts里面有几个Server端点是在黑名单里面的
    quarantineSet.retainAll(candidateHosts);
 
    // If enough hosts are bad, we have no choice but start over again
    //这个百分比默认是0.66,约等于2/3,
    //举个栗子,如果candidateHosts=3,那么阈值threshold就等于1(3*0.66=1.98,在int强转型就等于1了...)
    int threshold = (int) (candidateHosts.size() * transportConfig.getRetryableClientQuarantineRefreshPercentage());
    //Prevent threshold is too large
    if (threshold > candidateHosts.size()) {
        //防止阈值过大,这个百分比有可能被人误设成大于1的值
        threshold = candidateHosts.size();
    }
    if (quarantineSet.isEmpty()) {
        //黑名单是空的,不进行过滤
        // no-op
    } else if (quarantineSet.size() >= threshold) {
        //黑名单的数量大于这个阈值了,清空黑名单,不进行过滤
        //设置阈值目的就是在于防止所有的serverlist都不可用,都被拉黑了
        //所以要清空黑名单,重新进行尝试
        logger.debug("Clearing quarantined list of size {}", quarantineSet.size());
        quarantineSet.clear();
    } else {
        //如果小于阈值,那么过滤掉黑名单里面的端点
        List<EurekaEndpoint> remainingHosts = new ArrayList<>(candidateHosts.size());
        for (EurekaEndpoint endpoint : candidateHosts) {
            if (!quarantineSet.contains(endpoint)) {
                remainingHosts.add(endpoint);
            }
        }
        candidateHosts = remainingHosts;
    }
 
    return candidateHosts;
}

这个RetryableEurekaHttpClient的重试机制基本上就讲差不多了,如果有想自己去调试的同学,可以在Client端按照如下配置defaultZone,然后只开启一台Eureka Server(server2)然后在RetryableEurekaHttpClient类的execute方法里面打上断点,debug启动Client即可(register-with-eureka和fetch-registry属性一定要有一个配置成true)

defaultZone: http://server3:50220/eureka,http://server1:50100/eureka,http://server2:50300/eureka

 

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值