Cara membuat proses Java berjalan di Linux / Docker sederhana dan mudah

Sebagai seorang insinyur DevOps, saya sering bekerja untuk mengotomatisasi pemasangan dan konfigurasi berbagai sistem TI di berbagai lingkungan: dari wadah ke cloud. Saya harus bekerja dengan banyak sistem berdasarkan Java stack: dari yang kecil (seperti Tomcat) hingga skala besar (Hadoop, Cassandra, dll.).


Selain itu, hampir setiap sistem seperti itu, bahkan yang paling sederhana, untuk beberapa alasan memiliki sistem peluncuran unik yang kompleks. Paling tidak, ini adalah skrip shell multi-line, seperti di Tomcat , dan bahkan seluruh kerangka kerja, seperti di Hadoop . "Pasien" saya saat ini dalam seri ini, yang mengilhami saya untuk menulis artikel ini, adalah repositori artefak Nexus OSS 3 , skrip peluncuran yang membutuhkan ~ 400 baris kode.


Opacity, redundansi, dan kompleksitas skrip startup menciptakan masalah bahkan ketika secara manual menginstal satu komponen pada sistem lokal. Sekarang bayangkan bahwa Anda perlu mengemas satu set komponen dan layanan tersebut dalam wadah Docker, menulis lapisan abstraksi lain di sepanjang garis orkestrasi yang memadai, menyebarkannya dalam kluster Kubernetes dan mengimplementasikan proses ini sebagai pipa CI / CD ...


Singkatnya, mari kita lihat contoh Nexus 3 yang disebutkan di atas, bagaimana mengembalikan dari labirin skrip shell ke sesuatu yang lebih mirip dengan java -jar <program.jar> , mengingat ketersediaan alat-alat DevOps modern yang nyaman.


Dari mana datangnya kompleksitas ini?


Singkatnya, di zaman kuno, ketika UNIX tidak ditanya lagi: "dalam arti Linux?", Tidak ada Systemd dan Docker dan yang lainnya, skrip shell portabel (skrip init) dan PID- digunakan untuk mengontrol proses file. Skrip init mengatur pengaturan lingkungan yang diperlukan, yang berbeda di UNIX yang berbeda, dan, tergantung pada argumen, memulai proses atau memulai kembali / menghentikannya menggunakan ID dari file PID. Pendekatannya sederhana dan jelas, tetapi skrip ini berhenti bekerja di setiap situasi yang tidak biasa, membutuhkan intervensi manual, tidak memungkinkan Anda untuk menjalankan beberapa salinan dari proses ... tetapi bukan itu intinya.


