实例解读:如何减少Docker中的Java内存消耗

    xiaoxiao2021-04-18  188

    最近,我所在的团队面临着部署微服务(Java+SpringMVC in Docker on AWS)的问题。主要问题是,很多非常轻巧的应用程序消耗了太多的内存。因此,我们经过多方尝试找到了在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选项提供有关内存配置的一些信息:

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

    正如前文所提到的600Mb就足够了,所以启动了一个容器,其容量如下:

    docker run -m 600m

    那么你觉得这样会发生什么?我们的集装箱会由于内存不足被DD(Docker daemon)杀死。这个容器是在本地启动的,具有完全相同的参数,通过逐步增加容器的内存限制,我们达到了700…我开玩笑的,我们有850mb。

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

    Heap Size与我们本地相同:

      但Docker显示了一些疯狂的统计数据:

      发生了什么事?情况变得非常混乱...

    我们花了很多时间去搞清楚这些数据为什么会有争议,在搜索过程中发现我们并不是个例。在查找了很多资料,分析了 应用之后,我们几乎找到了答案。大部分额外的内存都是被用于存储编译的类及其元数据。

    那么有关JavaVM / Docker统计数据的争议数字呢?事实证明,Java VisualVM不了解OffHeap的内容,因此,使用此工具调查Java应用程序的内存消耗会非常棘手。此外,了解您使用的JVM选项也是至关重要。在我看来,指定-Xmx=512m告诉JVN分配512mb Heap,但是它并不告诉JVM整个内存使用要限制在512mb,所以就会出现代码缓存和其它非Heap数据占用内存直至超过。所以,要指定总内存需要使用XX:MaxRAM参数。请注意,使用MaxRam = 512m,你的Heap约为250mb,这时就要注意应用程序JVM选项。

    星期一早上,寻找灵丹妙药...

    NMT和Java VisualVM Memory Sampler 可以帮助我们发现内部核心框架在内存中多次重写的依赖,并且重复的数量等于微服务中子模块的数量。如果想要更好的掌握这一点,首先要说明一下我们的“微服务”结构:

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

    据我所知,构建这样的应用程序是一个很大的错误。首先,每个* .war已经作为一个单独的应用程序部署在Jetty servlet容器中,这是非常奇怪的。因为根据定义,微服务应该是一个部署应用程序(部署单元)。其次,Jetty在内存中分别保存每个* .war的所有必需库,即使所有这些库具有相同的版本。因此,DB连接、核心框架的各种基本功能等都会在内存中重复。

    一般的解决方案就是重构,让应用成为真正的微服务器。此外,我们可能还需要一整套的Jetty,但是一般人听到它的报价就能望而却步。圈内有句著名的评论“不要在Jetty中部署应用程序,而是要在应用程序中部署Jetty。

    我们决定尝试使用嵌入式Jetty的Spring Boot,因为它是独立应用程序中最常用的工具,尤其是在我们的案例中。配置很少,没有XML,每个Spring框架都有优势,还有很多插件,自动配置。除此之外,网上还有大量的实用教程和文章,这些都让Spring Boot看起来是个不错的选择。

    由于我们不再需要单独的Jetty应用程序服务器,所以我们将基础Docker的图像更改为轻量级的openjdk。

    openjdk:8-jre-alpine

    根据新的要求重构了应用程序之后,我们得到了类似下图的东西:

      现在一切都准备好了,我们来测试一下。

    先从Java VirtualVM进行测试:

    从数据中可以看出,虽然新版本有了一些改进,但是与之前的应用程序相比变化并没有那么大:

      然后,我们再来看看Docker的统计数据:

      结果很明显,我们已经减少了内存消耗。

    结论

    对我们团队来说,这是一个很有意思的挑战。找到导致错误或者某件事情的根本原因,会让你在特定领域中看得更深更广。网上的资源非常丰富,如果你下定决心去查找答案,那么就一定会找得到。另外,从这次事件中也认识到,不要完全信任Java VisualVM的内存消耗预计。

    对于技术的学习和性能的提高,我有三个更多和大家分享,阅读更多,改进更多,调查更多。另外,避免常规,尽可能自动化,这样你可以更有效的进行工作。

    本文转自d1net(转载)


    最新回复(0)