JavaScript 操作符 Optional chaining (?.)

今天查一个问题的时候, 去reivew 别人最近提交的代码, 发现有下面这个改动, 感觉很奇怪, 怎么去掉了为空的短路判断并且中间加了一个问号? 很是疑惑. 难道提交之前碰到键盘了?
diff.png
再想想, 如果说这样, 本地测试应该就不会过吧, 于是开始怀疑自己. 果不其然, JavaScript 真的有这种操作符.



- 阅读剩余部分 -

通过ldd找到依赖的共享库

经常做docker image, 就会考虑到底用哪个base image 最好? 是用 scratch, 还是用 busybox? 还是用 alpine? 这不仅仅关乎image 的大小, 还关乎带了哪些软件? 是不是经常要打漏洞的patch 等. 在一次尝试中, 忘记这些最精简的 image 中可能连最基本的glibc 都没有, 直接复制可执行文件上去, 发现无法运行, 于是开始研究一下到底缺少了哪些运行时库, 于是开始了研究ldd.


一开始, 我们做了一个最简单打印 hello world 的 c 代码, hello.c 如下:

#include<stdio.h>

int main(int argc, char* argv[]) {
    printf("hello  %s\n", "world");
}

然后编译 并本地运行:

$ gcc -o hello hello.c
$ ./hello
hello  world

开始制作 docker image, Dockerfile 内容:

FROM scratch
COPY ./hello /
CMD ["/hello"]

制作 docker image 的命令:

$ docker build -t helloimage .
Sending build context to Docker daemon  25.09kB
Step 1/3 : FROM scratch
 --->
Step 2/3 : COPY ./hello /
 ---> d86014f25f94
Step 3/3 : CMD ["/hello"]
 ---> Running in ca6e160bbde4
Removing intermediate container ca6e160bbde4
 ---> 51874d5170b3
Successfully built 51874d5170b3
Successfully tagged helloimage:latest

然后运行这个新的image:

docker run --rm helloimage
exec /hello: no such file or directory

为啥我明明已经复制 hello 这个可执行文件到了跟目录, 为啥还说没这个文件? 于是用 dive 命令去查看image的文件结构:
dive.png
从这个图中, 也明显看到: 该文件就是明明在那里.

这时候突然想到, 也许是它的动态链接库文件不存在, 导致它无法加载, 最终报出这个错误. 那么看看它到底需要什么动态链接库吧:

$ docker run --rm helloimage ldd /hello
docker: Error response from daemon: failed to create shim task: OCI runtime create failed: runc create failed: unable to start container process: exec: "ldd": executable file not found in $PATH: unknown.

奥, 原来这个scratch 啥都没有, ldd 当然也不存在了. 只好本地看看:

$ ldd ./hello
    linux-vdso.so.1 (0x00007ffec9f73000)
    libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f5b7a06b000)
    /lib64/ld-linux-x86-64.so.2 (0x00007f5b7a276000)

本地 ldd 给出了, hello 其实需要3个动态链接库. 第一个 vdso 其实是一个虚拟的动态链接库, 它不 map 到任何磁盘文件, 主要用来给 clib 使用并加速系统性能的. 第二个 libc.so.6 是 c 的lib, 它mapping 到磁盘文件 /lib/x86_64-linux-gnu/libc.so.6. 第三个 ld-linux-x86-64.so.2 其实是链接器本身.

$ ls -lah /lib/x86_64-linux-gnu/libc.so.6
lrwxrwxrwx 1 root root 12 Apr  6  2022 /lib/x86_64-linux-gnu/libc.so.6 -> libc-2.31.so
$ /lib64/ld-linux-x86-64.so.2
Usage: ld.so [OPTION]... EXECUTABLE-FILE [ARGS-FOR-PROGRAM...]

所以, 我们复制过去的 hello 文件缺少2个动态库文件, 导致它无法运行. 那么如何解决呢?
方案一: 静态编译:
先静态编译hello.c 到 staticHello

$ gcc --static -o staticHello hello.c
$ ls
Dockerfile  hello  hello.c  staticHello

然后修改 Dockerfile:

FROM scratch
COPY ./staticHello /
CMD ["/staticHello"]

然后build image, 并运行:

