把大象裝入貨柜里——Java容器內(nèi)存拆解

[圖片源:https://bell-sw.com/announcements/2020/10/28/JVM-in-Linux-containers-surviving-the-isolation/]

介紹

相信很多人都知道仑扑,云環(huán)境中兴喂,所有服務(wù)都必須作資源限制。內(nèi)存作為一個(gè)重要資源當(dāng)然不會例外惜辑。限制說得容易,但如何在限制的同時(shí)签舞,保證服務(wù)的性能指標(biāo)(SLA)就是個(gè)技術(shù)和藝術(shù)活次洼。

為應(yīng)用內(nèi)存設(shè)置上限,從來不是個(gè)容易的事扯饶。因?yàn)樵O(shè)置上限的理據(jù)是:

  • 應(yīng)用程序?qū)?nèi)存的使用和回收邏輯,而這個(gè)邏輯一般異常地復(fù)雜
  • 現(xiàn)代操作系統(tǒng)復(fù)雜的虛擬內(nèi)存管理池颈、物理內(nèi)存分配尾序、回收機(jī)制

如果是 Java ,還要加上:

  • JVM 中各類型組件的內(nèi)存管理機(jī)制

以上 3 個(gè)方面還可以進(jìn)一步細(xì)分躯砰。每一個(gè)細(xì)分都有它的內(nèi)存機(jī)制每币。而只要我們漏算了其中一個(gè),就有可能讓應(yīng)用總內(nèi)存使用超限琢歇。

而讓人揪心的是兰怠,當(dāng)應(yīng)用總內(nèi)存使用超限時(shí),操作系統(tǒng)會無情地殺死應(yīng)用進(jìn)程(OOM, Out Of Memory)李茫。而很多人對這一無所覺揭保,只知道容器重啟了。而這可能是連鎖反應(yīng)的開端:

  • 如果容器 OOM 的原因只是個(gè)偶然涌矢,那還好說掖举。如果是個(gè) BUG 引起的,那么這種 OOM 可能會在服務(wù)的所有容器中逐個(gè)爆發(fā)娜庇,最后服務(wù)癱瘓
  • 原來服務(wù)容器群的資源就緊張塔次,一個(gè)容器 OOM 關(guān)閉了,負(fù)載均衡把流量分到其它容器名秀,于是其它容器也出現(xiàn)同樣的 OOM励负。最后服務(wù)癱瘓

JVM 是個(gè) Nice 的經(jīng)理,在發(fā)現(xiàn)內(nèi)存緊張時(shí)匕得,就不厭其煩地停止應(yīng)用線程和執(zhí)行 GC继榆,而這種內(nèi)存緊張的信號巾表,在設(shè)計(jì)界稱為“背壓(Backpressure)”。
但操作系統(tǒng)相反略吨,是個(gè)雷厲風(fēng)行的司令集币,一發(fā)現(xiàn)有進(jìn)程超限,直接一槍 OOM Killed翠忠。

或者你深入研究過 cgroup memory鞠苟,它其實(shí)也有一個(gè) Backpressure 的通知機(jī)制,不過現(xiàn)在的容器和 JVM 均忽略之秽之。

終上所述当娱,容器進(jìn)程 OOM Kllled 是件應(yīng)該避免,但需要深入研究才能避免的事情考榨。

網(wǎng)路上跨细,我們可以找到很多現(xiàn)實(shí)案例和教訓(xùn):


Java 內(nèi)存管理很復(fù)雜。我們對它了解越多河质,應(yīng)用出現(xiàn) OOM Killed 的可能性就越低冀惭。下面我拿一個(gè)遇到的測試案例進(jìn)行分析。

分析報(bào)告分為兩個(gè)部分:

  1. 研究應(yīng)用實(shí)測出的指標(biāo)愤诱、內(nèi)存消耗云头,內(nèi)存限制配置
  2. 潛在的問題和改進(jìn)建議

測試環(huán)境

主機(jī):裸機(jī)(BareMetal)
CPU: 40 cores, 共 80 個(gè)超線程
Linux:
  Kernel: 5.3.18
  glibc: libc-2.26.so
