在 filter 中使用错误的方法获取 ip 地址造成性能问题排查

2021-08-21 23:15:42   最后更新: 2021-08-21 23:15:42   访问数量:46




 

本周进行了一个关于通过 java 代码获取本机 ip 地址的线上性能优化,这篇文章做一个总结,也提供一些 java 线上优化排查思路和更进一步的思考与总结。

 

 

2.1 发现锁等待

 

近期发现线上部分机器的性能有一定的下降,于是到线上机器上通过 jstack 命令打印堆栈信息,看到发生了很多锁等待:

 

 

 

2.2 最近一次修改

 

最近一次修改,是为了在日志中打印本机 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(); } }

 

 

果然存在加锁逻辑。

 

这个方法的执行逻辑是:

 

  1. 调用 Inet4AddressImpl.getLocalHostName() 获取本机 hostname;
  2. 通过 synchronized 加锁;
  3. 尝试从缓存中获取;
  4. 如果从缓存中获取失败或缓存失效(失效时间:5秒),则通过本机 hostname 调用 nameService.lookupAllHostAddr() 获取 hostname 对应的 ip;
  5. 如果获取成功则将获取到的 ip 放入缓存中。

 

2.3 现象分析

 

  1. 由于本地 ip 属于静态信息,不应该通过 filter 机制在每次调用中临时获取,而是应该在项目启动时获取一次,然后存储在全局的固定位置中,例如单例的类实例或是 System.property 等;

 

  1. 能够显著影响线上性能,说明很可能并没有获取到本机 ip 放入缓存,导致每次调用都执行了全部逻辑,这条待下文验证。

 

 

要想知道 InetAddress.getLocalHost() 具体干了什么,我们需要了解 Inet4AddressImpl.getLocalHostName() 与 nameService.lookupAllHostAddr() 两个方法做了什么。

 

3.1 查看 native 代码对应的 C 语言代码

 

查看 native 方法对应的 c 代码,可以知道:

 

  1. Inet4AddressImpl.getLocalHostName() 调用的是 C 语言标准库的 gethostname() 函数;
  2. nameService.lookupAlHostAddr() 调用的是 C 语言标准库的 gethostbyname() 函数。

 

3.2 C 语言标准库函数的实现

 

  1. 在 linux 系统中,标准库的 gethostname() 函数是通过系统调用 uname() 实现的;
  2. 标准库的 gethostbyname() 函数则是用以下方式实现的:

 

  • https://garlicspace.com/2019/05/11/gethostbyname%E5%87%BD%E6%95%B0%E5%AE%9E%E7%8E%B0%E5%88%86%E6%9E%90/#gethostbyname_glibc229

 

gethostbyname() 函数的主要流程如下:

 

  1. 通过与 nscd 进程通信,获取 /etc/hosts 和 /etc/resolv.conf 文件内容,如果在 /etc/hosts 文件内容中没有匹配到对应的 ip 地址,则通过 /etc/resolv.conf 中配置的 DNS 地址,向 DNS 服务器发出域名解析请求;
  2. 如果 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。

 

符合我们上文的猜测。

 

 

除了由于 /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 地址。

 

这个获取方法不仅避免了由于配置错误或没有配置造成的获取问题,也避免了锁等待造成的性能问题,经过测试,性能有了显著提升。

 

 

经过上述分析,有以下优化点:

 

  1. 本机 ip 等固定信息,不要在 filter 中获取,而要改为 spring 启动时获取一次,以避免性能损失。
  2. 不要使用 InetAddress.getLocalHost() 的方式获取本机 IP,而要使用 NetworkInterface 来获取,InetAddress.getLocalHost() 有以下问题:
    1. 通过 hosts 文件、DNS 服务获取,存在取不到或取到不正确的情况;
    2. 访问 DNS 服务存在性能问题;
    3. InetAddress.getLocalHost() 实现中加了 synchronized 锁,并发环境中会进一步影响性能。

 

 

欢迎关注微信公众号,以技术为主,涉及历史、人文等多领域的学习与感悟,每周三到七篇推文,全部原创,只有干货没有鸡汤

 

 

 

线上事故记录






技术帖      技术分享      java      filter      spring      线上事故     


京ICP备2021035038号