$ docker build -t helloimage .
$ docker run --rm helloimage
hello  world

完美运行.

方案二: 复制这些缺少的动态库文件过去:
首先复制需要的2个动态库到当前目录 (因为build 的context 是当前目录, 其它目录文件它看不到)

cp /lib/x86_64-linux-gnu/libc.so.6 ./
cp /lib64/ld-linux-x86-64.so.2 ./

然后修改 Dockerfile:

FROM scratch
COPY libc.so.6 /lib/x86_64-linux-gnu/
COPY ld-linux-x86-64.so.2 /lib64/
COPY ./hello /
CMD ["/hello"]

接着 build image 并且运行:

docker build -t helloimage .
Sending build context to Docker daemon  3.121MB
Step 1/5 : FROM scratch
 --->
Step 2/5 : COPY libc.so.6 /lib/x86_64-linux-gnu/
 ---> d72d728c639a
Step 3/5 : COPY ld-linux-x86-64.so.2 /lib64/
 ---> dafed808d94e
Step 4/5 : COPY ./hello /
 ---> d90b74b43ead
Step 5/5 : CMD ["/hello"]
 ---> Running in 27d2b82c8d52
Removing intermediate container 27d2b82c8d52
 ---> 1c419a965e87
Successfully built 1c419a965e87
Successfully tagged helloimage:latest
$ docker run --rm helloimage
hello  world

完美运行.

所以, 2种方案都可以解决这个问题.

关于ldd

回过头来, 我们关注一下 ldd. ldd(List Dynamic Dependencies) 显示依赖的共享对象. 比如 ls 命令依赖这些动态库:

$ ldd /bin/ls
    linux-vdso.so.1 (0x00007fff7834a000)
    libselinux.so.1 => /lib/x86_64-linux-gnu/libselinux.so.1 (0x00007fb672c18000)
    libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fb672a26000)
    libpcre2-8.so.0 => /usr/lib/x86_64-linux-gnu/libpcre2-8.so.0 (0x00007fb672995000)
    libdl.so.2 => /lib/x86_64-linux-gnu/libdl.so.2 (0x00007fb67298f000)
    /lib64/ld-linux-x86-64.so.2 (0x00007fb672c7c000)
    libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007fb67296c000)

ldd 是怎么工作的? ldd 调用标准的链接器 ld.so, 同时设置环境变量 LD_TRACE_LOADED_OBJECTS=1, 这样 ld 就会检查所依赖的所有动态库, 并且找到合适的库并且加载到内存. 这样 ldd 就能记录这些被加载到库以及它们在内存的地址. vdso 和 ld.so 是2个特殊的库.

其实 ldd 是一个shell 脚本, 我们能查看它的源文件:

$ which ldd
/usr/bin/ldd
$ file /usr/bin/ldd
/usr/bin/ldd: Bourne-Again shell script, ASCII text executable
$ less /usr/bin/ldd

另外, 可执行文件的依赖库可以通过 objdump 找到:

$ objdump -p hello | grep NEEDED

如何做一个最小的docker image?

当我们想做一个docker image 的时候, 经常想把它做的足够小, 不仅能够节省磁盘空间, 还能减少不必要的依赖, 加快启动, 减少可能出现的漏洞. 那么有哪些最精简的base image 呢?


  1. 如果你的app的所有依赖都是静态编译的, 那么你可以使用 scratch, 顾名思义, 它是一张白纸, 我们只能使用kernel 提供的服务. 它没有shell, 没有libc, 没有各种实用命令, 用户/组, 没有包管理器, 啥都没有.
  2. 如果你 busybox 提供的实用工具已经能满足, 那么你可以使用 busybox. busybox 是嵌入式linux上的瑞士军刀, 它把其它Linux 上常见的一些实用工具在一个很小的可执行文件内全部实现, 虽然选项更少, 但是基本能完成大部分常见功能.
  3. 如果你在 busybox的基础上还需要 libc库和包管理工具(package repo), 那么可以使用 alpine. alpine 在busybox的基础上, 增加了 musl libc 和 包管理器.
  4. 或者你可以尝试一下 https://github.com/GoogleContainerTools/distroless.

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 竟然没有, 可是能用, 还很管用.