如何使在Linux / Docker上运行的Java流程简单明了

作为DevOps工程师,我经常致力于在各种环境(从容器到云)中自动化各种IT系统的安装和配置。 我必须使用基于Java堆栈的许多系统:从小型系统(例如Tomcat)到大型系统(Hadoop,Cassandra等)。


而且,由于某种原因,几乎每个这样的系统,即使是最简单的系统,都具有复杂的独特启动系统。 至少,它们是多行shell脚本(例如在Tomcat中) ,甚至是整个框架(在Hadoop中)Nexus OSS 3工件存储库是本系列中我目前启发我写这篇文章的“患者”, 其启动脚本需要约400行代码。


即使在本地系统上手动安装一个组件,启动脚本的不透明性,冗余性和复杂性也会造成问题。 现在想象一下,您需要在Docker容器中打包一组此类组件和服务,沿着适当的编排编写另一层抽象,将其部署在Kubernetes集群中,并将此过程实现为CI / CD管道...


简而言之,让我们看一下提到的Nexus 3的示例,在方便的现代DevOps工具可用的情况下,如何从外壳脚本的迷宫中返回类似于java -jar <program.jar>


这种复杂性从何而来?


简而言之,在远古时代,当问到“ Linux的意义上没有提及UNIX”时,没有提到UNIX,没有Systemd和Docker等,便使用了可移植的shell脚本(初始脚本)和PID-控制过程文件。 初始化脚本会设置必要的环境设置,在不同的UNIX中,这些设置会有所不同,并且根据参数,使用PID文件中的ID来启动进程或重新启动/停止进程。 该方法简单明了,但是这些脚本在每种不寻常的情况下都无法使用,需要手动干预,不允许您运行该过程的多个副本……但并非重点。