Java: 1.8.0_261-b12
Web/Servlet 容器: Jetty

配置容量

POD 容量配置

    resources:
      limits:
        cpu: "8"
        memory: 4Gi
        # 4Gi = 4 * 1024Mb = 4*1024*1024k = 4194304k = 4294967296 bytes = 4096Mb
      requests:
        cpu: "2"
        memory: 4Gi

JVM 容量配置

開始說 JVM 容量配置前捐友,我假設(shè)你已經(jīng)對 JVM 內(nèi)存使用情況有個(gè)基本印象:

java-mem-model.png

圖片源:https://www.twblogs.net/a/5d80afd1bd9eee541c349550?lang=zh-cn

下面是我在測試環(huán)境收集到的配置:

配置 實(shí)際生效配置(Mbyte)
Young Heap + Old Heap -Xmx3G -XX:+AlwaysPreTouch 3072
MaxMetaspaceSize [默認(rèn)] Unlimited
CompressedClassSpaceSize [默認(rèn)] 1024
MaxDirectMemorySize [默認(rèn)] 3072
ReservedCodeCacheSize [默認(rèn)] 240
ThreadStackSize*maxThreadCount [默認(rèn)] * 276(實(shí)測線程數(shù)) 276
匯總 7684 + (沒限制 MaxMetaspaceSize)
神秘的 MaxDirectMemorySize 默認(rèn)值

MaxDirectMemorySize 默認(rèn)值淫半,https://docs.oracle.com/javase/8/docs/technotes/tools/unix/java.html 如事說:

Sets the maximum total size (in bytes) of the New I/O (the java.nio package) direct-buffer allocations. Append the letter k or K to indicate kilobytes, m or M to indicate megabytes, g or G to indicate gigabytes. By 默認(rèn), the size is set to 0, meaning that the JVM chooses the size for NIO direct-buffer allocations automatically.

意思就是說了等于沒說 ??。

在我的測試環(huán)境中, 我使用 Arthas attached 到 JVM 然后查看內(nèi)部的靜態(tài)變量:

[arthas@112]$ dashboard
ognl -c 30367620 '@io.netty.util.internal.PlatformDependent@maxDirectMemory()'
@Long[3,221,225,472]

ognl '@java.nio.Bits@maxMemory'
@Long[3,221,225,472]

3221225472/1024/1024 = 3072.0 Mb

如果你想深入匣砖,請參考資料:

MaxDirectMemorySize ~= `from -Xmx (Young Heap + Old Heap )` - `Survivor(Young) Capacity` ~= 3G
maxThreadCount 最大線程數(shù)來源

既然上面用了 Arthas 科吭, 下面學(xué)是繼續(xù) Arthas 吧:

[arthas@112]$ dashboard
   Threads Total: 276

應(yīng)用使用的是 Jetty, 線程池配置 jetty-threadpool.xml

<Configure>
  <New id="threadPool" class="org.eclipse.jetty.util.thread.QueuedThreadPool">
    <Set name="maxThreads" type="int"><Property name="jetty.threadPool.maxThreads" deprecated="threads.max" default="200"/></Set>
...
  </New>
</Configure>

因?yàn)槌?Jetty猴鲫,還有其它各種線程对人。

使用量

Java 的視角看使用量

容量配置 生效配置(Mbyte) 實(shí)際使用(Mbyte)
Young Heap + Old Heap -Xmx3G -XX:+AlwaysPreTouch 3072 3072
MaxMetaspaceSize [默認(rèn)] Unlimited 128
CompressedClassSpaceSize [默認(rèn)] 1024 15
MaxDirectMemorySize [默認(rèn)] 3072 270
ReservedCodeCacheSize [默認(rèn)] 240 82
ThreadStackSize*maxThreadCount [默認(rèn)]*276線程 276 276
Sum 7684 + (沒限制 MaxMetaspaceSize) 3843

如何采集實(shí)際使用量

  • ReservedCodeCache

在應(yīng)用經(jīng)過熱身、壓力測試之后拂共,用 Arthas attached:

[arthas@112]$ dashboard
code_cache : 82Mb
  • DirectMemory
