Blog Teknik - Prexy: pencarian super cepat dan mengganti aturan
Selamat datang di blog teknik pertama kami. Mungkin sedikit lebih teknis daripada yang biasa Anda baca di blog-blog lain, tetapi kami telah melakukan yang terbaik untuk membuatnya dapat dimengerti oleh semua orang. Pada artikel ini kita akan membahas tentang Prexy, sebuah perangkat lunak baru di dalam Clonable yang digunakan untuk menerapkan aturan substitusi.
Latar belakang
As a user of Clonable, you may already be familiar with the substitution rule functionality. You can use these search and replace rules to exchange pieces of text or code with a self-selected variant. For example, you can use this to replace an API key or an analytics ID so you can do different analysis for your original site and your translated clones. Internally, these same rules are also used for replacing your original domain name to your clone's domain name and a number of other things.
Since the very first version of Clonable, this functionality has never been updated, which in itself is not surprising, because it did its job just fine. Still, improvements were possible, both in terms of performance and usability. Once in a while, we Clonable take a piece of our product that hasn't been spent for a long time to start improving it. For example, last year we overhauled all of our data infrastructure to make it faster, more scalable, and more fault-tolerant, and before that we also rebuilt the URL translation functionality from the ground up. This quarter it was the turn of the substitution rules.
Situasi dan keterbatasan lama
Substitution rules are applied in Clonable only at the very last stage, just before sending the response back to the client. In the first version of Clonable, we therefore chose to implement this within the NGINX web server using replace-filter-nginx-module, a module created by OpenResty. The module uses sregex as its streaming regex engine, also created by OpenResty. The streaming aspect is important in this case because we don't want to load each response fully into memory. If we did, a number of large responses could cause NGINX to crash or crash because of insufficient memory availability. A second important feature of sregex, is that it can process multiple lines in parallel. This is because each clone already has some standard rules and, in addition, sometimes some rules added specifically for the clone. If these could not be handled in parallel, we would still need to buffer the entire response, because we would need to be able to match it again after the first rule for the second rule.
Namun, tahun lalu, kami semakin sering mengalami masalah kinerja yang aneh. Masalah-masalah ini sering kali hanya berlangsung sebentar, tetapi dapat meningkatkan waktu respons halaman beberapa detik dalam kasus-kasus ekstrem. Setelah satu lonjakan seperti itu, halaman sering dimuat secara normal lagi, sehingga masalahnya sulit untuk direproduksi. Untuk men-debug hal ini lebih lanjut, kami menambahkan header Clonable-Timings ke semua respons, yang memungkinkan kami untuk secara kasar menentukan langkah mana dalam proses penerjemahan yang menghabiskan banyak waktu. Hal ini menunjukkan bahwa di hampir semua kasus, upstream merespons dengan cepat, dan penerjemahannya juga cukup lancar. Namun, ada jeda waktu antara menyelesaikan terjemahan dan menyelesaikan permintaan sepenuhnya, dan ini memberi kami petunjuk bahwa ini mungkin berkaitan dengan aturan substitusi.
Model konkurensi dari NGINX
Namun, hal ini belum memberikan jawaban atas pertanyaan mengapa penundaan ini hanya terjadi secara sporadis, dan mengapa hal ini terjadi ketika server bahkan tidak dimuat hingga setengah dari kapasitasnya. Untuk lebih memahami fenomena ini, kita perlu melihat lebih dalam tentang bagaimana NGINX menangani beban kerja.
NGINX memiliki arsitektur pekerja dengan satu proses master dan beberapa proses pekerja. Koneksi didistribusikan di antara para pekerja untuk menangani beberapa permintaan secara bersamaan. Namun, pengaturan ini juga memiliki kekurangan: ketika sebuah pekerja sedang sibuk memproses sebuah permintaan, permintaan lain yang juga ditugaskan ke pekerja yang sama harus menunggu. Hal ini dapat menyebabkan satu permintaan yang berat menyebabkan penundaan untuk beberapa permintaan, bahkan jika pekerja lain tidak memiliki pekerjaan. Efek ini dapat dilihat dengan baik pada gambar di bawah ini. Selama pembuatan gambar ini, sebuah stress test dijalankan di 10 koneksi yang berbeda. Gambar tersebut kemudian dengan jelas menunjukkan bahwa 3 pekerja sedang sibuk, dan yang keempat hampir tidak melakukan apa-apa.

