Docker中的Java内存消耗优化以及我们如何使用Spring Boot

———— / BEGIN/ ————


如果您的Docker容器占用太多内存而无法达到最佳性能,请阅读下文以了解一个团队如何找到解决方案。

最近,我所在的团队在部署我们的微服务(AWS上Docker中的Java+SpringMVC)时遇到了一个问题。主要问题是,我们的轻量级应用程序占用了太多内存。因此,我们发现了Docker中Java在内存方面的许多棘手之处,并找到了通过重构和迁移到Spring Boot来减少内存消耗的方法。这项工作的结果非常吸引人,我决定与你们分享。

在部署之前,作为具有常识的开发人员,我们能够估计应用程序将消耗多少内存。为此,我们制定了一个清晰易懂的方程式来查找RSS

RSS = Heapsize + MetaSpace + OffHeap size  

这里OffHeap由线程堆栈,缓冲区,库(* .jars)和JVM代码本身组成,在这,我要引用另外一个概念——常驻集。

  常驻集 


常驻集大小是当前分配给进程并由进程使用的RAM数量。它包括代码、数据和共享库。

让我们根据本地Java VisualVM值找到它:

 

640?wx_fmt=png


640?wx_fmt=png


640?wx_fmt=png

RSS = 253(Heap) + 100(Metaspace) + 170(OffHeap) + 52*1(Threads) = 600Mb (max avarage)

RSS = 253(堆)+ 100(元空间)+ 170(OffHeap)+ 52 * 1(线程)= 600Mb(最大平均值)

我们得到的结果是:大概600Mb就足够了,我们选择了一个t2.micro AWS实例(带有1Gb RAM)进行部署,开始部署我们的app,首先,我想通过JVM选项提供一些关于内存配置的信息:

-XX:MaxHeapFreeRatio=70 
-XX:CompressedClassSpaceSize=64m 
-XX:ReservedCodeCacheSize=64m 
-XX:MaxMetaspaceSize=256m 
-Xms256m 
-Xmx750m 

此外,作为应用程序的基本图像,我们选择了  jetty:9-alpine,因为我们发现它是Jetty中Java * .wars中最轻量级的图像之一。

正如我所提到的,似乎600Mb就足够了,因此启动了一个具有以下内存限制的容器:

 
 

docker run -m 600m

那你觉得怎么样?由于内存不足,我们的容器被DD(Docker守护程序)杀死。这真的很令人惊讶,因为 这个容器已经在本地启动,  具有完全相同的参数(它可以是一个单独的讨论主题)。通过逐步增加容器的内存限制,我们达到了700 ...我在开玩笑,我们得到850Mb


  是真的吗?


经过一些观察和阅读有用的文章后,我们决定进行一些测量。结果非常奇怪和有争议。

  • 堆大小与我们之前(本地)发布的大小相同:


640?wx_fmt=png


  • Docker展示了一些疯狂的统计数据

640?wx_fmt=png


争议


怎么回事,伙计们?情况变得非常混乱......

我们花了很多时间寻找这些有争议的数字的解释,发现并不是只有我们才有这些问题。在阅读了更多的源代码并使用本机内存跟踪器分析了应用程序之后,我们离答案更近了。我可以总结。大部分额外的内存用于存储已编译的类及其元数据,您可能会问,关于JavaVM/Docker统计数据的争议性数字呢?好问题。事实证明,Java VisualVM对OffHeap关系很微妙,因此,使用这个工具来调查Java应用程序的内存消耗可能非常棘手。此外,了解您使用的JVM选项也非常重要。我发现,

指定-Xmx=512m来给JVM分配一个512mb堆内存,这是一个发现。它没有指定JVM将其整个内存使用限制在512mb,会有代码缓存和各种各样的堆外数据,要指定总内存,应该使用-XX:MaxRAM参数。注意,MaxRam=512m时,堆大小大约为250mb。请注意您的应用程序JVM选项。

NMTJavaVisualVM Memory Sampler使我们发现内部核心框架被多次复制为内存中的依赖项。并且重复的数量等于我们的微服务中的子模块的数量。为了更好地掌握这一点,我想说明我们的微服务结构:

640?wx_fmt=png


这是来自NMT(在我的本地机器上)的一个模块的快照(具有73MB加载的类元数据,42MB线程和37MB代码,包括libs):


640?wx_fmt=png

 

据我们所知,以这种方式构建应用程序是一个很大的错误。首先,每个*.war都被部署为Jettyservlet容器中的一个单独的应用程序,这是非常奇怪的,我同意,因为根据定义,微服务应该是一个部署应用程序(部署单元)。其次,Jetty在内存中分别为每个* .war保存所有必需的lib,即使所有这些库都具有相同的版本。结果,DB连接,来自核心框架的各种基本功能等在内存中被复制。

常识解决方案是重构并使我们的应用程序成为真正的微服务。此外,我们怀疑我们需要一整箱Jetty,我认为,你听到这句名言:

不要在Jetty中部署应用程序,在应用程序中部署Jetty

我们决定尝试使用嵌入式JettySpring Boot,因为它似乎是独立应用程序中最常用的工具,特别是在我们的案例中。几乎没有配置,没有XML每个Spring Framework优势和很多插件,这些能够自动配置,有大量实用的教程和文章展示了如何在互联网上使用它

此外,由于我们不再需要单独的Jetty应用程序服务器,因此我们将基本Docker镜像更改为简单的轻量级OpenJDK

 
 

openjdk:8-jre-alpine

然后,我们根据新要求重构了我们的应用程序。在一天结束时,我们得到了类似的东西:


640?wx_fmt=png

JavaVirtualVM中进行测量:


640?wx_fmt=png


640?wx_fmt=png


640?wx_fmt=png

做了一些改进后,但与之前版本的应用程序的所有工作和结果相比并没有那么大的差别:


640?wx_fmt=png

查看Docker的统计数据:

640?wx_fmt=png

太好了,我们的内存消耗减少了一半。

结论


对我们的团队来说,这是一个有趣的挑战。试图找出事物断裂的根本原因可以让你找到真正好奇的事实,并让你对某个特定领域的视野更深入、更宽广。相信互联网社区,因为我们经常试图解决这些难度类似的问题。另外,不要太过于相信Java VisualVM的内存消耗预算,一定要小心。

在Docker容器中有一个非常好的Java内存使用分析,可以在其中找到关于它如何工作的清晰解释和详细信息。我强烈推荐阅读本文,本文将使您对容器中的Java内存分配有一个深入的了解,您可能会在Docker中找到关于Java+内存的一些问题的答案

希望你会发现这很有帮助。请在下面的评论中分享您的反馈或问题。

640?wx_fmt=jpeg

长按二维码 ▲

订阅「架构师小秘圈」公众号

如有启发,帮我点个在看,谢谢↓

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值