K8s 应用的资源配置
云原生架构以 K8s 为基石,应用在 K8s 上部署,以容器组的形态运行。K8s 的资源模型有两个定义,资源请求(request)和资源限制(limit),K8s 保障容器拥有 request数量的资源,但不允许使用超过limit数量的资源。以如下的内存配置为例,容器至少能获得 1024Mi 的内存资源,但不允许超过 4096Mi,一旦内存使用超限,该容器将发生OOM,而后被 K8s 控制器重启。
spec:containers:- name: edasimage: alibaba/edasresources:requests:memory: "1024Mi"limits:memory: "4096Mi"command: ["java", "-jar", "edas.jar"]
容器 OOM
# 当前容器内存限制量$ cat sys/fs/cgroup/memory/memory.limit_in_bytes4294967296# 当前容器内存实际用量$ cat sys/fs/cgroup/memory/memory.usage_in_bytes39215104
JVM OOM
java.lang.OutOfMemoryError: Java heap space 堆内存溢出。当堆内存 (Heap Space) 没有足够空间存放新创建的对象时,就会抛出该错误。一般由于内存泄露或者堆的大小设置不当引起。对于内存泄露,需要通过内存监控软件查找程序中的泄露代码,而堆大小可以通过-Xms,-Xmx等参数修改。
java.lang.OutOfMemoryError: PermGen space Metaspace 永久代/元空间溢出。永久代存储对象包括class信息和常量,JDK 1.8 使用 Metaspace 替换了永久代(Permanent Generation)。通常因为加载的 class 数目太多或体积太大,导致抛出该错误。可以通过修改 -XX:MaxPermSize 或者 -XX:MaxMetaspaceSize 启动参数, 调大永久代/元空间大小。
java.lang.OutOfMemoryError: Unable to create new native thread 无法创建新线程。每个 Java 线程都需要占用一定的内存空间, 当 JVM 向底层操作系统请求创建一个新的 native 线程时, 如果没有足够的资源分配就会报此类错误。可能原因是 native 内存不足、线程泄露导致线程数超过操作系统最大线程数 ulimit 限制或是线程数超过 kernel.pid_max。需要根据情况进行资源升配、限制线程池大小、减少线程栈大小等操作。

代码段。一般指程序代码在内存中的映射,这里特别指出是 JVM 自身的代码,而不是Java代码。
数据段。在程序运行初已经对变量进行初始化的数据,此处是 JVM 自身的数据。
堆空间。运行时堆是 Java 进程和普通进程区别最大的一个内存段。Linux 进程内存模型里的堆是为进程在运行时动态分配的对象提供内存空间,而几乎所有JVM内存模型里的东西,都是 JVM 这个进程在运行时新建出来的对象。而 JVM 内存模型中的 Java 堆,只不过是 JVM 在其进程堆空间上建立的一段逻辑空间。
栈空间。存放进程的运行栈,此处并不是 JVM 内存模型中的线程栈,而是操作系统运行 JVM 本身需要留存的一些运行数据。
如上所述,堆空间作为 Linux 进程内存布局和 JVM 内存布局都有的概念,是最容易混淆也是差别最大的一个概念。Java 堆相较于 Linux 进程的堆,范围更小,是 JVM 在其进程堆空间上建立的一段逻辑空间,而进程堆空间还包含支撑 JVM 虚拟机运行的内存数据,例如 Java 线程堆栈、代码缓存、GC 和编译器数据等。
在 Java 开发者看来,Java 代码运行中开辟的对象都放在 Java 堆中,所以很多人会将 Java 堆内存等同于 Java 进程内存,将 Java 堆内存限制参数Xmx当作进程内存限制参数使用,并且把容器内存限制也设置为 Xmx 一样大小,然后悲催地发现容器被 OOM 了。

