Mengapa Rust Memimpin Tolok Ukur Kerangka Kerja TechEmpower

Sebenarnya, saya tidak berniat untuk melihat apa warna nyali Rust. Saya mengambil proyek hobi di Go, pergi ke GitHub untuk melihat keadaan fasthttp: apakah itu berkembang? Ya, setidaknya didukung? Tumbuh. Pergi, melihat di mana fasthttp duduk di benchmark TechEmpower . Saya melihat: dan di sana fasthttp hampir tidak menunjukkan setengah dari apa yang berhasil dilakukan pemimpin - untuk beberapa aksi pada beberapa Rust. Menyedihkan sekali.


Di sini saya akan melipat tangan saya, memukul kepala saya di lantai (tiga kali) dan berteriak: "Haleluya, sesungguhnya Rust adalah tuhan sejati, betapa buta sebelumnya saya!". Tapi entah gagangnya tidak berhasil, atau dahi menyesal ... Sebaliknya, saya masuk ke kode tes yang ditulis dalam Go dan tes actix-web di Rust. Untuk mengatasinya.


Setelah beberapa jam saya menemukan:


  1. mengapa kerangka kerja actix-web Rust menempati posisi pertama dalam semua tes TechEmpower,
  2. bagaimana Java memulai Script.

Sekarang saya akan menceritakan semuanya kepada Anda.


Apa itu Benchmark TechEmpower Framework?


Jika kerangka kerja web menunjukkan apakah itu akan atau, katakanlah, kadang-kadang berpikir tentang berbisik kepada teman-teman "saya cepat," maka itu pasti akan jatuh ke dalam Tolok Ukur Kerangka Kerja TechEmpower. Tempat populer untuk mengukur kinerja.


Situs ini memiliki desain yang khas: tab filter, putaran, kondisi dan hasil untuk berbagai jenis tes tersebar di halaman dengan tangan murah hati. Begitu murah hati dan luas sehingga Anda tidak memperhatikannya. Tapi ada baiknya mengklik pada tab, informasi di belakangnya berguna.


Cara termudah adalah dengan mendapatkan hasil tes plaintext, "Hello World!" untuk server web. Para penulis kerangka kerja biasanya memberikan tautan ke sana: kita seharusnya tinggal di seratus pertama. Kasingnya benar dan bermanfaat. Secara umum, membagikan plaintext itu baik untuk banyak orang, dan para pemimpin pergi dalam kelompok yang ketat.


Di dekatnya, pada tab-tab itu, adalah hasil pengujian jenis lain (skenario). Ada tujuh dari mereka, detail lebih lanjut dapat ditemukan di sini . Skenario ini menguji tidak hanya bagaimana kerangka / platform menangani pemrosesan permintaan http sederhana, tetapi kombinasi dengan klien basis data, mesin templat, atau serializer JSON.


Ada data uji di lingkungan virtual, pada perangkat keras fisik. Selain grafik, ada data tabular. Secara umum, banyak hal menarik, perlu digali, tidak hanya melihat posisi platform "Anda".


Hal pertama yang terlintas di pikiran saya setelah melalui hasil tes: "Mengapa semuanya SO SO berbeda dari plaintext?!". Dalam plaintext, para pemimpin pergi dalam kelompok yang ketat, tetapi ketika datang untuk bekerja dengan database, actix-web dipimpin oleh margin yang signifikan. Pada saat yang sama, ini menunjukkan waktu pemrosesan permintaan yang stabil. Setan.


Keanehan lain: solusi JavaScript yang sangat kuat. Ini disebut ex4x. Ternyata kodenya sedikit kurang lengkap di Jawa. Digunakan oleh Java runtime, JDBC. Kode JavaScript diterjemahkan ke dalam bytecode dan merekatkan pustaka Java. Mereka benar-benar mengambilnya - dan melampirkan Script ke Jawa. Trik wajah pucat tidak memiliki batas.


Cara melihat kode dan apa yang ada di dalamnya


Kode untuk semua tes ada di GitHub. Semuanya dalam satu repositori, yang sangat nyaman. Anda dapat mengkloning dan menonton, Anda dapat menonton langsung di GitHub. Pengujian ini melibatkan lebih dari 300 kombinasi kerangka kerja yang berbeda dengan serializer, mesin templat, dan klien basis data. Dalam bahasa pemrograman yang berbeda, dengan pendekatan pengembangan yang berbeda. Implementasi dalam satu bahasa terdekat, dapat dibandingkan dengan implementasi dalam bahasa lain. Kode ini dikelola oleh komunitas, bukan pekerjaan satu orang atau tim.


