Di bawah tenda, hh.ru berisi sejumlah besar layanan Java yang berjalan dalam wadah buruh pelabuhan. Selama operasi mereka, kami menghadapi banyak masalah non-sepele. Dalam banyak kasus, untuk mendapatkan bagian bawah dari solusi, saya harus google untuk waktu yang lama, membaca sumber OpenJDK dan bahkan profil layanan pada produksi. Dalam artikel ini saya akan mencoba menyampaikan intisari dari pengetahuan yang diperoleh dalam proses tersebut.
Batas CPU
Kami dulu tinggal di mesin virtual kvm dengan keterbatasan CPU dan memori dan, pindah ke Docker, menetapkan pembatasan serupa di cgroup. Dan masalah pertama yang kami temui justru batas CPU. Saya harus mengatakan segera bahwa masalah ini tidak lagi relevan untuk versi terbaru Java 8 dan Java ≥ 10. Jika Anda mengikuti perkembangan zaman, Anda dapat melewati bagian ini dengan aman.
Jadi, kami memulai layanan kecil di wadah dan melihat bahwa itu menghasilkan sejumlah besar utas. Atau CPU mengkonsumsi jauh lebih dari yang diharapkan, batas waktu berapa banyak yang sia-sia. Atau inilah situasi sebenarnya: di satu mesin layanan mulai normal, dan di mesin lain, dengan pengaturan yang sama, crash, dipaku oleh pembunuh OOM.
Solusinya ternyata sangat sederhana - hanya Java tidak melihat keterbatasan
--cpus
ditetapkan di buruh pelabuhan dan percaya bahwa semua kernel dari mesin host dapat diakses. Dan mungkin ada banyak dari mereka (dalam pengaturan standar kami - 80).
Perpustakaan menyesuaikan ukuran kumpulan utas dengan jumlah prosesor yang tersedia - karenanya jumlah utas yang sangat besar.
Java sendiri menskala jumlah thread GC dengan cara yang sama, sehingga konsumsi CPU dan waktu tunggu - layanan mulai menghabiskan sejumlah besar sumber daya untuk pengumpulan sampah, menggunakan bagian terbesar dari kuota yang dialokasikan untuk itu.
Juga, perpustakaan (khususnya Netty) dapat, dalam kasus-kasus tertentu, menyesuaikan ukuran memori off-pinggul dengan jumlah CPU, yang mengarah pada kemungkinan tinggi melebihi batas yang ditetapkan untuk wadah ketika dijalankan pada perangkat keras yang lebih kuat.
Pada awalnya, ketika masalah ini memanifestasikan dirinya, kami mencoba menggunakan putaran kerja berikut:
- Mencoba menggunakan beberapa layanan
libnumcpus - perpustakaan yang memungkinkan Anda untuk "menipu" Java dengan mengatur sejumlah prosesor yang tersedia;
- secara eksplisit menunjukkan jumlah utas GC,
- secara eksplisit menetapkan batas penggunaan buffer byte langsung.
Tapi, tentu saja, bergerak dengan kruk seperti itu tidak nyaman, dan pindah ke Jawa 10 (dan kemudian Jawa 11), di mana semua masalah ini
tidak ada , adalah solusi nyata. Dalam keadilan, perlu dikatakan bahwa dalam delapan juga, semuanya baik-baik saja dengan
pembaruan 191 , dirilis pada Oktober 2018. Pada saat itu sudah tidak relevan bagi kami, yang juga saya harapkan untuk Anda.
Ini adalah salah satu contoh di mana memperbarui versi Java tidak hanya memberikan kepuasan moral, tetapi juga keuntungan nyata yang nyata dalam bentuk operasi yang disederhanakan dan peningkatan kinerja layanan.
Docker dan mesin kelas server
Jadi, di Java 10, opsi
-XX:ActiveProcessorCount
dan
-XX:+UseContainerSupport
muncul (dan di-backport ke Java 8), dengan mempertimbangkan batas cgroup default. Sekarang semuanya indah. Atau tidak?
Beberapa waktu setelah kami pindah ke Jawa 10/11, kami mulai melihat beberapa keanehan. Untuk beberapa alasan, di beberapa layanan, grafik GC tampak seperti mereka tidak menggunakan G1:
Ini, secara sederhana, sedikit tidak terduga, karena kami tahu pasti bahwa G1 adalah kolektor default, dimulai dengan Java 9. Pada saat yang sama, tidak ada masalah seperti itu di beberapa layanan - G1 dihidupkan, seperti yang diharapkan.
Kita mulai memahami dan menemukan sesuatu yang
menarik . Ternyata jika Java berjalan pada kurang dari 3 prosesor dan dengan batas memori kurang dari 2 GB, maka itu menganggap dirinya sebagai klien dan tidak memungkinkan untuk menggunakan apa pun selain SerialGC.
By the way, ini hanya mempengaruhi
pilihan GC dan tidak ada hubungannya dengan -client / -server dan opsi kompilasi JIT.
Jelas, ketika kami menggunakan Java 8, itu tidak memperhitungkan batas buruh pelabuhan dan berpikir bahwa ia memiliki banyak prosesor dan memori. Setelah memutakhirkan ke Java 10, banyak layanan dengan batas yang ditetapkan lebih rendah tiba-tiba mulai menggunakan SerialGC. Untungnya, ini diperlakukan sangat sederhana - dengan secara eksplisit mengatur opsi
-XX:+AlwaysActAsServerClassMachine
.
Batas CPU (ya, lagi) dan fragmentasi memori
Melihat grafik dalam pemantauan, kami entah bagaimana memperhatikan bahwa Resident Set Size dari wadah terlalu besar - sebanyak tiga kali lebih banyak dari ukuran pinggul maksimum. Mungkinkah ini terjadi dalam beberapa mekanisme rumit berikutnya yang berskala sesuai dengan jumlah prosesor dalam sistem dan tidak tahu tentang keterbatasan buruh pelabuhan?
Ternyata mekanismenya sama sekali tidak rumit - ini adalah malloc terkenal dari glibc. Singkatnya, glibc menggunakan arena yang disebut untuk mengalokasikan memori. Saat membuat, setiap utas ditugaskan ke salah satu arena. Ketika utas yang menggunakan glibc ingin mengalokasikan sejumlah memori di tumpukan asli untuk kebutuhannya dan memanggil malloc, maka memori tersebut dialokasikan di arena yang ditugaskan padanya. Jika arena menyajikan beberapa utas, maka utas ini akan bersaing untuk itu. Semakin banyak arena, semakin sedikit kompetisi, tetapi semakin banyak fragmentasi, karena setiap arena memiliki daftar area bebas sendiri.
Pada sistem 64-bit, jumlah arena default diatur ke 8 * jumlah CPU. Jelas, ini adalah overhead yang besar bagi kami, karena tidak semua CPU tersedia untuk wadah. Selain itu, untuk aplikasi berbasis Java, persaingan untuk arena tidak begitu relevan, karena sebagian besar alokasi dilakukan di Java-heap, memori yang dapat dialokasikan sepenuhnya pada saat startup.
Fitur malloc ini telah dikenal
sejak lama , dan juga solusinya - untuk menggunakan variabel lingkungan
MALLOC_ARENA_MAX
untuk secara eksplisit menunjukkan jumlah arena. Sangat mudah dilakukan untuk wadah apa pun. Berikut adalah efek menentukan
MALLOC_ARENA_MAX = 4
untuk backend utama kami:
Ada dua contoh pada grafik RSS: dalam satu (biru) kita nyalakan
MALLOC_ARENA_MAX
, yang lain (merah) baru saja kita restart. Perbedaannya jelas.
Tapi setelah itu, ada keinginan yang masuk akal untuk mencari tahu apa yang pada umumnya digunakan memori Jawa. Apakah mungkin untuk menjalankan microservice di Jawa dengan batas memori 300-400 megabyte dan tidak takut itu akan jatuh dari Java-OOM atau tidak dibunuh oleh sistem OOM killer?
Kami memproses Java-OOM
Pertama-tama, Anda perlu mempersiapkan diri untuk fakta bahwa OOM tidak bisa dihindari, dan Anda harus menanganinya dengan benar - setidaknya menyelamatkan pinggul. Anehnya, usaha sederhana ini pun memiliki nuansa tersendiri. Misalnya, dump hip tidak ditimpa - jika dump hip dengan nama yang sama sudah disimpan, maka yang baru tidak akan dibuat.
Java dapat
secara otomatis menambahkan nomor seri dump dan memproses id ke nama file, tetapi ini tidak akan membantu kami. Nomor seri tidak berguna, karena ini adalah OOM, dan bukan hip-dump yang diminta secara rutin - aplikasi restart setelah itu, mengatur ulang penghitung. Dan id proses tidak cocok, karena di buruh pelabuhan selalu sama (paling sering 1).
Karena itu, kami sampai pada opsi ini:
-XX:+HeapDumpOnOutOfMemoryError
-XX:+ExitOnOutOfMemoryError
-XX:HeapDumpPath=/var/crash/java.hprof
-XX:OnOutOfMemoryError="mv /var/crash/java.hprof /var/crash/heapdump.hprof"
Ini cukup sederhana dan dengan beberapa peningkatan Anda bahkan dapat mengajarkan untuk menyimpannya tidak hanya hip-dump terbaru, tetapi untuk kebutuhan kita ini lebih dari cukup.
Java OOM bukan satu-satunya hal yang harus kita hadapi. Setiap wadah memiliki batas pada memori yang ditempatinya, dan itu dapat dilampaui. Jika ini terjadi, maka wadah tersebut dibunuh oleh sistem OOM killer dan restart (kami menggunakan
restart_policy: always
). Secara alami, ini tidak diinginkan, dan kami ingin belajar cara menetapkan batas sumber daya yang digunakan oleh JVM dengan benar.
Mengoptimalkan pemakaian memori
Tetapi sebelum menetapkan batasan, Anda perlu memastikan bahwa JVM tidak membuang sumber daya. Kami telah berhasil mengurangi konsumsi memori dengan menggunakan batasan jumlah CPU dan variabel
MALLOC_ARENA_MAX
. Apakah ada cara "hampir bebas" lainnya untuk melakukan ini?
Ternyata ada beberapa trik lagi yang akan menghemat sedikit memori.
Yang pertama adalah penggunaan opsi
-Xss
(atau
-XX:ThreadStackSize
), yang mengontrol ukuran tumpukan untuk utas. Default untuk JVM 64-bit adalah 1 MB. Kami menemukan bahwa 512 KB sudah cukup untuk kami. Karena itu, StackOverflowException belum pernah ditangkap sebelumnya, tetapi saya akui bahwa ini tidak cocok untuk semua orang. Dan untung dari ini sangat kecil.
Yang kedua adalah
-XX:+UseStringDeduplication
(dengan G1 GC diaktifkan). Ini memungkinkan Anda untuk menghemat memori dengan menciutkan duplikat baris karena beban prosesor tambahan. Pengorbanan antara memori dan CPU hanya tergantung pada aplikasi spesifik dan pengaturan mekanisme deduplikasi itu sendiri. Baca
dok dan uji dalam layanan Anda, kami memiliki opsi ini belum menemukan aplikasinya.
Dan akhirnya, metode yang tidak cocok untuk semua orang (tapi itu cocok untuk kita) adalah dengan menggunakan
jemalloc bukan dari malloc asli. Implementasi ini diarahkan untuk mengurangi fragmentasi memori dan dukungan multithreading yang lebih baik dibandingkan dengan malloc dari glibc. Untuk layanan kami, jemalloc memberikan sedikit peningkatan memori dibandingkan malloc dengan
MALLOC_ARENA_MAX=4
, tanpa mempengaruhi kinerja secara signifikan.
Opsi lain, termasuk yang dijelaskan oleh Alexei Shipilev di
JVM Anatomy Quark # 12: Native Memory Tracking , tampak agak berbahaya atau menyebabkan penurunan kinerja yang nyata. Namun, untuk tujuan pendidikan, saya sarankan membaca artikel ini.
Sementara itu, mari kita beralih ke topik berikutnya dan, akhirnya, coba pelajari cara membatasi konsumsi memori dan pilih batas yang benar.
Membatasi pemakaian memori: heap, non-heap, direct memory
Untuk melakukan semuanya dengan benar, Anda perlu mengingat memori apa yang secara umum terdiri dari Java. Pertama, mari kita lihat kumpulan yang statusnya dapat dimonitor melalui JMX.
Yang pertama, tentu saja,
pinggul . Sederhana saja: kami
-Xmx
, tapi bagaimana cara melakukannya dengan benar? Sayangnya, tidak ada resep universal di sini, semuanya tergantung pada aplikasi dan memuat profil. Untuk layanan baru, kami mulai dengan ukuran tumpukan yang relatif masuk akal (128 MB) dan, jika perlu, menambah atau menguranginya. Untuk mendukung yang sudah ada, ada pemantauan dengan grafik konsumsi memori dan metrik GC.
Pada saat yang sama dengan
-Xms == -Xmx
kita mengatur
-Xms == -Xmx
. Kami tidak memiliki memori yang berlebihan, sehingga kami berkepentingan agar layanan menggunakan sumber daya yang kami berikan semaksimal mungkin. Selain itu, dalam layanan biasa kami menyertakan
-XX:+AlwaysPreTouch
dan mekanisme Transparent Huge Pages:
-XX:+UseTransparentHugePages -XX:+UseLargePagesInMetaspace
. Namun, sebelum mengaktifkan THP, baca
dokumentasi dengan seksama dan uji bagaimana layanan berperilaku dengan opsi ini untuk waktu yang lama. Kejutan tidak dikesampingkan pada mesin dengan RAM yang tidak mencukupi (misalnya, kami harus mematikan THP di bangku tes).
Berikutnya adalah
non-heap . Memori non-tumpukan meliputi:
- Metaspace dan Ruang Kelas Terkompresi,
- Kode Cache.
Pertimbangkan kolam-kolam ini secara berurutan.
Tentu saja, semua orang telah mendengar tentang
Metaspace , saya tidak akan membicarakannya secara detail. Ini menyimpan metadata kelas, metode bytecode, dan sebagainya. Faktanya, penggunaan Metaspace secara langsung tergantung pada jumlah dan ukuran kelas yang dimuat, dan Anda dapat menentukannya, seperti hip, hanya dengan meluncurkan aplikasi dan menghapus metrik melalui JMX. Secara default, Metaspace tidak dibatasi oleh apa pun, tetapi cukup mudah untuk melakukan ini dengan opsi
-XX:MaxMetaspaceSize
.
Ruang Kelas Terkompresi adalah bagian dari Metaspace dan muncul ketika opsi
-XX:+UseCompressedClassPointers
diaktifkan (diaktifkan secara default untuk tumpukan kurang dari 32 GB, yaitu, ketika itu dapat memberikan peningkatan memori nyata). Ukuran kumpulan ini dapat dibatasi oleh opsi
-XX:CompressedClassSpaceSize
, tetapi tidak masuk akal, karena Ruang Kelas Terkompresi termasuk dalam Metaspace dan jumlah total memori yang dikunci untuk Metaspace dan Ruang Kelas Terkompresi pada akhirnya terbatas pada satu
-XX:MaxMetaspaceSize
.
Omong-omong, jika Anda melihat pembacaan JMX, maka jumlah memori non-heap selalu dihitung sebagai
jumlah dari Metaspace, Ruang Kelas Terkompresi dan Kode Cache. Bahkan, Anda hanya perlu meringkas Metaspace dan CodeCache.
Jadi, di non-heap hanya
Code Cache yang tersisa - repositori kode yang dikompilasi oleh kompiler JIT. Secara default, ukuran maksimumnya diatur ke 240 MB, dan untuk layanan kecil beberapa kali lebih besar dari yang diperlukan. Ukuran Cache Kode dapat diatur dengan opsi
-XX:ReservedCodeCacheSize
. Ukuran yang benar hanya dapat ditentukan dengan menjalankan aplikasi dan mengikutinya di bawah profil memuat khas.
Penting untuk tidak membuat kesalahan di sini, karena Cache Kode tidak cukup menghapus kode lama dan dingin dari cache (opsi
-XX:+UseCodeCacheFlushing
diaktifkan secara default), dan ini, pada gilirannya, dapat menyebabkan konsumsi CPU yang lebih tinggi dan penurunan kinerja. . Akan lebih bagus jika Anda bisa melempar OOM ketika Code Cache meluap, untuk ini bahkan ada
-XX:+ExitOnFullCodeCache
, tetapi, sayangnya, itu hanya tersedia dalam
versi pengembangan JVM.
Kumpulan terakhir yang berisi informasi dalam JMX adalah
memori langsung . Secara default, ukurannya tidak terbatas, jadi penting untuk menetapkan semacam batasan untuk itu - setidaknya perpustakaan seperti Netty, yang secara aktif menggunakan buffer byte langsung, akan dipandu olehnya. Tidaklah sulit untuk menetapkan batas menggunakan
-XX:MaxDirectMemorySize
, dan, sekali lagi, hanya pemantauan yang akan membantu kami menentukan nilai yang benar.
Jadi apa yang kita dapatkan sejauh ini?
Memori proses Java =
Heap + Metaspace + Kode Cache + Memori Langsung =
-Xmx +
-XX: MaxMetaspaceSize +
-XX: ReservedCodeCacheSize +
-XX: MaxDirectMemorySize
Mari kita coba menggambar semuanya pada bagan dan membandingkannya dengan wadah buruh pelabuhan RSS.
Baris di atas adalah RSS dari wadah dan itu adalah satu setengah kali lebih banyak dari konsumsi memori JVM, yang dapat kita pantau melalui JMX.
Menggali lebih jauh!
Membatasi konsumsi memori: Pelacakan Memori Asli
Tentu saja, selain memori heap, non-heap, dan direct, JVM menggunakan sejumlah kumpulan memori lainnya. Bendera
-XX:NativeMemoryTracking=summary
akan membantu kami
-XX:NativeMemoryTracking=summary
. Dengan mengaktifkan opsi ini, kami akan dapat memperoleh informasi tentang kumpulan yang diketahui oleh JVM, tetapi tidak tersedia di JMX. Anda dapat membaca lebih lanjut tentang menggunakan opsi ini dalam
dokumentasi .
Mari kita mulai dengan yang paling jelas - memori yang ditempati oleh
tumpukan benang . NMT menghasilkan sesuatu seperti berikut ini untuk layanan kami:
Utas (dicadangkan = 32166KB, berkomitmen = 5358KB)
(utas # 52)
(tumpukan: dicadangkan = 31920KB, berkomitmen = 5112KB)
(malloc = 185KB # 270)
(arena = 61KB # 102)
Ngomong-ngomong, ukurannya juga dapat ditemukan tanpa Native Memory Tracking, menggunakan jstack dan menggali sedikit ke
/proc/<pid>/smaps
. Andrey Pangin memberikan
utilitas khusus untuk ini.
Ukuran
Ruang Kelas Bersama bahkan lebih mudah untuk dievaluasi:
Ruang kelas bersama (disediakan = 17084KB, berkomitmen = 17084KB)
(mmap: dicadangkan = 17084KB, berkomitmen = 17084KB)
Ini adalah mekanisme Berbagi Data Kelas,
-Xshare
dan
-XX:+UseAppCDS
. Di Java 11, opsi
-Xshare
diatur ke otomatis secara default, yang berarti bahwa jika Anda memiliki
$JAVA_HOME/lib/server/classes.jsa
(ini ada dalam gambar docker OpenJDK resmi), ia akan memuat peta memori- Ohm saat startup JVM, mempercepat waktu startup. Dengan demikian, ukuran Ruang Kelas Bersama mudah untuk ditentukan jika Anda mengetahui ukuran jsa-arsip.
Berikut ini adalah struktur
pengumpul sampah asli:
GC (dicadangkan = 42137KB, berkomitmen = 41801KB)
(malloc = 5705KB # 9460)
(mmap: dicadangkan = 36432KB, berkomitmen = 36096KB)
Alexey Shipilev dalam manual yang sudah disebutkan tentang Native Memory Tracking
mengatakan bahwa mereka menempati sekitar 4-5% dari ukuran heap, tetapi dalam pengaturan kami untuk heap kecil (hingga beberapa ratus megabyte) overhead mencapai 50% dari ukuran heap.
Banyak ruang dapat ditempati oleh
tabel simbol :
Simbol (dilindungi = 16421KB, berkomitmen = 16421KB)
(malloc = 15261KB # 203089)
(arena = 1159KB # 1)
Mereka menyimpan nama metode, tanda tangan, serta tautan ke string yang diinternir. Sayangnya, tampaknya mungkin untuk mengestimasi ukuran tabel simbol hanya post factum menggunakan Native Memory Tracking.
Apa yang tersisa Menurut Native Memory Tracking, cukup banyak hal:
Kompiler (dicadangkan = 509KB, berkomitmen = 509KB)
Internal (dicadangkan = 1647KB, berkomitmen = 1647KB)
Lainnya (dicadangkan = 2110KB, berkomitmen = 2110KB)
Arena Chunk (disediakan = 1712KB, berkomitmen = 1712KB)
Logging (dicadangkan = 6KB, berkomitmen = 6KB)
Argumen (milik = 19KB, berkomitmen = 19KB)
Modul (disediakan = 227KB, berkomitmen = 227KB)
Tidak dikenal (disediakan = 32KB, berkomitmen = 32KB)
Tetapi semua ini membutuhkan sedikit ruang.
Sayangnya, banyak dari area memori yang disebutkan tidak dapat dibatasi atau dikendalikan, dan jika bisa, konfigurasi akan berubah menjadi neraka. Bahkan memantau status mereka adalah tugas yang tidak sepele, karena dimasukkannya Native Memory Tracking sedikit menguras kinerja aplikasi dan memungkinkannya pada produksi dalam layanan kritis bukanlah ide yang baik.
Namun demikian, demi kepentingan, mari kita coba untuk merefleksikan semua grafik yang dilaporkan oleh Pelacakan Memori Asli:
Tidak buruk! Perbedaan yang tersisa adalah overhead untuk fragmentasi / alokasi memori (sangat kecil, karena kami menggunakan jemalloc) atau memori yang dialokasikan libs asli. Kami hanya menggunakan salah satunya untuk penyimpanan efisien dari pohon awalan.
Jadi, untuk kebutuhan kita, cukup membatasi apa yang kita bisa: Heap, Metaspace, Code Cache, Direct Memory. Untuk yang lainnya, kami meninggalkan beberapa dasar yang masuk akal, yang ditentukan oleh hasil pengukuran praktis.
Setelah berurusan dengan CPU dan memori, kami beralih ke sumber daya berikutnya di mana aplikasi dapat bersaing - ke disk.
Java dan drive
Dan dengan mereka, semuanya sangat buruk: mereka lambat dan dapat menyebabkan aplikasi terlihat kusam. Karena itu, kami melepaskan ikatan Java dari disk sebanyak mungkin:
- Kami menulis semua log aplikasi ke syslog lokal melalui UDP. Ini menyisakan beberapa peluang bahwa log yang diperlukan akan hilang di suatu tempat di sepanjang jalan, tetapi, seperti yang telah ditunjukkan oleh praktik, kasus-kasus seperti itu sangat jarang.
- Kami akan menulis log JVM dalam tmpfs, untuk ini kita hanya perlu memasang docker ke lokasi yang diinginkan dengan
/dev/shm
.
Jika kita menulis log di syslog atau di tmpfs, dan aplikasi itu sendiri tidak menulis apa pun ke disk kecuali untuk dump hip, maka ternyata cerita dengan disk dapat dianggap ditutup pada ini?
Tentu saja tidak.
Kami memperhatikan grafik durasi stop-the-world jeda dan kami melihat gambar sedih - Stop-The-World-jeda pada host adalah ratusan milidetik, dan pada satu host mereka dapat mencapai hingga satu detik:
Tidak perlu dikatakan bahwa ini berdampak negatif pada aplikasi? Di sini, misalnya, adalah grafik yang mencerminkan waktu respons layanan menurut pelanggan:
Ini adalah layanan yang sangat sederhana, sebagian besar memberikan tanggapan dalam cache, jadi dari mana waktu penghalang seperti itu, dimulai dengan persentil ke-95? Layanan lain memiliki gambar yang serupa, selain itu, timeout hujan dengan keteguhan yang patut ditiru saat mengambil koneksi dari kumpulan koneksi ke database, ketika mengeksekusi permintaan, dan sebagainya.
Apa hubungannya dengan drive itu? - kamu bertanya. Ternyata sangat banyak hubungannya dengan itu.
Analisis terperinci masalah menunjukkan bahwa jeda STW yang panjang muncul karena fakta bahwa utas pergi ke titik aman untuk waktu yang lama. Setelah membaca kode JVM, kami menyadari bahwa selama sinkronisasi utas pada safepoint, JVM dapat menulis file
/tmp/hsperfdata*
melalui peta memori, yang diekspor beberapa statistik. Utilitas seperti
jstat
dan
jstat
menggunakan
jps
jstat
.
Nonaktifkan pada mesin yang sama dengan opsi
-XX:+PerfDisableSharedMem
dan ...
Metrik treadpool dermaga menstabilkan:
(, ):
, , , .
?
Java- , , , .
Nuts and Bolts , . , . , , JMX.
, . .
statsd JVM, (heap, non-heap ):
, , .
— , , , , ? . () -, , RPS .
: , . . ammo-
. . . :
.
, . , , - , , .
Kesimpulannya
, Java Docker — , . .