$ java -Xms300m -Xmx300m -XX:+UseG1GC -XX:NativeMemoryTracking=summary -jar app.jar
此处限制最大堆内存为 300M,使用 G1 作为 GC 算法,开启 NMT 追踪进程的内存使用情况。
注意:启用 NMT 会导致 5% -10% 的性能开销。
$ jcmd <pid> VM.native_memory summary scale=MB
JVM 总内存
Native Memory Tracking:Total: reserved=1764MB, committed=534MB
NMT 报告显示进程当前保留内存为 1764MB,已提交内存为 534MB,远远高于最大堆内存 300M。保留指为进程开辟一段连续的虚拟地址内存,可以理解为进程可能使用的内存量;提交指将虚拟地址与物理内存进行映射,可以理解为进程当前占用的内存量。
Java Heap
Java Heap (reserved=300MB, committed=300MB)(mmap: reserved=300MB, committed=300MB)
Java 堆内存如设置的一样,实际开辟了 300M 的内存空间。
Metaspace
Class (reserved=1078MB, committed=61MB)(classes #11183)(malloc=2MB #19375)(mmap: reserved=1076MB, committed=60MB)
加载的类被存储在 Metaspace,此处元空间加载了 11183 个类,保留了近 1G,提交了 61M。
加载的类越多,使用的元空间就越多。元空间大小受限于-XX:MaxMetaspaceSize(默认无限制)和 -XX:CompressedClassSpaceSize(默认 1G)。
Thread
Thread (reserved=60MB, committed=60MB)(thread #61)(stack: reserved=60MB, committed=60MB)
JVM 线程堆栈也需要占据一定空间。此处 61 个线程占用了 60M 空间,每个线程堆栈默认约为 1M。堆栈大小由 -Xss 参数控制。
Code Cache
Code (reserved=250MB, committed=36MB)(malloc=6MB #9546)(mmap: reserved=244MB, committed=30MB)
GC
GC (reserved=47MB, committed=47MB)(malloc=4MB #11696)(mmap: reserved=43MB, committed=43MB)
GC 垃圾收集器也需要一些内存空间支撑 GC 操作,GC 占用的空间与具体选用的 GC 算法有关,此处的 GC 算法使用了 47M。在其他配置相同的情况下,换用 SerialGC:
GC (reserved=1MB, committed=1MB)(mmap: reserved=1MB, committed=1MB)
可以看到 SerialGC 算法仅使用 1M 内存。这是因为 SerialGC 是一种简单的串行算法,涉及数据结构简单,计算数据量小,所以内存占用也小。但是简单的 GC 算法可能会带来性能的下降,需要平衡性能和内存表现进行选择。
Symbol
Symbol (reserved=15MB, committed=15MB)(malloc=11MB #113566)(arena=3MB #1)
非 JVM 内存
Total memory = Heap + Code Cache + Metaspace + Thread stacks +Symbol + GC + Direct buffers + JNI + ...
经常有用户反馈,为什么相同的一份代码,在线上容器里跑总是要比本地跑更耗内存,甚至出现 OOM。可能的情况的情况有如下几种:
没有使用容器感知的 JVM 版本
线上业务耗费更多内存
使用容器感知的 JDK 版本。对于使用 Cgroup V1 的集群,需要升级至 8u191+、Java 9、Java 10 以及更高版本;对于使用 Cgroup V2 的集群,需要升级至 8u372+ 或 Java 15 及更高版本。 使用 NativeMemoryTracking(NMT) 了解应用的 JVM 内存用量。NMT 能够追踪 JVM 的内存使用情况,在测试阶段可以使用 NMT 了解程序JVM使用内存的大致分布情况,作为内存容量配置的参考依据。JVM 参数 -XX:NativeMemoryTracking 用于启用 NMT,开启 NMT 后,可以使用 jcmd 命令打印 JVM 内存的占用情况。 根据 Java 程序内存使用量设置容器内存 limit。容器 Cgroup 内存限制值来源于对容器设置的内存 limit 值,当容器进程使用的内存量超过 limit,就会发生容器 OOM。为了程序在正常运行或业务波动时发生 OOM,应该按照 Java 进程使用的内存量上浮 20%~30% 设置容器内存 limit。如果初次运行的程序,并不了解其实际内存使用量,可以先设置一个较大的 limit 让程序运行一段时间,按照观测到的进程内存量对容器内存 limit 进行调整。 OOM 时自动 dump 内存快照,并为 dump 文件配置持久化存储,比如使用 PVC 挂载到 hostPath、OSS 或 NAS,尽可能保留现场数据,支撑后续的故障排查。
▲ 点击上方卡片关注K8s技术圈,掌握前沿云原生技术