[arthas@112]$ 
ognl '@java.nio.Bits@reservedMemory.get()'
@Long[1,524,039]
ognl -c 30367620 '@io.netty.util.internal.PlatformDependent@usedDirectMemory()'
@Long[268,435,456]
  • Metaspace
  • CompressedClassSpaceSize
$ jcmd $PID GC.heap_info

 garbage-first heap   total 3145728K, used 1079227K [0x0000000700000000, 0x0000000700106000, 0x00000007c0000000)
  region size 1024K, 698 young (714752K), 16 survivors (16384K)
 Metaspace       used 127,323K, capacity 132,290K, committed 132,864K, reserved 1,167,360K
  class space    used 14,890K, capacity 15,785K, committed 15,872K, reserved 1,048,576K

原生應(yīng)用的視角看使用量

原生應(yīng)用的視角看使用量牺弄,包括下面這個(gè)方面:

  • *lib.so 動態(tài)庫占用: 16Mb
  • *.jar 文件映射占用: 8Mb
  • GC 算法消耗: 未調(diào)查
  • glibc malloc 空間回收不及時(shí)消耗: 158Mb

總的原生應(yīng)用消耗: 16+8+158 = 182Mb

小結(jié)一下:
Java 角度看使用量: 3843Mb
總應(yīng)用使用量 = 3843 + 158 ~= 4001Mb

4001Mb,這里我們沒有算 *lib.so 動態(tài)庫占用*.jar 文件映射占用宜狐。為什么势告?將在下面內(nèi)容中作出解釋。
4001Mb 這個(gè)數(shù)字有點(diǎn)可怕抚恒,離容器配置的上限 4096Mb 不遠(yuǎn)了咱台。但這個(gè)數(shù)字有一定水分。為什么俭驮?將在下面內(nèi)容中作出解釋回溺。

以下我嘗試分析每個(gè)子項(xiàng)的數(shù)據(jù)來源

*lib.so 動態(tài)庫占用

運(yùn)行命令:

pmap -X $PID

部分輸出:

         Address Perm   Offset Device      Inode     Size     Rss     Pss Referenced Anonymous  Mapping
...
    7f281b1b1000 r-xp 00000000  08:03 1243611251       48      48       3         48         0  /lib64/libcrypt-2.26.so
    7f281b1bd000 ---p 0000c000  08:03 1243611251     2044       0       0          0         0  /lib64/libcrypt-2.26.so
    7f281b3bc000 r--p 0000b000  08:03 1243611251        4       4       4          4         4  /lib64/libcrypt-2.26.so
    7f281b3bd000 rw-p 0000c000  08:03 1243611251        4       4       4          4         4  /lib64/libcrypt-2.26.so
...
    7f28775a5000 r-xp 00000000  08:03 1243611255       92      92       5         92         0  /lib64/libgcc_s.so.1
    7f28775bc000 ---p 00017000  08:03 1243611255     2048       0       0          0         0  /lib64/libgcc_s.so.1
    7f28777bc000 r--p 00017000  08:03 1243611255        4       4       4          4         4  /lib64/libgcc_s.so.1
    7f28777bd000 rw-p 00018000  08:03 1243611255        4       4       4          4         4  /lib64/libgcc_s.so.1
    7f28777be000 r-xp 00000000  08:03 1800445487      224      64       4         64         0  /opt/jdk1.8.0_261/jre/lib/amd64/libsunec.so
    7f28777f6000 ---p 00038000  08:03 1800445487     2044       0       0          0         0  /opt/jdk1.8.0_261/jre/lib/amd64/libsunec.so
    7f28779f5000 r--p 00037000  08:03 1800445487       20      20      20         20        20  /opt/jdk1.8.0_261/jre/lib/amd64/libsunec.so
    7f28779fa000 rw-p 0003c000  08:03 1800445487        8       8       8          8         8  /opt/jdk1.8.0_261/jre/lib/amd64/libsunec.so
