Bagaimana cara meningkatkan kinerja Java secara signifikan?

23

Tim di LMAX memiliki presentasi tentang bagaimana mereka dapat melakukan 100rb TPS kurang dari 1 ms latensi . Mereka telah membuat cadangan presentasi itu dengan blog , kertas teknis (PDF) dan kode sumbernya sendiri.

Baru-baru ini, Martin Fowler menerbitkan sebuah makalah yang luar biasa tentang arsitektur LMAX dan menyebutkan bahwa mereka sekarang mampu menangani enam juta pesanan per detik dan menyoroti beberapa langkah yang diambil tim untuk naik urutan urutan besar dalam kinerja.

Sejauh ini saya sudah menjelaskan bahwa kunci kecepatan Business Logic Processor adalah melakukan semuanya secara berurutan, dalam memori. Hanya melakukan ini (dan tidak ada yang benar-benar bodoh) memungkinkan pengembang untuk menulis kode yang dapat memproses 10K TPS.

Mereka kemudian menemukan bahwa berkonsentrasi pada elemen sederhana dari kode yang baik dapat membawa ini ke kisaran 100K TPS. Ini hanya membutuhkan kode yang diperhitungkan dengan baik dan metode-metode kecil - pada dasarnya ini memungkinkan Hotspot untuk melakukan pekerjaan yang lebih baik untuk mengoptimalkan dan agar CPU menjadi lebih efisien dalam menyimpan kode ketika sedang berjalan.

Butuh sedikit lebih pintar untuk naik urutan besarnya. Ada beberapa hal yang menurut tim LMAX bermanfaat untuk mencapainya. Salah satunya adalah menulis implementasi kustom dari koleksi Java yang dirancang untuk menjadi cache-friendly dan hati-hati dengan sampah.

Teknik lain untuk mencapai tingkat kinerja teratas adalah dengan menaruh perhatian pada pengujian kinerja. Saya sudah lama memperhatikan bahwa orang berbicara banyak tentang teknik untuk meningkatkan kinerja, tetapi satu hal yang benar-benar membuat perbedaan adalah mengujinya

Fowler menyebutkan bahwa ada beberapa hal yang ditemukan, tetapi dia hanya menyebut pasangan.

Adakah arsitektur, perpustakaan, teknik, atau "hal" lain yang membantu untuk mencapai tingkat kinerja seperti itu?

Dakotah Utara
sumber
11
"Arsitektur, perpustakaan, teknik, atau" hal "apa yang membantu untuk mencapai tingkat kinerja seperti itu?" Kenapa bertanya? Quote yang satu daftar definitif. Ada banyak dan banyak hal lain, tidak ada yang memiliki dampak baik pada item dalam daftar itu. Apa pun yang orang dapat sebutkan tidak akan membantu daftar itu. Mengapa meminta ide-ide buruk ketika Anda mengutip salah satu daftar optimisasi terbaik yang pernah dihasilkan?
S.Lott
Akan menyenangkan untuk mempelajari alat mana yang mereka gunakan untuk melihat bagaimana kode yang dihasilkan berjalan pada sistem.
1
Saya telah mendengar orang bersumpah dengan semua jenis teknik. Apa yang saya temukan paling efektif adalah profil tingkat sistem. Ini dapat menunjukkan Anda hambatan dalam cara program dan beban kerja Anda menjalankan sistem. Saya akan menyarankan mengikuti pedoman terkenal tentang kinerja dan menulis kode modular sehingga Anda dapat dengan mudah menyetelnya nanti ... Saya tidak berpikir Anda bisa salah dengan profil sistem.
ritesh

Jawaban:

21

Ada berbagai macam teknik untuk pemrosesan transaksi berkinerja tinggi dan yang ada di artikel Fowler hanyalah salah satu dari sekian banyak teknik yang ada. Daripada mendaftar banyak teknik yang mungkin atau mungkin tidak berlaku untuk situasi siapa pun, saya pikir lebih baik untuk membahas prinsip-prinsip dasar dan bagaimana LMAX menangani sejumlah besar dari mereka.

Untuk sistem pemrosesan transaksi skala tinggi, Anda ingin melakukan semua hal berikut sebanyak mungkin:

  1. Minimalkan waktu yang dihabiskan di tingkat penyimpanan paling lambat. Dari tercepat hingga paling lambat pada server modern yang Anda miliki: CPU / L1 -> L2 -> L3 -> RAM -> Disk / LAN -> WAN. Lompatan dari bahkan disk magnetik modern tercepat ke RAM paling lambat adalah lebih dari 1000x untuk akses sekuensial ; akses acak bahkan lebih buruk.

  2. Minimalkan atau hilangkan waktu yang dihabiskan untuk menunggu . Ini berarti berbagi negara sesedikit mungkin, dan, jika negara harus dibagi, menghindari kunci eksplisit bila memungkinkan.

  3. Sebarkan beban kerja. CPU belum mendapatkan jauh lebih cepat dalam beberapa tahun terakhir, tetapi mereka telah mendapatkan lebih kecil, dan 8 core sangat umum pada server. Di luar itu, Anda bahkan dapat menyebarkan pekerjaan melalui beberapa mesin, yang merupakan pendekatan Google; hal yang hebat tentang ini adalah bahwa ia menskala segala sesuatu termasuk I / O.

