
扫描下方二维码了解详情,试听课程

本文来源:一枝花算不算浪漫
《互联网 Java 工程师面试突击(第3季)》免费升级,由原来的70讲增至160讲,内容扩充一倍多,升级部分内容请参见文末
前言
SpringCloud-Netflix进行业务开发,那么线上注册中心肯定也是用了集群部署,问题来了:
背景
Sentry平台报警,多个业务服务在和注册中心交互时,例如续约和注册表增量拉取等都报了
Request execution failed with message : Connection refused的警告:

Request execution succeeded on retry #2的日志。

Eureka注册中心集群如何实现客户端请求负载及故障转移?
注册中心集群负载测试
4c8g的配置,业务端配置注册中心地址如下(
这里的peer来代替具体的ip地址):
eureka.client.serviceUrl.defaultZone=http://peer1:8080/eureka/,http://peer2:8080/eureka/,http://peer3:8080/eureka/
Demo进行测试:
注册中心集群负载测试
EurekaServer服务的端口号来模拟注册中心集群部署,分别以
8761和
8762两个端口进行启动
2、启动客户端
SeviceA,配置注册中心地址为:
http://localhost:8761/eureka,http://localhost:8762/eureka
SeviceA时在发送注册请求的地方打断点:
AbstractJerseyEurekaHttpClient.register(),如下图所示:

8761这个端口的服务。
ServiceA中注册中心的配置:
http://localhost:8762/eureka,http://localhost:8761/eureka5、重新启动
SeviceA然后查看端口,如下图所示:

此时看到请求注册中心,连接的是
8762这个端口的服务。
注册中心故障转移测试
EurekaServer服务,再启动一个客户端
ServiceA。启动成功后,关闭一个
8761端口对应的服务,查看此时客户端是否会自动迁移请求到
8762端口对应的服务:
8761和
8762两个端口号启动
EurekaServer2、启动
ServiceA,配置注册中心地址为:
http://localhost:8761/eureka,http://localhost:8762/eureka3、启动成功后,关闭
8761端口的
EurekaServer4、在
EurekaClient端
发送心跳请求的地方打上断点:
AbstractJerseyEurekaHttpClient.sendHeartBeat()5、查看断点处数据,第一次请求的
EurekaServer是
8761端口的服务,因为该服务已经关闭,所以返回的
response是
null
6、第二次会重新请求
8762端口的服务,返回的
response为状态为
200,故障转移成功,如下图:

思考
Demo,我以为
EurekaClient每次都会取
defaultZone配置的第一个
host作为请求
EurekaServer的请求的地址,如果该节点故障时,会自动切换配置中的下一个
EurekaServer进行重新请求。
EurekaClient每次请求真的是以配置的
defaultZone配置的第一个服务节点作为请求的吗?这似乎也太弱了!!?
EurekaServer集群不就成了
伪集群!!?除了客户端配置的第一个节点,其它注册中心的节点都只能作为备份和故障转移来使用!!?
客户端请求负载原理
原理图解

EurekaClient端的
IP作为随机的种子,然后随机打乱
serverList,例如我们在商品服务(192.168.10.56)中配置的注册中心集群地址为:
peer1,peer2,peer3,打乱后的地址可能变成
peer3,peer2,peer1。
peer1,peer2,peer3,打乱后的地址可能变成
peer2,peer1,peer3。
EurekaClient每次请求
serverList中的第一个服务,从而达到负载的目的。
代码实现
com.netflix.discovery.shared.resolver.ResolverUtils.randomize()中:

random是通过我们
EurekaClient端的
ipv4做为随机的种子,生成一个重新排序的
serverList,也就是对应代码中的
randomList,所以每个
EurekaClient获取到的
serverList顺序可能不同,在使用过程中,取列表的第一个元素作为
server端
host,从而达到负载的目的。