...
    7f28f43a7000 r-xp 00000000  08:03 1243611284       76      76       3         76         0  /lib64/libresolv-2.26.so
    7f28f43ba000 ---p 00013000  08:03 1243611284     2048       0       0          0         0  /lib64/libresolv-2.26.so
    7f28f45ba000 r--p 00013000  08:03 1243611284        4       4       4          4         4  /lib64/libresolv-2.26.so
    7f28f45bb000 rw-p 00014000  08:03 1243611284        4       4       4          4         4  /lib64/libresolv-2.26.so
    7f28f45bc000 rw-p 00000000  00:00          0        8       0       0          0         0  
    7f28f45be000 r-xp 00000000  08:03 1243611272       20      20       1         20         0  /lib64/libnss_dns-2.26.so
    7f28f45c3000 ---p 00005000  08:03 1243611272     2044       0       0          0         0  /lib64/libnss_dns-2.26.so
    7f28f47c2000 r--p 00004000  08:03 1243611272        4       4       4          4         4  /lib64/libnss_dns-2.26.so
    7f28f47c3000 rw-p 00005000  08:03 1243611272        4       4       4          4         4  /lib64/libnss_dns-2.26.so
    7f28f47c4000 r-xp 00000000  08:03 1243611274       48      48       2         48         0  /lib64/libnss_files-2.26.so
    7f28f47d0000 ---p 0000c000  08:03 1243611274     2044       0       0          0         0  /lib64/libnss_files-2.26.so
    7f28f49cf000 r--p 0000b000  08:03 1243611274        4       4       4          4         4  /lib64/libnss_files-2.26.so
    7f28f49d0000 rw-p 0000c000  08:03 1243611274        4       4       4          4         4  /lib64/libnss_files-2.26.so
    7f28f49d1000 rw-p 00000000  00:00          0     2072    2048    2048       2048      2048  
    7f28f4bd7000 r-xp 00000000  08:03 1800445476       88      88       6         88         0  /opt/jdk1.8.0_261/jre/lib/amd64/libnet.so
    7f28f4bed000 ---p 00016000  08:03 1800445476     2044       0       0          0         0  /opt/jdk1.8.0_261/jre/lib/amd64/libnet.so
    7f28f4dec000 r--p 00015000  08:03 1800445476        4       4       4          4         4  /opt/jdk1.8.0_261/jre/lib/amd64/libnet.so
    7f28f4ded000 rw-p 00016000  08:03 1800445476        4       4       4          4         4  /opt/jdk1.8.0_261/jre/lib/amd64/libnet.so
    7f28f4dee000 r-xp 00000000  08:03 1800445477       68      64       4         64         0  /opt/jdk1.8.0_261/jre/lib/amd64/libnio.so
    7f28f4dff000 ---p 00011000  08:03 1800445477     2044       0       0          0         0  /opt/jdk1.8.0_261/jre/lib/amd64/libnio.so
    7f28f4ffe000 r--p 00010000  08:03 1800445477        4       4       4          4         4  /opt/jdk1.8.0_261/jre/lib/amd64/libnio.so
    7f28f4fff000 rw-p 00011000  08:03 1800445477        4       4       4          4         4  /opt/jdk1.8.0_261/jre/lib/amd64/libnio.so

?? 如果你不太了解 Linux 的 memory map 和 pmap 的輸出,建議閱讀: https://www.labcorner.de/cheat-sheet-understanding-the-pmap1-output/
如果你懶惰如我遗遵,我還是上個(gè)圖吧:

大家知道萍恕,現(xiàn)代操作系統(tǒng)都有進(jìn)程間共享物理內(nèi)存的機(jī)制,以節(jié)省物理內(nèi)存车要。如果你了解COW(Copy on Write)就更好了雄坪。一臺物理機(jī)上,運(yùn)行著多個(gè)容器屯蹦,而容器的鏡像其實(shí)是分層的维哈。對于同一個(gè)機(jī)構(gòu)生成的不同服務(wù)的鏡像,很多時(shí)候是會基于同一個(gè)基礎(chǔ)層登澜,而這個(gè)基礎(chǔ)層包括是 Java 的相關(guān)庫阔挠。而所謂的層不過是主機(jī)上的目錄。即不同容器可能會共享讀(Mapping)同一文件脑蠕。

