本周进行了一个关于通过 java 代码获取本机 ip 地址的线上性能优化,这篇文章做一个总结,也提供一些 java 线上优化排查思路和更进一步的思考与总结。
近期发现线上部分机器的性能有一定的下降,于是到线上机器上通过 jstack 命令打印堆栈信息,看到发生了很多锁等待:
最近一次修改,是为了在日志中打印本机 ip 而增加了获取本机 ip 并放入 log4j2 的 mdc 的 filter:
@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() 的源码:
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();
}
}
果然存在加锁逻辑。
这个方法的执行逻辑是:
要想知道 InetAddress.getLocalHost() 具体干了什么,我们需要了解 Inet4AddressImpl.getLocalHostName() 与 nameService.lookupAllHostAddr() 两个方法做了什么。
查看 native 方法对应的 c 代码,可以知道:
gethostbyname() 函数的主要流程如下:
由于线上机器没有 nscd 进程,而 /etc/nsswitch.conf 中配置的是 “hosts: files dns”,表示先读取 /etc/hosts,如果在 /etc/hosts 文件内容中没有匹配到对应的 ip 地址则查询 DNS。
通过循环调用测试代码,并通过 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。
符合我们上文的猜测。
除了由于 /etc/hosts 文件与 DNS 中都没有本机 hostname 的对应配置造成获取本机 ip 地址失败同时性能受到影响外,按照这样的获取机制,一旦 hosts 文件中配置的本机 hostname 对应的 ip 有误,就会导致取到错误的本机 ip。
事实上,java 还提供了另一种方法获取本机 ip:
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 地址。
这个获取方法不仅避免了由于配置错误或没有配置造成的获取问题,也避免了锁等待造成的性能问题,经过测试,性能有了显著提升。
经过上述分析,有以下优化点: