在做 Java 程序容器化时都会遇到一个问题,ENTRYPOINT ["java", "$JAVA_OPTS", "-jar", ...]
这样的写法 $JAVA_OPTS
就是个字符串无法在运行时展开。为了不把参数硬编码到容器里,每次调整参数重新构建镜像,可以有多种方案,先介绍几种不够好的方案。
-
ENTRYPOINT java $JAVA_OPTS -jar ...
,这种方式的问题是 java 不是容器主进程(至于为什么要保证 java 是主进程,又是一个话题,是容器化基本最佳实践之一); -
ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -jar ..."]
,这种写法其实等价于上面一种方式,上面一种方式在运行时就是以/bin/sh -c "java $JAVA_OPTS -jar ..."
方式运行的,所以缺陷也是相同的; -
ENTRYPOINT ["entrypoint.sh"]
然后在脚本中启动 java,使用脚本对于需要在启动时做复杂操作的容器比较有用,但是对启动 java 来说未免小题大作,并且同样有 java 不是容器主进程的问题。
从 shell 角度出发,解决非主进程问题的方案是使用 exec
命令,exec
在启动其后参数中的指令时,不会创建子进程而是用指令进程替换自身,使指令进程占用自身的 PID(exec 其后的第一个指令替换了自身之后,后续的其它指令自然也不会被执行了)。于是上面
这三个方案可以改为:
ENTRYPOINT exce java $JAVA_OPTS -jar ...
ENTRYPOINT ["sh", "-c", "exce java $JAVA_OPTS -jar ..."]
- 脚本里写
exec java $JAVA_OPTS -jar ...
使用 exec 可以解决之前的问题,但是随之而来的问题是……丑,任何额外的命令都会破坏整洁,对于追求 clean code 的程序员来说 Dockerfile 也必须是整洁的。还好 java 是一个成熟的生态,其实本身提供了相应的环境变量 JDK_JAVA_OPTIONS
和 JAVA_TOOL_OPTIONS
:
JDK_JAVA_OPTIONS
是在 Java 9 引入的,java 程序启动时不需要在命令行指定就会自动读取的环境变量,它略微有些限制,主要是为了防止滥用不允许使用可能改变主类或者让主类不执行的参数,通常需要指定的内存、GC 等参数都可以使用。遇到不允许使用的参数时 java 会直接报错并退出,所以只要程序顺利启动就不用担心使用了不允许使用的参数。在这里指定的参数无法覆盖命令行的相同参数,需要锁定的配置可以直接指定在 ENTRYPOINT
中。
JAVA_TOOL_OPTIONS
是存在很久的环境变量,这个环境变量同样不需要在命令行显式指定。它名字中的 TOOL 提示了除了 java 命令之外其它 java 工具命令例如 javac 之类的也会去读取这个变量的值。在这里指定的参数既不能覆盖命令行的相同参数,也不能覆盖 JDK_JAVA_OPTIONS
中的相同参数,优先级最低。
除此之外,还有各家专用的一些环境变量,比如 Oracle 家的 _JAVA_OPTIONS、IBM 家的 IBM_JAVA_OPTIONS,它们通常提供了覆盖命令行上相同参数的能力,但是环境变量名却不可移植,在 Xxx as Code 的时代并不是个好选择。
综上所述,可以得出这样的决策路径:
- Java 9 及以上的 java 命令使用
JDK_JAVA_OPTIONS
- CI/CD 或者打包工具之类的非 java 命令时使用
JAVA_TOOL_OPTIONS
- Java 9 以下的 java 命令使用
JAVA_TOOL_OPTIONS
极特殊情况下需要覆盖命令行上的参数时,先反思自己,再反思自己,最后找各家自己定义的环境变量。
最后一个问题,看到这里可能会有疑问,设置环境变量会不会影响到其它 java 进程?如果遵循了容器化的最佳实践,那答案显然是不会,而且即使在主机上,要想多个进程间环境变量互不影响也是很简单的事情不是吗?