回到我們的主題购撼,內(nèi)存限制。容器通過 cgroup 限制內(nèi)存谴仙。而 cgroup 會記賬容器內(nèi)進(jìn)程的每一次內(nèi)存分配迂求。而文件映射共享內(nèi)存的計(jì)算方法顯然要特別處理,因?yàn)榭缌诉M(jìn)程和容器』味澹現(xiàn)在能查到的資料是說揩局,只有第一個(gè)讀/寫這塊 mapping 內(nèi)存的 cgroup 才記賬(https://www.kernel.org/doc/Documentation/cgroup-v1/memory.txt 中 [2.3 Shared Page Accounting])。所以這個(gè)賬比較難預(yù)計(jì)的掀虎,一般我們只做再壞情況的保留凌盯。

*.jar mapping 占用

pmap -X $PID

記賬原理和上面的 *.so 類似。不過 Java 9 后烹玉,就不再做 *.jar mapping 了驰怎。就算是 Java 8 ,也只是 mapping 文件中的目錄結(jié)構(gòu)部分二打。

在我的測試中县忌,只使用了 8Mb 內(nèi)存.

glibc malloc 消耗

Java 在兩種情況下使用 glibc malloc:

  1. NIO Direct Byte Buffer / Netty Direct Byte Buffer
  2. JVM 內(nèi)部基礎(chǔ)程序

業(yè)界對 glibc malloc 的浪費(fèi)頗有微詞. 主要集中在不及時(shí)的內(nèi)存歸還(給操作系統(tǒng))。這種浪費(fèi)和主機(jī)的 CPU 數(shù)成比例继效,可參考:

不幸的是症杏,我的測試環(huán)境是祼機(jī),所有 CPU 都給容器看到了莲趣。而主機(jī)是 80 個(gè) CPU 的鸳慈。那么問題來了,如何測量浪費(fèi)了多少喧伞?
glibc 提供了一個(gè) malloc_stats(3) 函數(shù)走芋,它會輸出堆信息(包括使用和保留)到標(biāo)準(zhǔn)輸出流绩郎。那么問題又來了。如果調(diào)用這個(gè)函數(shù)翁逞?修改代碼肋杖,寫JNI嗎?當(dāng)然可以挖函。不過状植,作為一個(gè) Geek,當(dāng)然要使用 gdb 怨喘。

cat <<"EOF" > ~/.gdbinit
handle SIGSEGV nostop noprint pass
handle SIGBUS nostop noprint pass
handle SIGFPE nostop noprint pass
handle SIGPIPE nostop noprint pass
handle SIGILL nostop noprint pass
EOF

export PID=`pgrep java`
gdb --batch --pid $PID --ex 'call malloc_stats()'

輸出:

Arena 0:
system bytes     =     135168
in use bytes     =      89712
Arena 1:
system bytes     =     135168
in use bytes     =       2224
Arena 2:
system bytes     =     319488
in use bytes     =      24960
Arena 3:
system bytes     =     249856
in use bytes     =       2992
...
Arena 270:
system bytes     =    1462272
in use bytes     =     583280
Arena 271:
system bytes     =   67661824
in use bytes     =   61308192


Total (incl. mmap):
system bytes     =  638345216
in use bytes     =  472750720
max mmap regions =         45
max mmap bytes   =  343977984

所以結(jié)果是: 638345216 - 472750720 = 165594496 ~= 158Mb
即浪費(fèi)了 158Mb津畸。因?yàn)槲覝y試場景負(fù)載不大,在負(fù)載大必怜,并發(fā)大的場景下肉拓,80個(gè)CPU 的浪費(fèi)遠(yuǎn)不止這樣。

有一點(diǎn)需要指出的梳庆,操作系統(tǒng)物理內(nèi)存分配是 Lazy 分配的暖途,即只在實(shí)際讀寫內(nèi)存時(shí),才分配膏执,所以驻售,上面的 158Mb 從操作系統(tǒng)的 RSS 來看,可能會變小更米。

GC 內(nèi)存消耗

未調(diào)查

tmpfs 內(nèi)存消耗

未調(diào)查

操作系統(tǒng) RSS