Kode benchmark adalah tempat yang tepat untuk memperluas wawasan Anda. Sangat menarik untuk menganalisis bagaimana orang yang berbeda menyelesaikan masalah yang sama. Tidak banyak kode, perpustakaan dan solusi yang digunakan mudah dibedakan. Saya tidak menyesal sama sekali bahwa saya sampai di sana. Saya belajar banyak. Pertama-tama tentang Rust.


Sebelum Rust, saya punya ide yang sangat kabur. Setiap artikel tentang C, C ++, D, dan terutama Go pasti memiliki beberapa komentator yang menjelaskan secara rinci dan dengan kesedihan bahwa kesombongan, omong kosong dan kebodohan ditulis dalam sesuatu yang lain, selama ada Gascony Karat. Kadang-kadang mereka begitu terbawa sehingga mereka memberikan contoh kode daripada orang yang tidak siap atau sedikit yang menerima didorong ke pingsan: "Mengapa, mengapa, mengapa semua simbol ini?!"


Karena itu, membuka kode itu menakutkan.


Saya melihat. Ternyata program di Rust dapat dibaca. Selain itu, kode dibaca dengan baik sehingga saya bahkan menginstal Rust, mencoba untuk mengkompilasi tes dan bermain-main sedikit dengannya.


Di sini saya hampir meninggalkan bisnis ini, karena kompilasi berlangsung lama. Waktu yang sangat lama Jika saya adalah D'Artagnan, atau bahkan hanya seorang penderita penyakit kera, saya akan bergegas ke Gascony, dan seribu iblis akan jejak dengan sedih. Tapi saya berhasil. Saya minum teh lagi. Tampaknya bahkan tidak satu cangkir: di laptop saya, kompilasi pertama memakan waktu sekitar 20 menit, namun, semuanya berjalan lebih menyenangkan. Mungkin sampai peti besar pembaruan berikutnya.


Tapi bukankah Rust itu sendiri?


Tidak. Bukan bahasa pemrograman.


Tentu saja, Rust adalah bahasa yang indah. Kuat, fleksibel, meskipun karena kebiasaan dan bertele-tele. Tetapi bahasa itu sendiri tidak akan menulis kode cepat. Bahasa adalah salah satu alat, salah satu keputusan yang dibuat oleh programmer.


Seperti yang saya katakan - membagikan plaintext dengan cepat diperoleh oleh banyak orang. Kinerja kerangka kerja actix-web, fasthttp dan lainnya saat memproses permintaan sederhana cukup sebanding, yaitu, bahasa lain memiliki kemampuan teknis untuk bersaing dengan Rust.


Actix-web itu sendiri, tentu saja, adalah "yang harus disalahkan": produk yang cepat, pragmatis, luar biasa. Serialisasi sangat mudah, mesin templatnya bagus - ini juga banyak membantu.


Terutama, hasil tes yang bekerja dengan database berbeda.


Setelah menggali sedikit dalam kode, saya menyoroti tiga perbedaan utama yang (menurut saya) membantu tes actix untuk melepaskan diri dari pesaing dalam tes sintetis:


  1. Mode operasi tokio-postgres pipelined pipeline;
  2. Menggunakan satu koneksi dengan uji Rust alih-alih kumpulan koneksi dengan tes yang ditulis dalam Go;
  3. Memperbarui tolok ukur actix dengan satu perintah yang dikirim melalui kueri sederhana alih-alih mengirim beberapa perintah UPDATE.

Apa jenis mode konveyor?


Berikut ini cuplikan dari dokumentasi tokio-postgres (digunakan dalam tolok ukur pustaka klien PostgreSQL) yang menjelaskan apa yang dimaksud oleh pengembangnya:


Sequential Pipelined | Client | PostgreSQL | | Client | PostgreSQL | |----------------|-----------------| |----------------|-----------------| | send query 1 | | | send query 1 | | | | process query 1 | | send query 2 | process query 1 | | receive rows 1 | | | send query 3 | process query 2 | | send query 2 | | | receive rows 1 | process query 3 | | | process query 2 | | receive rows 2 | | | receive rows 2 | | | receive rows 3 | | | send query 3 | | | | process query 3 | | receive rows 3 | | 

Klien dalam mode pipelined (pipelined) tidak menunggu respons PostgreSQL, tetapi mengirimkan permintaan berikutnya sementara PostgreSQL memproses yang sebelumnya. Dapat dilihat bahwa dengan cara ini Anda dapat memproses urutan permintaan basis data yang sama secara signifikan lebih cepat.


Jika koneksi dalam mode pipelined adalah duplex (memberikan kemungkinan memperoleh hasil secara paralel dengan pengiriman), kali ini mungkin sedikit berkurang. Tampaknya sudah ada versi eksperimental tokio-postgres di mana koneksi duplex dibuka.


Karena klien PostgreSQL mengirim beberapa pesan (Parse, Bind, Execute, dan Sync) ke setiap permintaan SQL yang dikirim untuk dieksekusi, dan menerima respons terhadap mereka, mode pipelined akan lebih efektif bahkan ketika memproses permintaan tunggal.


Dan mengapa tidak di Go?


Karena Go biasanya menggunakan kumpulan koneksi database. Koneksi tidak dimaksudkan untuk digunakan secara paralel.


Jika Anda menjalankan kueri SQL yang sama melalui kumpulan, bukan dari satu koneksi, maka secara teoritis Anda bisa mendapatkan waktu eksekusi lebih pendek dengan klien serial biasa daripada ketika bekerja melalui satu koneksi, baik itu tiga kali disalurkan melalui pipa:


 | Connection | Connection 2 | Connection 3 | PostgreSQL | |----------------|----------------|----------------|-----------------| | send query 1 | | | | | | send query 2 | | process query 1 | | receive rows 1 | | send query 3 | process query 2 | | | receive rows 2 | | process query 3 | | | receive rows 3 | | 

Sepertinya kulit domba (mode konveyor) tidak sebanding dengan lilin.


Hanya di bawah beban tinggi jumlah koneksi ke server PostgreSQL bisa menjadi masalah.


Dan apa hubungan jumlah koneksi dengan itu?


Intinya di sini adalah bagaimana server PostgreSQL merespons peningkatan jumlah koneksi.


Grup kolom kiri menunjukkan naik turunnya kinerja PostgreSQL tergantung pada jumlah koneksi terbuka:



( Diadaptasi dari pos Percona )


Dapat dilihat bahwa dengan peningkatan jumlah koneksi terbuka, kinerja server PostgreSQL turun dengan cepat.


Selain itu, membuka koneksi langsung tidak "gratis." Segera setelah membuka klien mengirim informasi layanan, "setuju" dengan server PostgreSQL tentang bagaimana permintaan akan diproses.


Oleh karena itu, dalam praktiknya, Anda harus membatasi jumlah koneksi aktif ke PostgreSQL, seringkali juga meneruskannya melalui pgbouncer atau pengembaraan lainnya.


Jadi mengapa actix-web lebih cepat?


Pertama, actix-web itu sendiri sangat cepat. Dialah yang menetapkan "langit-langit", dan dia sedikit lebih tinggi dari yang lain. Perpustakaan lain yang digunakan (serde, yarde) juga sangat, sangat produktif. Tetapi bagi saya, dalam tes yang bekerja dengan PostgreSQL dimungkinkan untuk lepas karena server actix-web memulai satu utas pada inti prosesor. Setiap utas membuka hanya satu koneksi ke PostgreSQL.


Semakin sedikit koneksi aktif, semakin cepat PostgreSQL berfungsi (lihat grafik di atas).


Klien yang beroperasi dalam mode pipelined (tokio-postgres) memungkinkan Anda untuk secara efektif menggunakan satu koneksi dengan PostgreSQL untuk pemrosesan paralel permintaan pengguna. Penangan permintaan HTTP membuang perintah SQL mereka dalam satu antrian dan berbaris di yang lain untuk menerima hasil. Hasilnya menyenangkan, keterlambatan minimal, semua orang senang. Kinerja keseluruhan lebih tinggi daripada sistem dengan kumpulan koneksi.


Jadi, Anda perlu meninggalkan kolam, menulis klien pipa PostgreSQL, dan kebahagiaan dan kecepatan luar biasa akan segera datang?


Mungkin Tapi tidak sekaligus.


Ketika mode konveyor tidak mungkin untuk menyimpan dan tentu saja tidak akan menyimpan


Skema yang digunakan dalam kode benchmark tidak akan berfungsi dengan transaksi PostgreSQL.


Dalam tolok ukur, transaksi tidak diperlukan dan kode ditulis dengan mempertimbangkan bahwa tidak akan ada transaksi. Dalam praktiknya, itu terjadi.


Jika kode backend membuka transaksi PostgreSQL (misalnya, untuk membuat perubahan dalam dua tabel atom), semua perintah yang dikirim melalui koneksi ini akan dieksekusi di dalam transaksi ini.


