Como tornar os processos Java em execução no Linux / Docker simples e diretos

Como engenheiro de DevOps, frequentemente trabalho para automatizar a instalação e configuração de uma variedade de sistemas de TI em vários ambientes: dos contêineres à nuvem. Eu tive que trabalhar com muitos sistemas baseados na pilha Java: de pequeno (como Tomcat) a grande escala (Hadoop, Cassandra, etc.).


Além disso, quase todos os sistemas, mesmo o mais simples, por algum motivo, possuíam um sistema de lançamento único e complexo. No mínimo, esses eram scripts shell de várias linhas, como no Tomcat , e até estruturas inteiras, como no Hadoop . Meu atual "paciente" nesta série, que me inspirou a escrever este artigo, é o repositório de artefatos do Nexus OSS 3 , cujo script de inicialização requer ~ 400 linhas de código.


A opacidade, redundância e complexidade dos scripts de inicialização cria problemas, mesmo ao instalar manualmente um componente no sistema local. Agora imagine que você precisa empacotar um conjunto desses componentes e serviços em um contêiner Docker, escrever outra camada de abstração ao longo das linhas de orquestração adequada, implantá-lo em um cluster Kubernetes e implementar esse processo como um pipeline de CI / CD ...


Em resumo, vejamos o exemplo do Nexus 3 mencionado, como retornar do labirinto de scripts de shell para algo mais semelhante ao java -jar <program.jar> , dada a disponibilidade de ferramentas modernas e práticas de DevOps.


De onde vem essa complexidade?


Em poucas palavras, nos tempos antigos, quando o UNIX não era perguntado novamente: "no sentido do Linux?", Não havia Systemd e Docker e outros, scripts de shell portáteis (scripts de inicialização) e PID eram usados ​​para controlar os processos arquivos Os scripts init definem as configurações de ambiente necessárias, que foram diferentes em diferentes UNIX e, dependendo dos argumentos, iniciaram o processo ou o reiniciaram / pararam usando o ID do arquivo PID. A abordagem é simples e clara, mas esses scripts deixaram de funcionar em todas as situações incomuns, exigindo intervenção manual, não permitiram a execução de várias cópias do processo ... mas não o ponto.


