InetAddress.getLocalHost() 执行很慢?
背景介绍
某次在 SpringBoot 2.2.0 项目的一个配置类中引入了这么一行代码:
InetAddress.getLocalHost().getHostAddress()
导致项目启动明显变慢。同时报出了相关的警告信息:
2022-10-03 23:32:01.806 [TID: N/A] WARN [main] o.s.b.StartupInfoLogger - InetAddress.getLocalHost().getHostName() took 5007 milliseconds to respond. Please verify your network configuration (macOS machines may need to add entries to /etc/hosts).

根据报警信息可知,只要获取主机信息的耗时超过了阈值HOST_NAME_RESOLVE_THRESHOLD=200ms,就会提示这个信息。很明显,我们的耗时已经超过5s。同时,如果为 Mac 系统,还会贴心地提示在/etc/hosts文件中配置本地dns。
我们看看目前hosts文件中的配置:
127.0.0.1 localhost
255.255.255.255 broadcasthost
::1 localhost
根据网上各种文章的提示,我们将主机名追加进去,变成这样:
127.0.0.1 localhost xiaoxi666s-MacBook-Pro.local
255.255.255.255 broadcasthost
::1 localhost
其中,xiaoxi666s-MacBook-Pro.local 就是我的主机名。
注:更改hosts文件内容后,可使用命令
sudo killall -HUP mDNSResponder刷新dns,无需重启电脑。
再次启动 SpringBoot 程序,我们发现警告信息消失了,也就意味着主机信息获取的耗时不会超过200ms。
那么问题来了,这背后究竟是什么机制,让我们一探究竟。
使用Wireshark抓包看看
由于我们要获取自己的主机信息,这里走的是本地回环网络,因此选中Loopback网络接口:

先把hosts改回去,抓一下hosts文件改动前的网络包:

按照时间顺序,可以将抓到的网络包分为三段,每段中又可以分为Ipv4和Ipv6两种地址的请求。
其中用到的协议是 mdns,也即多播dns(Multicast DNS),它主要实现了在没有传统 dns 服务器的情况下使局域网内的主机实现相互发现和通信,使用的端口为 5353,遵从 dns 协议。随便点开一个请求查看详情便可以得到验证:

另外,网络包中的目标ip 224.0.0.251是 Mac 的官方 mdns 查询地址,详情可参见https://github.com/apple-oss-distributions/mDNSResponder/tree/mDNSResponder-1096.100.3
实际多次测试发现,主机信息都在第三次发送网络包后返回(阻塞在 InetAddress.getLocalHost() 方法上。参见下图,阻塞在第18行,5秒后才跳到第19行)。从上图的时间线看,约在8秒时返回,整体耗时与上面报出的 5007ms 吻合。再仔细观察网络包,看起来是连续发了三次请求。第一次在 3.1s 时发出,第二次在 4.1s 时发出,第三次在 7.1s 时发出,重试间隔分别为 1s 和 3s,看起来像是一种指数退避的重试。当然,8秒左右时返回结果,就对应第一次请求,剩下两次请求的结果被忽略了。

我们再看看hosts中添加主机信息后,对应的网络包:

啊噢,这次没有抓到任何相关的网络包,猜测直接读取了hosts文件拿到了主机名,根本没走网络。
那么,这段获取主机信息的程序究竟是怎么运作的呢,hosts文件中没有添加主机名时,时间都耗在了哪里?
看看对应的源码
源码比较好找,参见下图:

我们再次把hosts中的主机名去掉,并使用 Arthas 工具的 trace 命令看看链路耗时:

提示:如果抓包时出现 No class or method is affected 的报错,可查看对应的日志文件进行排查,见下图:


可知需要提升下权限,执行命令 options unsafe true 后,再尝试使用 trace命令即可。
但好巧不巧,居然抓不到调用链?那我们试试用 Arthas 的 profiler 命令生成一下火焰图吧:

可以看到很多编译相关的,我们忽略之,只把主机信息获取的那部分放大:

哦吼,时间基本都耗在了 InetAddress.getAddressesFromNameService 这行代码:

往下追溯,可知时间基本耗在了 nameService.lookupAllHostAddr:


再往下就到了native方法:

于是我们到 jdk 源码中看看(我用的 jdk8):

接下来需要找 getaddrinfo 的实现,由于不知道具体的实现源码在哪里,于是我们在网上找一下 Linux 系统的源码作为参考,参见:https://codebrowser.dev/glibc/glibc/sysdeps/posix/getaddrinfo.c.html#getaddrinfo

