Cara lain untuk mengoptimalkan gambar buruh pelabuhan untuk aplikasi Java

Kisah optimasi gambar untuk aplikasi java dimulai dengan artikel blog musim semi Spring Boot in a Container . Ini membahas berbagai aspek membuat gambar buruh pelabuhan untuk aplikasi booting musim semi, termasuk masalah yang menarik seperti mengurangi ukuran gambar. Untuk tim kami, ini relevan karena beberapa alasan, jadi kami memutuskan untuk menerapkan solusi ini ke aplikasi kami.


Seperti yang sering terjadi, tidak semuanya lepas landas pertama kali, ada nuansa dengan proyek multi-modul dan upaya untuk mengarahkan semua ini pada sistem CI, jadi pada artikel ini Anda akan menemukan solusi untuk masalah ini.


Tujuan dari optimasi adalah untuk mengurangi perbedaan antara gambar yang dihasilkan dari perakitan ke perakitan, yang memberikan hasil yang baik dalam proses pengiriman berkelanjutan, jadi jika Anda tertarik untuk meminimalkan ukuran gambar seperti itu, Anda dapat merujuk ke artikel lain di hub


Jika Anda tidak perlu menjelaskan mengapa Anda harus melakukan sesuatu dengan aplikasi boot multi-meter sebelum menempatkannya pada gambar, Anda dapat langsung melanjutkan ke deskripsi pendekatan optimasi . Jika Anda berhasil berkenalan dengan artikel dari blog musim semi, Anda dapat melanjutkan ke solusi dari masalah yang ditemukan .


Mengapa ini semua, atau sisi lain dari guci lemak


Secara default, jar yang dihasilkan oleh Spring Boot adalah file jar yang dapat dieksekusi berisi kode aplikasi dan semua dependensinya.


Keuntungan dari pendekatan ini jelas: nyaman untuk bekerja dengan satu file, ia memiliki semua yang Anda butuhkan untuk menjalankan melalui java -jar <myapp>.jar . Dockerfile sepele dan tidak menarik.


Kelemahannya adalah penyimpanan yang tidak efisien. Dalam aplikasi boot klasik, rasio kode dan pustaka jelas tidak mendukung kode kami. Misalnya, aplikasi kosong dengan komponen web dan pustaka untuk bekerja dengan database, yang dapat dihasilkan melalui start.spring.io , akan memakan waktu 20mb, di mana 98% akan menjadi pustaka. Dan rasio ini tidak banyak berubah selama proses pengembangan.


Tapi kami mengumpulkan aplikasi lebih dari satu kali, tetapi secara teratur di server CI, dan kemudian menggunakan rantai lingkungan. Dengan demikian, 10 majelis tumbuh pada 200mb, dan 100 - pada 2gb, yang modifikasi akan memakan waktu sangat sedikit.


Dapat diperdebatkan bahwa untuk biaya penyimpanan saat ini, ini adalah angka yang konyol dan Anda tidak perlu menghabiskan waktu untuk optimasi tersebut, tetapi semuanya tergantung pada ukuran organisasi dan jumlah aplikasi yang gambarnya perlu disimpan. Kondisi penyebaran juga dapat sangat memotivasi: ketika registri dan server berada di dekat, bahkan perbedaan 100mb tidak terlalu terlihat, tetapi dalam sistem terdistribusi ini bisa jauh lebih penting, terutama ketika Anda perlu menggunakan negara-negara tertentu seperti China dengan firewall dan saluran yang tidak stabil ke dunia luar.


Jadi, dengan alasan yang ada, saatnya untuk mengoptimalkan.


Kami mengoptimalkan perakitan, atau Apa yang bisa dipelajari dari blog musim semi


Artikel ini menawarkan solusi yang masuk akal: alih-alih satu lapisan yang dihasilkan oleh perintah COPY my-jar.jar app.jar , kita perlu membuat beberapa lapisan.
Satu lapisan akan berisi perpustakaan, yang kedua adalah kode kita sendiri. Untuk melakukan ini, Anda perlu membuka zip file jar dan menyalin konten ke berbagai lapisan gambar.


Script untuk mempersiapkan file jar terlihat seperti ini:


 #!/bin/sh set -e path_to_jar=$1 dir=$(dirname "${path_to_jar}") jar_name=$(basename "${path_to_jar}") mkdir -p "${dir}/docker-dist" && cd "${dir}/docker-dist" jar -xf ../"${jar_name}" 