Jadi, jika Anda hati-hati melihat skrip startup yang disebutkan di atas dalam proyek Java, Anda dapat melihat tanda-tanda yang jelas dari pendekatan prasejarah ini, termasuk bahkan penyebutan SunOS, HP-UX dan UNIX lainnya. Biasanya, skrip tersebut melakukan sesuatu seperti ini:


  • gunakan sintaks POSIX shell dengan semua kruknya untuk portabilitas UNIX / Linux
  • tentukan versi OS dan lepaskan melalui rilis uname , /etc/*release , dll.
  • mereka mencari JRE / JDK di sudut sistem file dan memilih versi yang paling "cocok" sesuai dengan aturan yang cerdas, kadang-kadang juga spesifik untuk setiap OS
  • Parameter numerik JVM dihitung, misalnya, ukuran memori ( -Xms , -Xms ), jumlah benang GC, dll.
  • mengoptimalkan JVM melalui -XX parameter dengan mempertimbangkan spesifikasi versi JRE / JDK yang dipilih
  • cari komponen, pustaka, jalurnya di direktori di sekitarnya, file konfigurasi, dll.
  • menyesuaikan lingkungan: ulimit, variabel lingkungan, dll.
  • menghasilkan CLASSPATH dengan loop seperti: for f in $path/*.jar; do CLASSPATH="${CLASSPATH}:$f"; done for f in $path/*.jar; do CLASSPATH="${CLASSPATH}:$f"; done
  • argumen baris perintah parsed: start|stop|restart|reload|status|...
  • kompilasi perintah Java yang pada akhirnya harus Anda jalankan dari atas
  • dan akhirnya jalankan perintah java ini . Seringkali, file PID terkenal yang sama, & , nohup , port TCP khusus dan trik lainnya dari abad terakhir digunakan secara eksplisit atau implisit (lihat contoh dari Karaf )

Skrip peluncuran Nexus 3 yang disebutkan di atas adalah contoh yang cocok untuk skrip semacam itu.


Bahkan, semua logika skrip yang tercantum di atas, seolah-olah, sedang mencoba untuk mengganti administrator sistem, yang akan menginstal dan mengkonfigurasi semuanya secara manual untuk sistem tertentu dari awal hingga akhir. Tetapi secara umum, tidak mungkin untuk memperhitungkan persyaratan dari sistem yang paling beragam. Karena itu, ternyata, sebaliknya, sakit kepala, baik untuk pengembang yang perlu mendukung skrip ini, dan untuk insinyur sistem yang perlu memahami skrip ini nanti. Dari sudut pandang saya, jauh lebih mudah bagi seorang insinyur sistem untuk memahami parameter JVM sekali dan mengkonfigurasinya sebagaimana mestinya, daripada memahami seluk-beluk skrip startupnya setiap kali Anda menginstal sistem baru.


Apa yang harus dilakukan


Maafkan KISS dan YAGNI ada di tangan kita. Apalagi tahun 2018 ada di halaman, artinya:


  • dengan sedikit pengecualian, UNIX == Linux
  • masalah kontrol proses diselesaikan baik untuk server terpisah ( Systemd , Docker ), dan untuk kluster ( Kubernetes , dll.)
  • Ada banyak alat manajemen konfigurasi yang mudah digunakan ( Kemungkinan , dll.)
  • otomatisasi total telah sampai pada administrasi dan telah memantapkan dirinya secara menyeluruh: alih-alih secara manual membuat "server kepingan salju" yang rapuh secara unik , sekarang dimungkinkan untuk secara otomatis merakit mesin virtual terpadu dan wadah menggunakan sejumlah alat yang mudah digunakan, termasuk Ansible and Docker yang disebutkan di atas.
  • alat untuk mengumpulkan statistik runtime banyak digunakan, baik untuk JVM itu sendiri ( contoh ) dan untuk aplikasi Java ( contoh )
  • dan, yang paling penting, para ahli muncul: insinyur sistem dan DevOps yang dapat menggunakan teknologi yang tercantum di atas dan memahami cara memasang JVM dengan benar pada sistem tertentu dan kemudian menyesuaikannya berdasarkan statistik runtime yang dikumpulkan

Jadi mari kita kembali ke fungsionalitas skrip startup, dengan mempertimbangkan poin-poin yang tercantum di atas, tanpa mencoba melakukan pekerjaan untuk insinyur sistem, dan menghapus semua yang "tidak perlu" dari sana.


  • Sintaks shell POSIX/bin/bash
  • Deteksi versi OS ⇒ UNIX == Linux, jika ada parameter khusus OS, Anda dapat menggambarkannya dalam dokumentasi
  • Pencarian JRE / JDK ⇒ kami memiliki satu-satunya versi, dan ini adalah OpenJDK (yah, atau Oracle JDK, jika Anda benar-benar membutuhkannya), java dan perusahaan berada di jalur sistem standar
  • perhitungan parameter numerik JVM, tuning JVM ⇒ ini dapat dijelaskan dalam dokumentasi penskalaan aplikasi
  • cari komponen dan pustaka Anda ⇒ jelaskan struktur aplikasi dan cara mengkonfigurasinya dalam dokumentasi
  • pengaturan lingkungan ⇒ menjelaskan persyaratan dan fitur dalam dokumentasi
  • Generasi CLASSPATH-cp path/to/my/jars/* atau bahkan, secara umum, Uber-JAR
  • parsing argumen baris perintah ⇒ tidak akan ada argumen, karena manajer proses akan mengurus semuanya kecuali peluncuran
  • Perakitan perintah Java
  • eksekusi perintah java

Sebagai hasilnya, kita hanya perlu merakit dan menjalankan perintah Java dari bentuk java <opts> -jar <program.jar> menggunakan manajer proses yang dipilih (Systemd, Docker, dll.). Semua parameter dan opsi ( <opts> ) diserahkan pada kebijaksanaan insinyur sistem, yang akan menyesuaikannya dengan lingkungan tertentu. Jika daftar opsi <opts> cukup panjang, Anda dapat kembali lagi ke ide skrip startup, tetapi, dalam hal ini, sekompak dan seringkas mungkin , mis. tidak mengandung logika perangkat lunak apa pun.


Contoh


Sebagai contoh, mari kita lihat bagaimana Anda dapat menyederhanakan skrip startup Nexus 3 .


Opsi termudah, agar tidak masuk ke dalam hutan skrip ini - jalankan saja dalam kondisi nyata ( ./nexus start ) dan lihat hasilnya. Misalnya, Anda dapat menemukan daftar lengkap argumen dari aplikasi yang berjalan di tabel proses (via ps -ef ), atau menjalankan skrip dalam mode debug ( bash -x ./nexus start ) untuk mengamati seluruh proses eksekusi dan, pada akhirnya, perintah peluncuran.


Saya berakhir dengan perintah Java berikut
 /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 

Pertama, terapkan beberapa trik sederhana untuk itu:


  • ubah /the/long/and/winding/road/to/my/java ke java , karena berada di jalur sistem
  • letakkan daftar parameter Java dalam larik terpisah, urutkan dan hapus duplikat

Kami sudah mendapatkan sesuatu yang lebih mudah dicerna
 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 

Sekarang Anda bisa masuk lebih dalam.


Install4j adalah installer Java yang grafis. Tampaknya digunakan untuk instalasi awal sistem. Kami tidak membutuhkannya di server, kami menghapusnya.


Kami menyetujui penempatan komponen Nexus dan data pada sistem file:


  • letakkan aplikasi itu sendiri di /opt/nexus-<version>
  • untuk kenyamanan, buat tautan simbolis /opt/nexus -> /opt/nexus-<version>
  • tempatkan skrip itu sendiri dan bukan yang asli sebagai /opt/nexus/bin/nexus
  • semua data Nexus kami akan terletak pada sistem file terpisah yang dipasang sebagai /data/nexus

Penciptaan direktori dan tautan adalah tujuan dari sistem manajemen konfigurasi (untuk segala hal tentang semua 5-10 baris dalam Ansible), jadi mari kita serahkan tugas ini kepada teknisi sistem.


Biarkan skrip kami saat startup mengubah direktori kerja ke /opt/nexus - maka kita dapat mengubah jalur ke komponen Nexus ke yang relatif.


Opsi bentuk -Dkaraf.* pengaturan untuk Apache Karaf , wadah OSGi yang menjadi tempat Nexus kami jelas "dikemas". Ubah karaf.home , karaf.base , karaf.etc dan karaf.data sesuai penempatan komponen, menggunakan jalur relatif jika memungkinkan.


Melihat bahwa CLASSPATH terdiri dari daftar file jar yang terletak di direktori lib/ sama, ganti seluruh daftar ini dengan lib/* (Anda juga harus mematikan ekspansi wildcard dengan set -o noglob ).


Ubah java menjadi exec java sehingga skrip kami tidak memulai java sebagai proses anak (manajer proses tidak akan melihat proses anak ini), tetapi "ganti" itu sendiri dengan java ( deskripsi exec ).


Mari kita lihat apa yang terjadi:


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

Total 27 baris alih-alih> 400, transparan, jelas, deklaratif, tidak ada logika yang tidak perlu. Jika perlu, skrip ini dapat dengan mudah diubah menjadi templat untuk Ansible / Puppet / Chef dan hanya menambahkan logika yang diperlukan untuk situasi tertentu.


Script ini dapat digunakan sebagai ENTRYPOINT di Dockerfile atau dipanggil dalam file-sistem Systemd, pada saat yang sama menyetel ulimit dan parameter sistem lainnya di sana, misalnya:


 [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 

Kesimpulan


Kesimpulan apa yang bisa ditarik dari artikel ini? Pada prinsipnya, ada beberapa poin:


  1. Setiap sistem memiliki tujuannya sendiri, yaitu, tidak perlu memalu paku dengan mikroskop.
  2. Aturan Kesederhanaan (KISS, YAGNI) - untuk hanya menerapkan apa yang diperlukan untuk situasi tertentu.
  3. Dan yang paling penting: keren bahwa ada spesialis TI dari berbagai profil. Mari berinteraksi dan membuat sistem IT kami lebih sederhana, lebih jelas, dan lebih baik! :)

Terima kasih atas perhatian anda! Saya akan dengan senang hati memberi tanggapan dan diskusi yang membangun dalam komentar.

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


All Articles