Portanto, se você observar cuidadosamente os scripts de inicialização mencionados acima nos projetos Java, poderá ver os sinais óbvios dessa abordagem pré-histórica, incluindo até a menção de SunOS, HP-UX e outros UNIXs. Normalmente, esses scripts fazem algo assim:


  • use a sintaxe do shell POSIX com todas as suas muletas para portabilidade UNIX / Linux
  • determinar a versão do sistema operacional e liberar através de uname , /etc/*release , etc.
  • eles pesquisam o JRE / JDK nos cantos do sistema de arquivos e selecionam a versão mais "adequada" de acordo com regras inteligentes, às vezes também específicas para cada SO
  • Os parâmetros numéricos da JVM são calculados, por exemplo, tamanho da memória ( -Xms , -Xmx ), o número de encadeamentos do GC etc.
  • otimizar a JVM através de -XX parâmetros, levando em consideração as especificidades da versão selecionada do JRE / JDK
  • procure seus componentes, bibliotecas, caminhos para eles nos diretórios, arquivos de configuração, etc.
  • personalizar o ambiente: ulimits, variáveis ​​de ambiente etc.
  • gerar CLASSPATH com um loop como: for f in $path/*.jar; do CLASSPATH="${CLASSPATH}:$f"; done for f in $path/*.jar; do CLASSPATH="${CLASSPATH}:$f"; done
  • argumentos de linha de comando analisados: start|stop|restart|reload|status|...
  • compile o comando Java que você precisa executar a partir do acima
  • e finalmente execute este comando java . Freqüentemente, os mesmos arquivos PID notórios, & , nohup , portas TCP especiais e outros truques do século passado são usados ​​explícita ou implicitamente (veja o exemplo do Karaf )

O script de inicialização do Nexus 3 mencionado é um exemplo adequado desse script.


De fato, toda a lógica de script listada acima, por assim dizer, está tentando substituir o administrador do sistema, que instalaria e configuraria tudo manualmente para um sistema específico, do começo ao fim. Mas, em geral, é impossível levar em consideração quaisquer requisitos dos mais diversos sistemas. Portanto, resulta, pelo contrário, uma dor de cabeça, tanto para desenvolvedores que precisam suportar esses scripts quanto para engenheiros de sistema que precisam entender esses scripts posteriormente. Do meu ponto de vista, é muito mais fácil para um engenheiro de sistema entender os parâmetros da JVM uma vez e configurá-lo como deveria, do que entender os meandros de seus scripts de inicialização toda vez que você instala um novo sistema.


O que fazer?


Perdoe! KISS e YAGNI estão em nossas mãos. Além disso, o ano de 2018 é no quintal, o que significa que:


  • com muito poucas exceções, UNIX == Linux
  • o problema de controle de processo é resolvido para um servidor separado ( Systemd , Docker ) e para clusters ( Kubernetes etc.)
  • Existem várias ferramentas convenientes de gerenciamento de configuração ( Ansible , etc.)
  • a automação total chegou à administração e já se solidificou completamente: em vez de configurar manualmente "servidores de floco de neve" únicos e frágeis , agora é possível montar automaticamente máquinas virtuais e contêineres reproduzíveis unificados usando várias ferramentas convenientes, incluindo o Ansible e o Docker acima mencionados
  • As ferramentas para coletar estatísticas de tempo de execução são amplamente utilizadas, tanto para a própria JVM ( exemplo ) quanto para um aplicativo Java ( exemplo )
  • e, o mais importante, surgiram especialistas: engenheiros de sistema e DevOps que podem usar as tecnologias listadas acima e entender como instalar corretamente a JVM em um sistema específico e, posteriormente, ajustá-lo com base nas estatísticas de tempo de execução coletadas

Então, vamos examinar a funcionalidade dos scripts de inicialização novamente, levando em consideração os pontos listados acima, sem tentar fazer o trabalho para o engenheiro de sistema e remover todos os "desnecessários" de lá.


  • Sintaxe do shell POSIX/bin/bash
  • Detecção de versão do SO ⇒ UNIX == Linux, se houver parâmetros específicos do SO, você poderá descrevê-los na documentação
  • Pesquisa JRE / JDK ⇒ temos a única versão, e este é o OpenJDK (bem, ou Oracle JDK, se você realmente precisar), java e a empresa estão no caminho padrão do sistema
  • cálculo de parâmetros numéricos JVM, ajustando JVM ⇒ isso pode ser descrito na documentação de dimensionamento do aplicativo
  • procure seus componentes e bibliotecas ⇒ descrever a estrutura do aplicativo e como configurá-lo na documentação
  • configuração do ambiente ⇒ descrever os requisitos e recursos na documentação
  • Geração CLASSPATH-cp path/to/my/jars/* ou mesmo, em geral, Uber-JAR
  • Analisando Argumentos da Linha de Comandos ⇒ não haverá argumentos, porque o gerente de processo cuidará de tudo, exceto o lançamento
  • Montagem de Comando Java
  • execução de comando java

Como resultado, basta montar e executar um comando Java no formato java <opts> -jar <program.jar> usando o gerenciador de processos selecionado (Systemd, Docker, etc.). Todos os parâmetros e opções ( <opts> ) são deixados a critério do engenheiro do sistema, que os ajustará a um ambiente específico. Se a lista de opções <opts> bastante longa, você poderá voltar à ideia de um script de inicialização, mas, nesse caso, o mais compacto e declarativo possível , ou seja, não contém nenhuma lógica de software.


Exemplo


Como exemplo, vamos ver como você pode simplificar o script de inicialização do Nexus 3 .


A opção mais fácil, para não entrar na selva desse script - basta executá-lo em condições reais ( ./nexus start ) e ver o resultado. Por exemplo, você pode encontrar a lista completa de argumentos do aplicativo em execução na tabela de processos (via ps -ef ) ou executar o script no modo de depuração ( bash -x ./nexus start ) para observar todo o processo de sua execução e, no final, o comando de inicialização.


Eu terminei com o seguinte comando 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 

Primeiro, aplique alguns truques simples:


  • mude /the/long/and/winding/road/to/my/java para java , porque está no caminho do sistema
  • coloque a lista de parâmetros Java em uma matriz separada, classifique-a e remova duplicatas

Já temos algo mais digerível
 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 

Agora você pode ir fundo.


O Install4j é um instalador Java gráfico. Parece ser usado para a instalação inicial do sistema. Nós não precisamos dele no servidor, nós o removemos.


Concordamos com o posicionamento dos componentes e dados do Nexus no sistema de arquivos:


  • coloque o aplicativo em /opt/nexus-<version>
  • por conveniência, crie um link simbólico /opt/nexus -> /opt/nexus-<version>
  • coloque o próprio script em vez do original como /opt/nexus/bin/nexus
  • todos os dados do nosso Nexus estarão em um sistema de arquivos separado montado como /data/nexus

A própria criação de diretórios e links é o destino dos sistemas de gerenciamento de configuração (para tudo sobre todas as linhas de 5 a 10 no Ansible), então vamos deixar essa tarefa para os engenheiros de sistema.


Deixe nosso script na inicialização alterar o diretório de trabalho para /opt/nexus - então podemos alterar os caminhos dos componentes do Nexus para os relativos.


Opções do formulário -Dkaraf.* as configurações do Apache Karaf , o contêiner OSGi no qual nosso Nexus está obviamente "empacotado". Altere karaf.home , karaf.base , karaf.etc e karaf.data acordo com a localização dos componentes, usando caminhos relativos, se possível.


Visto que CLASSPATH consiste em uma lista de arquivos jar que estão no mesmo diretório lib/ , substitua toda a lista por lib/* (você também precisará desativar a expansão de curinga com set -o noglob ).


Altere java para exec java para que nosso script não inicie o java como um processo filho (o gerenciador de processos não verá esse processo filho), mas "substitua" a si mesmo por java ( descrição do exec ).


Vamos ver o que aconteceu:


 #!/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 

Um total de 27 linhas, em vez de> 400, transparente, clara, declarativa, sem lógica desnecessária. Se necessário, esse script pode ser facilmente transformado em um modelo para Ansible / Puppet / Chef e adicionar apenas a lógica necessária para uma situação específica.


Esse script pode ser usado como um ENTRYPOINT em um Dockerfile ou chamado no arquivo de unidade Systemd, ao mesmo tempo ajustando ulimits e outros parâmetros do sistema, por exemplo:


 [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 

Conclusão


Que conclusões podem ser tiradas deste artigo? Em princípio, tudo se resume a alguns pontos:


  1. Cada sistema tem seu próprio objetivo, ou seja, não é necessário martelar as unhas com um microscópio.
  2. Regras de simplicidade (KISS, YAGNI) - para implementar apenas o necessário para uma determinada situação específica.
  3. E o mais importante: é legal que existam especialistas em TI de diferentes perfis. Vamos interagir e tornar nossos sistemas de TI mais simples, claros e melhores! :)

Obrigado pela atenção! Ficarei feliz em receber comentários e discussões construtivas nos comentários.

Source: https://habr.com/ru/post/pt415893/


All Articles