本文来自于HeapDump性能社区! !有性能问题,上HeapDump性能社区!
最近,我所在的团队在部署我们的微服务(AWS 上的 Docker 中的 Java+SpringMVC)时遇到了问题,一个非常轻量级的应用却消耗了太多内存。于是,我们在 Docker 中发现了许多关于 Java 内存的线索,并找到了通过重构和迁移到 Spring Boot 来减少其消耗的解决方法。
这里分享一下整个过程:
在部署微服务之前,我们惯例要预估下内存,于是制定了一个清晰且简单的方程式来找到RSS:
RSS = Heap size + MetaSpace + OffHeap size
这里的OffHeap由线程堆栈、缓冲区、库 (*.jars) 和 JVM 代码本身组成。
Resident Set Size是当前分配给进程并由进程使用的 RAM 量。它包括代码、数据和共享库。
我们根据本地 Java VisualVM 值来查找:
RSS = 253(Heap) + 100(Metaspace) + 170(OffHeap) + 52*1(Threads) = 600Mb (max avarage)
……看完这个,当时觉得万事大吉,就等完成了撸串去!
既然大概600Mb就够了。我们就选择了一个 t2.micro AWS 实例(具有 1Gb RAM)进行了部署。
首先,去JVM 设置了一些和内存相关的配置:
-XX:MinHeapFreeRatio=10 \
-XX:MaxHeapFreeRatio=70 \
-XX:CompressedClassSpaceSize=64m \
-XX:ReservedCodeCacheSize=64m \
-XX:MaxMetaspaceSize=256m \
-Xms256m \
-Xmx750m \
然后,选择了 *jetty:9-alpine *作为程序的基础镜像,毕竟它是 Jetty 中 Java *.wars 最轻量级的镜像之一。
最后,既然 600Mb 就够了,那就得启动一下内存限制的docker容器:
docker run -m 600m
转折来了!
由于内存不足,启动以后,容器就被 DD(Docker 守护进程)杀死了
很奇怪,** 毕竟这个容器已经 使用完全相同的参数**在本地启动。
于是我们通过逐步增加容器的内存限制,我们达到了850Mb。
为什么?百思不得其解!
于是到处找了一些文章,看来别人的一些案例以后,我们决定进行了一些分析,尝试定位问题。
结果争议更大了!
-
堆大小与我们之前的(本地)启动相同:
-
但 Docker 显示了一些疯狂的统计数据:
why?问题越来越多了!
我们花了很多时间为这些有争议的数字寻找解释,发现并不是只有我们遇到了类似问题。
在阅读了更多文章,并使用 Native Memory Tracker 分析了程序以后,得出一个结论:
原来大多数额外内存已用于存储已编译的类及其元数据。你也许会问那JavaVM/Docker 统计数据呢?事实证明,Java VisualVM 对 OffHeap 一无所知,因此,使用此工具调查 Java 应用程序的内存消耗可能完全没用。
此外,了解你设置的 JVM 参数配置也很重要。我们发现虽然指定 - Xmx=512m告诉 JVM 分配一个512mb的堆,但是它并没有告诉 JVM 将其整个内存使用量限制为512mb。
有代码缓存和各种其他堆外数据时,要指定总内存,您应该使用 -XX:MaxRAM参数。请注意,在MaxRam=512m时,您的堆大约为 250mb。小心并注意您的应用程序 JVM 选项。
于是我们继续深入,寻找解决方案:
NMT 和 Java VisualVM Memory Sampler 帮助我们发现我们的内部核心框架在内存中被多次复制为依赖项。重复的数量等于我们微服务中子模块的数量。为了更好地理解这一点,我想说明我们的“微服务”结构:
这是 NMT(在我的本地机器上)为一个模块(加载了73MB的类元数据、42MB线程和37MB的代码,包括库)的快照:
据我们所知,以这种方式构建我们的程序是一个大错误。首先,每个 *.war 都作为一个单独的应用程序部署在一个 Jetty servlet 容器中,这很奇怪,首先根据定义,微服务应该是一个用于部署的应用程序(部署单元)。
其次,Jetty 在内存中分别保存每个 *.war 所需的所有库,即使所有这些库都具有相同的版本。结果,数据库连接、核心框架中的各种基本功能等都在内存中复制。
一个常识性的解决方案是重构并使我们的应用程序成为真正的微服务。此外,我们怀疑我们是否需要一整包 Jetty,更何况网上都在警告:
“不要在 Jetty 中部署你的程序,要在你的程序中部署 Jetty。”
我们决定尝试使用嵌入式 Jetty 的 Spring Boot,因为它似乎是独立应用程序最常用的工具,尤其是在我们的案例中。很少的配置,没有 XML,每个 Spring 框架的优势和很多(很多)插件,它们会自动配置自己。有大量实用教程和文章展示了如何在互联网上使用它。
此外,由于我们不再需要单独的 Jetty 应用程序服务器,我们将基础 Docker 映像更改为简单的轻量级OpenJDK。
openjdk:8-jre-alpine
然后,我们根据新的需求重构了我们的应用程序。在一天结束时,我们得到了类似的东西:
既然有了想法,那就去试!
问题解决了么?
让我们从我们的 Java VirtualVM 进行测量。
唔。看起来我们做了一些改进,但与以前版本的应用程序的所有努力和结果相比,并没有那么大:
但是让我们看一下 Docker 统计数据:
万岁!我们的内存消耗减半。
结论
这对我们的团队来说是一个很好且有趣的挑战。试图找出问题的根因可以让你发现真相,并让你找到特定领域的知识盲区。
同时要相信社区的力量,不要重复造轮子,你遇到的问题别人也遇到过!你可以在社区找到各类案例和答案。另外,不要完全相信来自 Java VisualVM 的内存消耗估计,哈哈。
更多案例:
又发现一个导致JVM物理内存消耗大的Bug(已提交Patch)
一文深度分析Java内存模型
一次 Java 内存泄漏排查过程,涨姿势
小心踩雷,一次Java内存泄漏排查实战
分析和解决JAVA 内存泄露的实战例子