内部的具体实现基本都是和操作系统交互,我们简单瞄几眼就行。另外,在 getaddrinfo 源码中没有找到火焰图给出的调用链,我们暂时不再深入。

目前,我们知道了方法 getaddrinfo 会被调用,因此简单写段 c 程序复现一下:
#include<sys/time.h>
#include <iostream>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netdb.h>
#include <netinet/in.h>
#include <arpa/inet.h> using namespace std; int main(){
char* hostname = "xiaoxi666s-MacBook-Pro.local"; addrinfo hints, *res;
in_addr addr;
int err; struct timeval start, end;
gettimeofday(&start, NULL); memset(&hints, 0, sizeof(addrinfo));
hints.ai_socktype = SOCK_STREAM;
hints.ai_family = AF_INET;
if((err = getaddrinfo(hostname, NULL, &hints, &res)) != 0){
// 打印耗时(异常情况)
gettimeofday(&end, NULL);
printf("times=%d\n", end.tv_usec - start.tv_usec); printf("error %d : %s\n", err, gai_strerror(err));
return 1;
}
// 打印耗时(正常情况)
gettimeofday(&end, NULL);
printf("times=%d\n", end.tv_usec - start.tv_usec); addr.s_addr = ((sockaddr_in*)(res->ai_addr))->sin_addr.s_addr;
printf("ip addresss: %s\n", inet_ntoa(addr)); freeaddrinfo(res);
return 0;
}
其中的 hostname 即为主机名 xiaoxi666s-MacBook-Pro.local,我们在 Java 项目中调试时也可以看到,上面的程序中直接将其写死。
运行程序,对比下 hosts 文件中 没有添加主机名 和 添加主机名后的输出结果:
# hosts 文件中没有添加主机名
times=6431
error 8 : nodename nor servname provided, or not known
# hosts 文件中添加主机名
times=1789
ip addresss: 127.0.0.1
可以看到,当 hosts 文件中没有添加主机名时,根本找不到对应的网络地址(因为 dns 中也没有解析到),添加之后就能返回对应的 ip 127.0.0.1 了。
这里有几个地方需要注意:
即使 hosts 文件中添加主机名,标准 Linux 的 getaddrinfo 方法执行时,也会有接近两秒的耗时,但我们在 Java 代码中运行时却只有几十毫秒;
前文我们使用 Wireshark 抓包时提到,mdns 查询时存在重试机制,但标准 Linux 的 getaddrinfo 方法中没有看到对应的代码;
前面提到的5秒返回结果,其实不是返回结果,而是超时了。但标准 Linux 的 getaddrinfo 方法中没有看到对应的超时控制代码;
因此,我们可以大胆猜测 MaxOS 系统对标准 Linux 代码进行了修改,加了本地缓存、重试、超时等机制。
接着上面的第3点,回到 Java 项目调试一下,看看为什么超时了还能返回结果。
当 hosts 文件中没有添加主机名时,会返回本机所有的 ip 地址:

当 hosts 文件中添加主机名后,只会返回配置的 127.0.01 的 ip 地址:

其中,当 hosts 文件中没有添加主机名时,getaddrinfo 调用返回错误码,此时 jdk 会转而调用 lookupIfLocalhost 方法,它内部调用了操作系统的 getifaddrs 方法,以获取本机所有 ip 地址:


