1. 引言
本周进行了一个关于通过 java 代码获取本机 ip 地址的线上性能优化,这篇文章做一个总结,也提供一些 java 线上优化排查思路和更进一步的思考与总结。
2. 排查过程
2.1 发现锁等待
近期发现线上部分机器的性能有一定的下降,于是到线上机器上通过 jstack 命令打印堆栈信息,看到发生了很多锁等待:
2.2 最近一次修改
最近一次修改,是为了在日志中打印本机 ip 而增加了获取本机 ip 并放入 log4j2 的 mdc 的 filter:
代码语言:javascript复制@Component
public class LocalIpFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain)
throws ServletException, IOException {
String localIp;
try {
localIp = InetAddress.getLocalHost().getHostAddress();
} catch (Exception ignore) {
localIp = "unknown";
}
MDC.put("local_ip", localIp);
filterChain.doFilter(httpServletRequest, httpServletResponse);
}
}
2.3 InetAddress.getLocalHost() 源码
查看 InetAddress.getLocalHost() 的源码:
代码语言:javascript复制public static InetAddress getLocalHost() throws UnknownHostException {
SecurityManager security = System.getSecurityManager();
try {
String local = impl.getLocalHostName();
if (security != null) {
security.checkConnect(local, -1);
}
if (local.equals("localhost")) {
return impl.loopbackAddress();
}
InetAddress ret = null;
synchronized (cacheLock) {
long now = System.currentTimeMillis();
if (cachedLocalHost != null) {
if ((now - cacheTime) < maxCacheTime) // Less than 5s old?
ret = cachedLocalHost;
else
cachedLocalHost = null;
}
// we are calling getAddressesFromNameService directly
// to avoid getting localHost from cache
if (ret == null) {
InetAddress[] localAddrs;
try {
localAddrs =
InetAddress.getAddressesFromNameService(local, null);
} catch (UnknownHostException uhe) {
// Rethrow with a more informative error message.
UnknownHostException uhe2 =
new UnknownHostException(local ": "
uhe.getMessage());
uhe2.initCause(uhe);
throw uhe2;
}
cachedLocalHost = localAddrs[0];
cacheTime = now;
ret = localAddrs[0];
}
}
return ret;
} catch (java.lang.SecurityException e) {
return impl.loopbackAddress();
}
}
果然存在加锁逻辑。
这个方法的执行逻辑是:
- 调用 Inet4AddressImpl.getLocalHostName() 获取本机 hostname;
- 通过 synchronized 加锁;
- 尝试从缓存中获取;
- 如果从缓存中获取失败或缓存失效(失效时间:5秒),则通过本机 hostname 调用 nameService.lookupAllHostAddr() 获取 hostname 对应的 ip;
- 如果获取成功则将获取到的 ip 放入缓存中。
2.3 现象分析
- 由于本地 ip 属于静态信息,不应该通过 filter 机制在每次调用中临时获取,而是应该在项目启动时获取一次,然后存储在全局的固定位置中,例如单例的类实例或是 System.property 等;
- 能够显著影响线上性能,说明很可能并没有获取到本机 ip 放入缓存,导致每次调用都执行了全部逻辑,这条待下文验证。
3. 抓包源码分析
要想知道 InetAddress.getLocalHost() 具体干了什么,我们需要了解 Inet4AddressImpl.getLocalHostName() 与 nameService.lookupAllHostAddr() 两个方法做了什么。
3.1 查看 native 代码对应的 C 语言代码
查看 native 方法对应的 c 代码,可以知道:
- Inet4AddressImpl.getLocalHostName() 调用的是 C 语言标准库的 gethostname() 函数;
- nameService.lookupAllHostAddr() 调用的是 C 语言标准库的 gethostbyname() 函数。
3.2 C 语言标准库函数的实现
- 在 linux 系统中,标准库的 gethostname() 函数是通过系统调用 uname() 实现的;
- 标准库的 gethostbyname() 函数则是用以下方式实现的:
- https://garlicspace.com/2019/05/11/gethostbyname函数实现分析/#gethostbyname_glibc229
gethostbyname() 函数的主要流程如下:
- 通过与 nscd 进程通信,获取 /etc/hosts 和 /etc/resolv.conf 文件内容,如果在 /etc/hosts 文件内容中没有匹配到对应的 ip 地址,则通过 /etc/resolv.conf 中配置的 DNS 地址,向 DNS 服务器发出域名解析请求;
- 如果 nscd 进程不存在,则通过 /etc/nsswitch.conf 中配置的获取顺序到指定目标中获取。
由于线上机器没有 nscd 进程,而 /etc/nsswitch.conf 中配置的是 “hosts: files dns”,表示先读取 /etc/hosts,如果在 /etc/hosts 文件内容中没有匹配到对应的 ip 地址则查询 DNS。
3.3 验证
通过循环调用测试代码,并通过 strace 命令抓取系统调用信息,可以看到:
strace -tt -T -f -e ‘trace=!futex,epoll_wait’ -p {pid}
可见,如上文所述,机器确实在读取 hosts 文件后与 127.0.0.1:53 通信,127.0.0.1:53 就是 /etc/resolv.conf 文件中配置的 DNS 服务 ip 与端口。
进一步,我们通过 tcpdump 对 lo 网卡 53 端口抓包,再用 wireshark 分析:
tcpdump -i lo port 53 -w output.pcap
可见,程序无法通过 127.0.0.1:53 获取到 DNS 中的本机 ip。
符合我们上文的猜测。
4. 解决方案
除了由于 /etc/hosts 文件与 DNS 中都没有本机 hostname 的对应配置造成获取本机 ip 地址失败同时性能受到影响外,按照这样的获取机制,一旦 hosts 文件中配置的本机 hostname 对应的 ip 有误,就会导致取到错误的本机 ip。
事实上,java 还提供了另一种方法获取本机 ip:
代码语言:javascript复制public List<String> getLocalIps() {
try {
List<String> ipList = new ArrayList<>();
Enumeration<NetworkInterface> networkInterfaces = NetworkInterface.getNetworkInterfaces();
while (networkInterfaces.hasMoreElements()) {
NetworkInterface networkInterface = networkInterfaces.nextElement();
Enumeration<InetAddress> inetAddresses = networkInterface.getInetAddresses();
while (inetAddresses.hasMoreElements()) {
InetAddress inetAddress = inetAddresses.nextElement();
if (inetAddress instanceof Inet4Address && !"127.0.0.1".equals(inetAddress.getHostAddress())) {
ipList.add(inetAddress.getHostAddress());
}
}
}
} catch (Exception ignore) { }
return ipLIst;
}
通过查看源码:
https://github.com/openjdk/jdk/blob/739769c8fc4b496f08a92225a12d07414537b6c0/src/java.base/unix/native/libnet/NetworkInterface.c
NetworkInterface.getNetworkInterfaces() 方法是通过 linux 系统调用 ioctl 传入 SIOCGIFCONF 参数获取的,与 ifconfig 底层实现相同,可以获取到真实的 ip 地址。
这个获取方法不仅避免了由于配置错误或没有配置造成的获取问题,也避免了锁等待造成的性能问题,经过测试,性能有了显著提升。
5. 结论
经过上述分析,有以下优化点:
- 本机 ip 等固定信息,不要在 filter 中获取,而要改为 spring 启动时获取一次,以避免性能损失。
- 不要使用 InetAddress.getLocalHost() 的方式获取本机 IP,而要使用 NetworkInterface 来获取,InetAddress.getLocalHost() 有以下问题:
- 通过 hosts 文件、DNS 服务获取,存在取不到或取到不正确的情况;
- 访问 DNS 服务存在性能问题;
- InetAddress.getLocalHost() 实现中加了 synchronized 锁,并发环境中会进一步影响性能。