RSS(pmap -X $PID) = 3920MB欺栗。即操作系統(tǒng)認(rèn)為使用了 3920MB 的物理內(nèi)存。

CGroup 限制

cgroup limit 4Gi = 4*1024Mb = 4096Mb
pagecache 可用空間 : 4096 - 3920 = 176Mb

下面看看 cgroup 的 memory.stat 文件

$ cat cgroup `memory.stat` file
    rss 3920Mb
    cache 272Mb
    active_anon 3740Mb
    inactive_file 203Mb
    active_file 72Mb  # bytes of file-backed memory on active LRU list

細(xì)心如你會發(fā)現(xiàn):

3920 + 272 = 4192 > 4096Mb

不對啊壳快,為何還不 OOM killed?

說來話長纸巷, pagecache 是塊有彈性的內(nèi)存空間,當(dāng)應(yīng)用需要 anonymous 內(nèi)存時(shí)眶痰,內(nèi)核可以自動回收 pagecache.

?? 感興趣可參考:
https://engineering.linkedin.com/blog/2016/08/don_t-let-linux-control-groups-uncontrolled
https://github.com/kubernetes/kubernetes/issues/43916
https://www.kernel.org/doc/html/latest/admin-guide/cgroup-v1/memory.html

潛在問題和推薦解決方法

Native Buffer 限制

默認(rèn) MaxDirectMemorySize ~= -Xmx - survivor size ~= 3G .

這在高并發(fā)時(shí),內(nèi)存得不到及時(shí)回收時(shí)梯啤,會使用大量的 Direct Byte Buffer竖伯。所以建議顯式設(shè)置限制:

java ... -XX:MaxDirectMemorySize=350Mb

?? 感興趣可參考:

  • Cassandra 客戶端和 Redisson 均基于 Netty,固均使用了 Native Buffer. 注意的是 NettyUnsafe.class 基礎(chǔ)上因宇,還有內(nèi)部的內(nèi)存池七婴。

glibc malloc arena 的浪費(fèi)

在我的測試環(huán)境中,主機(jī)有 80 個(gè)CPU察滑。glibc 為了減少多線程分配內(nèi)存時(shí)的鎖競爭打厘,在高并發(fā)時(shí)最多為每個(gè) CPU 保留 8 個(gè)內(nèi)存塊(Arena),而 Arena 的空間歸還給操作系統(tǒng)的時(shí)機(jī)是不可預(yù)期的贺辰,和堆中內(nèi)存碎片等情況有關(guān)户盯。
在我的測試環(huán)境中觀察的結(jié)果是:共創(chuàng)建了 271 個(gè)Arena嵌施。使用了 608Mb 的 RSS。而實(shí)際程序用到的內(nèi)存只有 450Mb莽鸭。浪費(fèi)了 157 Mb吗伤。浪費(fèi)的情況有隨機(jī)性,和內(nèi)存碎片等情況有關(guān)硫眨。對于容器足淆,我們不可能分配所有主機(jī)的 CPU〗父螅可以設(shè)置一個(gè)顯式上限是合理的巧号,且這個(gè)上限和容器的 memory limit、CPU limit 應(yīng)該聯(lián)動姥闭。

MALLOC_ARENA_MAX 這個(gè)環(huán)境變量就是用于配置這個(gè)上限的裂逐。

  • 和內(nèi)存使用的聯(lián)系:
    我們實(shí)測中,共使用了 700Mb glibc 堆內(nèi)存. 而每個(gè) Arena 大小為 64Mb. 所以:
700/64=10 Arena
  • 和容器 cpu limit 的聯(lián)系:
8 cpu * (每個(gè)cpu 8 arena) = 64 Arena.

我們保守地使用大的保留空間:

export MALLOC_ARENA_MAX=64

?? 感興趣可參考:
https://www.gnu.org/software/libc/manual/html_node/Memory-Allocation-Tunables.html

Jetty 線程池

經(jīng)調(diào)查泣栈,每 API 的調(diào)用用時(shí)大約 100 ms卜高。而現(xiàn)有配置指定了最大 200 個(gè)線程。所以:

200 thread / 0.1s = 2000 TPS