Menurut Fowler, LMAX mengambil pendekatan berikut untuk masing-masing:

  1. Simpan semua status dalam memori setiap saat. Sebagian besar mesin database sebenarnya akan melakukan ini, jika seluruh database dapat masuk dalam memori, tetapi mereka tidak ingin meninggalkan apa pun untuk kesempatan, yang dapat dimengerti pada platform perdagangan real-time. Untuk melakukan ini tanpa menambahkan satu ton risiko, mereka harus membangun banyak cadangan ringan dan infrastruktur failover.

  2. Gunakan antrian bebas kunci ("pengganggu") untuk aliran acara masukan. Berbeda dengan antrian pesan tahan lama tradisional yang pasti tidak bebas kunci, dan pada kenyataannya biasanya melibatkan transaksi yang didistribusikan dengan sangat lambat .

  3. Tidak banyak. LMAX melempar yang ini di bawah bus atas dasar bahwa beban kerja saling tergantung; hasil satu mengubah parameter untuk yang lain. Ini adalah peringatan kritis , dan yang secara eksplisit dipanggil Fowler. Mereka membuat beberapa penggunaan concurrency dalam rangka memberikan kemampuan failover, tapi semua logika bisnis diproses pada thread tunggal .

LMAX bukan satu-satunya pendekatan untuk OLTP skala tinggi. Dan meskipun itu cukup brilian dalam dirinya sendiri, Anda tidak perlu menggunakan teknik-teknik yang canggih untuk melakukan tingkat kinerja itu.

Dari semua prinsip di atas, # 3 mungkin yang paling penting dan paling efektif, karena, sejujurnya, perangkat keras itu murah. Jika Anda dapat mempartisi beban kerja dengan benar di setengah lusin inti dan beberapa lusin mesin, maka langit adalah batas untuk teknik Komputasi Paralel konvensional . Anda akan terkejut betapa banyak throughput yang Anda dapat lakukan dengan apa-apa selain sekelompok antrian pesan dan distributor round-robin. Ini jelas tidak seefisien LMAX - sebenarnya bahkan tidak dekat - tetapi throughput, latensi, dan efektivitas biaya adalah masalah yang terpisah, dan di sini kita berbicara secara khusus tentang throughput.

Jika Anda memiliki jenis kebutuhan khusus yang sama dengan yang dilakukan LMAX - khususnya, keadaan bersama yang sesuai dengan kenyataan bisnis yang bertentangan dengan pilihan desain tergesa-gesa - maka saya akan menyarankan untuk mencoba komponen mereka, karena saya belum melihat banyak lain yang sesuai dengan persyaratan tersebut. Tetapi jika kita hanya berbicara tentang skalabilitas tinggi maka saya mendorong Anda untuk melakukan lebih banyak penelitian ke dalam sistem terdistribusi, karena mereka adalah pendekatan kanonik yang digunakan oleh sebagian besar organisasi saat ini (Hadoop dan proyek terkait, ESB dan arsitektur terkait, CQRS yang juga Fowler juga menyebutkan, dan sebagainya).

SSD juga akan menjadi game-changer; bisa dibilang, mereka sudah ada. Anda sekarang dapat memiliki penyimpanan permanen dengan waktu akses yang mirip dengan RAM, dan meskipun SSD tingkat server masih sangat mahal, mereka akhirnya akan turun harga begitu tingkat adopsi tumbuh. Ini telah diteliti secara luas dan hasilnya cukup membingungkan dan hanya akan menjadi lebih baik dari waktu ke waktu, sehingga keseluruhan konsep "simpan semuanya dalam ingatan" jauh lebih penting daripada dulu. Jadi sekali lagi, saya akan mencoba untuk fokus pada konkurensi jika memungkinkan.

