Fabric8 Kubernetes Client 7.0.0内存泄漏深度分析与案例实践

ops/2024/12/22 7:08:43/

Fabric8_Kubernetes_Client_700_0">Fabric8 Kubernetes Client 7.0.0内存泄漏深度分析与案例实践

摘要

在构建基于
Vert.x Http Proxy 开发业务聚合网关时,我们面临了内存泄漏挑战,该网关主要负责对接
Kubernetes API 并提供API服务。本文将介绍我们如何通过heapdump分析、普罗米修斯监控和JFR记录来诊断问题,并最终定位并解决内存泄漏,确保了系统的稳定性和性能。

引言

本文将分享我们在诊断和解决 Fabric8 Kubernetes Client 7.0.0 内存泄漏问题上的实践经验,希望能为同样面临这一挑战的开发者提供参考。

内存泄漏概述

在我们的生产环境中,堆内存使用量在一段时间内持续增长,没有出现预期的下降。通过
Prometheus监控工具,我们发现这种增长最终导致了堆内存溢出。这意味着系统可用的堆内存被耗尽,导致Java虚拟机无法为新对象分配空间,最终触发了OutOfMemoryError 错误。这种内存泄漏问题不仅影响了应用程序的性能,还可能导致服务中断和系统崩溃,对业务连续性构成了严重威胁。

环境准备与工具选择

应用环境配置

运行环境

  • JDK:Amazon Corretto 21-al2023-jdk
  • OS:CentOS 8

为了在生产环境中进行调试,我们集成了 Alibaba Arthas 到 Java 应用镜像中; Docker镜像集成Arthas的方法:Arthas in Docker。

JVM 参数配置

-XX:+UseZGC -Xmx600m -Xms600m -XX:MaxMetaspaceSize=200m
-Djdk.internal.httpclient.disableHostnameVerification
-Djdk.virtualThreadScheduler.maxPoolSize=512
-Duser.timezone=Asia/Shanghai -Dfile.encoding=UTF-8
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=log/heapdump.hprof
-XX:ErrorFile=log/error_%p_%t.log
-Xlog:gc*:file=log/gc_%p_%t.log:tags,uptime,time,level,tags:filecount=10,filesize=10M

• -XX:+UseZGC:启用Z Garbage Collector(ZGC),这是一种低延迟垃圾回收器,适用于大堆内存管理。
• -Xmx600m:设置JVM堆内存的最大值为600MB。
• -Xms600m:设置JVM堆内存的初始大小为600MB,这有助于避免JVM在运行时增加堆内存大小。
• -XX:MaxMetaspaceSize=200m:设置元空间(Metaspace)的最大大小为200MB,元空间用于存储类的元数据。
• -Djdk.internal.httpclient.disableHostnameVerification:禁用HTTP客户端的主机名验证,通常用于测试环境。
• -Djdk.virtualThreadScheduler.maxPoolSize=512:设置虚拟线程调度器的最大池大小为512,影响并发处理能力。
• -Duser.timezone=Asia/Shanghai:设置JVM的时区为上海时区。
• -Dfile.encoding=UTF-8:设置文件编码为UTF-8,确保字符编码的一致性。
• -XX:+HeapDumpOnOutOfMemoryError:当JVM因内存溢出而终止时,生成堆内存转储(heap dump)。
• -XX:HeapDumpPath=log/heapdump.hprof:指定堆内存转储文件的路径和文件名。
• -XX:ErrorFile=log/error_%p_%t.log:设置错误日志文件的路径和命名模式,%p和%t是进程ID和时间戳的占位符。
• -Xlog:gc*:file=log/gc_%p_%t.log:tags,uptime,time,level,tags:filecount=10,filesize=10M:配置垃圾回收日志的输出,包括日志文件的路径、命名模式、日志轮转策略和文件大小限制。

HeapDump 分析

  • 使用Arthas工具导出heapdump,具体方法参见:Arthas Heapdump。

目前生产环境已经配置对应上面 JVM 参数,我们直接提取 heapdump 文件进行分析

工具

  • IDEA
  • VisualVM
  • Eclipse Memory Analyzer Tool (MAT)
  • Java Mission Control(JMC)

我们现在以 IDEA 作为工具进行分析

诊断

识别内存占用对象: 通过分析heapdump文件,我们发现了几个占用大量内存的对象:io.netty.buffer.PoolThreadCache
org.apache.logging.log4j.core.async.RingBufferLogEventio.netty.channel.nio.NioEventLoop