Dockerfile menggunakan multi-stage build mungkin terlihat seperti ini


 FROM openjdk:8-jdk-alpine as build WORKDIR /wd COPY prepare_for_docker.sh /usr/local/bin/prepare_for_docker COPY target/demo.jar /wd/app.jar RUN prepare_for_docker /wd/app.jar FROM openjdk:8-jdk-alpine COPY --from=build /wd/docker-dist/BOOT-INF/lib /app/lib COPY --from=build /wd/docker-dist/META-INF /app/META-INF COPY --from=build /wd/docker-dist/BOOT-INF/classes /app ENTRYPOINT ["java","-cp","app:app/lib/*","com.example.demo.DemoApplication"] 

Pada tahap pertama, kita menyalin semua yang kita butuhkan, menjalankan skrip untuk membongkar file jar, dan pada tahap kedua kita membuat pustaka yang terpisah dan kode kita secara berlapis-lapis.


Sangat mudah untuk memastikan operabilitas:


  1. Mengumpulkan untuk pertama kalinya
  2. Buat perubahan apa pun pada kode kami.
  3. Kami meluncurkan docker build lagi dan melihat baris yang dihargai Using cache saat menyalin seluruh direktori lib
     ... Step 5/10 : RUN prepare_for_docker app.jar ---> Running in c8e422491eb2 Removing intermediate container c8e422491eb2 ---> c7dcec4ae18a Step 6/10 : FROM openjdk:8-jdk-alpine ---> a3562aa0b991 Step 7/10 : COPY --from=build /wd/docker-dist/BOOT-INF/lib /app/lib ---> Using cache ---> 01b600d7e350 Step 8/10 : COPY --from=build /wd/docker-dist/META-INF /app/META-INF ---> Using cache ---> 5c0c03a3c8f1 Step 9/10 : COPY --from=build /wd/docker-dist/BOOT-INF/classes /app ---> 5ffed6ee5696 Step 10/10 : ENTRYPOINT ["java","-cp","app:app/lib/*","com.example.demo.DemoApplication"] ---> Running in 99957250fe5d Removing intermediate container 99957250fe5d ---> 6735799d9f32 Successfully built 6735799d9f32 Successfully tagged boot2-sample:latest 

Cara yang jelas untuk meningkatkan pendekatan ini adalah membangun gambar dasar kecil dengan skrip agar tidak menyeretnya dari proyek ke proyek. Dengan demikian, lapisan pertama menjadi lebih ringkas.


 FROM zeldigas/java-layered-builder as build COPY target/demo.jar app.jar RUN prepare_for_docker app.jar 

Kami sedang menyelesaikan solusinya


Seperti yang sudah disebutkan di awal artikel, solusinya berhasil, tetapi selama operasi ditemukan beberapa masalah yang akan dibahas nanti.


Tidak semua file di lib sama lib sama perpustakaan


Jika proyek Anda multi-modul (setidaknya ada modul A, di mana modul B tergantung, dirakit sebagai tabung lemak pegas), menerapkan solusi asli untuk itu, Anda akan menemukan bahwa tidak ada caching lapisan terjadi. Apa yang salah?


Masalahnya ada di modul tambahan: mereka adalah sumber perubahan konstan untuk layer, bahkan jika Anda tidak membuat perubahan pada kode modul. Ini karena keanehan membuat file jar (dengan gradle, situasinya sedikit lebih baik, tetapi tidak yakin). Tugas mendapatkan artefak yang dapat direproduksi bukanlah topik artikel ini (meskipun, tentu saja, ini menarik dan dapat dicapai), jadi kami akan beralih ke solusi yang cukup sederhana.


Kami mendistribusikan konten lib ke dalam 2 direktori, setelah membongkar, memisahkan modul proyek dari perpustakaan lain. Mari selesaikan skrip pembongkaran guci lemak:


 #!/bin/sh set -e path_to_jar=$1 shift #(1) app_modules=$* #(2) dir=$(dirname "${path_to_jar}") jar_name=$(basename "${path_to_jar}") mkdir -p "${dir}/docker-dist" && cd "${dir}/docker-dist" jar -xf ../"${jar_name}" if [ -n "${app_modules}" ]; then #(3) mkdir app-lib for i in $app_modules; do mv "BOOT-INF/lib/$i"* app-lib #(4) done fi 

Akibatnya, skrip mulai mendukung transfer parameter tambahan (lihat 1 dan 2). Jika argumen tambahan (3) diteruskan, masing-masing dianggap sebagai awalan untuk nama file yang kami pindahkan (4) ke direktori terpisah.