Karena koneksi dengan PostgreSQL digunakan secara paralel, semuanya menjadi kacau. Perintah yang harus dijalankan dalam transaksi yang dirancang oleh pengembang dicampur dengan perintah sql yang diprakarsai oleh penangan permintaan http paralel. Kami akan menerima kehilangan data acak dan masalah dengan integritasnya.


Jadi halo transaksi - selamat tinggal penggunaan paralel satu koneksi. Anda harus memastikan bahwa koneksi tidak digunakan oleh penangan permintaan http lain. Anda harus berhenti memproses permintaan http masuk sebelum menutup transaksi, atau menggunakan kumpulan untuk transaksi, membuka beberapa koneksi ke server database. Ada beberapa implementasi pool untuk Rust, dan bukan satu. Selain itu, mereka ada di Rust secara terpisah dari implementasi klien database. Anda dapat memilih sesuai selera, warna, bau atau secara acak. Go tidak bekerja seperti itu. Kekuatan generik, ya.


Poin penting: dalam tes, kode yang saya cari, transaksi tidak terbuka. Pertanyaan ini sama sekali tidak sepadan. Kode benchmark dioptimalkan untuk tugas tertentu dan kondisi operasi aplikasi yang sangat spesifik. Keputusan untuk menggunakan satu koneksi per aliran server mungkin dibuat secara sadar dan ternyata sangat efektif.


Apakah ada hal lain yang menarik dalam kode benchmark?


Ya


Skenario untuk mengukur kinerja dijabarkan dengan sangat rinci. Serta kriteria yang harus dipenuhi oleh kode yang berpartisipasi dalam pengujian. Salah satunya adalah bahwa semua permintaan ke server database harus dieksekusi secara berurutan.


Fragmen kode berikut (sedikit disingkat) sepertinya tidak memenuhi kriteria:


  let mut worlds = Vec::with_capacity(num); //  num    PostgreSQL for _ in 0..num { let w_id: i32 = self.rng.gen_range(1, 10_001); worlds.push( self.cl .query(&self.world, &[&w_id]) .into_future() .map(move |(row, _)| { // ... }), ); } //     stream::futures_unordered(worlds) .collect() .and_then(move |worlds| { // ... }) 

Semuanya tampak seperti peluncuran khas proses paralel. Tetapi, karena satu koneksi ke PostgreSQL digunakan, permintaan ke server database dikirim secara berurutan. Satu per satu Seperti yang diminta. Tidak ada kejahatan


Kenapa begitu Yah, pertama, dalam kode (itu diberikan di kantor editorial, yang bekerja di ronde ke-18) async / menunggu belum digunakan, itu muncul di Rust nanti. Dan melalui futures num , lebih mudah untuk mengirim query SQL "secara paralel" - seperti pada kode di atas. Ini memungkinkan Anda untuk mendapatkan beberapa peningkatan kinerja tambahan: sementara PostgreSQL menerima dan memproses permintaan SQL pertama, sisanya dimasukkan ke dalamnya. Server web tidak menunggu hasil dari masing-masing, tetapi beralih ke tugas-tugas lain dan kembali ke memproses permintaan http hanya ketika semua query SQL selesai.


Untuk PostgreSQL, bonusnya adalah tipe kueri yang sama dalam konteks yang sama (koneksi) berjalan berurutan. Kemungkinan bahwa rencana kueri tidak akan dibangun kembali meningkat.


Ternyata keuntungan dari mode pipeline (lihat diagram dari dokumentasi tokio-postgres) sepenuhnya dieksploitasi bahkan ketika memproses satu permintaan http.


Apa lagi


Menggunakan protokol kueri sederhana untuk pembaruan kumpulan


Protokol komunikasi antara klien dan server PostgreSQL memungkinkan metode alternatif untuk mengeksekusi perintah SQL. Protokol biasa (Extended Query) melibatkan pengiriman beberapa pesan kepada klien: Parse, Bind, Execute, dan Sync. Alternatif adalah protokol Pertanyaan Sederhana, yang menurutnya satu pesan sudah cukup untuk mengeksekusi perintah dan mendapatkan hasil - Permintaan.


Perbedaan utama antara protokol yang biasa adalah transfer parameter permintaan: mereka dikirimkan secara terpisah dari perintah itu sendiri. Itu lebih aman. Protokol yang disederhanakan mengasumsikan bahwa semua parameter kueri SQL akan dikonversi ke string dan dimasukkan ke dalam tubuh kueri.


