当前位置: 首页 > article >正文

Fabric8 Kubernetes Client 7.0.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-* 线程,由此可以推断是线程资源疯狂创建导致资源耗尽。

在这里插入图片描述

性能分析与诊断

Java Flight Recorder (JFR) 概述

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

JFR 记录

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

具体使用教程:Arthas JFR。

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 client
    return 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();
        }
        // 获取 httpclient
        HttpClient 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();
    }

    @Override
    public 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 value
            if (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.kler.cn/a/444848.html

相关文章:

  • 未来趋势系列 篇五:自主可控科技题材解析和股票梳理
  • PHP接入美团联盟推广
  • 有监督学习 vs 无监督学习:机器学习的两大支柱
  • FingerprintJS的使用
  • CVE-2024-32709 WordPress —— Recall 插件存在 SQL 注入漏洞
  • 数据结构与算法学习笔记----SPFA判断负环
  • Immer编写更简单的逻辑
  • SpringBoot3+Vue3开发在线考试系统
  • 说说你对 css3 display:flex 弹性盒模型 的理解
  • 阿里云 ECS 实例上升级 Docker 并使用多阶段构建
  • STM8单片机学习笔记·GPIO的片上外设寄存器
  • 轻松拿捏Spring
  • Arcgis for javascript 开发学习经验
  • 相机主要调试参数
  • LDO输入电压不满足最小压差时输出会怎样?
  • uboot, s5pv210, 内存讲解(3)
  • 【Nginx-4】Nginx负载均衡策略详解
  • 在Windows下安装redis
  • Python知识分享第三十一天-Numpy和Pnadas入门
  • 林子雨-大数据课程实验报告(二)
  • 气象与旅游之间的关系,如果借助高精度预测提高旅游的质量
  • 安徽移动携手开源网安亮相2024中国国际车联网技术大会,共筑车联网安全新壁垒
  • 无人设备遥控器之通讯技术篇
  • 技术速递|.NET 9 简介
  • 计算机基础 试题
  • 【C++】sophus : sim_details.hpp 实现了矩阵函数 W、其导数,以及其逆 (十七)