Mengapa Perselisihan Bermigrasi dari Pergi ke Karat



Rust menjadi bahasa kelas satu di berbagai bidang. Kami di Discord berhasil menggunakannya di sisi server dan klien. Misalnya, di sisi klien dalam pipa penyandian video untuk Go Live, dan di sisi server untuk fungsi Elixir NIF (Native Implemented Functions).

Kami baru-baru ini secara dramatis meningkatkan kinerja satu layanan, menulis ulang dari Go to Rust. Artikel ini akan menjelaskan mengapa masuk akal bagi kami untuk menulis ulang layanan, bagaimana kami melakukannya dan berapa banyak produktivitas yang ditingkatkan.

Baca Layanan Pelacakan Negara (Baca Negara)


Perusahaan kami dibangun di sekitar satu produk, jadi mari kita mulai dengan beberapa konteks, apa sebenarnya yang kami transfer dari Go ke Rust. Ini adalah layanan Read States. Satu-satunya tugasnya adalah melacak saluran dan pesan mana yang Anda baca. Status Baca diakses setiap kali Anda terhubung ke Perselisihan, setiap kali Anda mengirim pesan, dan setiap kali Anda membaca pesan. Singkatnya, status dibaca terus menerus dan berada di "jalur panas". Kami ingin memastikan bahwa Perselisihan selalu cepat, jadi pemeriksaan negara harus cepat.

Implementasi layanan on Go tidak memenuhi semua persyaratan. Sebagian besar waktu itu bekerja dengan cepat, tetapi setiap beberapa menit ada penundaan yang kuat, terlihat oleh pengguna. Setelah memeriksa situasinya, kami memutuskan bahwa penundaan itu disebabkan oleh fitur-fitur utama Go: model ingatannya dan pengumpul sampah (GC).

Why Go Tidak Memenuhi Tujuan Kinerja Kami


Untuk menjelaskan mengapa Go tidak memenuhi target kinerja kami, pertama-tama kami perlu membahas struktur data, skala, pola akses, dan arsitektur layanan.

Untuk menyimpan informasi status, kami menggunakan struktur data, yang disebut: Status Baca. Ada miliaran dari mereka dalam Perselisihan: satu negara untuk setiap pengguna per saluran. Setiap negara memiliki beberapa penghitung, yang harus diperbarui secara atom dan sering kali diatur ulang ke nol. Misalnya, salah satu penghitung adalah nomor @mentiondi saluran.

Untuk dengan cepat memperbarui penghitung atom, setiap server Read States memiliki cache yang terakhir digunakan (LRU). Setiap cache memiliki jutaan pengguna dan puluhan juta negara. Cache diperbarui ratusan ribu kali per detik.

Untuk keamanan, cache disinkronkan dengan cluster basis data Cassandra. Saat kunci didorong keluar dari cache, kami memasukkan status pengguna ini dalam basis data. Di masa mendatang, kami berencana memperbarui basis data dalam waktu 30 detik dengan setiap pembaruan status. Ini adalah puluhan ribu catatan dalam database setiap detik.

Grafik di bawah ini menunjukkan waktu respons dan beban CPU pada interval waktu puncak untuk layanan Go 1. Dapat dilihat bahwa penundaan dan semburan beban pada CPU terjadi kira-kira setiap dua menit.



Jadi dari mana datangnya penundaan setiap dua menit?


Di Go, memori tidak segera dibebaskan saat kunci didorong keluar dari cache. Sebaliknya, pengumpul sampah berjalan secara berkala dan mencari bagian memori yang tidak digunakan. Ini banyak pekerjaan yang bisa memperlambat program.

Sangat mungkin bahwa penurunan layanan kami secara berkala dikaitkan dengan pengumpulan sampah. Tapi kami menulis kode Go yang sangat efisien dengan alokasi memori yang minimal. Seharusnya tidak banyak sampah yang tersisa. Apa masalahnya?

Mengaduk-aduk kode sumber Go, kami belajar bahwa Go secara paksa memulai pengumpulan sampah setidaknya setiap dua menit . Terlepas dari ukuran tumpukan, jika GC tidak mulai selama dua menit, Go akan memaksanya untuk memulai.

Kami memutuskan bahwa jika Anda menjalankan GC lebih sering, Anda dapat menghindari puncak ini dengan penundaan besar, jadi kami menetapkan titik akhir dalam layanan untuk mengubah nilai GC Persen saat itu juga . Sayangnya, konfigurasi GC Persen tidak memengaruhi apa pun. Bagaimana ini bisa terjadi? Ternyata GC tidak ingin memulai lebih sering, karena kami tidak mengalokasikan memori cukup sering.