Aaronaught
sumber
Membahas prinsip-prinsip ini adalah prinsip-prinsip yang mendasarinya bagus dan komentar Anda sangat bagus dan ... kecuali makalah fowler tidak memiliki referensi dalam catatan kaki untuk men-cache algoritma lupa en.wikipedia.org/wiki/Cache-oblivious_algorithm (yang cocok dengan kategori nomor 1 yang Anda miliki di atas) Saya tidak akan pernah menemukan mereka. Jadi ... sehubungan dengan setiap kategori yang Anda miliki di atas, apakah Anda tahu 3 hal teratas yang harus diketahui seseorang?
Dakotah North
@Dakotah: Saya bahkan tidak akan mulai khawatir tentang cache lokalitas kecuali dan sampai saya benar-benar menghilangkan disk I / O, yang merupakan tempat sebagian besar waktu dihabiskan menunggu di sebagian besar aplikasi. Selain itu, apa yang Anda maksud dengan "3 hal teratas yang harus diketahui seseorang"? 3 Atas apa, untuk mengetahui tentang apa?
Aaronaught
Lompatan dari latensi akses RAM (~ 10 ^ -9s) ke latensi disk magnetik (~ rata-rata 10 ~ -3s) adalah beberapa perintah lain yang besarnya lebih besar dari 1000x. Bahkan SSD masih memiliki waktu akses yang diukur dalam ratusan mikrodetik.
Sedate Alien
@Sedate: Latensi ya, tapi ini lebih merupakan masalah throughput daripada latensi mentah, dan begitu Anda melewati waktu akses dan menjadi kecepatan transfer total, disk tidak terlalu buruk. Itu sebabnya saya membuat perbedaan antara akses acak dan berurutan; untuk skenario akses acak itu tidak terutama menjadi isu latency.
Aaronaught
@Aaronaught: Setelah membaca kembali, saya kira Anda benar. Mungkin suatu titik harus dibuat bahwa semua akses data harus sekuensial mungkin; manfaat signifikan juga dapat diperoleh saat mengakses data yang dipesan dari RAM.
Sedate Alien
10

Saya pikir pelajaran terbesar untuk dipelajari dari ini adalah Anda harus mulai dengan dasar-dasarnya:

  • Algoritma yang baik, struktur data yang sesuai, dan tidak melakukan apa pun "benar-benar bodoh"
  • Kode yang difaktorkan dengan baik
  • Pengujian kinerja

Selama pengujian kinerja, Anda membuat profil kode Anda, menemukan kemacetan, dan memperbaikinya satu per satu.

Terlalu banyak orang yang melompat langsung ke bagian "perbaiki satu per satu". Mereka menghabiskan banyak waktu menulis "implementasi kustom dari koleksi java", karena mereka hanya tahu bahwa seluruh alasan sistem mereka lambat adalah karena kesalahan cache. Itu mungkin faktor yang berkontribusi, tetapi jika Anda langsung mengubah kode tingkat rendah seperti itu, Anda mungkin akan kehilangan masalah lebih besar menggunakan ArrayList ketika Anda harus menggunakan LinkedList, atau bahwa alasan sebenarnya sistem Anda adalah lambat karena ORM Anda memuat anak-anak dari suatu entitas dengan malas dan karenanya melakukan 400 perjalanan terpisah ke database untuk setiap permintaan.

Adam Jaskiewicz
sumber
7

Secara khusus tidak akan mengomentari kode LMAX karena saya pikir itu cukup banyak dijelaskan, tetapi di sini ada beberapa contoh hal yang telah saya lakukan yang menghasilkan peningkatan kinerja yang terukur.