Solusi menarik yang digunakan dalam benchmark actix-web adalah memperbarui beberapa entri tabel dengan satu perintah yang dikirim melalui protokol Pertanyaan Sederhana.


Menurut patokan, saat memproses permintaan pengguna, server web harus memperbarui beberapa catatan dalam tabel, menulis angka acak. Jelas, memperbarui catatan secara berurutan dengan kueri berurutan membutuhkan waktu lebih lama dari satu kueri yang memperbarui semua catatan sekaligus.


Permintaan yang dihasilkan dalam kode tes terlihat seperti ini:


 UPDATE world SET randomnumber = temp.randomnumber FROM (VALUES (1, 2), (2, 3) ORDER BY 1) AS temp(id, randomnumber) WHERE temp.id = world.id 

Di mana (1, 2), (2, 3) adalah pasangan pengidentifikasi baris / nilai baru dari bidang nomor acak.


Jumlah catatan yang diperbarui adalah variabel, persiapan (persiapan) terlebih dahulu tidak masuk akal. Karena data untuk memperbarui adalah angka, dan sumbernya dapat dipercaya (kode tes itu sendiri), tidak ada risiko injeksi SQL, data hanya dimasukkan dalam tubuh SQL dan semuanya dikirim menggunakan protokol Pertanyaan Sederhana.


Pertanyaan Sederhana dikabarkan sekitar. Saya bertemu rekomendasi: "Bekerja hanya pada protokol Pertanyaan Sederhana, dan semuanya akan cepat dan baik." Saya melihatnya dengan banyak skeptisisme. Pertanyaan Sederhana memungkinkan Anda untuk mengurangi jumlah pesan yang dikirim ke server PostgreSQL dengan memindahkan pemrosesan parameter kueri ke sisi klien. Anda dapat melihat keuntungan untuk kueri yang dihasilkan secara dinamis dengan sejumlah parameter variabel. Untuk jenis kueri SQL yang sama (yang lebih umum), keuntungannya tidak jelas. Baik dan seberapa aman pemrosesan parameter kueri akan berubah, dalam kasus Simple Query menentukan implementasi pustaka klien.


Seperti yang saya tulis di atas, dalam hal ini, badan query SQL dihasilkan secara dinamis, data numerik dan dihasilkan oleh server itu sendiri. Kombinasi sempurna untuk Permintaan Sederhana. Tetapi bahkan dalam kasus ini, ada baiknya menguji opsi lain. Alternatif tergantung pada platform PostgreSQL dan klien: pgx (klien untuk Go) memungkinkan untuk mengirim paket perintah, JDBC - untuk mengeksekusi satu perintah beberapa kali berturut-turut dengan parameter yang berbeda. Kedua solusi dapat berjalan pada kecepatan yang sama atau bahkan lebih cepat.


Jadi mengapa Rust memimpin?


Pemimpin, tentu saja, bukan Rust. Tes berdasarkan actix-web memimpin - dialah yang menetapkan "langit-langit" kinerja. Ada, misalnya, roket dan besi, yang menempati posisi sederhana. Tetapi pada saat ini, itu adalah actix-web yang menentukan potensi untuk menggunakan Rust dalam pengembangan web. Bagi saya, potensinya sangat tinggi.


Server "rahasia" lain yang tidak terlihat, tetapi penting berdasarkan pada actix-web, yang memungkinkan kami untuk mengambil tempat pertama di semua tolok ukur TechEmpower - dalam cara kerjanya dengan PostgreSQL:


  1. Hanya satu koneksi dengan PostgreSQL per aliran server web terbuka. Koneksi ini menggunakan mode pipelined, yang memungkinkannya digunakan secara efektif untuk pemrosesan paralel permintaan pengguna.
  2. Semakin sedikit koneksi aktif, semakin cepat PostgreSQL merespons. Kecepatan pemrosesan permintaan pengguna meningkat. Pada saat yang sama, di bawah beban, seluruh sistem bekerja lebih stabil (keterlambatan dalam memproses permintaan yang masuk lebih rendah, mereka tumbuh lebih lambat).

Di mana kecepatan penting, opsi ini mungkin akan lebih cepat daripada menggunakan multiplexer (seperti pgbouncer dan pengembaraan). Dan tentu saja dia lebih cepat dalam tolok ukur.


Sangat menarik bagaimana async / menunggu, yang muncul di Rust, dan drama terbaru dengan actix-web akan memengaruhi popularitas Rust dalam pengembangan web. Menarik juga bagaimana hasil tes akan berubah setelah memprosesnya pada async / menunggu.

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


All Articles