思考
EurekaClient的
IP进行负载的,所以刚才通过
DEMO程序结果就能解释的通了,因为我们做实验都是用的同一个
IP,所以每次都是会访问同一个
Server节点。
Server节点吗?
Peer1,第二次访问
Peer2,第三次访问
Peer3,第四次继续访问
Peer1等,循环往复……
EurekaClient节点,3个
EurekaServer节点。
Client节点的
IP区间为:
192.168.0.0 ~ 192.168.255.255,这里面共覆盖6w多个
ip段,测试代码如下:
/**
* 模拟注册中心集群负载,验证负载散列算法
*
* @author 一枝花算不算浪漫
* @date 2020/6/21 23:36
*/
public class EurekaClusterLoadBalanceTest {
public static void main(String[] args) {
testEurekaClusterBalance();
}
/**
* 模拟ip段测试注册中心负载集群
*/
private static void testEurekaClusterBalance() {
int ipLoopSize = 65000;
String ipFormat = "192.168.%s.%s";
TreeMap<String, Integer> ipMap = Maps.newTreeMap();
int netIndex = 0;
int lastIndex = 0;
for (int i = 0; i < ipLoopSize; i++) {
if (lastIndex == 256) {
netIndex += 1;
lastIndex = 0;
}
String ip = String.format(ipFormat, netIndex, lastIndex);
randomize(ip, ipMap);
System.out.println("IP: " + ip);
lastIndex += 1;
}
printIpResult(ipMap, ipLoopSize);
}
/**
* 模拟指定ip地址获取对应注册中心负载
*/
private static void randomize(String eurekaClientIp, TreeMap<String, Integer> ipMap) {
List<String> eurekaServerUrlList = Lists.newArrayList();
eurekaServerUrlList.add("http://peer1:8080/eureka/");
eurekaServerUrlList.add("http://peer2:8080/eureka/");
eurekaServerUrlList.add("http://peer3:8080/eureka/");
List<String> randomList = new ArrayList<>(eurekaServerUrlList);
Random random = new Random(eurekaClientIp.hashCode());
int last = randomList.size() - 1;
for (int i = 0; i < last; i++) {
int pos = random.nextInt(randomList.size() - i);
if (pos != i) {
Collections.swap(randomList, i, pos);
}
}
for (String eurekaHost : randomList) {
int ipCount = ipMap.get(eurekaHost) == null ? 0 : ipMap.get(eurekaHost);
ipMap.put(eurekaHost, ipCount + 1);
break;
}
}
private static void printIpResult(TreeMap<String, Integer> ipMap, int totalCount) {
for (Map.Entry<String, Integer> entry : ipMap.entrySet()) {
Integer count = entry.getValue();
BigDecimal rate = new BigDecimal(count).divide(new BigDecimal(totalCount), 2, BigDecimal.ROUND_HALF_UP);
System.out.println(entry.getKey() + ":" + count + ":" + rate.multiply(new BigDecimal(100)).setScale(0, BigDecimal.ROUND_HALF_UP) + "%");
}
}
}
负载测试结果如下:

IP负载并不是一个好的方案。
Ribbon默认的轮询算法
RoundRobinRule,【一起学源码-微服务】Ribbon 源码四:进一步探究Ribbon的IRule和IPing 。

故障转移原理
原理图解

serverList按照
client端的
ip进行重排序后,每次都会请求第一个元素作为和
Server端交互的
host,如果请求失败,会尝试请求
serverList列表中的第二个元素继续请求,这次请求成功后,会将此次请求的
host放到全局的一个变量中保存起来,下次
client端再次请求 就会直接使用这个
host。
代码实现
com.netflix.discovery.shared.transport.decorator.RetryableEurekaHttpClient.execute()中:

第101行,获取 client
上次成功server
端的host
,如果有值则直接使用这个host第105行, getHostCandidates()
是获取client
端配置的serverList
数据,且通过ip
进行重排序的列表第114行, candidateHosts.get(endpointIdx++)
,初始endpointIdx=0
,获取列表中第1个元素作为host
请求第120行,获取返回的 response
结果,如果返回的状态码是200
,则将此次请求的host
设置到全局的delegate
变量中第133行,执行到这里说明第120行执行的 response
返回的状态码不是200
,也就是执行失败,将全局变量delegate
中的数据清空再次循环第一步,此时 endpointIdx=1
,获取列表中的第二个元素作为host
请求依次执行,第100行的循环条件 numberOfRetries=3
,最多重试2次就会跳出循环
总结
Eureka集群下
Client端请求时负载均衡的选择以及集群故障时自动重试请求的实现原理。
END
《Java工程师面试突击第三季》加餐部分大纲:(注:1-66讲的大纲请扫描文末二维码,在课程详情页获取)



详细的课程内容和试听,大家可以扫描下方二维码了解:

文章转载自石杉的架构笔记,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。