对应的源码可以参考https://codebrowser.dev/glibc/glibc/sysdeps/unix/sysv/linux/ifaddrs.c.html。
总结
本文以 Java 中获取主机名慢的场景为契机,使用多种技术手段研究背后的原理,包括使用 Wireshark 抓包,使用 Arthas 工具定位到性能瓶颈,再转到 jdk 中查看对应的 native 方法实现,由于没找到最底层调用链路源码,转而参照标准Linux的相关源码,简单复现了上述场景。
进一步地,由于没找到最底层调用链路源码,我们根据现象猜测的本地缓存、重试、超时等机制没有得到验证,有兴趣的同学可以进一步研究探索。
参考文章
InetAddress.getLocalHost() 执行很慢?的更多相关文章
- mac系统InetAddress.getLocalHost().getHostAddress() 很慢
java.net.InetAddress.getLocalHost() 这个调用在新版的mac os中可能会耗时很久. 解决办法有两种: 1. 就是把本机的hostname 添加到 hosts 中,例 ...
- at java.net.InetAddress.getLocalHost(InetAddress.java:1475)
今天在centos 安装hadoop安装完成后执行wordcount的时候报如下错误: at java.net.InetAddress.getLocalHost(InetAddress.java:14 ...
- 坑爹的InetAddress getLocalHost函数
今天在跑dubbo 的 DemoService 2.5.4-SNAPSHOT版本的时候,遇到到一个奇怪的问题.consumer怎么都连接不上provider的服务.最后才发现是由于dubbo自 己实现 ...
- 为什么Sql Server的查询有时候第一次执行很慢,第二次,第三次执行就变快了
老外提问: Hi, I have an sql query which takes 8 seconds in the first run. The next run there after takes ...
- 存储过程被程序和第三方客户端执行很慢,而sql server management studio执行速度正常
来自:http://blog.csdn.net/pgbiao/article/details/22388945 原因分析:由于存储过程是预编译的, 在第一次执行的时候, 会生成执行计划, 以后执行的时 ...
- Linux并发执行很简单,这么干就对了
嗯,就像标题说的那么简单而已 &的并发功能 time for i in `grep server /etc/hosts | awk '{print $1}'`; do (ssh $i &quo ...
- nvidia-smi命令执行很慢,如何改进
初次安装好nvidia的驱动,每次执行nvidia-smi命令时,要5秒以上. 可通过如下命令进行改进: nvidia-persistenced --persistence-mode
- Dubbo_创建Dubbo服务并在ZooKeeper注册,然后通过Jar包执行
一.安装ZooKeeper(略) 二.创建Dubbo服务 1.DemoService 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 ...
- hadoop2 作业执行过程之作业提交
hadoop2.2.0.centos6.5 hadoop任务的提交常用的两种,一种是测试常用的IDE远程提交,另一种就是生产上用的客户端命令行提交 通用的任务程序提交步骤为: 1.将程序打成jar包: ...
随机推荐
- 启用Hyper-v后,重启后界面提示 无法完成功能配置,正在撤销更改
安装docker后,提示需要启用hyper-v,在控制面板中勾选Hyper-v,然后重启,更新快完成就提示无法完成功能配置,正在撤销更改 解决方法 方法1 控制面板一个一个选 方法2 百度了n多内容, ...
- 【Azure Developer】在Azure VM (Windows) 中搭建 kafka服务,并且通过本地以及远程验证 发送+消费 消息
问题描述 查看了 "How to Install and Run Apache Kafka on Windows? " 一文后,成功安装了Kafka服务,但是如何使用呢?如何在其他 ...
- Vue 状态管理之vuex && {mapState,mapGetters}
1 # 一.理解vuex 2 1.概念:专门在Vue中实现集中式状态(数据)管理的一个Vue插件,对vue应用中多个组件的共享状态进行集中式的管理(读写),也是一种组件间通信的方式,且适用于任意组件间 ...
- 在DELL服务器上安装windows2012 r2服务器系统
主要过程: 1.准备安装光盘,开启服务器,当出现画面按F10进入服务器自带光盘系统安装向导.(若没有系统光盘,可以用软蝶通刻一个服务系统到+R的光盘).进入后选择设置和安装系统. 2.开始安装前,提示 ...
- Luogu2783 有机化学之神偶尔会做作弊 (树链剖分,缩点)
当联通块size<=2时不管 #include <iostream> #include <cstdio> #include <cstring> #includ ...
- virtual box 安装虚拟机如何全屏
最近由于要进行微信小程序的开发.可恶的腾讯狗没有提供linux版的开发工具!不得以安装虚拟机!暗号虚拟机后发现win10的界面实在是太小,于是乎去解决这个问题!其实很简单,一般来是说直接点虚拟机面板上 ...
- fast json 乱序问题解决过程
解决问题:保存到redis中的jsonstring在转回jsonObject的时候乱序: 解决方案:https://inlhx.iteye.com/blog/2312512 解决过程: 1 看fast ...
- 对DDD使用的一些建议
群里经常看到类似于"看了DDD之后就不会写代码了"的情况,趁最近学车的间隙,写写我的看法. 关于这个事儿,我是觉得:当没有DDD的时候,如果你知道怎么做,那就那么做好了,不要考虑D ...
- CCF NOI Online 2021 提高组 赛后心得
T1 做个,不会,拿到 20 pts 跑路. 注意后面有个 K = 1 的部分分,这个可以递推求 b 的个数,然后直接乘上 a0 . 官方正解讲得极其详细,我还是第一次见到可以 O(K2) 做 1~n ...
- 中国剩余定理+扩展中国剩余定理 讲解+例题(HDU1370 Biorhythms + POJ2891 Strange Way to Express Integers)
0.引子 每一个讲中国剩余定理的人,都会从孙子的一道例题讲起 有物不知其数,三三数之剩二,五五数之剩三,七七数之剩二.问物几何? 1.中国剩余定理 引子里的例题实际上是求一个最小的x满足 关键是,其中 ...