在这里插入图片描述

对象分析
  • org.apache.logging.log4j.core.async.RingBufferLogEvent:这是Apache Log4j 2中用于异步日志记录的高性能数据结构。默认配置下,Log4j2会使用一个较大的
    RingBuffer来缓冲日志内容,这可能导致大量内存被预留,对象数量为 262144 属于正常对象数量。
1. 默认配置: Apache Log4j2在异步模式下使用RingBuffer来缓冲所有的日志内容。根据搜索结果,Log4j2默认使用的RingBuffer槽位数量是262144个(即256 * 1024)。
2. 内存占用: 这个默认配置会导致大约40兆字节的初始内存预留。这意味着,当Log4j2异步日志系统初始化时,会预先分配约40MB的内存空间来存储日志事件。
3. 配置调整: 如果默认的RingBuffer大小不适合你的应用环境,你可以通过设置系统属性log4j2.asyncLoggerRingBufferSize来调整RingBuffer的大小。这个属性允许你根据应用的实际需求来设置一个更合适的值,以处理突发的日志活动。
最小值和限制: RingBuffer的最小大小是128个槽位。一旦RingBuffer在首次使用时被预分配,它在系统的生命周期内将不会增长或缩小。
  • io.netty.buffer.PoolThreadCache:这是Netty中的内存池,用于减少内存分配和回收的开销,大量占用,可能存在异常。
属于内存池(ByteBuf 的内存池)。它主要是为了减少内存的重复分配和回收,特别是在线程池模型中。
Netty 中的 ByteBuf 是它用于网络数据交换的核心缓冲区对象。为了高效地管理这些缓冲区,Netty 使用了内存池来缓存这些缓冲区的内存,以减少分配和回收内存的开销。每个线程有一个自己的 PoolThreadCache,用于存储和复用该线程之前分配的 ByteBuf。这样可以避免每次都去全局共享的内存池中分配内存,提高性能。
  • io.netty.channel.nio.NioEventLoop
NioEventLoop的设计允许Netty在高负载下高效地处理大量并发连接,这是通过减少线程间上下文切换和优化锁的使用来实现的。
在内存泄漏分析中,如果发现NioEventLoop对象数量异常增多,可能表明存在线程泄漏问题,一般都是只有少量线程。

根据 Heapdump 的内存泄漏分析,NioEventLoop 存在 839 个,我们可以猜测是线程资源发生泄漏

我们继续双击查看 io.netty.buffer.PoolThreadCache ,然后看到下图,点击标签
Shortest Path,逐步查看对象,发现 GC Root(垃圾回收根对象)是 VertxThread ,更加说明很有可能是线程资源泄漏导致的

在这里插入图片描述

Shortest Path 是指从某个对象到 GC Root(垃圾回收根对象)之间的最短引用链路径。GC Root 是 JVM 中的活动对象,包括线程、静态变量等。最短路径帮助我们追踪对象从 GC Root 出发到当前对象的引用链,且路径上没有冗余的引用

下一步我们继续细看其中一个 VertxThread 是线程对象,发现 namevert.x-eventloop-thread-3

在这里插入图片描述

我们回看下面的图在 Summary 标签看到存在大量 vert.x-eventloop-thread-* 线程,由此可以推断是线程资源疯狂创建导致资源耗尽。

在这里插入图片描述

性能分析与诊断

JFR__128">Java Flight Recorder (JFR) 概述

Java Flight Recorder (JFR) 是 Java Virtual Machine (JVM) 的一个特性,它允许开发者记录和分析Java应用程序的运行时行为,JFR 可以用于诊断对象的分配和回收。通过记录对象分配事件和垃圾回收事件,开发者可以追踪对象的生命周期,包括它们是如何被创建和销毁的。这对于识别内存泄漏和优化内存使用非常有价值。

JFR__132">JFR 记录

根据现有的监控数据(比如:Prometheus)的内存异常波动频率,一般 15 分钟线程数会增加几百个,为了进一步诊断线程资源泄漏问题,我们在生产环境中启动了
Arthas 并使用 JFR 进行记录,记录 15 分钟左右的日志。

具体使用教程:Arthas JFR

JFR_139">JFR文件分析

  • 按照惯例,我们依旧使用 IDEA 打开 jfr 文件,然后看到 tab 页面,查看 Events 事件 -> Java Thread Start,选中一个相关线程的日志内容查看

在这里插入图片描述

  • 我们发现线程创建始于 业务控制器 Controller -> Service

