在上文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如下:
/**
* 重寫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);
}
}
};
}
單線程
下面看我的測試,首先分享測試用例:
public static void main(String[] args) {
String url = "http://fun.tester:12345/"
def get = getHttpGet(url)
def test = {
getHttpResponse(get)
}
10.times {
test()
}
}
控制臺輸出:
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之間隨機出現,但這明顯不符合我們的需求。
多線程
測試用例如下:
public static void main(String[] args) {
String url = "http://fun.tester:12345/"
def get = getHttpGet(url)
def test = {
fun {
getHttpResponse(get)
}
}
10.times {
test()
}
}
控制臺輸出:
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;,這個之前分享過,這里不多講了,上用例:
用例同多線程用例
控制臺輸出:
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方法,而每個線程請求耗時比較高,原因是因為每個線程去獲取到鏈接資源之后,會重新進行建聯的過程導致的。
實踐出真知,奇怪的知識又增加了。