2022年10月

Java Metaspace 造成的full GC

老司机 Kyle 又一次问我有没有兴趣查一个问题, 直接扔过来一个app的监控 dashboard, 又是 GC overhead 100%. 不过这次有点曲折.


监控数据如下:
dashboard.png

解读:

  1. GC overhead 100%, 也就是Java 进程占用的CPU 全部用来做GC 了;
  2. JVM CPU usage 从20% 升到了35%, 也就是Java 进程使用的CPU 量增加了很多;
  3. OOM 的数量从0变成了每秒几个;
  4. 有意思的是 Java 的可用heap 从2G 突然变成了5.1G.

背景相关:
这个app 设置的Java heap 是5.5G.

从上面信息大致可以猜测, 可能遇到如下问题:

  1. 大量调用 System.gc() 或 Runtime.gc();
  2. Metaspace (或Java8之前永久代) 用尽;
  3. 或者 Native 内存被用尽;

于是使用 jstat 命令查看 gccause:

$ jdk/bin/jstat -gccause 1075  2000 5
  S0     S1     E      O      M     CCS    YGC     YGCT    FGC    FGCT     GCT    LGCC                 GCC
  0.00 100.00   0.00   4.14  69.08  32.72  48945 2441.067  1848  806.736 3247.803 Metadata GC Threshold Metadata GC Threshold
  0.00   0.00   0.00   4.55  69.08  32.72  48947 2441.076  1853  808.914 3249.990 Metadata GC Threshold Last ditch collection
  0.00 100.00   0.00   5.07  69.08  32.72  48950 2441.092  1858  811.101 3252.193 Metadata GC Threshold Metadata GC Threshold
  0.00 100.00   0.00   4.82  69.08  32.72  48952 2441.102  1862  812.887 3253.989 Metadata GC Threshold Metadata GC Threshold
  0.00   0.00   0.00   3.93  69.08  32.72  48954 2441.111  1867  815.072 3256.183 Metadata GC Threshold Last ditch collection

上面各列名的解释:
S0: Survive 0; S1: Survive 1; E: Eden; O: Old; M: Metaspace; CCS: Compressed class; YGC: Young GC Count; YGCT: Young GC Time; FGC: Full GC Count; FGCT: Full GC Time; GCT: GC Time; LGCC: Last GC Cause; GCC: GC Cause this time.

从上面统计数据可以看到: Full GC(FGC) 的数量在持续的10s 内持续增加, 造成的原因就是: Metadata GC Threshold 和 Last ditch collection.

在解释上面2个原因之前, 我们可以看看 Metaspace 的使用策略. Metaspace 是用来替换之前的永久代的, 并且从Native 内存申请, 用来存放类的元数据. 从操作系统申请的native 内存被分成 chunks, 每个chunk 被分配给每个 ClassLoader, 这个chunk里面的class 如果全部被回收的时候, 这个 chunk 也会被回收. 另外这些Native 内存使用mmap分配, 不是使用malloc.

从网上找来一张好图:
concepts.png

上面图上有几个概念:

  1. reserved: Java 应用虚拟内存里面保留的大小, 这个这是虚拟内存申请量, 和实际使用量没关系;
  2. committed: 从操作系统(OS)里面申请的 native 内存的大小, 被分为若干个不同大小的 chunks;
  3. capacity: 被分配给 ClassLoader 的所有 chunk 内存之和, 不包含没被分配的 free chunk;
  4. chunk: 分成2种: 被分配给classLoader的和未被分配的;
  5. used: 所有被分配的 chunk 里面的使用量之和. 一个被分配的chunk 也是慢慢被使用完的.
    上面图里面没有涉及到的概念:
  6. Metaspace occupied threshold(high watermark), 它一般是committed 的一个百分比, 由 -XX:MaxMetaspaceFreeRatio 和 -XX:MinMetaspaceFreeRatio 这个2个参数决定.

当 ClassLoader 要分配一个新的class空间的时候, 它检查自己拥有的chunk, 如果当前拥有的chunk内部空间不足的时候, 就会去free chunk 里申请一个新的chunk. JVM 内部维护一个metaspace 已经被占用的 threshold (high watermark), 当已经占用的量(capacity) 触及这个 threshold 的时候, 就会触发一个 Full GC (Metadata GC Threshold) GC. 当这个Full GC 还不能回收到足够的空间时, 就会看到 Full GC (Last ditch collection).

