写点什么

Java 自定义 DNS 解析器负载均衡实现

作者:FunTester
  • 2022 年 2 月 10 日
  • 本文字数:4219 字

    阅读完需:约 14 分钟

在上文Java自定义DNS解析器实践中,我们没有讲到org.apache.http.conn.DnsResolver具体如何实现负载均衡,今天我们就分享一下,负载均衡的具体实现。

InMemoryDnsResolver 被淘汰

首先上期文章提到的org.apache.http.impl.conn.InMemoryDnsResolver类是无法实现负载均衡的,原因是这个实现类是将hostIP存在一个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.0INFO-> 14.460 main 请求uri:http://fun.tester:12345/ , 耗时:451 ms , HTTPcode: 200INFO-> 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.0INFO-> 04.843 F-4 /127.0.0.1INFO-> 04.843 F-7 /0.0.0.0INFO-> 04.844 F-2 /0.0.0.0INFO-> 04.844 F-10 /0.0.0.0INFO-> 04.844 F-1 /0.0.0.0INFO-> 04.844 F-5 /127.0.0.1INFO-> 04.844 F-3 /127.0.0.1INFO-> 04.844 F-8 /0.0.0.0INFO-> 04.844 F-9 /127.0.0.1
INFO-> 04.903 F-7 请求uri:http://fun.tester:12345/ , 耗时:309 ms , HTTPcode: 200INFO-> 04.903 F-3 请求uri:http://fun.tester:12345/ , 耗时:309 ms , HTTPcode: 200INFO-> 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.0INFO-> 03.961 F-6 请求uri:http://fun.tester:12345/ , 耗时:299 ms , HTTPcode: 200INFO-> 03.961 F-5 请求uri:http://fun.tester:12345/ , 耗时:299 ms , HTTPcode: 200INFO-> 03.961 F-4 请求uri:http://fun.tester:12345/ , 耗时:300 ms , HTTPcode: 200INFO-> 03.961 F-7 请求uri:http://fun.tester:12345/ , 耗时:300 ms , HTTPcode: 200INFO-> 03.961 F-3 请求uri:http://fun.tester:12345/ , 耗时:300 ms , HTTPcode: 200INFO-> 03.961 F-2 请求uri:http://fun.tester:12345/ , 耗时:300 ms , HTTPcode: 200INFO-> 03.961 F-1 请求uri:http://fun.tester:12345/ , 耗时:300 ms , HTTPcode: 200INFO-> 03.961 F-9 请求uri:http://fun.tester:12345/ , 耗时:300 ms , HTTPcode: 200INFO-> 03.961 F-8 请求uri:http://fun.tester:12345/ , 耗时:300 ms , HTTPcode: 200INFO-> 03.961 F-10 请求uri:http://fun.tester:12345/ , 耗时:300 ms , HTTPcode: 200WARN-> 04.673 Deamon 异步线程池关闭!
复制代码


这里看到虽然我起了 10 个线程分别执行请求,但是每个请求的耗时都是非常长的,但是只有F-5这个线程执行了一次org.apache.http.impl.conn.SystemDefaultDnsResolver#resolve方法,由于HttpClient只有一个连接。所以应当是每个连接创建的时候会调用org.apache.http.impl.conn.SystemDefaultDnsResolver#resolve方法,而每个线程请求耗时比较高,原因是因为每个线程去获取到链接资源之后,会重新进行建联的过程导致的。


实践出真知,奇怪的知识又增加了。

Have Fun ~ Tester !

发布于: 刚刚阅读数: 3
用户头像

FunTester

关注

公众号:FunTester,750篇原创,欢迎关注 2020.10.20 加入

公众号FunTester,坚持原创文章的测试人,一个有趣的灵魂。

评论

发布
暂无评论
Java自定义DNS解析器负载均衡实现