Contoh Dockerfile untuk skenario dengan satu tambahan. shared-module dan versi 1.0-SNAPSHOT


 FROM openjdk:8-jdk-alpine as build COPY target/demo.jar /wd/app.jar RUN prepare_for_docker /wd/app.jar shared-module-1.0 FROM openjdk:8-jdk-alpine COPY --from=build /wd/docker-dist/BOOT-INF/lib /app/lib COPY --from=build /wd/docker-dist/app-lib /app/lib COPY --from=build /wd/docker-dist/META-INF /app/META-INF COPY --from=build /wd/docker-dist/BOOT-INF/classes /app ENTRYPOINT ["java","-cp","app:app/lib/*","com.example.demo.DemoApplication"] 

Jalankan di server CI


Setelah men-debug semuanya secara lokal, puas dengan hasilnya, kami mulai berjalan di server CI dan dari build log ditemukan bahwa mukjizat tidak terjadi, atau lebih tepatnya hasilnya tidak konstan: dalam beberapa kasus, caching dilakukan, dan saat berikutnya semua layer baru.


Akibatnya, pelakunya ditemukan - cache buruh pelabuhan, atau lebih tepatnya tidak ada dalam kasus agen yang berbeda (perakitan kami tidak dipaku ke agen spesifik dari sistem CI). Ternyata, jika tidak ada lapisan yang sesuai di cache buruh pelabuhan, maka lapisan dengan checksum yang berbeda diperoleh dari set file yang sama. Anda dapat memverifikasi ini secara lokal, dengan menjalankan build dengan opsi --no-cache , atau dengan --no-cache kedua kali dengan terlebih dahulu menghapus gambar dan semua lapisan perantara. Akibatnya, Anda mendapatkan lapisan checksum yang sama sekali berbeda, yang meniadakan semua upaya sebelumnya.


Tanpa cache yang tepat, kita mendapatkan lapisan yang berbeda


Ada beberapa cara untuk menyelesaikan masalah:


  1. Jika sistem CI Anda mendukung hal ini di luar kotak (misalnya, Circle CI di bagian paket memiliki dukungan bawaan untuk cache bersama selama pertemuan)
  2. Mengacak bagian dengan cache buruh pelabuhan antara agen
  3. Manfaatkan docker manajemen cache --cache-from ( --cache-from )

Kami pergi ke jalan ketiga, karena dalam kasus kami itu yang paling sederhana. Opsi ini memungkinkan Anda untuk memberi tahu daemon buruh pelabuhan gambar mana yang harus diperhitungkan dan mencoba digunakan untuk caching selama perakitan. Anda dapat menentukan gambar sebanyak yang Anda anggap perlu, yang utama adalah bahwa mereka ada di sistem file. Jika gambar yang ditentukan tidak ada, itu hanya akan diabaikan, jadi Anda harus menarik sebelum membangun.


Inilah yang terlihat seperti wadah perakitan dengan pendekatan ini:


 set -e version=... #      docker pull registy.example.com/my-image:latest || true #         docker build -t registry.example.com/my-image:$version --cache-from registry.example.com/my-image:latest . #   registry    latest docker tag registry.example.com/my-image:$version registry.example.com/my-image:latest docker push registry.example.com/my-image:$version docker push registry.example.com/my-image:latest 

Kami mencoba menggunakan kembali layer hanya dari gambar terbaru, yang sering kali cukup, tetapi tidak ada yang mengganggu untuk menghasilkan logika yang lebih kompleks dan melacak beberapa versi atau mengandalkan id dari vcs commit.


Kami menyesuaikan pendekatan ini dengan kemampuan CI Anda dan mendapatkan penggunaan kembali lapisan yang dapat diandalkan dengan perpustakaan.


Total


Solusi ini menunjukkan hasil yang baik, terutama ketika digunakan dalam proyek-proyek dengan tahap pengembangan aktif dan saluran pipa CD yang disetel. Grafik di bawah ini menunjukkan hasil penerapan optimasi ke salah satu aplikasi. Terlihat jelas bahwa pertumbuhan linier telah berubah menjadi spasmodik mulai dari perakitan ke-70 (kegagalan pada tahun 60-an dihubungkan secara tepat dengan pekerjaan debugging pada build agent). Emisi setelah dikaitkan dengan memperbarui gambar dasar (tinggi) dan perpustakaan (lebih rendah)



Optimalisasi penyimpanan dalam kasus kami adalah bonus yang menyenangkan, namun merupakan bonus sekunder. Akselerasi penyebaran versi baru di atas versi lama di beberapa daerah jauh lebih menyenangkan.


Perlu dicatat bahwa teknik ini cukup kompatibel dengan pendekatan lain yang bertujuan mengurangi ukuran gambar tunggal (alpine dan gambar dasar ringan lainnya, runtime kustom untuk aplikasi). Hal utama adalah mengikuti aturan umum untuk merakit gambar dalam hal caching dan memastikan bahwa hasilnya dapat direproduksi.

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


All Articles