在我們的測試中南片,單容器的 TPS 不出 1000掺涛。所以 100 個(gè)線程足以。減少線程數(shù)的好處是疼进,可以同時(shí)可以減少過度的線程上下文切換薪缆、cgroup CPU 限流(cpu throttling)、線程堆棧內(nèi)存伞广、Native Buffer 內(nèi)存拣帽。讓請求堆在 Request Queue,而不是內(nèi)核的 Runnale Queue嚼锄。

<!-- jetty-threadpool.xml -->
<Configure>
  <New id="threadPool" class="org.eclipse.jetty.util.thread.QueuedThreadPool">
...
    <Set name="maxThreads" type="int"><Property name="jetty.threadPool.maxThreads" deprecated="threads.max" default="100"/></Set>
...
  </New>
</Configure>

Java code cache 慢漲

在我們測試中减拭,在經(jīng)過系統(tǒng)預(yù)熱后,Java code cache 仍然會慢漲区丑。Java 8 的 code cache 最大值是 240Mb拧粪。 如果 code cache 消耗了大量的內(nèi)存,可能會觸發(fā) OOM killed沧侥。 所以還是要作顯式限制的可霎。 從測試環(huán)境的觀察,100Mb 的空間已經(jīng)足夠宴杀。

java ... -XX:ReservedCodeCacheSize=100M -XX:UseCodeCacheFlushing=true

?? 感興趣可參考:
https://docs.oracle.com/javase/8/embedded/develop-apps-platforms/codecache.htm

容器的內(nèi)存限制

從上面的調(diào)查可知癣朗, 3G java heap + JVM overhead + DirectByteBuffer 已經(jīng)很接近 4Gi 的容器內(nèi)存上限了。在高并發(fā)情況下旺罢,OOM killed 風(fēng)險(xiǎn)還是很高的旷余。而且這個(gè)問題在測試環(huán)境不一定能出現(xiàn)绢记,有它的隨機(jī)性。

cgroup 對容器接近 OOM 的次數(shù)是有記錄(memory.failcnt)的荣暮,在測試時(shí)發(fā)現(xiàn)這個(gè)數(shù)字在慢張庭惜。在內(nèi)存緊張的時(shí)候,內(nèi)核通過丟棄文件緩存(pagecache)來優(yōu)先滿足應(yīng)用對內(nèi)存的需求穗酥。而丟棄文件緩存意味什么护赊?更慢的讀,更頻繁和慢的寫硬盤砾跃。如果應(yīng)用有讀寫IO壓力骏啰,如果讀 *.jar,寫日志抽高,那么 IO 慢問題會隨之而來判耕。

watch cat ./memory.failcnt 
19369

?? 感興趣可參考:
https://engineering.linkedin.com/blog/2016/08/don_t-let-linux-control-groups-uncontrolled
https://www.kernel.org/doc/Documentation/cgroup-v1/memory.txt
https://srvaroa.github.io/jvm/kubernetes/memory/docker/oomkiller/2019/05/29/k8s-and-java.html

對于我的應(yīng)用,我建議是放寬內(nèi)存限制:

    resources:
      limits:
        memory: 4.5Gi
      requests:
        memory: 4.5Gi

展望

不全面地說翘骂,從服務(wù)運(yùn)維者的角度看, 服務(wù)的資源分配基于這些系數(shù):

  • 容器的 SLA
    • 目標(biāo)容器的呑吐量

如我把上面系數(shù)作為一個(gè)工具程序的 輸入, 那么 輸出 應(yīng)該是:

  • 應(yīng)該部署多少個(gè)容器
  • 每個(gè)容器的資源配置應(yīng)該如何
    • CPU
      • 容器 CPU limit
      • 應(yīng)用線程池 limit
    • Memory
      • 容器 memory limit
      • 應(yīng)用線程池d limit:
        • java: 堆內(nèi)/堆外

?? 有一個(gè)開源工具可參考:
https://github.com/cloudfoundry/java-buildpack-memory-calculator

免責(zé)聲明

