前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >10.3.Docker中的Java内存消耗优化以及我们如何使用Spring Boot

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

作者头像
itjim
修改2020-01-21 16:50:59
4K0
修改2020-01-21 16:50:59
举报
文章被收录于专栏:springboot解析springboot解析

如果您的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
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选项提供一些关于内存配置的信息:

代码语言:javascript
复制
-XX:MaxHeapFreeRatio=70 
代码语言:javascript
复制
-XX:CompressedClassSpaceSize=64m 
代码语言:javascript
复制
-XX:ReservedCodeCacheSize=64m 
代码语言:javascript
复制
-XX:MaxMetaspaceSize=256m 
代码语言:javascript
复制
-Xms256m 
代码语言:javascript
复制
-Xmx750m 

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

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

docker run -m 600m

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

是真的吗?

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

  • 堆大小与我们之前(本地)发布的大小相同:
640?wx_fmt=png
640?wx_fmt=png
  • 但Docker展示了一些疯狂的统计数据
640?wx_fmt=png
640?wx_fmt=png

争议

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

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

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

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

640?wx_fmt=png
640?wx_fmt=png

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

640?wx_fmt=png
640?wx_fmt=png

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

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

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

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

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

openjdk:8-jre-alpine

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

640?wx_fmt=png
640?wx_fmt=png

从JavaVirtualVM中进行测量:

640?wx_fmt=png
640?wx_fmt=png
640?wx_fmt=png
640?wx_fmt=png
640?wx_fmt=png
640?wx_fmt=png

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

640?wx_fmt=png
640?wx_fmt=png

查看Docker的统计数据:

640?wx_fmt=png
640?wx_fmt=png

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

结论

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

在Docker容器中有一个非常好的Java内存使用分析,可以在其中找到关于它如何工作的清晰解释和详细信息。

本文系转载,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文系转载前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
相关产品与服务
容器镜像服务
容器镜像服务(Tencent Container Registry,TCR)为您提供安全独享、高性能的容器镜像托管分发服务。您可同时在全球多个地域创建独享实例,以实现容器镜像的就近拉取,降低拉取时间,节约带宽成本。TCR 提供细颗粒度的权限管理及访问控制,保障您的数据安全。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档