Waktunya untuk Prexy
Dengan kemacetan yang terlihat jelas, kami membuat rencana untuk memperbaikinya. Proyek ini diberi nama Prexy, sebuah penggabungan dari RegEx dan Proxy. Ada 3 persyaratan utama untuk hasil akhirnya:
Ini harus menjadi pengganti langsung untuk pengaturan saat ini. Aturan penggantian harus diterapkan dengan cara yang sama untuk menghindari kerusakan pada pengaturan yang ada.
Aturan substitusi harus dialirkan untuk mencegah solusi menggunakan terlalu banyak memori.
Solusi baru harus lebih cepat daripada solusi saat ini.
Pertama, kami harus mencari runtime multi-threaded yang kuat yang dapat memproses permintaan secara efisien. Setelah mempertimbangkan beberapa opsi, pilihannya jatuh pada runtime Tokyo untuk bahasa pemrograman Rust. Rust dikenal karena memungkinkan Anda untuk menulis program yang cepat tanpa risiko keamanan seperti bahasa tingkat rendah lainnya seperti C++. Tokyo runtime adalah runtime asinkron yang fleksibel yang dibuat untuk aplikasi jaringan. Salah satu fitur yang paling penting bagi kami adalah fakta bahwa runtime ini bersifat mencuri kerja. Ini berarti, tidak seperti NGINX, sebuah permintaan tidak terikat pada satu pekerja, tetapi pekerja yang tidak memiliki pekerjaan dapat "mencuri" pekerjaan dari pekerja lain. Dengan cara ini, sumber daya server kami dapat digunakan dengan lebih baik.
Prototipe pertama
Setelah memilih teknologi yang mendasari, kami memutuskan untuk membuat prototipe awal untuk memperkirakan secara kasar seberapa besar peningkatan kecepatan yang akan diberikan oleh proyek ini dan apakah proyek ini sepadan. Sebagai implementasi pertama untuk mesin regex (bagian yang benar-benar memproses dan menerapkan aturan), kami menggunakan pustaka regex standar (atau peti seperti yang disebut dalam terminologi Rust). Mesin ini, seperti sregex, tidak melakukan pelacakan balik, yang berarti lebih sedikit risiko masalah ReDoS. Dalam ReDoS, sebuah ekspresi reguler menghadapi input yang begitu banyak sehingga waktu yang dibutuhkan untuk mengevaluasi input meningkat secara eksponensial. Hal ini dapat menjadi bencana bagi kinerja, dan menyebabkan cloudflare mengalami pemadaman besar. Jadi bagi kami, penting bahwa mesin yang kami gunakan tidak melakukan backtracking.
Dengan menggunakan mesin regex ini, kami membangun prototipe awal. Prototipe ini menggunakan aturan yang dikodekan dengan keras dan mesin regex menyangga seluruh respons alih-alih mengalirkannya, tetapi ini sudah cukup untuk pengujian kinerja awal.
Dengan menggunakan mesin regex ini, kami membangun prototipe awal. Prototipe ini menggunakan aturan yang dikodekan dengan keras dan mesin regex menyangga seluruh respons alih-alih mengalirkannya, tetapi ini sudah cukup untuk uji kinerja awal. Pengaturan pengujian kami terdiri dari dua VM, dengan satu VM menjalankan pengaturan di mana solusi lama dan solusi baru dapat berjalan. Dengan begitu, kami bisa dengan mudah membuat perbandingan antara kedua metode tersebut. Server lainnya menjalankan NGINX yang berfungsi sebagai server asal dan melayani file uji. Pada server yang sama juga menjalankan tool wrk, yang digunakan untuk membuat beban. Ini tidak sepenuhnya optimal, karena untuk data murni Anda lebih suka menjalankan generator beban pada VM yang terpisah, tetapi VM ini memiliki sumber daya yang cukup sehingga NGINX dan wrk tidak saling mengganggu.
Hasil pengujian pertama sangat jelas: Prexy sekitar 22 kali lebih cepat dalam menangani satu permintaan (lihat gambar di bawah). Hal ini memberikan lampu hijau terakhir pada proyek ini, karena dengan perbedaan yang begitu besar, ada cukup ruang untuk menyerap beberapa penurunan kinerja yang mungkin terjadi dengan melengkapi fungsionalitasnya.

Setelah pengujian awal, kami mengimplementasikan sejumlah pengoptimalan yang jelas, seperti caching ekspresi reguler yang dikompilasi. Kami juga menerapkan cara yang lebih efisien untuk menggunakan kembali koneksi ke upstream. Dengan ini kami mencapai peningkatan kecepatan sekitar 20%. Kami kemudian juga menguji kedua solusi di bawah beban puncak, mengirimkan sebanyak mungkin permintaan ke server. Sekali lagi, perbedaannya terlihat jelas dan Prexy mencapai throughput sekitar 25x lebih tinggi dari solusi lama.