Every coin has two sides壁熄, 應(yīng)用調(diào)優(yōu)更是,每種調(diào)優(yōu)方法均有其所需要的環(huán)境前提碳竟,不然就不叫調(diào)優(yōu)草丧,直接上開源項(xiàng)目的默認(rèn)配置 Pull Request 了。大師常說莹桅,不要簡單 copy 調(diào)參就用踩寇。要考慮自己的實(shí)際情況聋迎,然后作充分測試方可使用。

體會

2016 年開始头滔,各大公司開始追趕時(shí)尚啡莉,把應(yīng)該的應(yīng)用放入容器胰耗。而由于很多舊項(xiàng)目和組件在設(shè)計(jì)時(shí)秧饮,沒考慮在一個(gè)受限容器中運(yùn)行巧还,說白了,就是非 contaier aware娶桦。時(shí)隔數(shù)年贾节,情況有所好轉(zhuǎn),但還是有不少坑衷畦。而作為一個(gè)合格的架構(gòu)師,除了 PPT 和遠(yuǎn)方外知牌,我們還得有個(gè)玻璃心祈争。

以上是對一個(gè) Java 容器內(nèi)存的分析,如果你對 Java 容器 CPU和線程參數(shù)有興趣角寸,請移步:Java 容器化的歷史坑(史坑) - 資源限制篇菩混。

用一個(gè)漫畫了結(jié)本文:


?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末忿墅,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子沮峡,更是在濱河造成了極大的恐慌疚脐,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,941評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件邢疙,死亡現(xiàn)場離奇詭異棍弄,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)疟游,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,397評論 3 395
  • 文/潘曉璐 我一進(jìn)店門呼畸,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人颁虐,你說我怎么就攤上這事蛮原。” “怎么了另绩?”我有些...
    開封第一講書人閱讀 165,345評論 0 356
  • 文/不壞的土叔 我叫張陵儒陨,是天一觀的道長。 經(jīng)常有香客問我笋籽,道長蹦漠,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,851評論 1 295
  • 正文 為了忘掉前任干签,我火速辦了婚禮津辩,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘容劳。我一直安慰自己喘沿,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,868評論 6 392
  • 文/花漫 我一把揭開白布竭贩。 她就那樣靜靜地躺著蚜印,像睡著了一般。 火紅的嫁衣襯著肌膚如雪留量。 梳的紋絲不亂的頭發(fā)上窄赋,一...
    開封第一講書人閱讀 51,688評論 1 305
  • 那天,我揣著相機(jī)與錄音楼熄,去河邊找鬼忆绰。 笑死,一個(gè)胖子當(dāng)著我的面吹牛可岂,可吹牛的內(nèi)容都是我干的错敢。 我是一名探鬼主播,決...
    沈念sama閱讀 40,414評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼缕粹,長吁一口氣:“原來是場噩夢啊……” “哼稚茅!你這毒婦竟也來了纸淮?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,319評論 0 276
  • 序言:老撾萬榮一對情侶失蹤亚享,失蹤者是張志新(化名)和其女友劉穎咽块,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體欺税,經(jīng)...
    沈念sama閱讀 45,775評論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡侈沪,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,945評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了魄衅。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片峭竣。...
    茶點(diǎn)故事閱讀 40,096評論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖晃虫,靈堂內(nèi)的尸體忽然破棺而出皆撩,到底是詐尸還是另有隱情,我是刑警寧澤哲银,帶...
    沈念sama閱讀 35,789評論 5 346
  • 正文 年R本政府宣布扛吞,位于F島的核電站,受9級特大地震影響荆责,放射性物質(zhì)發(fā)生泄漏滥比。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,437評論 3 331
  • 文/蒙蒙 一做院、第九天 我趴在偏房一處隱蔽的房頂上張望盲泛。 院中可真熱鬧,春花似錦键耕、人聲如沸寺滚。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,993評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽村视。三九已至,卻和暖如春酒奶,著一層夾襖步出監(jiān)牢的瞬間蚁孔,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,107評論 1 271
  • 我被黑心中介騙來泰國打工惋嚎, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留杠氢,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,308評論 3 372
  • 正文 我出身青樓另伍,卻偏偏與公主長得像修然,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子质况,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,037評論 2 355

推薦閱讀更多精彩內(nèi)容