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

server/2024/12/24 4:23:53/

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/server/152668.html

相关文章

day14-16系统服务管理和ntp和防火墙

一、自有服务概述 服务是一些特定的进程&#xff0c;自有服务就是系统开机后就自动运行的一些进程&#xff0c;一旦客户发出请求&#xff0c;这些进程就自动为他们提供服务&#xff0c;windows系统中&#xff0c;把这些自动运行的进程&#xff0c;称为"服务" window…

【从零开始入门unity游戏开发之——C#篇21】C#面向对象的封装——`this`扩展方法、运算符重载、内部类、`partial` 定义分部类

文章目录 一、this扩展方法1、扩展方法的基本语法2、使用扩展方法3、扩展方法的注意事项5、扩展方法的限制6、总结 二、运算符重载1、C# 运算符重载2、运算符重载的基本语法3. 示例&#xff1a;重载加法运算符 ()4、使用重载的运算符5、支持重载的运算符6、不能重载的运算符7、…

sql注入之union注入

Sql注入之union注入攻击 今天讲讲sql注入攻击流程 事先声明&#xff0c;本文仅仅作为学习使用&#xff0c;因个人原因导致的后果&#xff0c;皆与本人无关&#xff0c;后果由个人承担。 本次演示靶机为封神台里的题目&#xff0c;具体连接如下 https://hack.zkaq.cn/battle…

Mybatis加密解密查询操作(sql前),where要传入加密后的字段时遇到的问题

项目场景&#xff1a; 提示&#xff1a;这里简述项目相关背景&#xff1a; 例如&#xff1a;Mybatis加密解密查询操作&#xff08;sql前&#xff09;&#xff0c;where要传入加密后的字段时遇到的问题 问题描述 提示&#xff1a;这里描述项目中遇到的问题&#xff1a; 例如…

React 工具和库面试题(一)

1. 如何在 React 项目中使用 Hooks 从服务端获取数据&#xff1f; 在 React 中&#xff0c;我们通常使用 useEffect Hook 来进行副作用操作&#xff0c;比如从服务端获取数据&#xff0c;结合 useState 来管理数据状态。 基本步骤&#xff1a; 使用 useEffect 来执行异步操作…

git 怎么删除一个远程分支

在Git中&#xff0c;删除远程分支是一个相对简单的操作。以下是删除远程分支的步骤&#xff1a; 打开命令行工具&#xff1a; 打开你的命令行工具&#xff08;如Terminal、Git Bash、Cmder等&#xff09;。 切换到你的仓库&#xff1a; 使用cd命令切换到你的Git仓库目录。 检…

基于Linux编写C语言基础命令

目录 一、常用的Linux命令 1、改变及显示目录命令&#xff1a;cd、pwd、ls。 1.1、cd&#xff08;Change Directory&#xff09; 1.2、pwd&#xff08;Print Working Directory&#xff09; 1.3、ls&#xff08;List&#xff09; 2、文件及目录的创建、复制、删除和移动命…

【蓝桥杯】43688-《Excel地址问题》

Excel地址问题 题目描述 Excel 单元格的地址表示很有趣&#xff0c;它可以使用字母来表示列号。比如&#xff0c; A 表示第 1 列&#xff0c; B 表示第 2 列&#xff0c; … Z 表示第 26 列&#xff0c; AA 表示第 27 列&#xff0c; AB 表示第 28 列&#xff0c; … BA 表示…