在这里插入图片描述

  • 继续看下去,发现内部业务 k8s 请求每次会创建一个 Fabric8 Kubernetes Client 导致疯狂创建 Vertx 实例导致创建大量
    EventLoop 线程

在这里插入图片描述

源码分析

根据上面 JFR Event 日志,我们可以得知一些堆栈信息,关键代码已经很明显了

  • 问题出现在业务代码的方法:CCSERawServiceImpl#client(java.lang.String)

@Override
public KubernetesClient client(String cluster) {// feign 请求获取 kubeconfig yaml 配置var config = kubeConfig(cluster);var kubeConfig = Config.fromKubeconfig(config);// new clientreturn new KubernetesClientBuilder().withConfig(kubeConfig).build();
}
  • 继续追索图片中的堆栈源码

在这里插入图片描述

HttpClient client = getHttpClient();这一行代码是创建 HttpClient

  public KubernetesClient build() {if (config == null) {config = new ConfigBuilder().build();}try {if (factory == null) {// 创建 httpclient 工厂,关键代码在这里, KubernetesClientBuilder一直 new ,这个工厂就一直创建this.factory = HttpClientUtils.getHttpClientFactory();}// 获取 httpclientHttpClient client = getHttpClient();return clazz.getConstructor(HttpClient.class, Config.class, ExecutorSupplier.class, KubernetesSerialization.class).newInstance(client, config,executorSupplier, kubernetesSerialization);} catch (InstantiationException | IllegalAccessException | IllegalArgumentException | InvocationTargetException| NoSuchMethodException | SecurityException e) {throw KubernetesClientException.launderThrowable(e);}
}

继续查看 getHttpClientFactory 方法,是通过 ServiceLoader 初始化实例

  public static HttpClient.Factory getHttpClientFactory() {HttpClient.Factory factory = getFactory(ServiceLoader.load(HttpClient.Factory.class, Thread.currentThread().getContextClassLoader()));if (factory == null) {factory = getFactory(ServiceLoader.load(HttpClient.Factory.class, HttpClientUtils.class.getClassLoader()));if (factory == null) {throw new KubernetesClientException("No httpclient implementations found on the context classloader, please ensure your classpath includes an implementation jar");}}LOGGER.debug("Using httpclient {} factory", factory.getClass().getName());return factory;
}

查看 HttpClient.Factory 实现类是 io.fabric8.kubernetes.client.vertx.VertxHttpClientFactory ,发现
Fabric8 Kubernetes Client 7.0.0 默认使用 Vert.x,下面的注释已经很明显了,每次初始化 VertxHttpClientFactory 会创建一个新的
Vertx 实例。现在一个应用一直创建 VertxHttpClientFactory 的话,会有无数个 Vertx 实例

public class VertxHttpClientFactory implements io.fabric8.kubernetes.client.http.HttpClient.Factory {private final Vertx vertx;public VertxHttpClientFactory() {// 注意:每次初始化 VertxHttpClientFactory 会创建一个新的 Vertx 实例this.vertx = createVertxInstance();}@Overridepublic VertxHttpClientBuilder<VertxHttpClientFactory> newBuilder() {return new VertxHttpClientBuilder<>(this, vertx);}private static synchronized Vertx createVertxInstance() {// We must disable the async DNS resolver as it can cause issues when resolving the Vault instance.// This is done using the DISABLE_DNS_RESOLVER_PROP_NAME system property.// The DNS resolver used by vert.x is configured during the (synchronous) initialization.// So, we just need to disable the async resolver around the Vert.x instance creation.final String originalValue = System.getProperty(DISABLE_DNS_RESOLVER_PROP_NAME);Vertx vertx;try {System.setProperty(DISABLE_DNS_RESOLVER_PROP_NAME, "true");vertx = Vertx.vertx(new VertxOptions().setFileSystemOptions(new FileSystemOptions().setFileCachingEnabled(false).setClassPathResolvingEnabled(false)));} finally {// Restore the original valueif (originalValue == null) {System.clearProperty(DISABLE_DNS_RESOLVER_PROP_NAME);} else {System.setProperty(DISABLE_DNS_RESOLVER_PROP_NAME, originalValue);}}return vertx;}// 省略代码...
}

问题定位与解决方案

问题定位

综合分析结果,我们精确定位了内存泄漏的根源是线程资源泄漏,导致线程池中的线程数量异常增长,导致每个线程的
PoolThreadCache 对象数量急剧增加,最终消耗完所有可用内存。
Fabric8 Kubernetes Client 7.0.0 版本每次创建 Client 没有复用 Vertx 实例,都会生成新的,导致线程泄漏。

问题定位步骤总结

  • 生产 Java 应用配置 JVM 启动参数开启 OOM 堆溢出 dump
  • 导出 Heap Dump
  • 根据 Heap Dump 分析大对象或内存占用较大的对象、对象资源信息、线程资源情况
  • 根据监控数据,内存异常波动频率,启动 Arthas: JFR 记录生成日志
  • 分析 JFR 文件,追索异常对象生成堆栈
  • 分析源码查找原因
  • 定位问题并解决问题

解决方案

  1. Fabric8 Kubernetes Client 对象尽量复用,我们系统暂时改造使用 Caffeine Map 缓存对象,过期自动 close Client
  2. 对于现有系统,用户可以自定义 kubeConfig 创建多个 Client 调用,根本问题还是 Fabric8 Kubernetes Client 内部对于
    Vert.x 的错误使用,只能等待升级最新版本。(在 Github 已经找到相关 issue:https://github.com/fabric8io/kubernetes-client/issues/6709 )

http://www.ppmy.cn/ops/143961.html

相关文章

彻底认识和理解探索分布式网络编程中的SSL安全通信机制

探索分布式网络编程中的SSL安全通信机制 SSL的前提介绍SSL/TLS协议概述SSL和TLS建立在TCP/IP协议的基础上分析一个日常购物的安全问题 基于SSL的加密通信SSL的安全证书SSL的证书的实现安全认证获取对应的SSL证书方式权威机构获得证书创建自我签名证书 SSL握手通信机制公私钥传输…

flask before_request 请求拦截器返回无值则放行,有值则拦截

环境 Python 3.11.5 Flask 2.2.2完整代码如下&#xff1a; from flask import Flask, make_response, Blueprintapp Flask(__name__) user_blue Blueprint(user, __name__, url_prefix/api/user) user_blue.before_request def befor…

RTMP、RTSP、RTP、HLS、MPEG-DASH协议的简介,以及应用场景

​实时视频传输协议 1. RTMP&#xff08;Real Time Messaging Protocol&#xff09; 简介&#xff1a;RTMP是由Adobe公司开发的实时消息传输协议&#xff0c;主要用于流媒体数据的传输。它基于TCP传输&#xff0c;具有低延迟、高可靠性的特点。特点&#xff1a;RTMP支持多种视…

汽车高分子材料光老化试验方法汇总

材料老化测试的重要性 材料老化测试是材料科学中的一项关键技术&#xff0c;它涉及对材料在自然环境下长期使用后性能变化的预测和评估。这项技术对于橡胶、塑料、绝缘材料等的热氧老化&#xff0c;以及电子元件和塑料产品的换气老化至关重要。光老化测试模拟了太阳光、温度和湿…

Redis 常用指令

GET&#xff1a;用户获取key1的值 127.0.0.1:6379> get tom "bob" 127.0.0.1:6379> get bob "tom"SET&#xff1a;用于设置key的值 SET指令&#xff1a;用于设置key的值 127.0.0.1:6379> set tom "bob" OK 127.0.0.1:6379> set bo…

反无人机防御系统概述!

一、定义与工作原理 反无人机防御系统是指利用频谱侦测探测、雷达探测、无线电干扰压制等技术实现对非法入侵无人机进行管控防御的系统。它采用多种技术手段&#xff0c;如雷达、光电传感器、红外线探测器等&#xff0c;通过实时监测无人机的位置、速度、航迹、姿态等信息&…

力扣--LCR 53.最大数组和

题目 给你一个整数数组 nums &#xff0c;请你找出一个具有最大和的连续子数组&#xff08;子数组最少包含一个元素&#xff09;&#xff0c;返回其最大和。 子数组 是数组中的一个连续部分。 示例 1&#xff1a; 输入&#xff1a;nums [-2,1,-3,4,-1,2,1,-5,4] 输出&…

JavaSE——绘图入门

一、Java绘图坐标体系 下图说明了Java坐标系&#xff0c;坐标原地位于左上角&#xff0c;以像素为单位。在Java坐标系中&#xff0c;第一个是x坐标&#xff0c;表示当前位置为水平方向&#xff0c;距离坐标原点x个像素&#xff1b;第二个是y坐标&#xff0c;表示当前位置为垂直…