Kami mulai menggali lebih jauh. Ternyata penundaan besar seperti itu tidak terjadi karena jumlah besar memori yang dibebaskan, tetapi karena pemulung sampah memindai seluruh cache LRU untuk memeriksa semua memori. Kemudian kami memutuskan bahwa jika kami mengurangi cache LRU, maka volume pemindaian akan berkurang. Oleh karena itu, kami menambahkan satu parameter lagi ke layanan untuk mengubah ukuran cache LRU, dan mengubah arsitektur, memecah LRU menjadi banyak cache terpisah di setiap server.

Dan begitulah yang terjadi. Dengan cache yang lebih kecil, penundaan puncak berkurang.

Sayangnya, kompromi dengan penurunan cache LRU meningkatkan persentil ke-99 (yaitu, nilai rata-rata untuk sampel 99% dari keterlambatan meningkat, tidak termasuk yang puncak). Ini karena mengurangi cache mengurangi kemungkinan bahwa Status Baca pengguna akan berada dalam cache. Jika tidak ada di sini, maka kita harus beralih ke database.

Setelah sejumlah besar pengujian beban pada berbagai ukuran cache, kami menemukan pengaturan yang dapat diterima. Meskipun tidak ideal, itu adalah solusi yang memuaskan, jadi kami meninggalkan layanan untuk waktu yang lama untuk bekerja seperti itu.

Pada saat yang sama, kami mengimplementasikan Rust dengan sangat sukses dalam sistem Perselisihan lain, dan sebagai hasilnya kami membuat keputusan bersama untuk menulis kerangka kerja dan perpustakaan untuk layanan baru hanya di Rust. Dan layanan ini tampaknya menjadi kandidat yang sangat baik untuk porting ke Rust: itu kecil dan otonom, dan kami berharap Rust akan memperbaiki ledakan ini dengan penundaan dan pada akhirnya membuat layanan lebih menyenangkan bagi pengguna 2.

Manajemen Memori dalam Karat


Rust sangat cepat dan efisien dengan memori: dengan tidak adanya lingkungan runtime dan pengumpul sampah, sangat cocok untuk layanan berkinerja tinggi, aplikasi tertanam, dan mudah diintegrasikan dengan bahasa lain. 3

Rust tidak memiliki pengumpul sampah, jadi kami memutuskan bahwa tidak akan ada penundaan, seperti Go.

Dalam manajemen memori, ia menggunakan pendekatan yang agak unik dengan gagasan "memiliki" memori. Singkatnya, Rust melacak siapa yang berhak membaca dan menulis ke memori. Dia tahu kapan suatu program menggunakan memori, dan segera membebaskannya begitu memori tidak lagi diperlukan. Rust memberlakukan aturan memori pada waktu kompilasi, yang secara virtual menghilangkan kemungkinan kesalahan memori pada saat run time. 4Anda tidak perlu melacak memori secara manual. Kompiler akan menangani ini.

Dengan demikian, dalam versi Karat, saat Status Baca dikecualikan dari cache LRU, memori segera dibebaskan. Memori ini tidak duduk dan tidak menunggu pengumpul sampah. Rust tahu bahwa itu tidak lagi digunakan dan segera melepaskannya. Tidak ada proses dalam runtime untuk memindai memori mana yang akan dibebaskan.

Karat Asinkron


Tapi ada satu masalah dengan ekosistem Rust. Pada saat implementasi layanan kami, tidak ada fungsi asinkron yang layak di cabang Rust yang stabil. Untuk layanan jaringan, pemrograman asinkron adalah suatu keharusan. Komunitas telah mengembangkan beberapa perpustakaan, tetapi dengan koneksi non-sepele dan pesan kesalahan yang sangat bodoh.

Untungnya, tim Rust bekerja keras untuk menyederhanakan pemrograman asinkron, dan sudah tersedia di saluran yang tidak stabil (Nightly).

Perselisihan tidak pernah takut untuk belajar teknologi baru yang menjanjikan. Misalnya, kami adalah salah satu pengguna pertama Elixir, React, React Native, dan Scylla. Jika beberapa teknologi terlihat menjanjikan dan memberi kita keuntungan, maka kita siap menghadapi kesulitan implementasi yang tidak terhindarkan dan ketidakstabilan alat canggih. Ini adalah salah satu alasan kami begitu cepat menjangkau audiensi 250 juta pengguna dengan kurang dari 50 programmer di negara bagian itu.

Pengenalan fungsi asinkron baru dari saluran Rust yang tidak stabil adalah contoh lain dari kesediaan kami untuk mengadopsi teknologi baru yang menjanjikan. Tim teknik memutuskan untuk mengimplementasikan fungsi yang diperlukan tanpa menunggu dukungan mereka dalam versi stabil. Bersama dengan perwakilan komunitas lainnya, kami telah mengatasi semua masalah yang muncul, dan sekarang Rust tidak sinkrondipelihara di cabang yang stabil. Nilai tukar kami telah terbayar.

Implementasi, stress testing dan peluncuran


