John Miiler 是 ebay 团队的高级后端工程师,负责各种我的项目,包含结账和领取零碎。作为公司解脱繁多业务的致力的一部分,他的团队正试图将业务逻辑一块一块地提取到独自的微服务中。他分享了他的团队如何解决在提取图像处理微服务时遇到的内存应用问题。
最近提取的 microservice 是一种图像处理服务,它对图像进行大小调整、裁剪、从新编码和执行其余解决操作。这个服务是一个在 Docker 容器中应用 springboot 构建的 Java 应用程序,并部署到 AWS 托管的 Kubernetes 集群中。在实现该服务时,咱们偶尔发现了一个微小的问题:该服务存在内存应用问题。本文将探讨咱们辨认和解决这些问题的办法。我将从对个别记忆问题的简要介绍开始,而后深入研究解决这个问题的过程。
我认为是内存泄露。然而在应用内存分析器(MAT)时,当我比拟一个快照和另一个快照的内存应用状况时,我的“惊喜”时刻到来了,我意识到问题在于 springboot 产生的线程数
内存问题概述
有许多类型的谬误间接或间接地影响应用程序。本文次要探讨其中的两个问题:OOM(内存不足)谬误和内存透露。考察这类谬误可能是一项艰巨的工作,咱们将具体介绍在咱们开发的服务中修复此类谬误所采取的步骤。
1. 理解 OOM 谬误并确定其起因
OOM 谬误代表第一类内存问题。它能够归结为一个试图在堆上分配内存的应用程序。然而,因为各种起因,操作系统或虚拟机(对于 JVM 应用程序)无奈满足该申请,因而,应用程序的过程会立刻进行。
使辨认和修复变得十分艰难的是,它能够在任何时候从代码中的任何地位产生。因而,仅仅查看一些日志来确定触发它的代码行通常是不够的。一些最常见的起因是:
- 应用程序须要比操作系统提供更多的内存。有时这能够通过简略地增加更多的 RAM 来解决。然而,在一台机器上能够增加多少 RAM 是有限度的,所以应该尽可能防止内存的适度应用。
- Java 应用程序应用默认的 JVM 内存限度,这通常相当激进。随着应用程序规模的增长和更多个性的增加,它最终会超过这个限度,并将被 JVM 杀死。
- 内存透露对于长时间运行的过程来说,最终将导致没有足够的资源。
有各种各样的开源和开源工具,用于查看过程的内存应用状况以及它是如何演变的。咱们将在前面的局部探讨这些工具。
2. 理解内存透露
首先让咱们理解什么是内存透露。内存透露是一种资源透露类型,当程序开释抛弃的内存时产生故障,导致性能受损或失败。当一个对象存储在内存中,但运行的代码无法访问时,也可能产生这种状况。
这听起来很形象,但在现实生活中,内存透露到底是什么样子呢?让咱们看一个用垃圾回收(GC)语言编写的应用程序内存透露的典型示例。
该图显示了旧的 Gen 内存(老年代对象)的内存模式。绿线显示调配的内存,紫色线显示 GC 对 Old Gen memory 执行扫描阶段后的理论内存使用量,垂直红线显示 GC 步骤前后内存使用量的差别。
正如您在本例中所看到的,每个垃圾收集步骤都会稍微缩小内存使用量,但总体而言,调配的空间会随着工夫的推移而增长。此模式示意并非所有调配的内存都能够开释。
3. 内存透露的次要起因
内存透露有多种起因。咱们将在这里探讨最常见的。第一个也是最容易被忽视的起因是 动态变量的滥用 。在 Java 应用程序中,只有所有者类加载到 Java 虚拟机(JVM)中,动态字段就存在于内存中。如果类自身是动态的,那么将在整个程序执行过程中加载该类,因而类和动态字段都不会被垃圾回收。
这个问题的理论解决办法出乎意料地简略。咱们抉择将默认线程池从 200 个线程笼罩到 16 个线程。
未敞开的流和连贯是内存透露的另一个起因。一般来说,操作系统只容许无限数量的关上的文件流,因而,如果应用程序遗记敞开这些文件流,在一段时间后,最终将无奈关上新文件。
同样,容许的凋谢连贯的数量也受到限制。如果一个人连贯到一个数据库但没有敞开它,在关上肯定数量的这样的连贯之后,它将达到全局限度。在此之后,应用程序将无奈再与数据库通信,因为它无奈关上新的连贯。
最初,内存透露的最初一个次要起因是未开释的本机对象。如果本机库自身有破绽,那么应用本机库 JNI 的 Java 应用程序很容易遇到内存透露。这些类型的透露通常是最难调试的,因为大多数时候,您不肯定领有本机库的代码,并且通常将其用作黑盒。
对于本机库内存透露的另一个方面是,JVM 垃圾收集器甚至不晓得本机库调配的堆内存。因而,人们只能应用其余工具来解决此类透露问题。
好吧,实践够了。让咱们看一个实在的场景:
4. 案例钻研 - 修复图像处理服务中的 OOM 问题
现状
如简介局部所述,咱们始终致力于图像处理服务。以下是开发初始阶段内存应用模式的外观:
在这张图中,Y 轴上的数字示意内存的 GiBs。红线示意 JVM 可用于堆内存(1GiB)的相对最大值。深灰色线示意一段时间内的均匀理论堆使用量,灰色虚线示意随时间推移的最小和最大理论堆使用量。结尾和结尾处的峰值示意应用程序被重新部署的工夫,因而在本例中能够疏忽它们。
该图显示了一个显著的趋势,因为它从大概须要的 300MB 堆开始,而后在短短几天内增长到超过 800MiB,而在 Docker 容器中运行的应用程序将因为 OOM 而被杀死。
为了更好地阐明这种状况,让咱们也看看在同一时间段内应用程序的其余指标。
看看这个图,内存透露的惟一迹象是堆使用率和 GC 旧 gen 大小随着工夫的推移而增长。当堆空间使用量达到 1GiB 时,运行 Docker 容器的 Kubernetes pod 就要被杀死了。每一个其余指标看起来都很稳固:线程数始终放弃在略低于 40 的程度,加载的类的数量也很稳固,非堆的应用也很稳固。
这些图表中惟一短少的变量是垃圾收集工夫。它与堆上调配的内存成比例减少。这意味着响应工夫越来越慢,利用程序运行的工夫越长。
5. 故障排除
咱们试图解决这个问题的第一步是确保所有的流和连贯都敞开了。有些角落的案子咱们一开始没有波及。然而,所有都没有扭转。咱们察看到的行为和以前齐全一样。这意味着咱们必须更深刻地开掘。
下一步是查看本机内存的应用状况,并确保最终开释所有调配的内存。咱们用来为服务做重载的 OpenCV 库不是 java 库,而是本地 C ++ 库。它提供了一个能够在应用程序中应用的 Java 本机接口。
因为咱们晓得 OpenCV 有可能透露 Java 本机内存,所以咱们确保所有 OpenCV Mat 对象都被开释,并在返回响应之前显式地调用 GC。依然没有明确的透露指示器,内存应用模式也没有任何变动。
到目前为止还没有明确的批示,是时候用专用工具进一步剖析内存应用状况了。首先,咱们钻研了内存分析器工具中的内存转储。
第一个转储是在应用程序启动后生成的,只有几个申请。第二个转储是在应用程序达到 1GiB 堆使用率之前生成的。咱们剖析了在这两种状况下调配的内容和可能引起问题的内容。乍一看没有什么不寻常的事。
而后咱们决定比拟堆上最须要的内存。令咱们诧异的是,堆上存储了相当多的申请和响应对象。这是“bingo”时刻。
深入研究这个内存转储,咱们发现堆上存储了 44 个响应对象,比初始转储中的响应对象要高得多。这 44 个响应对象实际上都存储了本人的 launchDurlClassLoader,因为它位于一个独自的线程中。每个对象的保留内存大小都超过 3MiB。
咱们容许应用程序为咱们的用例应用很多的线程。默认状况下,springboot 应用程序应用大小为 200 的线程池来解决 web 申请。这对于咱们的服务来说太大了,因为每个申请 / 响应都须要几 MB 的内存来保留原始 / 调整大小的图像。因为线程只是按需创立的,所以应用程序开始时的堆使用量很小,但随着每个新申请的减少,使用量越来越高。
这个问题的理论解决办法出乎意料地简略。咱们抉择将默认线程池从 200 个线程缩小到 16 个线程。这就彻底解决了咱们的内存问题。当初堆终于稳固了,因而 GC 也更快了。
6. 辨认内存问题的工具
在考察和排除此问题的过程中,咱们应用了几个被证实是必不可少的工具:
Datadog
咱们手头上的第一个工具是针对 JVM 度量的 DataDog APM 仪表板,它非常容易应用,容许咱们取得下面的图形和仪表板。
Jemalloc 和 jeprof
咱们用来剖析堆使用率和本机内存应用状况的另一个工具是 jemalloc 库的应用状况来剖析对 malloc 的调用。为了可能应用 jemalloc,须要应用 apt get install libjemalloc dev 进行装置,而后在运行时将其注入 Java 应用程序:
LD_PRELOAD=/usr/lib/x86_64-linux-gnu/libjemalloc.so MALLOC_CONF=prof:true,lg_prof_interval:30,lg_prof_sample:17 java [arguments omitted for simplicity ...] -jar imageprocessing.jar
总而言之,我认为是内存透露。然而最初发现问题出在 springboot 生成的线程数上。
原文链接:http://javakk.com/982.html
如果感觉本文对你有帮忙,能够点赞关注反对一下