`
城的灯
  • 浏览: 152366 次
  • 性别: Icon_minigender_1
  • 来自: 上海
社区版块
存档分类
最新评论

外部HTTP服务访问慢的Case

    博客分类:
  • 2015
 
阅读更多

原文发布在:www.yangguo.info

问题现象

最近由于业务需求,重写了一套访问中航信IBE+的代理层。由于机票业务分为国内和国际两部分,这两部分都需要访问IBE+,之前是各自为战,各自都有一套访问逻辑。这样做的后果就是在流控(IBE+对QPS有一定的限制)和接口管理方面得不到很好的控制,因此才写了这样的一个代理层。这样的设计其实没有任何问题,在微服务和SOA大行其道的今天这都是主流设计思路。该系统很快便上线了,但是很快我就发现IBE+的服务访问都非常慢,因为之前我没有涉及到这块,我就问了下之前负责这块的同事,他们说的确比较慢,我也就没有太上心。突然有天我打开生产环境的监控,我便发现了问题,大概30秒左右便有一次非常慢的操作。关于30秒的问题,之前我通过tcpdump抓包分析过,以为是Http Basic鉴权导致的,在新写的代理层中已经对此处进行了优化,但是结果看来不是很理想,然后便有了这个Case。


问题定位

猜测+初步验证

30秒左右就会慢一次,思来想去就只有DNS的问题,因为Java的DNS默认缓存大概就是30秒。然后我迅速写了如下的一段测试代码:

import java.net.Inet4Address;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.Date;

/**
 * Created by IntelliJ IDEA
 * User:杨果
 * Date:15/6/18
 * Time:下午2:55
 * <p/>
 * Description:DNS解析速度测试
 */

public class Test {
    public static void main(String args[]) throws UnknownHostException, InterruptedException {
        for (int i = 0; i < 10; i++) {
            String host = "agibe.travelsky.com";

            long begin1 = new Date().getTime();
            Inet4Address.getByName(host);
            long end1 = new Date().getTime();
            System.out.println(end1 - begin1);


            long begin2 = new Date().getTime();
            InetAddress.getAllByName(host);
            long end2 = new Date().getTime();
            System.out.println(end2 - begin2);

            System.out.println("----");
            Thread.sleep(1000*5);
        }
    }
}

放到生产环境的服务器一测,果然每个30秒便巨慢一次,大概每次解析在5秒钟左右。网上还有一套通过反射来获取Java DNS缓存时间的测试代码,代码非常简单如下所示:

import java.lang.reflect.Field;
import java.net.InetAddress;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Map;

/**
 * Created by IntelliJ IDEA
 * User:杨果
 * Date:15/5/7
 * Time:下午5:10
 * <p/>
 * Description:
 * <p/>
 * dns缓存时间测试
 */
public class DnsTester {
    public static void main(String[] args) throws Exception {
        System.out.println("start loop\n\n");
        for (int i = 0; i < 30; ++i) {
            Date d = new Date();
            SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
            System.out.println("current time:" + sdf.format(d));
            InetAddress addr1 = InetAddress.getByName("www.baidu.com");
            String addressCache = "addressCache";
            System.out.println(addressCache);
            printDNSCache(addressCache);
            System.out.println("getHostAddress:" + addr1.getHostAddress());
            System.out.println("*******************************************");
            System.out.println("\n");
            java.lang.Thread.sleep(10000);
        }

        System.out.println("end loop");
    }


    private static void printDNSCache(String cacheName) throws Exception {
        Class<InetAddress> klass = InetAddress.class;
        Field acf = klass.getDeclaredField(cacheName);
        acf.setAccessible(true);
        Object addressCache = acf.get(null);
        Class cacheKlass = addressCache.getClass();
        Field cf = cacheKlass.getDeclaredField("cache");
        cf.setAccessible(true);
        Map<String, Object> cache = (Map<String, Object>) cf.get(addressCache);
        for (Map.Entry<String, Object> hi : cache.entrySet()) {
            Object cacheEntry = hi.getValue();
            Class cacheEntryKlass = cacheEntry.getClass();
            Field expf = cacheEntryKlass.getDeclaredField("expiration");
            expf.setAccessible(true);
            long expires = (Long) expf.get(cacheEntry);

            System.out.println(hi.getKey() + " " + new Date(expires) + " " + hi.getValue());
        }
    }
}

深入代码分析+测试

经过初步的分析和测试,我已经找到问题的所在,现在就要深入代码进行分析看看是否跟我的猜想吻合。由于我的代码使用的是HTTP Client4.3.2,没有配置DnsResolver,那么肯定使用的是SystemDefaultDnsResolver。这点确认之后,便可以证明第一步的验证逻辑没有偏离方向。然后我便写了如下的一段Btrace脚本,看看线上的服务究竟慢在哪里,脚本如下:

@BTrace
public class HttpClientSystemDefaultDnsResolverTracer {
    @TLS
    static long beginTime;

    @OnMethod(
            clazz = "org.apache.http.impl.conn.SystemDefaultDnsResolver",
            method = "resolve"
    )
    public static void traceGetByNameBegin() {
        beginTime = timeMillis();
    }

    @OnMethod(
            clazz = "org.apache.http.impl.conn.SystemDefaultDnsResolver",
            method = "resolve",
            location = @Location(Kind.RETURN)
    )
    public static void traceGetByNameEnd() {
        println(strcat(strcat("org.apache.http.impl.conn.SystemDefaultDnsResolver.resolve 耗时:", str(timeMillis() - beginTime)), "ms"));
    }
}

虽然BTrace比较安全,但是我将一台机器的流量切走了大部分,只留下很少的部分来做测试,测试下来果然跟我第一步吻合。

寻找修复方案

做了上面充足的测试之后,我拿着手上的数据找到复杂网络的同学,开始跟他定位问题。当我看见/etc/resolv.conf下面的nameserver 114.114.114.114的时候,我心中一震。我马上让他切换到我们的DNS之后,再一运行第一步的测试代码,速度果然马上提升了好几个数量级。我心中一喜,马上让他将机器的DNS切成我们自己的DNS。然后开始开始观察,经过两个小时之后,我发现速度还是没有提升起来。我就开始纳闷了,这是什么问题呢,我们问题不是已经定位到了嘛?我开始怀疑是不是进程没有重启的原因,我马上找了一台没有修改成我们自己DNS的机器将第一个程序跑起来。然后我开始修改DNS,果然修改之后程序的解析速度没有提升,重启进程问题解决。

看着QPS的缓慢爬升以及Average Response Time的缓慢下降,心中暗爽。你可能以为这个Case已经结束,的确当时我也是这样认为的。

DNS解析真的慢?

大概过了两天,我突然觉得意识到,在这个Case中我遗漏掉了什么。114真的有这慢吗?虽然作为一个经常被吐槽最容易被污染DNS,114的速度我记得还是相当靠谱的啊。Google了一圈,在Stack OverFlow上找到一些讨论,说是IPV6的原因导致的。

我马上进行了尝试,首先我不指定ipv4,例子如下:

[root@centos87 ~]# wget agibe.travelsky.com
--2015-07-01 17:56:13--  http://agibe.travelsky.com/
Resolving agibe.travelsky.com... 122.119.122.38
Connecting to agibe.travelsky.com|122.119.122.38|:80... connected.
HTTP request sent, awaiting response... 200 OK
Length: 239 [text/html]
Saving to: “index.html.3”

100%[=============================================================================================>] 239         --.-K/s   in 0s

2015-07-01 17:56:18 (21.4 MB/s) - “index.html.3” saved [239/239]

然后我再指定ipv4试了一把,如下:

root@centos87 ~]# wget -4 agibe.travelsky.com
--2015-07-01 17:57:20--  http://agibe.travelsky.com/
Resolving agibe.travelsky.com... 122.119.122.38
Connecting to agibe.travelsky.com|122.119.122.38|:80... connected.
HTTP request sent, awaiting response... 200 OK
Length: 239 [text/html]
Saving to: “index.html.4”

100%[=============================================================================================>] 239         --.-K/s   in 0s

2015-07-01 17:57:20 (21.3 MB/s) - “index.html.4” saved [239/239]

结果相当清晰,罪魁祸首就是ipv6。那为啥我换成我们自己的DNS就好了,我又跟负责网络的同学进行了沟通,原来他在DNS服务器上将IPV6的转发进行了关闭,所以可以很快返回。为了证明我的猜想是正确的,我又找了一台DNS为修改的服务器,让运维的同学将ipv6给禁用,经过测试果然奏效。至于如何关闭ipv6,网上有太多教程了。我在测试时候用了下面的方式,个人觉得太暴力,不推荐使用,不过应急可以试试。

echo 1 > /proc/sys/net/ipv6/conf/all/disable_ipv6

Java DNS Lookup

上面通过DNS和系统层面都可以解决该问题,现在便到我们代码层面了。我们知道并非所有操作系统都支持 ipv6 协议,Java networking stack优先尝试检测ipv6 。当然你也可以利用系统属性禁用它。

解析步骤如下:

  1. Java networking stack 先确认底层操作系统是否支持ipv6。如果支持ipv6,Java将尝试使用ipv6 stack。
  2. 在双堆栈(dual-stack,指ipv4 stack+ipv6 stack)系统上,将创建一个ipv6 socket。在 separate-stack 系统上,事情要复杂得多,Java 将创建两个socket,一个给ipv4 一个给ipv6。
  3. 对于客户端TCP应用,一旦socket连上了,那 internet-protocol family type 就固定了,多余的那个socket就关闭了。对于服务器端TCP应用,由于不知道下一个客户端请求用什么ip family type,所以这两个 sockets 将继续保留。对于UDP应用,这两个sockets始终都需要保留。

java ipv6 相关的系统参数

  1. 首选的协议栈:ipv4还是ipv6;
  2. 首选的地址族(address family type):inet4 还是 inet6。

由于在一个双堆栈系统上,ipv6 socket 能与 ipv4 和 ipv6 对端交互,所以 ipv6 stack 是默认首选项。你可以通过如下系统参数修改配置:

java.net.preferIPv4Stack=<true|false>

对应的 java 代码是:

java.lang.System.setProperty("java.net.preferIPv4Stack", "true");

默认我们首选 ipv4 地址族,也可以通过如下系统参数修改配置:

java.net.preferIPv6Addresses=<true|false>

对应的 java 代码是:

java.lang.System.setProperty("java.net.preferIPv6Addresses", "false");

然后我又用下面的代码进行了尝试,代码如下:

import java.net.Inet4Address;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.Date;

/**
 * Created by IntelliJ IDEA
 * User:杨果
 * Date:15/6/18
 * Time:下午2:55
 * <p/>
 * Description:IPV4 DNS解析速度测试
 */
public class Test {
    public static void main(String args[]) throws UnknownHostException, InterruptedException {
        java.lang.System.setProperty("java.net.preferIPv4Stack", "true");
        java.lang.System.setProperty("java.net.preferIPv6Addresses", "false");
        for (int i = 0; i < 10; i++) {
            String host = "agibe.travelsky.com";

            long begin1 = new Date().getTime();
            Inet4Address.getByName(host);
            long end1 = new Date().getTime();
            System.out.println(end1 - begin1);


            long begin2 = new Date().getTime();
            InetAddress.getAllByName(host);
            long end2 = new Date().getTime();
            System.out.println(end2 - begin2);

            System.out.println("----");
            Thread.sleep(1000*5);
        }
    }
}

结论

看似简单的结论背后,如果去深挖,总会发现很多有趣的东西,enjoy yourself。

分享到:
评论

相关推荐

    Oracle数据库外部表.doc

    **外部表(External Tables)**是Oracle数据库中一种特殊的数据存储方式,它允许用户通过普通的SQL查询操作来访问存储在数据库之外的数据文件中的数据。这种设计的主要目的是为了提高数据加载速度,并支持多种格式的...

    C# 访问URL接口进行调用代码实例

    ### C# 访问URL接口进行调用代码实例解析 #### 概述 在现代软件开发中,HTTP请求是客户端与服务器端交互的基础之一。本文将深入探讨如何使用C#语言来实现对URL接口的访问,并通过具体示例代码来展示这一过程。此...

    Swift语言中的一些访问控制设置详解

    在这个例子中,`Cricket`是`public`的,而`Tennis`是`internal`的,因此`Tennis`可以访问`Cricket`的私有成员,但`Tennis`本身不能在模块外部访问。 总结起来,Swift的访问控制机制为开发者提供了精细的控制,以...

    必看!100道Java程序员面试题(含答案)!.pdf,这是一份不错的文件

    - 内部类是在外部类内部定义的类,它可以访问外部类的所有成员,包括私有成员,而子类只能访问父类的公有和受保护的成员。 2. **Java访问修饰符**: - `public`:全局可访问,任何地方都可以。 - `protected`:...

    每天一个Case(010).pdf

    2. 路由和NAT(网络地址转换):路由是决定数据包如何在网络中传输的技术,NAT则是让内部网络使用私有IP地址通过一个公网IP地址访问外部网络的技术。 3. BGP(边界网关协议)和路由聚合:BGP是一种在不同网络间交换...

    jQuery多功能弹出层插件Lightcase源码.zip

    Lightcase是一款基于jQuery的弹出层插件,它提供了丰富的功能,可以用于创建高质量的全屏画廊、视频播放、iframe加载以及任何其他类型的富媒体内容的展示。这个压缩包包含的是Lightcase插件的源代码,对于开发者来说...

    CASE知识管理系统.pptx

    5. **实用的人员授权功能**:确保信息安全,不同角色的用户拥有不同的访问权限。 6. **模板自定义**:提供文档模板,方便快速创建和编辑文件。 7. **人事管理**:涵盖了从招聘到离职的完整员工生命周期管理,包括...

    Nginx实现最简单的负载均衡web访问操作说明

    其中,Nginx Server将作为负载均衡器,通过轮询策略将外部请求分发到两台Web Server上。 #### 四、操作步骤 ##### 1. 下载与安装 Nginx 首先,我们需要下载并安装Nginx。这里采用源码编译安装的方式: ```bash #...

    Summary Use Case for Multi-threading Progress Sessions

    当系统接收到外部事件时,可以启动一个新的线程来处理该事件,而不会阻塞主线程或其他正在运行的线程。这种非阻塞的事件处理方式提高了系统的响应速度和稳定性。 #### RMI响应(RMI Response) 远程方法调用(RMI...

    bugfree帮助文档

    - 访问http://&lt;servername&gt;/bugfree/install,完成环境检查和安装。 - 创建附件上传目录BugFile,并设置权限。 2. **从旧版本升级到BugFree 3.0**: - 升级前备份数据库和虚拟目录,确保数据安全。 - 下载新...

    梁向东+饿了么API框架的实践

    1. API Everything概念:API Everything是指将SOA(面向服务的架构)中的服务接口适配给外部访问,使得不同的客户端如Web和APP能够通过HTTP协议进行访问。这种模式下,服务接口不仅限于内部通信,还可供外部系统或...

    枚举类型的使用

    case WeekDays.Monday: Console.WriteLine("今天是星期一"); break; //... default: Console.WriteLine("未知的日期"); break; } ``` 总之,枚举在C#中是一种强大的工具,可以帮助我们编写更加清晰、可维护...

    软件工程填空题.pdf

    18. CASE工具的平台集成:CASE工具运行在同一操作系统上,确保兼容性和协同工作。 19. 数据存储对象:在数据流图中表示数据的存储和处理的地方。 20. 差别估算:基于类似项目的经验,估算新项目成本的方法。 以上...

    信息安全_数据安全_Use Case Development as a Driver.pdf

    业务安全关注于保护企业的业务流程不受外部攻击;渗透测试是一种模拟攻击的手段,用来评估系统的安全性;web安全是指保护网站免受攻击的技术和策略;解决方案则是指针对特定安全问题提出的解决方法。 标签中的...

    信息安全_数据安全_The Case for Building Your Own S.pdf

    《The Case for Building Your Own S》一文探讨了构建自定义安全解决方案的必要性,尤其在应对不断演变的威胁环境中,自定义方案能更好地适应组织的独特需求。本文将深入解析数据安全、安全架构、应急响应和安全分析...

    一句话判断数据库是否web和数据库分离

    - **安全性**:通过将数据库服务器单独部署,可以限制外部直接访问数据库,从而降低数据泄露的风险。 - **稳定性**:将数据库与Web应用分开部署有助于减少相互之间的影响,即使Web服务出现故障也不会直接影响到...

    MySQL安装使用说明基于Windows平台

    由于MySQL服务通过TCP协议在端口3306上监听连接请求,因此需要配置防火墙规则以允许外部访问该端口。如果不想进行复杂的防火墙配置,可以直接禁用Windows防火墙。如果希望启用防火墙,就需要手动添加一个入站规则,...

    busybox配置telnetd .

    4. **防火墙设置**:由于telnet服务使用的是TCP端口23,所以需要打开防火墙规则以允许外部访问。在iptables环境下,可以添加如下规则: ```bash iptables -A INPUT -p tcp --dport 23 -j ACCEPT ``` 并且,...

    FTP的内部命令详解

    - 示例:在需要通过代理服务器访问FTP服务器时使用。 45. **put local-file [remote-file]** - 功能:从本地上传文件到远程服务器。 - 示例:`put localfile.txt remotefile.txt`,将本地的`localfile.txt`上传...

Global site tag (gtag.js) - Google Analytics