Mesin regex streaming
Salah satu persyaratan dari proyek ini adalah mesin regex yang digunakan harus streaming. Regex yang dibuat pada prototipe tidak demikian, sehingga diperlukan pekerjaan tambahan di area ini. Namun, regex crate ternyata sangat dioptimalkan, jadi kami memutuskan untuk menggunakan implementasi ini sebagai dasar untuk mesin streaming kami. Ketika melakukan streaming data melalui regex engine, ada beberapa hal yang penting untuk diingat. Pertama, Anda tidak tahu data apa yang akan datang. Oleh karena itu, Anda harus mulai bekerja dengan pencocokan parsial: pencocokan yang belum lengkap, tetapi sudah mencocokkan 1 karakter atau lebih. Saat menemukan kecocokan lengkap, Anda harus memeriksa bahwa tidak ada kecocokan parsial yang tumpang tindih, karena ini juga bisa menjadi kecocokan (dan kecocokan yang lebih panjang menang atas kecocokan yang lebih pendek jika terjadi tumpang tindih). Jadi, Anda hanya dapat mulai memproses pencocokan jika semua pencocokan parsial yang ada saat ini ternyata bukan merupakan pencocokan yang lengkap.
Pengoptimalan lebih lanjut
Seperti yang diharapkan, membangun ulang mesin regex berdampak negatif pada kinerja Prexy. Namun, masih ada banyak margin dibandingkan dengan solusi lama, dan dengan menambahkan optimasi lebih lanjut, kami bahkan mengungguli kinerja sebelum pembangunan ulang mesin regex. Salah satu pengoptimalan yang kami terapkan adalah mengingat apakah akan menerapkan aturan ke file tertentu. Aset statis seperti file CSS dan JS hampir tidak pernah berubah dan karena itu tidak ada gunanya untuk menjalankan aturan setiap saat jika aturan tersebut tidak pernah cocok dengan file tertentu. Alternatifnya adalah dengan menyimpan hasil penggantian di cache, tetapi kelemahan dari strategi ini adalah dibutuhkan banyak memori untuk menyimpan semua file. Dengan mengingat aturan mana yang akan diterapkan, kita hanya membutuhkan beberapa byte per file dengan menyimpan informasi sebagai bitmap.
Selain itu, kami memanfaatkan sepenuhnya pengoptimalan di dalam mesin regex dengan, misalnya, tidak menyimpan grup-grup yang ditangkap ketika mereka tidak digunakan dalam penggantian. Hasilnya, mesin regex hanya perlu mengingat awal dan akhir seluruh pertandingan, yang menghemat pekerjaan. Kami juga menonaktifkan grup capturing yang diberi nama, karena hal ini cukup rumit pada varian streaming dan karena solusi lama juga tidak mendukung hal itu, maka hal ini tidak terlalu diperlukan. Semua pengoptimalan ini pada akhirnya memberikan performa sebagai berikut:

Hasil akhirnya
After testing Prexy extensively to ensure that its behavior was indeed the same as the old solution, we began rolling it out incrementally. Over a period of about a week, we activated Prexy for each clone. In our monitoring, the timing of activation was often easy to see. A decrease of about 50% in response times was not rare to see. Minor differences of about 10% were also seen with other customers who had very few substitution rules. Our own website became about 30% faster (~50ms).
Di seluruh infrastruktur kami, kami melihat perbedaan berikut ini setelah meluncurkan Prexy.
Konsumsi RAM 33% lebih sedikit
Penggunaan CPU 20% lebih sedikit
10-50 % respons lebih cepat
Jadi secara keseluruhan, kami dapat mengatakan bahwa Prexy adalah proyek yang sukses. Dengan mengganti modul NGINX yang sudah ketinggalan zaman, kami sekarang dapat menggunakan teknik yang lebih baru dan lebih efisien yang membuat perbedaan yang jelas dan terukur bagi semua pelanggan kami. Di masa depan, kami akan terus meningkatkan Clonable, baik dari segi fitur-fitur baru maupun meningkatkan fungsionalitas yang sudah ada.
Terima kasih telah membaca blog teknik ini. Beri tahu kami jika Anda ingin mendapatkan lebih banyak wawasan teknis seperti ini ke dalam produk kami lebih sering. Tertarik dengan proyek ini? Silakan lihat halaman lowongan kerja kami :)
Mulailah menggunakan Clonable secara gratis hari ini.
Temukan cara untuk mengelola situs web internasional secara terpusat dari satu platform.