verbose GC log 里面的日志:

2022-10-01T18:11:27.974-0700: 407174.877: [Full GC (Metadata GC Threshold) 2022-10-01T18:11:27.974-0700: 407174.877: [GC concurrent-root-region-scan-end, 0.0001638 secs]
2022-10-01T18:11:27.974-0700: 407174.877: [GC concurrent-mark-start]
 189M->189M(5120M), 0.4585377 secs]
   [Eden: 0.0B(390.0M)->0.0B(390.0M) Survivors: 2048.0K->0.0B Heap: 189.4M(5120.0M)->189.4M(5120.0M)], [Metaspace: 362167K->362167K(1525760K)]
Heap after GC invocations=50066 (full 1356):
 garbage-first heap   total 5242880K, used 193982K [0x00000006a0800000, 0x00000006a0a05000, 0x00000007e0800000)
  region size 2048K, 0 young (0K), 0 survivors (0K)
 Metaspace       used 362167K, capacity 367544K, committed 524288K, reserved 1525760K
  class space    used 15775K, capacity 16721K, committed 48228K, reserved 1048576K
}
 [Times: user=0.65 sys=0.01, real=0.46 secs]
{Heap before GC invocations=50066 (full 1356):
 garbage-first heap   total 5242880K, used 193982K [0x00000006a0800000, 0x00000006a0a05000, 0x00000007e0800000)
  region size 2048K, 0 young (0K), 0 survivors (0K)
 Metaspace       used 362167K, capacity 367544K, committed 524288K, reserved 1525760K
  class space    used 15775K, capacity 16721K, committed 48228K, reserved 1048576K
2022-10-01T18:11:28.433-0700: 407175.336: [Full GC (Last ditch collection)  189M->189M(5120M), 0.4538734 secs]

-- 未完待续

mongo express MongoError: command listCollections requires authentication

为了连接一个MongoDB server 省事, 不想装本地app, 于是想使用docker 装一个 Web 版本的 Mongo express. 在启动的时候, 总是报这个错: MongoError: command listCollections requires authentication

我的连接URL是: mongodb://user1:pwd1@mymongo.tianxioahui.com:27017/test_db. 可是根据官方的说明, 不论怎么写 docker command 都不行.

$docker run --rm -e ME_CONFIG_MONGODB_SERVER=mymongo.tianxioahui.com \
                 -e ME_CONFIG_BASICAUTH_USERNAME=user1 \
                 -e ME_CONFIG_BASICAUTH_PASSWORD=pwd1 \
                 -e ME_CONFIG_MONGODB_ENABLE_ADMIN=false \
                 -e ME_CONFIG_MONGODB_AUTH_DATABASE=test_db
                 -p 8083:8081 --name myMongo  mongo-express

(node:6) UnhandledPromiseRejectionWarning: MongoError: command listCollections requires authentication
    at Connection.<anonymous> (/node_modules/mongodb/lib/core/connection/pool.js:453:61)
    at Connection.emit (events.js:314:20)
    at processMessage (/node_modules/mongodb/lib/core/connection/connection.js:456:10)
    at Socket.<anonymous> (/node_modules/mongodb/lib/core/connection/connection.js:625:15)
    at Socket.emit (events.js:314:20)
    at addChunk (_stream_readable.js:297:12)
    at readableAddChunk (_stream_readable.js:272:9)
    at Socket.Readable.push (_stream_readable.js:213:10)
    at TCP.onStreamRead (internal/stream_base_commons.js:188:23)

可是不论怎么调可用的参数, 总是报这个错. Google 了一下, 发现2021年6月就有人报这个错: https://github.com/mongo-express/mongo-express/issues/720

解决方式也很简单, 直接用一个连接URL 替换其他环境变量:

sudo docker run --rm -e ME_CONFIG_MONGODB_URL=mongodb://user1:pwd1@mymongo.tianxioahui.com:27017/test_db  -p 8083:8081 --name myMongo mongo-express

可是, 可是, 这个环境变量ME_CONFIG_MONGODB_URLhttps://hub.docker.com/_/mongo-express 竟然没有, 可是能用, 还很管用.