Como ingeniero de DevOps, a menudo trabajo en la automatización de la instalación y configuración de una variedad de sistemas de TI en diversos entornos: desde contenedores hasta la nube. Tuve que trabajar con muchos sistemas basados en la pila de Java: desde pequeños (como Tomcat) hasta a gran escala (Hadoop, Cassandra, etc.).
Además, casi todos esos sistemas, incluso el más simple, por alguna razón tenían un complejo sistema de lanzamiento único. Como mínimo, se trataba de scripts de shell de varias líneas, como en Tomcat , e incluso marcos completos, como en Hadoop . Mi "paciente" actual en esta serie, que me inspiró a escribir este artículo, es el repositorio de artefactos Nexus OSS 3 , cuyo script de lanzamiento toma ~ 400 líneas de código.
La opacidad, la redundancia y la complejidad de los scripts de inicio crean problemas incluso cuando se instala manualmente un componente en el sistema local. Ahora imagine que necesita empaquetar un conjunto de dichos componentes y servicios en un contenedor Docker, escribir otra capa de abstracción siguiendo las líneas de una orquestación adecuada, implementarla en un clúster de Kubernetes e implementar este proceso como una tubería de CI / CD ...
En resumen, veamos el ejemplo del mencionado Nexus 3, cómo regresar del laberinto de scripts de shell a algo más similar a java -jar <program.jar>
, dada la disponibilidad de herramientas DevOps modernas y convenientes.
¿De dónde viene esta complejidad?
En pocas palabras, en la antigüedad, cuando a UNIX no se le volvía a preguntar: "¿en el sentido de Linux?", No había Systemd y Docker y otros, se utilizaron scripts de shell portátiles (scripts de inicio) y PID para controlar los procesos. archivos. Las secuencias de comandos de inicio establecen las configuraciones de entorno necesarias, que eran diferentes en diferentes UNIX, y, dependiendo de los argumentos, iniciaron el proceso o lo reiniciaron / detuvieron usando la ID del archivo PID. El enfoque es simple y claro, pero estos scripts dejaron de funcionar en cualquier situación inusual, requiriendo intervención manual, no le permitieron ejecutar varias copias del proceso ... pero no era el punto.
Por lo tanto, si observa detenidamente los scripts de inicio mencionados anteriormente en los proyectos Java, puede ver los signos obvios de este enfoque prehistórico, incluida incluso la mención de SunOS, HP-UX y otros UNIX. Por lo general, estos scripts hacen algo como esto:
- use la sintaxis de shell POSIX con todas sus muletas para la portabilidad UNIX / Linux
- determinar la versión del sistema operativo y liberarlo a través de
uname
, /etc/*release
, etc. - buscan JRE / JDK en los rincones del sistema de archivos y seleccionan la versión más "adecuada" de acuerdo con reglas inteligentes, a veces también específicas para cada sistema operativo
- Los parámetros numéricos de JVM se calculan, por ejemplo, el tamaño de la memoria (
-Xms
, -Xmx
), el número de subprocesos de GC, etc. - optimice JVM a través de parámetros
-XX
teniendo en cuenta los detalles de la versión seleccionada de JRE / JDK - busque sus componentes, bibliotecas, rutas a ellos en los directorios circundantes, archivos de configuración, etc.
- personalizar el entorno: ulimits, variables de entorno, etc.
- generar CLASSPATH con un bucle como:
for f in $path/*.jar; do CLASSPATH="${CLASSPATH}:$f"; done
for f in $path/*.jar; do CLASSPATH="${CLASSPATH}:$f"; done
- argumentos de línea de comandos analizados:
start|stop|restart|reload|status|...
- compile el comando Java que finalmente necesita ejecutar desde lo anterior
- y finalmente ejecuta este comando java . A menudo, los mismos archivos PID notorios,
&
, no, puertos TCP especiales y otros trucos del siglo pasado se usan explícita o implícitamente (vea el ejemplo de Karaf )
El script de lanzamiento de Nexus 3 mencionado es un ejemplo adecuado de dicho script.
De hecho, toda la lógica de script enumerada anteriormente, por así decirlo, está tratando de reemplazar al administrador del sistema, que instalaría y configuraría todo manualmente para un sistema específico de principio a fin. Pero, en general, es imposible tener en cuenta los requisitos de los sistemas más diversos. Por lo tanto, resulta, por el contrario, un dolor de cabeza, tanto para los desarrolladores que necesitan admitir estos scripts como para los ingenieros de sistemas que necesitan comprender estos scripts más adelante. Desde mi punto de vista, es mucho más fácil para un ingeniero de sistemas comprender los parámetros de JVM una vez y configurarlos como debería, que comprender las complejidades de sus scripts de inicio cada vez que instala un nuevo sistema.
Que hacer
Perdona! KISS y YAGNI están en nuestras manos. Además, el año 2018 está en el patio, lo que significa que:
- con muy pocas excepciones, UNIX == Linux
- El problema de control del proceso se resuelve tanto para un servidor separado ( Systemd , Docker ) como para clústeres ( Kubernetes , etc.)
- Hay un montón de herramientas de administración de configuración convenientes ( Ansible , etc.)
- la automatización total ha llegado a la administración y ya se ha solidificado por completo: en lugar de configurar manualmente "frágiles copos de nieve" únicos y frágiles, ahora es posible ensamblar automáticamente máquinas y contenedores virtuales reproducibles unificados utilizando una serie de herramientas convenientes, incluidas las mencionadas Ansible y Docker
- Las herramientas para recopilar estadísticas de tiempo de ejecución son ampliamente utilizadas, tanto para la propia JVM ( ejemplo ) como para una aplicación Java ( ejemplo )
- y, lo que es más importante, aparecieron expertos: ingenieros de sistemas y DevOps que pueden usar las tecnologías enumeradas anteriormente y comprender cómo instalar correctamente la JVM en un sistema específico y, posteriormente, ajustarlo según las estadísticas de tiempo de ejecución recopiladas
Así que veamos nuevamente la funcionalidad de los scripts de inicio, teniendo en cuenta los puntos enumerados anteriormente, sin intentar hacer el trabajo para el ingeniero del sistema, y eliminemos todos los "innecesarios" de allí.
Sintaxis de shell POSIX ⇒ /bin/bash
Detección de versión del sistema operativo ⇒ UNIX == Linux, si hay parámetros específicos del sistema operativo, puede describirlos en la documentaciónBúsqueda JRE / JDK ⇒ tenemos la única versión, y esta es OpenJDK (bueno, o Oracle JDK, si realmente lo necesita), java
y la compañía están en la ruta estándar del sistemacálculo de parámetros numéricos JVM, sintonización JVM ⇒ esto se puede describir en la documentación de escalado de la aplicaciónbusca tus componentes y bibliotecas ⇒ describa la estructura de la aplicación y cómo configurarla en la documentaciónconfiguración del entorno ⇒ describa los requisitos y características en la documentaciónGeneración CLASSPATH ⇒ -cp path/to/my/jars/*
o incluso, en general, Uber-JARanálisis de argumentos de línea de comando ⇒ no habrá argumentos, porque el administrador de procesos se encargará de todo excepto el lanzamiento- Conjunto de comandos de Java
- ejecución del comando java
Como resultado, solo necesitamos ensamblar y ejecutar un comando Java de la forma java <opts> -jar <program.jar>
usando el administrador de procesos seleccionado (Systemd, Docker, etc.). Todos los parámetros y opciones ( <opts>
) quedan a discreción del ingeniero del sistema, que los ajustará a un entorno específico. Si la lista de opciones <opts>
bastante larga, puede volver a la idea de un script de inicio, pero, en este caso, lo más compacto y declarativo posible , es decir. No contiene ninguna lógica de software.
Ejemplo
Como ejemplo, veamos cómo puede simplificar el script de inicio de Nexus 3 .
La opción más fácil, para no entrar en la jungla de este script, simplemente ejecútelo en condiciones reales ( ./nexus start
) y mire el resultado. Por ejemplo, puede encontrar la lista completa de argumentos de la aplicación en ejecución en la tabla de procesos (a través de ps -ef
), o ejecutar el script en modo de depuración ( bash -x ./nexus start
) para observar todo el proceso de su ejecución y, al final, el comando de lanzamiento.
Terminé con el siguiente comando de 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
Primero, aplique un par de trucos simples:
- cambie
/the/long/and/winding/road/to/my/java
a java
, porque está en la ruta del sistema - coloque la lista de parámetros de Java en una matriz separada, ordénela y elimine los duplicados
Ya tenemos algo más digerible 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
Ahora puedes profundizar.
Install4j es un instalador gráfico de Java. Parece ser utilizado para la instalación inicial del sistema. No lo necesitamos en el servidor, lo eliminamos.
Acordamos la ubicación de los componentes y datos de Nexus en el sistema de archivos:
- coloque la aplicación en
/opt/nexus-<version>
- para mayor comodidad, cree un enlace simbólico
/opt/nexus -> /opt/nexus-<version>
- coloque el script en lugar del original como
/opt/nexus/bin/nexus
- Todos los datos de nuestro Nexus se encontrarán en un sistema de archivos separado montado como
/data/nexus
La creación de directorios y enlaces es el destino de los sistemas de administración de configuración (para todo lo relacionado con las 5-10 líneas en Ansible), así que dejemos esta tarea a los ingenieros de sistemas.
Deje que nuestro script al inicio cambie el directorio de trabajo a /opt/nexus
; luego, podemos cambiar las rutas a los componentes de Nexus a los relativos.
Opciones del formulario -Dkaraf.*
las configuraciones para Apache Karaf , el contenedor OSGi en el que nuestro Nexus obviamente está "empaquetado". Cambie karaf.home
, karaf.base
, karaf.etc
y karaf.data
acuerdo con la ubicación de los componentes, utilizando rutas relativas si es posible.
Al ver que CLASSPATH consiste en una lista de archivos jar que se encuentran en el mismo directorio lib/
, reemplace toda esta lista con lib/*
(también deberá desactivar la expansión de comodines con set -o noglob
).
Cambie java
a exec java
para que nuestro script no inicie java
como un proceso secundario (el administrador de procesos no verá este proceso secundario), sino que "se reemplazará" por java
( descripción de exec ).
Veamos que pasó:
Un total de 27 líneas en lugar de> 400, transparente, claro, declarativo, sin lógica innecesaria. Si es necesario, este script se puede convertir fácilmente en una plantilla para Ansible / Puppet / Chef y agregar solo la lógica necesaria para una situación específica.
Este script se puede usar como ENTRYPOINT en un Dockerfile o en el archivo de unidad de Systemd, al mismo tiempo que ajusta ulimits y otros parámetros del sistema, por ejemplo:
[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
Conclusión
¿Qué conclusiones se pueden sacar de este artículo? En principio, se reduce a un par de puntos:
- Cada sistema tiene su propio propósito, es decir, no es necesario clavar clavos con un microscopio.
- Reglas de simplicidad (KISS, YAGNI): para implementar solo lo que se necesita para una situación específica dada.
- Y lo más importante: es genial que haya especialistas en TI de diferentes perfiles. ¡Interactuemos y hagamos que nuestros sistemas de TI sean más simples, claros y mejores! :)
Gracias por su atencion! Estaré encantado de recibir comentarios y discusiones constructivas en los comentarios.