在上文Java自定义DNS解析器实践中,我们没有讲到org.apache.http.conn.DnsResolver
具体如何实现负载均衡,今天我们就分享一下,负载均衡的具体实现。
InMemoryDnsResolver被淘汰
首先上期文章提到的org.apache.http.impl.conn.InMemoryDnsResolver
类是无法实现负载均衡的,原因是这个实现类是将host
和IP
存在一个java.util.concurrent.ConcurrentHashMap
中,然后解析的时候从java.util.concurrent.ConcurrentHashMap
根据host
获取到IP
的,所以无法进行负载均衡。
使用的Demo如下:
代码语言:javascript复制 /**
* 重写Java自定义DNS解析器,非负载均衡
*
* @return
*/
private static DnsResolver getDnsResolver2() {
InMemoryDnsResolver dnsResolver = new InMemoryDnsResolver();
try {
logger.warn("调用一次");
dnsResolver.add("fun.tester", InetAddress.getByName("127.0.0.1"));
} catch (Exception e) {
e.printStackTrace();
}
return dnsResolver;
其中org.apache.http.impl.conn.InMemoryDnsResolver#add
方法源码如下:
public void add(String host, InetAddress... ips) {
Args.notNull(host, "Host name");
Args.notNull(ips, "Array of IP addresses");
this.dnsMap.put(host, ips);
}
然后我们看一下org.apache.http.impl.conn.InMemoryDnsResolver#dnsMap
相关初始化代码:
/**
* In-memory collection that will hold the associations between a host name
* and an array of InetAddress instances.
*/
private final Map<String, InetAddress[]> dnsMap;
/**
* Builds a DNS resolver that will resolve the host names against a
* collection held in-memory.
*/
public InMemoryDnsResolver() {
dnsMap = new ConcurrentHashMap<String, InetAddress[]>();
}
SystemDefaultDnsResolver
最终我放弃了自定义的org.apache.http.conn.DnsResolver
接口的方案,选择了org.apache.http.impl.conn.SystemDefaultDnsResolver
重写resolve
方法的方案,具体实现如下:
/**
* 重写Java自定义DNS解析器,负载均衡
*
* @return
*/
private static DnsResolver getDnsResolver() {
return new SystemDefaultDnsResolver() {
@Override
public InetAddress[] resolve(final String host) throws UnknownHostException {
if (host.equalsIgnoreCase("fun.tester")) {
return new InetAddress[]{SourceCode.random(ips)};
} else {
return super.resolve(host);
}
}
};
}
其中ips
是全局的静态变量,初始化方法如下:
/**
* 初始化DNS配置IP
*
* @return
*/
private static List<InetAddress> getAddress() {
try {
return Arrays.asList(
InetAddress.getByName("127.0.0.1"),
InetAddress.getByName("0.0.0.0")
);
} catch (Exception e) {
FailException.fail("DNS IP解析失败!");
}
return null;
}
PS:如果你选择使用了自定义的DNS解析器,那么系统hosts配置的功能就会失效,所以谨慎使用。
测试
为了验证结果,我对com.funtester.httpclient.ClientManage#getDnsResolver
方法进行了改造,每次获取到IP的时候我都打印出来。
/**
* 重写Java自定义DNS解析器,负载均衡
*
* @return
*/
private static DnsResolver getDnsResolver() {
return new SystemDefaultDnsResolver() {
@Override
public InetAddress[] resolve(final String host) throws UnknownHostException {
if (host.equalsIgnoreCase("fun.tester")) {
InetAddress random = SourceCode.random(ips);
logger.info(random);
return new InetAddress[]{random};
} else {
return super.resolve(host);
}
}
};
}
单线程
下面看我的测试,首先分享测试用例:
代码语言:javascript复制 public static void main(String[] args) {
String url = "http://fun.tester:12345/"
def get = getHttpGet(url)
def test = {
getHttpResponse(get)
}
10.times {
test()
}
}
控制台输出:
代码语言:javascript复制INFO-> 13.691 main
###### # # # # ####### ###### ##### ####### ###### #####
# # # ## # # # # # # # #
#### # # # # # # #### ##### # #### #####
# # # # # # # # # # # # #
# ##### # # # ###### ##### # ###### # #
INFO-> 14.408 main /0.0.0.0
INFO-> 14.460 main 请求uri:http://fun.tester:12345/ , 耗时:451 ms , HTTPcode: 200
INFO-> 14.462 main 请求uri:http://fun.tester:12345/ , 耗时:2 ms , HTTPcode: 200
****省略多余的内容****
可以看出,单线程请求HTTP服务,DNS只会解析一次,经过多次尝试,解析的IP会在设定的两个IP之间随机出现,但这明显不符合我们的需求。
多线程
测试用例如下:
代码语言:javascript复制 public static void main(String[] args) {
String url = "http://fun.tester:12345/"
def get = getHttpGet(url)
def test = {
fun {
getHttpResponse(get)
}
}
10.times {
test()
}
}
控制台输出:
代码语言:javascript复制INFO-> 03.636 main
###### # # # # ####### ###### ##### ####### ###### #####
# # # ## # # # # # # # #
#### # # # # # # #### ##### # #### #####
# # # # # # # # # # # # #
# ##### # # # ###### ##### # ###### # #
INFO-> 04.581 Deamon 守护线程开启!
INFO-> 04.843 F-6 /0.0.0.0
INFO-> 04.843 F-4 /127.0.0.1
INFO-> 04.843 F-7 /0.0.0.0
INFO-> 04.844 F-2 /0.0.0.0
INFO-> 04.844 F-10 /0.0.0.0
INFO-> 04.844 F-1 /0.0.0.0
INFO-> 04.844 F-5 /127.0.0.1
INFO-> 04.844 F-3 /127.0.0.1
INFO-> 04.844 F-8 /0.0.0.0
INFO-> 04.844 F-9 /127.0.0.1
INFO-> 04.903 F-7 请求uri:http://fun.tester:12345/ , 耗时:309 ms , HTTPcode: 200
INFO-> 04.903 F-3 请求uri:http://fun.tester:12345/ , 耗时:309 ms , HTTPcode: 200
INFO-> 04.903 F-2 请求uri:http://fun.tester:12345/ , 耗时:309 ms , HTTPcode: 200
****省略多余的内容****
这下我们就能看出每个线程都执行了一次org.apache.http.impl.conn.SystemDefaultDnsResolver#resolve
方法,获取到了IP也是随机的,而且每次请求的耗时都是比较长的。这里让我心生疑惑,相当于每个线程请求都是重新重建了连接,于是就有了下面的测试。
单个连接
这里我把HttpClient的连接池的最大连接数改成了1:public static int MAX_PER_ROUTE_CONNECTION = 1;
或者public static int MAX_TOTAL_CONNECTION = 1;
,这个之前分享过,这里不多讲了,上用例:
用例同多线程用例
控制台输出:
代码语言:javascript复制INFO-> 02.928 main
###### # # # # ####### ###### ##### ####### ###### #####
# # # ## # # # # # # # #
#### # # # # # # #### ##### # #### #####
# # # # # # # # # # # # #
# ##### # # # ###### ##### # ###### # #
INFO-> 03.648 Deamon 守护线程开启!
INFO-> 03.910 F-5 /0.0.0.0
INFO-> 03.961 F-6 请求uri:http://fun.tester:12345/ , 耗时:299 ms , HTTPcode: 200
INFO-> 03.961 F-5 请求uri:http://fun.tester:12345/ , 耗时:299 ms , HTTPcode: 200
INFO-> 03.961 F-4 请求uri:http://fun.tester:12345/ , 耗时:300 ms , HTTPcode: 200
INFO-> 03.961 F-7 请求uri:http://fun.tester:12345/ , 耗时:300 ms , HTTPcode: 200
INFO-> 03.961 F-3 请求uri:http://fun.tester:12345/ , 耗时:300 ms , HTTPcode: 200
INFO-> 03.961 F-2 请求uri:http://fun.tester:12345/ , 耗时:300 ms , HTTPcode: 200
INFO-> 03.961 F-1 请求uri:http://fun.tester:12345/ , 耗时:300 ms , HTTPcode: 200
INFO-> 03.961 F-9 请求uri:http://fun.tester:12345/ , 耗时:300 ms , HTTPcode: 200
INFO-> 03.961 F-8 请求uri:http://fun.tester:12345/ , 耗时:300 ms , HTTPcode: 200
INFO-> 03.961 F-10 请求uri:http://fun.tester:12345/ , 耗时:300 ms , HTTPcode: 200
WARN-> 04.673 Deamon 异步线程池关闭!
这里看到虽然我起了10个线程分别执行请求,但是每个请求的耗时都是非常长的,但是只有F-5
这个线程执行了一次org.apache.http.impl.conn.SystemDefaultDnsResolver#resolve
方法,由于HttpClient
只有一个连接。所以应当是每个连接创建的时候会调用org.apache.http.impl.conn.SystemDefaultDnsResolver#resolve
方法,而每个线程请求耗时比较高,原因是因为每个线程去获取到链接资源之后,会重新进行建联的过程导致的。
实践出真知,奇怪的知识又增加了。