`
城的灯
  • 浏览: 150256 次
  • 性别: 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。

分享到:
评论

相关推荐

    防火墙与网络安全(1).pptx

    这种姿态假定防火墙应该阻塞 所有的信息,而每一种所期望的服务或应用都是实现在case- by-case的基础上。 允许没有特别拒绝的任何事情。这种姿态假定防火墙应该转发 所有的信息,任何可能存在危害的服务都应在case-...

    网站保姆测试版 0.0.0.1

    tipType 提示方式 Text 气泡提示,Program 外部程序提示,Http 远程WEB调用,sound 播放声音 tipMessage 提示内容/运行程序调用参数 页面 以asp.net(c#) 为例 protected void Page_Load(object sender, EventArgs e...

    100例单片机程序设计范例汇总

    【实例4】使用数据指针DPTR访问外部数据数据存储器: 4 【实例5】使用程序计数器PC查表: 4 【实例6】IF语句实例: 4 【实例7】SWITCH-CASE语句实例: 4 【实例8】FOR语句实例: 4 【实例9】WHILE语句实例: 5 ...

    基于Linux 的防火墙技术研究

    就是内部网和外部网的主机之间不能建立直接的连接,而是要通过代理服务进行转发。代理服务器 具有用户级的身份验证,完备的日志记录和帐号管理等较包过滤器更加安全的功能。然而,许多代理机 制需要客户端修改软件或...

    BBB_Slim_Case:Bealgebone Black 的 3D 可打印超薄线盒

    外壳设计为重量轻,外形尽可能小,同时仍可访问所有端口和排针。 本案例的模型是在 OpenSCAD 中设计的,可以使用该程序自由修改并导出为 STL 格式。 Beaglebone Black 的产品页面可在上找到 OpenSCAD 的信息可在该...

    oracle数据库dba管理手册

    2.3.10 外部文件访问 45 第3章 数据库逻辑设计 47 3.1 最终产品 47 3.2 优化软结构 47 3.2.1 起点:SYSTEM表空间 47 3.2.2 分离应用程序数据段:DATA 48 3.2.3 分离应用程序索引段:INDEXES 48 3.2.4 分离工具段:...

    系统设计方案模板.doc

    应用系统结构确认 支撑系统结构确认 系统集成确认 系统工作流程确认 系统详细界面划分 应用系统与支撑系统的详细界面划分 应用系统与支撑系统之间的界面包括系统主服务器与其他服务器的服务范围及访问方式 ...

    JAVA面试题最全集

    列出Jsp中包含外部文件的方式,两者有何区别。 6.说明Jsp中errorPage的作用,应用范围。 7.介绍在Jsp中如何使用JavaBeans。 8.简单介绍JSP的标记库 9.Jsp和Servlet中的请求转发分别如何实现。 三、J2EE相关知识...

    specimen-data-use-case:iDigBio API工作组资料库,用于获取R中的生物多样性标本数据

    该存储库的主要目标是概念性地记录用例,并在可能的情况下将用例链接到在该存储库内或外部维护的技术解决方案。 iDigBio API用户组(基于R) 该存储库支持的产品,该产品专注于使用iDigBio API(应用程序编程接口...

    STM32F429单片机读写(8通道16位同步ADC)AD7606 spi模式软件工程源码.zip

    启动文件配置了CPU主时钟频率、内部Flash访问速度和可选的外部SRAM FSMC初始化。 */ g_tAD7606.Range = 1; /* 10V */ bsp_spi_InitAD7606(); /* 配置AD7606所用的GPIO */ bsp_StartAutoTimer(0, 500); /* ...

    亮剑.NET深入体验与实战精要2

    7.7.2 手动发送HTTP请求调用Web Service 295 7.8 实现异步调用Web Service 297 7.9 如何保证Web Service的安全 299 7.9.1 通过SoapHeader来增强 Web Service的安全性 299 7.9.2 采用SSL实现加密传输 302 7.9.3 访问...

    亮剑.NET深入体验与实战精要3

    7.7.2 手动发送HTTP请求调用Web Service 295 7.8 实现异步调用Web Service 297 7.9 如何保证Web Service的安全 299 7.9.1 通过SoapHeader来增强 Web Service的安全性 299 7.9.2 采用SSL实现加密传输 302 7.9.3 访问...

    精通matlab7.0混合编程.

    2.4.4 switch-case结构 2.4.5 try-catch结构 2.5 m文件编程 2.6 m文件编程规范 2.7 m文件评述器 2.8 提高m文件执行效率的技巧 2.8.1 矢量化操作 2.8.2 给数组预定义维 2.8.3 下标或者索引操作 2.8.4 尽量多使用函数...

    SQL Server 2008编程入门经典(第3版)

    9.2.2 SQLServer中访问数据的方式 9.3 创建、修改和删除索引 9.3.1 CREATEINDEX语句 9.3.2 创建XML索引 9.3.3 随约束创建的隐含索引 9.3.4 在稀疏列和地理空间列上创建索引 9.4 明智地选择——在何时何地使用何种...

    软件设计方案(1).doc

    应用系统结构确认 支撑系统结构确认 系统集成确认 系统工作流程确认 2 系统详细界面划分 1 应用系统与支撑系统的详细界面划分 应用系统与支撑系统之间的界面包括系统主服务器与其他服务器的服务范围及访问 ...

    超级有影响力霸气的Java面试题大全文档

    与cgi的区别在于servlet处于服务器进程中,它通过多线程方式运行其service方法,一个实例可以服务于多个请求,并且其实例一般不会销毁,而CGI对每个请求都产生新的进程,服务完成后就销毁,所以效率上低于servlet。...

    java 面试题 总结

    与cgi的区别在于servlet处于服务器进程中,它通过多线程方式运行其service方法,一个实例可以服务于多个请求,并且其实例一般不会销毁,而CGI对每个请求都产生新的进程,服务完成后就销毁,所以效率上低于servlet。...

    Algo-ScriptML:Scratch的机器学习算法脚本。 机器学习模型和算法的实现只使用NumPy,重点是可访问性。 旨在涵盖从基础到高级的所有内容

    确保您不从外部来源复制代码,因为不会考虑该工作。 严禁抄袭。 您只能处理分配给您的问题。 如果您想为现有算法做出贡献,我们建议您在进行PR之前先创建一个问题,然后将PR链接到该问题。 如果您修改/添加了...

    oracle数据库11G初学者指南.Oracle.Database.11g,.A.Beginner's.Guide

    3.5.5 网络服务别名条目 3.5.6 本地命名方法 3.5.7 简单命名方法 3.5.8 外部命名方法 3.5.9 使用哪种命名方法 3.6 使用Oracle配置文件 3.7 使用管理工具 3.7.1 OEM/网格控件 3.7.2 Oracle网络管理器 3.7.3 OEM...

Global site tag (gtag.js) - Google Analytics