Seperti biasa, ini adalah teknik yang harus diterapkan setelah Anda tahu bahwa Anda memiliki masalah dan perlu meningkatkan kinerja - jika tidak, Anda mungkin hanya akan melakukan optimasi prematur.

  • Gunakan struktur data yang tepat, dan buat yang khusus jika diperlukan - desain struktur data yang benar jauh lebih baik dari perbaikan mikro, jadi lakukan ini terlebih dahulu. Jika algoritme Anda bergantung pada kinerja pada banyak akses acak O (1) yang cepat dibaca, pastikan Anda memiliki struktur data yang mendukung ini! Ada baiknya melompati beberapa simpai untuk mendapatkan ini dengan benar, misalnya menemukan cara Anda dapat mewakili data Anda dalam array untuk mengeksploitasi pembacaan terindeks O (1) yang sangat cepat.
  • CPU lebih cepat daripada akses memori - Anda dapat melakukan cukup banyak perhitungan dalam waktu yang diperlukan untuk membuat satu memori acak dibaca jika memori tidak ada dalam cache L1 / L2. Biasanya layak melakukan perhitungan jika ini menghemat memori Anda.
  • Bantu kompiler JIT dengan bidang pembuatan akhir , metode dan kelas - kelas final memungkinkan optimisasi spesifik yang benar - benar membantu kompiler JIT. Contoh spesifik:

    • Compiler dapat mengasumsikan bahwa kelas akhir tidak memiliki subclass, sehingga dapat mengubah panggilan metode virtual menjadi panggilan metode statis
    • Kompilator dapat memperlakukan bidang akhir statis sebagai konstanta untuk peningkatan kinerja yang bagus, terutama jika konstanta tersebut kemudian digunakan dalam perhitungan yang dapat dihitung pada waktu kompilasi.
    • Jika bidang yang berisi objek Java diinisialisasi sebagai final, maka pengoptimal dapat menghilangkan cek nol dan pengiriman metode virtual. Bagus.
  • Ganti kelas koleksi dengan array - ini menghasilkan kode yang kurang dapat dibaca dan lebih sulit untuk dipertahankan tetapi hampir selalu lebih cepat karena menghapus lapisan tipuan dan manfaat dari banyak optimisasi akses array yang bagus. Biasanya ide yang baik dalam inner loop / kode sensitif kinerja setelah Anda mengidentifikasinya sebagai hambatan, tetapi hindari sebaliknya demi keterbacaan!

  • Gunakan primitif sedapat mungkin - primitif secara fundamental lebih cepat daripada persamaan berbasis objeknya. Secara khusus, tinju menambahkan sejumlah besar overhead dan dapat menyebabkan jeda GC buruk. Jangan biarkan primitif apa pun dikotak jika Anda peduli dengan kinerja / latensi.

  • Minimalkan penguncian tingkat rendah - kunci sangat mahal pada tingkat rendah. Temukan cara untuk menghindari penguncian sepenuhnya, atau mengunci pada tingkat kasar sehingga Anda hanya perlu mengunci jarang pada blok data yang besar dan kode tingkat rendah dapat melanjutkan tanpa harus khawatir sama sekali tentang masalah penguncian atau masalah konkurensi.

  • Hindari mengalokasikan memori - ini mungkin sebenarnya memperlambat Anda secara keseluruhan karena pengumpulan sampah JVM sangat efisien, tetapi sangat membantu jika Anda mencoba untuk mendapatkan latensi yang sangat rendah dan perlu meminimalkan jeda GC. Ada struktur data khusus yang dapat Anda gunakan untuk menghindari alokasi - perpustakaan http://javolution.org/ khususnya sangat bagus dan terkenal untuk ini.
mikera
sumber
Saya tidak setuju dengan membuat metode final . JIT dapat mengetahui bahwa suatu metode tidak akan pernah diganti. Selain itu, jika subkelas dimuat nanti dapat membatalkan pengoptimalan. Perhatikan juga bahwa "hindari mengalokasikan memori" juga dapat membuat pekerjaan GC lebih sulit dan karenanya memperlambat Anda - jadi gunakan dengan hati-hati.
maaartinus
@maaartinus: mengenai finalbeberapa JIT mungkin mengetahuinya, yang lain mungkin tidak. Ini tergantung implementasi (seperti juga banyak tips penyesuaian kinerja). Setuju tentang alokasi - Anda harus membandingkan ini. Biasanya saya menemukan lebih baik untuk menghilangkan alokasi, tetapi YMMV.
mikera
4

Selain sudah dinyatakan dalam jawaban yang bagus dari Aaronaught, saya ingin mencatat bahwa kode seperti itu mungkin cukup sulit untuk dikembangkan, dipahami, dan didebug. "Meskipun sangat efisien ... sangat mudah untuk mengacaukan ..." sebagai salah satu dari mereka yang disebutkan di blog LMAX .

  • Untuk pengembang yang terbiasa dengan kueri-dan-kunci tradisional , pengkodean untuk pendekatan baru mungkin terasa seperti mengendarai kuda liar. Setidaknya itulah pengalaman saya sendiri ketika bereksperimen dengan Phaser yang konsepnya disebutkan dalam makalah teknis LMAX. Dalam pengertian itu saya akan mengatakan pendekatan ini memperdagangkan pertikaian kunci dengan pertengkaran otak pengembang .

Diberikan di atas, saya pikir mereka yang memilih Disruptor dan pendekatan serupa lebih baik memastikan bahwa mereka memiliki sumber daya pengembangan yang cukup untuk mempertahankan solusi mereka.

Secara keseluruhan, pendekatan Disruptor terlihat cukup menjanjikan bagi saya. Bahkan jika perusahaan Anda tidak mampu menggunakannya misalnya untuk alasan yang disebutkan di atas, pertimbangkan meyakinkan manajemen Anda untuk "menginvestasikan" beberapa upaya untuk mempelajarinya (dan SEDA secara umum) - karena jika mereka tidak melakukannya maka ada peluang bahwa suatu hari nanti pelanggan mereka akan membiarkan mereka mendukung beberapa solusi yang lebih kompetitif yang membutuhkan server 4x, 8x dll.

agas
sumber