下面有几个疑问是我看源码时问自己的,先提出来,希望看这篇文章的人带着疑问去读,然后初步介绍下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 {
EurekaHttpResponse execute(EurekaHttpClient delegate);
RequestType getRequestType();
}
//采用模板方法设计模式,从register、cancel、sendHeartBeat等行为中抽象出execute行为,让子类自己去定义具体实现
protected abstract EurekaHttpResponse execute(RequestExecutor requestExecutor);
下面只列出register方法
@Override
public EurekaHttpResponse register(final InstanceInfo info) {
//匿名接口的实现,调用子类的execute方法
return execute(new RequestExecutor() {
@Override
public EurekaHttpResponse execute(EurekaHttpClient delegate) {
//一步一步委托,最后委托给AbstractJerseyEurekaHttpClient
return delegate.register(info);
}
@Override
public RequestType getRequestType() {
return RequestType.Register;
}
});
}
这篇文章主要介绍RetryableEurekaHttpClient,算是比较重要的,后面有时间再去写其他几个。
顾名思义,可重试的HttpClient,那么这个类中一定会有重试机制的实现,我们先来看看它的execute(RequestExecutor
这个循环里面有一个getHostCandidates()方法,获取所有可用的Eureka Server端点,然后通过endpointIdx++遍历Eureka Server端点发送http请求,如果请求过程中出现超时等异常,注意catch代码块中并没有抛出异常,而是记录日志,然后将这个超时的Eureka Server端点加入黑名单quarantineSet中,继续进行for循环。
异常处理是重试机制重要的一环,如果这个地方没有try catch或者直接抛出异常,那么比如有三个serviceUrl,某一时间在向server3发起请求的时候出现异常,即便后面两个server1和server2是可用的,也不会去请求了(抛出异常,后面for循环代码不会执行了)。
那么由于numberOfRetries等于3,也就是说,最多重试三次,如果都不成功,即便第四个serviceUrl是可用的,也不会去尝试了。
@Override
protected EurekaHttpResponse execute(RequestExecutor requestExecutor) {
List 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 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 getHostCandidates() {
//获取所有的Eureka Server集群节点
List 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 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
Eureka高可用之Client重试机制:RetryableEurekaHttpClient_king-CSDN博客_eureka重试机制
https://blog.csdn.net/qq_36960211/article/details/85273392/