因此,如果仔细查看Java项目中上面提到的启动脚本,您会发现这种史前方法的明显迹象,甚至包括提到SunOS,HP-UX和其他UNIX。 通常,此类脚本会执行以下操作:


  • 使用POSIX shell语法及其所有拐杖来实现UNIX / Linux可移植性
  • 确定操作系统版本并通过uname/etc/*release/etc/*release
  • 他们在文件系统的角落中搜索JRE / JDK,并根据聪明的规则(有时也针对每个操作系统)选择最“合适”的版本
  • 计算JVM数值参数,例如,内存大小( -Xms-Xmx ),GC线程数等。
  • 考虑到所选JRE / JDK版本的细节,通过-XX参数优化JVM
  • 在周围的目录,配置文件等中查找它们的组件,库,它们的路径。
  • 自定义环境:ulimits,环境变量等。
  • 用如下循环生成CLASSPATH: for f in $path/*.jar; do CLASSPATH="${CLASSPATH}:$f"; done for f in $path/*.jar; do CLASSPATH="${CLASSPATH}:$f"; done
  • 解析的命令行参数: start|stop|restart|reload|status|...
  • 从上面编译最终需要执行的Java命令
  • 最后执行此java命令 。 通常,显式或隐式使用相同的臭名昭著的PID文件, &nohup ,特殊的TCP端口和其他上个世纪的技巧(请参阅Karaf中示例

提到的Nexus 3启动脚本就是这种脚本的合适示例。


实际上,上面列出的所有脚本逻辑都试图替换系统管理员,后者将为特定系统从头到尾手动安装和配置所有内容。 但是总的来说,不可能考虑到最多样化系统的任何要求。 因此,恰恰相反,对于需要支持这些脚本的开发人员以及以后需要了解这些脚本的系统工程师而言,这都是令人头疼的问题。 从我的角度来看,对于系统工程师而言,一次了解JVM参数并按需配置它要容易得多,而不是每次安装新系统时都要了解其启动脚本的复杂性。


怎么办


原谅! KISSYAGNI掌握在我们手中。 此外,2018年就在院子里,这意味着:


  • 除极少数例外外,UNIX == Linux
  • 对于单独的服务器( SystemdDocker )和集群( Kubernetes等)都解决了过程控制问题
  • 有很多方便的配置管理工具( Ansible等)
  • 全面的自动化已经进入管理,并且已经彻底固化:不再需要手动设置易碎的独特“雪花服务器”,现在可以使用许多便捷的工具,包括上述的Ansible和Docker,自动组装统一的可复制虚拟机和容器
  • JVM本身( 示例 )和Java应用程序( 示例 )都广泛使用了收集运行时统计信息的工具。
  • 最重要的是,专家出现了:系统工程师和DevOps工程师,他们可以使用上面列出的技术,并且了解如何在特定系统上正确安装JVM,然后根据收集的运行时统计信息对其进行调整。

因此,让我们再次考虑启动脚本的功能,同时考虑到上面列出的要点,而不必尝试为系统工程师做工作,并从那里删除所有“不必要的”脚本。


  • POSIX Shell语法/bin/bash
  • 操作系统版本检测 ⇒UNIX == Linux,如果有特定于操作系统的参数,则可以在文档中进行描述
  • JRE / JDK搜索 ⇒我们拥有唯一的版本,这是OpenJDK(或者,如果您确实需要,则为Oracle JDK), java和company在标准系统路径中
  • 计算JVM的数值参数,调整JVM ⇒这可以在应用程序扩展文档中描述
  • 搜索您的组件和库 ⇒在文档中描述应用程序的结构以及如何配置它
  • 环境设定 ⇒在文档中描述要求和功能
  • CLASSPATH生成 ⇒- -cp path/to/my/jars/*甚至通常是Uber-JAR
  • 解析命令行参数 ⇒没有论点,因为 流程经理将负责启动以外的所有工作
  • Java命令汇编
  • Java命令执行

结果,我们只需要使用选定的进程管理器(Systemd,Docker等)来组装并执行java <opts> -jar <program.jar>形式的Java命令。 所有参数和选项( <opts> )均由系统工程师自行决定,系统工程师将根据具体环境进行调整。 如果选项<opts>的列表很长,则可以再次返回启动脚本的概念,但是在这种情况下,应尽可能紧凑和声明性 ,即 不包含任何软件逻辑。


例子


作为示例,让我们看看如何简化Nexus 3启动脚本


最简单的选择,以免进入该脚本的丛林-仅在实际条件下运行它( ./nexus start ),然后查看结果。 例如,您可以在进程表中找到正在运行的应用程序的完整参数列表(通过ps -ef ),或者以调试模式运行脚本( bash -x ./nexus start )以观察其执行的整个过程,并在最后观察启动命令。


我结束了下面的Java命令
 /usr/java/jdk1.8.0_171-amd64/bin/java -server -Dinstall4j.jvmDir=/usr/java/jdk1.8.0_171-amd64 -Dexe4j.moduleName=/home/nexus/nexus-3.12.1-01/bin/nexus -XX:+UnlockDiagnosticVMOptions -Dinstall4j.launcherId=245 -Dinstall4j.swt=false -Di4jv=0 -Di4jv=0 -Di4jv=0 -Di4jv=0 -Di4jv=0 -Xms1200M -Xmx1200M -XX:MaxDirectMemorySize=2G -XX:+UnlockDiagnosticVMOptions -XX:+UnsyncloadClass -XX:+LogVMOutput -XX:LogFile=../sonatype-work/nexus3/log/jvm.log -XX:-OmitStackTraceInFastThrow -Djava.net.preferIPv4Stack=true -Dkaraf.home=. -Dkaraf.base=. -Dkaraf.etc=etc/karaf -Djava.util.logging.config.file=etc/karaf/java.util.logging.properties -Dkaraf.data=../sonatype-work/nexus3 -Djava.io.tmpdir=../sonatype-work/nexus3/tmp -Dkaraf.startLocalConsole=false -Di4j.vpt=true -classpath /home/nexus/nexus-3.12.1-01/.install4j/i4jruntime.jar:/home/nexus/nexus-3.12.1-01/lib/boot/nexus-main.jar:/home/nexus/nexus-3.12.1-01/lib/boot/org.apache.karaf.main-4.0.9.jar:/home/nexus/nexus-3.12.1-01/lib/boot/org.osgi.core-6.0.0.jar:/home/nexus/nexus-3.12.1-01/lib/boot/org.apache.karaf.diagnostic.boot-4.0.9.jar:/home/nexus/nexus-3.12.1-01/lib/boot/org.apache.karaf.jaas.boot-4.0.9.jar com.install4j.runtime.launcher.UnixLauncher start 9d17dc87 '' '' org.sonatype.nexus.karaf.NexusMain 

首先,对其应用一些简单的技巧:


  • /the/long/and/winding/road/to/my/javajava ,因为它在系统路径中
  • 将Java参数列表放在单独的数组中 ,对其进行排序并删除重复项

我们已经有了一些更容易消化的东西
 JAVA_OPTS = ( '-server' '-Dexe4j.moduleName=/home/nexus/nexus-3.12.1-01/bin/nexus' '-Di4j.vpt=true' '-Di4jv=0' '-Dinstall4j.jvmDir=/usr/java/jdk1.8.0_171-amd64' '-Dinstall4j.launcherId=245' '-Dinstall4j.swt=false' '-Djava.io.tmpdir=../sonatype-work/nexus3/tmp' '-Djava.net.preferIPv4Stack=true' '-Djava.util.logging.config.file=etc/karaf/java.util.logging.properties' '-Dkaraf.base=.' '-Dkaraf.data=../sonatype-work/nexus3' '-Dkaraf.etc=etc/karaf' '-Dkaraf.home=.' '-Dkaraf.startLocalConsole=false' '-XX:+LogVMOutput' '-XX:+UnlockDiagnosticVMOptions' '-XX:+UnlockDiagnosticVMOptions' '-XX:+UnsyncloadClass' '-XX:-OmitStackTraceInFastThrow' '-XX:LogFile=../sonatype-work/nexus3/log/jvm.log' '-XX:MaxDirectMemorySize=2G' '-Xms1200M' '-Xmx1200M' '-classpath /home/nexus/nexus-3.12.1-01/.install4j/i4jruntime.jar:/home/nexus/nexus-3.12.1-01/lib/boot/nexus-main.jar:/home/nexus/nexus-3.12.1-01/lib/boot/org.apache.karaf.main-4.0.9.jar:/home/nexus/nexus-3.12.1-01/lib/boot/org.osgi.core-6.0.0.jar:/home/nexus/nexus-3.12.1-01/lib/boot/org.apache.karaf.diagnostic.boot-4.0.9.jar:/home/nexus/nexus-3.12.1-01/lib/boot/' ) java ${JAVA_OPTS[*]} com.install4j.runtime.launcher.UnixLauncher start 9d17dc87 '' '' org.sonatype.nexus.karaf.NexusMain 

现在您可以深入了解。


Install4j是这样的图形Java安装程序。 它似乎用于系统的初始安装。 我们不需要服务器上的它,我们将其删除。


我们同意在文件系统上放置Nexus组件和数据:


  • 将应用程序本身放在/opt/nexus-<version>
  • 为了方便起见,创建一个符号链接/opt/nexus -> /opt/nexus-<version>
  • 将脚本本身而不是原始脚本放置为/opt/nexus/bin/nexus
  • Nexus的所有数据都位于一个单独的文件系统上,该文件系统安装为/data/nexus

目录和链接的创建是配置管理系统的命运(对于Ansible中所有5-10行的所有内容),因此让我们将这项任务留给系统工程师来完成。


让我们的脚本在启动时将工作目录更改为/opt/nexus然后我们可以将Nexus组件的路径更改为相对的路径。


格式为-Dkaraf.*选项-Dkaraf.* Apache Karaf的设置, Apache Karf是Nexus“明显地”包装在其中的OSGi容器。 根据组件的位置更改karaf.homekaraf.basekaraf.etckaraf.base ,并karaf.data使用相对路径。


看到CLASSPATH由位于同一lib/目录中的jar文件列表组成,请将整个列表替换为lib/* (您还必须使用set -o noglob关闭通配符扩展)。


exec java更改为exec java以便我们的脚本不会将java作为子进程启动(进程管理器将看不到该子进程),而是将自身替换为javaexec的描述 )。


让我们看看发生了什么:


 #!/bin/bash JAVA_OPTS=( '-Xms1200M' '-Xmx1200M' '-XX:+UnlockDiagnosticVMOptions' '-XX:+LogVMOutput' '-XX:+UnsyncloadClass' '-XX:LogFile=/data/nexus/log/jvm.log' '-XX:MaxDirectMemorySize=2G' '-XX:-OmitStackTraceInFastThrow' '-Djava.io.tmpdir=/data/nexus/tmp' '-Djava.net.preferIPv4Stack=true' '-Djava.util.logging.config.file=etc/karaf/java.util.logging.properties' '-Dkaraf.home=.' '-Dkaraf.base=.' '-Dkaraf.etc=etc/karaf' '-Dkaraf.data=/data/nexus/data' '-Dkaraf.startLocalConsole=false' '-server' '-cp lib/boot/*' ) set -o noglob cd /opt/nexus \ && exec java ${JAVA_OPTS[*]} org.sonatype.nexus.karaf.NexusMain 

一共有27行,而不是> 400行,透明,清晰,声明性,没有不必要的逻辑。 如有必要,可以将该脚本轻松地转换为Ansible / Puppet / Chef的模板,并仅添加特定情况所需的逻辑。


该脚本可用作Dockerfile中的ENTRYPOINT或在Systemd单元文件中调用,同时在其中调整ulimit和其他系统参数,例如:


 [Unit] Description=Nexus After=network.target [Service] Type=simple LimitNOFILE=1048576 ExecStart=/opt/nexus/bin/nexus User=nexus Restart=on-abort [Install] WantedBy=multi-user.target 

结论


从本文可以得出什么结论? 原则上,它可以归结为两点:


  1. 每个系统都有其自己的目的,即不必用显微镜锤打钉子。
  2. 简单性(KISS,YAGNI)规则-仅实现给定特定情况所需的内容。
  3. 最重要的是:很酷,有不同配置文件的IT专家。 让我们进行互动,使我们的IT系统更简单,更清晰,更好! :)

感谢您的关注! 我将很高兴在评论中提供反馈和建设性的讨论。

Source: https://habr.com/ru/post/zh-CN415893/


All Articles