Menulis ulang kode itu mudah. Kami mulai dengan siaran kasar, lalu menguranginya ke tempat-tempat yang masuk akal. Misalnya, Rust memiliki sistem tipe yang sangat baik dengan dukungan luas untuk obat generik (untuk bekerja dengan data jenis apa pun), jadi kami dengan tenang membuang kode Go, yang mengkompensasi kurangnya obat generik. Selain itu, model memori Rust memperhitungkan keamanan memori di utas yang berbeda, jadi kami membuang goroutine pelindung.

Pengujian beban segera menunjukkan hasil yang sangat baik. Kinerja layanan di Rust ternyata setinggi versi Go, tetapi tanpa peningkatan penundaan ini !

Biasanya, kami praktis tidak mengoptimalkan versi Rust. Tetapi bahkan dengan optimasi yang paling sederhana, Rust mampu mengungguli versi Go yang disetel dengan cermat.Ini adalah bukti fasih tentang betapa mudahnya untuk menulis program Rust yang efektif dibandingkan dengan pergi jauh ke Go.

Tapi kami tidak memuaskan kinerja quo yang sederhana. Setelah sedikit profil dan optimasi, kami melampaui Go dalam semua hal . Keterlambatan, CPU, dan memori - semuanya menjadi lebih baik di versi Rust.

Optimalisasi kinerja karat termasuk:

  1. Beralih ke BTreeMap alih-alih HashMap di cache LRU untuk mengoptimalkan penggunaan memori.
  2. Mengganti perpustakaan metrik asli dengan versi dengan dukungan untuk Rust konkurensi modern.
  3. Kurangi jumlah salinan dalam memori.

Puas, kami memutuskan untuk menggunakan layanan ini.

Peluncuran berjalan cukup lancar, saat kami melakukan stress test. Kami menghubungkan layanan ke satu test node, menemukan dan memperbaiki beberapa kasus batas. Segera setelah itu, mereka meluncurkan versi baru ke seluruh taman server.

Hasilnya ditunjukkan di bawah ini.

Grafik ungu adalah Go, grafik biru adalah Karat.



Tambah ukuran cache


Ketika layanan berhasil bekerja selama beberapa hari, kami memutuskan untuk meningkatkan cache LRU lagi. Seperti disebutkan di atas, dalam versi Go, ini tidak dapat dilakukan, karena waktu untuk pengumpulan sampah meningkat. Karena kami tidak lagi melakukan pengumpulan sampah, Anda dapat meningkatkan penghitungan cache dengan peningkatan kinerja yang lebih besar. Jadi, kami telah meningkatkan memori di server, mengoptimalkan struktur data untuk penggunaan memori yang lebih sedikit (untuk bersenang-senang) dan meningkatkan ukuran cache menjadi 8 juta status Baca Negara.

Hasil di bawah ini berbicara sendiri. Perhatikan bahwa waktu rata-rata sekarang diukur dalam mikrodetik, dan keterlambatan maksimum @mentiondiukur dalam milidetik.



Pengembangan ekosistem


Akhirnya, Rust memiliki ekosistem yang luar biasa yang tumbuh dengan cepat. Misalnya, baru-baru ini versi baru runtime asinkron yang kami gunakan adalah Tokio 0.2. Kami memperbarui, dan tanpa upaya dari pihak kami, secara otomatis mengurangi beban pada CPU. Pada grafik di bawah ini, Anda dapat melihat bagaimana beban telah menurun sejak sekitar 16 Januari.



Pikiran terakhir


Discord saat ini menggunakan Rust di banyak bagian dari tumpukan perangkat lunak: untuk GameSDK, menangkap dan menyandikan video di Go Live, Elixir NIF , beberapa layanan backend, dan banyak lagi.

Saat memulai proyek atau komponen perangkat lunak baru, kami pasti mempertimbangkan untuk menggunakan Rust. Tentu saja, hanya di tempat yang masuk akal.

Selain kinerja, Rust memberi pengembang banyak manfaat lainnya. Sebagai contoh, keamanan jenisnya dan pemeriksa pinjaman sangat menyederhanakan refactoring ketika persyaratan produk berubah atau fitur bahasa baru diperkenalkan. Ekosistem dan alatnya sangat bagus dan berkembang pesat.

Fakta menyenangkan: tim Rust juga menggunakan Perselisihan untuk berkoordinasi. Bahkan ada yang sangat bergunaServer komunitas karat , tempat kami terkadang mengobrol.



Catatan kaki


  1. Grafik diambil dari Go versi 1.9.2. Kami mencoba versi 1.8, 1.9 dan 1.10 tanpa perbaikan apa pun. Migrasi awal dari Go ke Rust selesai pada Mei 2019. [mengembalikan]
  2. Untuk kejelasan, kami tidak menyarankan menulis ulang semua yang ada di Rust tanpa alasan. [mengembalikan]
  3. Kutipan dari situs resmi. [mengembalikan]
  4. Tentu saja, sampai Anda menggunakan tidak aman . [mengembalikan]

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


All Articles