Node.js dan rendering server dalam Airbnb

Materi, terjemahan yang kami terbitkan hari ini, dikhususkan untuk kisah bagaimana Airbnb mengoptimalkan bagian server dari aplikasi web dengan memperhatikan penggunaan teknologi rendering server yang semakin meningkat. Selama beberapa tahun, perusahaan secara bertahap menggeser seluruh front-end ke arsitektur yang seragam , yang menurutnya halaman web adalah struktur hirarki komponen Bereaksi yang diisi dengan data dari API mereka. Secara khusus, selama proses ini ada pengabaian sistematis Ruby on Rails. Faktanya, Airbnb berencana untuk beralih ke layanan baru hanya berdasarkan Node.js, berkat halaman yang disiapkan sepenuhnya yang diberikan pada server akan dikirimkan ke browser pengguna. Layanan ini akan menghasilkan sebagian besar kode HTML untuk semua produk Airbnb. Mesin rendering yang dimaksud berbeda dengan sebagian besar layanan backend yang digunakan oleh perusahaan karena fakta bahwa itu tidak ditulis dalam Ruby atau Java. Namun, ini berbeda dari layanan Node.js tradisional yang penuh muatan, di mana model mental dan alat bantu yang digunakan di Airbnb dibuat.



Platform Node.js


Berpikir tentang platform Node.js, Anda dapat membayangkan bagaimana aplikasi tertentu, dibangun dengan mempertimbangkan kemampuan platform ini untuk pemrosesan data yang tidak sinkron, dengan cepat dan efisien melayani ratusan atau ribuan koneksi paralel. Layanan mengeluarkan data yang dibutuhkan dari mana-mana dan memprosesnya sedikit sehingga memenuhi kebutuhan sejumlah besar pelanggan. Pemilik aplikasi semacam itu tidak punya alasan untuk mengeluh, ia yakin dengan model ringan pemrosesan data simultan yang digunakan olehnya (dalam materi ini kami menggunakan kata "simultan" untuk mengirimkan istilah "bersamaan", untuk istilah "paralel" - "paralel"). Dia dengan sempurna menyelesaikan tugas yang ditetapkan untuknya.

Server Side Rendering (SSR) mengubah ide-ide dasar yang mengarah ke visi yang sama dari masalah ini. Jadi, rendering server membutuhkan banyak sumber daya komputasi. Kode di lingkungan Node.js dieksekusi dalam satu utas, sebagai hasilnya, untuk memecahkan masalah komputasi (tidak seperti tugas I / O), kode tersebut dapat dieksekusi secara bersamaan, tetapi tidak secara paralel. Platform Node.js mampu menangani sejumlah besar operasi I / O paralel, namun, ketika menyangkut komputasi, situasinya berubah.

Karena ketika menerapkan rendering sisi server, bagian komputasi dari tugas pemrosesan permintaan meningkat dibandingkan dengan bagian yang terkait dengan input / output, permintaan yang masuk secara bersamaan akan mempengaruhi kecepatan respons server karena fakta bahwa mereka bersaing untuk sumber daya prosesor. Perlu dicatat bahwa ketika menggunakan rendering asinkron, persaingan untuk sumber daya masih ada. Rendering Asynchronous memecahkan respons suatu proses atau browser, tetapi tidak memperbaiki situasi dengan penundaan atau konkurensi. Pada artikel ini, kami akan fokus pada model sederhana yang mencakup beban komputasi eksklusif. Jika kita berbicara tentang beban campuran, yang mencakup operasi input / output dan perhitungan, maka permintaan yang masuk secara bersamaan akan meningkatkan penundaan, tetapi dengan mempertimbangkan keuntungan dari throughput sistem yang lebih tinggi.

Pertimbangkan perintah dari bentuk Promise.all([fn1, fn2]) . Jika fn1 atau fn2 dijanjikan diselesaikan oleh subsistem I / O, maka selama pelaksanaan perintah ini adalah mungkin untuk mencapai eksekusi paralel operasi. Ini terlihat seperti ini:


Eksekusi paralel operasi melalui subsistem input / output

Jika fn1 dan fn2 adalah tugas komputasi, mereka akan dieksekusi sebagai berikut:


Tugas komputasi

Salah satu operasi harus menunggu penyelesaian operasi kedua, karena hanya ada satu utas di Node.js.

Dalam hal rendering server, masalah ini terjadi ketika proses server harus memproses beberapa permintaan secara bersamaan. Pemrosesan permintaan tersebut akan ditunda hingga permintaan yang diterima diproses lebih awal. Ini tampilannya.


Memproses permintaan bersamaan

Dalam praktiknya, pemrosesan permintaan seringkali terdiri dari banyak fase asinkron, bahkan jika mereka melibatkan beban komputasi yang serius pada sistem. Ini dapat mengarah pada situasi yang lebih sulit dengan bergantian tugas untuk memproses permintaan tersebut.

Misalkan kueri kami terdiri dari rantai tugas yang menyerupai ini: renderPromise().then(out => formatResponsePromise(out)).then(body => res.send(body)) . Ketika sepasang permintaan tersebut tiba di sistem, dengan interval kecil di antara mereka, kita dapat mengamati gambar berikut.


Memproses permintaan yang sampai pada interval kecil, masalah perjuangan untuk sumber daya prosesor

Dalam hal ini, dibutuhkan waktu dua kali lebih banyak untuk memproses setiap permintaan daripada yang dibutuhkan untuk memproses permintaan individu. Dengan peningkatan jumlah permintaan yang diproses secara bersamaan, situasinya menjadi lebih buruk.

Selain itu, salah satu tujuan khas implementasi SSR adalah kemampuan untuk menggunakan kode yang sama atau sangat mirip pada klien dan server. Perbedaan serius antara lingkungan ini adalah bahwa lingkungan klien pada dasarnya adalah lingkungan di mana satu klien beroperasi, dan lingkungan server, berdasarkan sifatnya, adalah lingkungan multi-klien. Apa yang bekerja dengan baik pada klien, seperti singletone atau pendekatan lain untuk menyimpan keadaan global aplikasi, menyebabkan kesalahan, kebocoran data, dan, secara umum, menjadi kebingungan, sementara memproses banyak permintaan yang tiba di server.

Fitur-fitur ini menjadi masalah dalam situasi di mana Anda perlu memproses beberapa permintaan secara bersamaan. Semuanya biasanya bekerja sangat normal di bawah beban yang lebih rendah di lingkungan yang nyaman dari lingkungan pengembangan, yang digunakan oleh satu klien secara pribadi seorang programmer.

Ini mengarah ke situasi yang sangat berbeda dari contoh aplikasi Node.js klasik. Perlu dicatat bahwa kami menggunakan runtime JavaScript untuk kumpulan perpustakaan kaya yang tersedia di dalamnya, dan karena fakta bahwa itu didukung oleh browser, dan bukan demi modelnya untuk pemrosesan data secara simultan. Dalam aplikasi ini, model asinkron pemrosesan data simultan menunjukkan semua kelemahannya, tidak dikompensasi oleh keuntungan, yang sangat sedikit atau tidak sama sekali.

Tutorial Proyek Hypernova


Layanan rendering baru kami, Hyperloop, akan menjadi layanan utama yang akan berinteraksi dengan pengguna Airbnb. Akibatnya, keandalan dan kinerjanya memainkan peran penting dalam memastikan kenyamanan bekerja dengan sumber daya. Saat memperkenalkan Hyperloop ke dalam produksi, kami memperhitungkan pengalaman yang kami peroleh saat bekerja dengan sistem rendering server kami sebelumnya - Hypernova .

Hypernova tidak berfungsi seperti layanan baru kami. Ini adalah sistem rendering murni. Ini dipanggil dari layanan Rail monolitik kami, yang disebut Monorail, dan hanya mengembalikan cuplikan HTML untuk komponen yang diberikan khusus. Dalam banyak kasus, "snippet" ini mewakili bagian terbesar dari halaman, dan Rails hanya menyediakan tata letak halaman. Dengan teknologi lawas, bagian dari halaman dapat dihubungkan bersama menggunakan ERB. Bagaimanapun, Hypernova tidak memuat data apa pun yang diperlukan untuk membentuk halaman. Ini adalah tugas Rails.

Dengan demikian, Hyperloop dan Hypernova memiliki kinerja komputasi yang serupa. Pada saat yang sama, Hypernova, sebagai layanan produksi dan pemrosesan volume lalu lintas yang signifikan, menyediakan bidang yang baik untuk pengujian, yang mengarah pada pemahaman tentang bagaimana penggantian Hypernova akan berperilaku dalam kondisi pertempuran.


Alur Kerja Hypernova

Inilah cara kerja Hypernova. Permintaan pengguna datang ke aplikasi Rails utama kami, Monorail, yang mengumpulkan properti komponen Bereaksi yang perlu ditampilkan pada halaman dan membuat permintaan ke Hypernova, melewati properti dan nama komponen ini. Hypernova merender komponen dengan properti untuk menghasilkan kode HTML yang perlu dikembalikan ke aplikasi Monorail, yang kemudian menyematkan kode ini dalam templat halaman dan mengirimkan semuanya kembali ke klien.


Mengirim halaman yang sudah jadi ke klien

Jika terjadi keadaan darurat (ini bisa menjadi kesalahan atau waktu tunggu respons) di Hypernova, ada opsi mundur, saat menggunakan komponen dan propertinya yang disematkan di halaman tanpa HTML yang dihasilkan di server, setelah itu semua ini dikirim ke klien dan diberikan di sana semoga berhasil. Ini membawa kami pada fakta bahwa kami tidak menganggap layanan Hypernova sebagai bagian penting dari sistem. Sebagai hasilnya, kami dapat memungkinkan terjadinya sejumlah kegagalan dan situasi tertentu di mana timeout dipicu. Dengan menyesuaikan batas waktu permintaan, kami, berdasarkan pengamatan, mengaturnya ke sekitar level P95. Akibatnya, tidak mengherankan bahwa sistem bekerja dengan tingkat respons batas waktu dasar kurang dari 5%.

Dalam situasi ketika lalu lintas mencapai nilai puncak, kami dapat melihat bahwa hingga 40% permintaan ke Hypernova ditutup oleh timeout di Monorail. Di sisi Hypernova, kami melihat puncak BadRequestError: Request aborted ketinggian rendah. Kesalahan ini, di samping itu, ada dalam kondisi normal, sementara dalam operasi normal, karena arsitektur solusi, kesalahan yang tersisa tidak terlalu terlihat.


Nilai batas waktu puncak (garis merah)

Karena sistem kami dapat bekerja tanpa Hypernova, kami tidak terlalu memperhatikan fitur-fitur ini, mereka dianggap lebih sebagai hal yang menjengkelkan, daripada masalah serius. Kami menjelaskan masalah ini dengan fitur platform, karena peluncuran aplikasi lambat karena operasi pengumpulan sampah awal yang agak sulit, karena kekhasan kompilasi kode dan penyimpanan data, dan karena alasan lain. Kami berharap bahwa rilis baru React atau Node akan mencakup peningkatan kinerja yang akan mengurangi kekurangan dari lambatnya peluncuran layanan.

Saya curiga bahwa apa yang terjadi kemungkinan besar merupakan hasil dari penyeimbangan muatan yang buruk atau konsekuensi dari masalah dalam penyebaran solusi, ketika peningkatan keterlambatan dimanifestasikan karena beban komputasi yang berlebihan pada proses. Saya menambahkan lapisan tambahan ke sistem untuk mencatat informasi tentang jumlah permintaan yang diproses secara bersamaan oleh proses individu, serta untuk mencatat kasus di mana proses menerima lebih dari satu permintaan untuk diproses.


Hasil penelitian

Kami menganggap lambatnya layanan sebagai biang keladinya keterlambatan, tetapi kenyataannya masalah itu disebabkan oleh permintaan paralel yang berjuang untuk waktu CPU. Menurut hasil pengukuran, ternyata waktu yang dihabiskan oleh permintaan untuk mengantisipasi penyelesaian pemrosesan permintaan lainnya sesuai dengan waktu yang dihabiskan untuk memproses permintaan. Selain itu, ini berarti bahwa peningkatan penundaan karena pemrosesan permintaan secara simultan terlihat sama dengan peningkatan penundaan karena peningkatan kompleksitas komputasi kode, yang mengarah pada peningkatan beban pada sistem saat memproses setiap permintaan.

Ini, di samping itu, membuatnya lebih jelas bahwa BadRequestError: Request aborted tidak dapat dengan percaya diri dijelaskan oleh startup sistem yang lambat. Kesalahan berlanjut dari kode parsing dari badan permintaan, dan terjadi ketika klien membatalkan permintaan sebelum server dapat sepenuhnya membaca tubuh permintaan. Klien berhenti bekerja, menutup koneksi, merampas kami dari data yang diperlukan untuk melanjutkan memproses permintaan. Sangat mungkin bahwa ini terjadi karena kami mulai memproses permintaan, setelah itu perulangan acara ternyata menjadi render yang diblokir untuk permintaan lain, dan kemudian kami kembali ke tugas yang terputus untuk menyelesaikannya, tetapi sebagai hasilnya ternyata klien yang mengirimi kami permintaan ini sudah terputus, membatalkan permintaan. Selain itu, data yang dikirim dalam permintaan ke Hypernova cukup banyak, rata-rata, di wilayah beberapa ratus kilobyte, dan ini, tentu saja, tidak berkontribusi untuk memperbaiki situasi.


Galat yang disebabkan oleh pemutusan klien yang tidak menunggu jawaban

Kami memutuskan untuk menangani masalah ini dengan menggunakan beberapa alat standar yang kami miliki pengalaman yang cukup. Kita berbicara tentang server proxy terbalik ( nginx ) dan load balancer ( HAProxy ).

Membalikkan proxy dan load balancing


Untuk memanfaatkan arsitektur prosesor multi-core, kami menjalankan beberapa proses Hypernova menggunakan modul cluster Node.js bawaan. Karena proses ini independen, kami dapat secara bersamaan memproses permintaan yang masuk.


Pemrosesan paralel permintaan tiba secara bersamaan

Masalahnya di sini adalah bahwa setiap proses Node benar-benar sibuk sepanjang waktu untuk memproses satu permintaan, termasuk membaca isi permintaan yang dikirim dari klien (Monorail memainkan perannya dalam kasus ini). Meskipun kita dapat membaca banyak pertanyaan dalam satu proses pada saat yang bersamaan, ketika sampai pada rendering, ia mengarah ke pergantian operasi komputasi.

Penggunaan sumber daya proses Node terkait dengan kecepatan klien dan jaringan.

Sebagai solusi untuk masalah ini, kami dapat mempertimbangkan server proxy reverse buffering, yang akan memungkinkan kami untuk mempertahankan sesi komunikasi dengan klien. Inspirasi untuk ide ini adalah server web unicorn, yang kami gunakan untuk aplikasi Rails kami. Prinsip-prinsip yang dinyatakan oleh unicorn dengan sempurna menjelaskan mengapa demikian. Untuk tujuan ini kami menggunakan nginx. Nginx membaca permintaan dari klien ke buffer, dan meneruskan permintaan ke server Node hanya setelah itu sepenuhnya dibaca. Sesi transfer data ini dilakukan pada mesin lokal, melalui antarmuka loopback atau menggunakan soket domain Unix, dan ini jauh lebih cepat dan lebih dapat diandalkan daripada mentransfer data antara komputer yang terpisah.


Nginx buffer permintaan dan kemudian mengirimkannya ke server Node

Karena kenyataan bahwa nginx sekarang terlibat dalam permintaan membaca, kami dapat mencapai pemuatan proses Node yang lebih seragam.

Pemuatan proses yang seragam menggunakan nginx

Selain itu, kami menggunakan nginx untuk menangani beberapa permintaan yang tidak memerlukan akses ke proses Node. Lapisan deteksi dan perutean layanan kami menggunakan /ping permintaan yang tidak membuat beban besar pada sistem untuk memverifikasi komunikasi antara host. Memproses semua ini di nginx menghilangkan sumber signifikan beban kerja tambahan (walaupun kecil) untuk Node.js.

Peningkatan selanjutnya menyangkut penyeimbangan muatan. Kita perlu membuat keputusan berdasarkan informasi tentang distribusi permintaan antar proses Node. Modul cluster mendistribusikan permintaan sesuai dengan algoritma round-robin, dalam kebanyakan kasus dengan upaya untuk mem-bypass proses yang tidak menanggapi permintaan. Dengan pendekatan ini, setiap proses menerima permintaan sesuai prioritas.

Modul cluster mendistribusikan koneksi, bukan permintaan, jadi semua ini tidak berfungsi sesuai kebutuhan. Situasi menjadi lebih buruk ketika koneksi terus-menerus digunakan. Setiap koneksi permanen dari klien terikat pada alur kerja tunggal tertentu, yang mempersulit distribusi tugas yang efisien.

Algoritma round-robin bagus ketika ada variabilitas yang rendah dalam penundaan permintaan. Misalnya, dalam situasi yang digambarkan di bawah ini.


Algoritma round-robin dan koneksi melalui mana permintaan diterima secara stabil

Algoritma ini sudah tidak begitu baik ketika Anda harus memproses permintaan dari berbagai jenis, untuk pemrosesan yang mungkin memerlukan biaya waktu yang sama sekali berbeda. Permintaan terbaru yang dikirim ke proses tertentu terpaksa menunggu penyelesaian pemrosesan semua permintaan yang dikirim sebelumnya, bahkan jika ada proses lain yang memiliki kemampuan untuk memproses permintaan tersebut.


Beban proses tidak merata

Jika Anda mendistribusikan kueri yang diperlihatkan di atas secara lebih rasional, Anda mendapatkan sesuatu seperti yang ditunjukkan pada gambar di bawah ini.


Distribusi permintaan secara rasional

Dengan pendekatan ini, menunggu diminimalkan dan menjadi mungkin untuk mengirim respons terhadap permintaan lebih cepat.

Ini dapat dicapai dengan menempatkan permintaan dalam antrian, dan menugaskan mereka untuk suatu proses hanya ketika tidak sibuk memproses permintaan lain. Untuk tujuan ini kami menggunakan HAProxy.


HAProxy dan proses load balancing

Ketika kami menggunakan HAProxy untuk menyeimbangkan beban di Hypernova, kami sepenuhnya menghilangkan puncak waktu habis, serta kesalahan BadRequestErrors .

Permintaan simultan juga merupakan penyebab utama keterlambatan selama operasi normal, pendekatan ini mengurangi penundaan tersebut. Salah satu konsekuensi dari ini adalah bahwa sekarang hanya 2% dari permintaan ditutup oleh batas waktu, dan bukan 5%, dengan pengaturan batas waktu yang sama. Fakta bahwa kami berhasil pindah dari situasi dengan kesalahan 40% ke situasi dengan pemicu waktu habis pada 2% kasus menunjukkan bahwa kami bergerak ke arah yang benar. Akibatnya, hari ini pengguna kami melihat layar memuat situs web jauh lebih jarang. Perlu dicatat bahwa stabilitas sistem akan sangat penting bagi kami dengan transisi yang diharapkan ke sistem baru yang tidak memiliki mekanisme cadangan yang sama dengan yang dimiliki Hypernova.

Detail tentang sistem dan pengaturannya


Agar semua ini berfungsi, Anda perlu mengkonfigurasi aplikasi nginx, HAProxy dan Node. Berikut adalah contoh aplikasi serupa yang menggunakan nginx dan HAProxy, menganalisis yang mana, Anda dapat memahami perangkat sistem yang dimaksud. Contoh ini didasarkan pada sistem yang kami gunakan dalam produksi, tetapi disederhanakan dan dimodifikasi sehingga dapat dieksekusi di latar depan atas nama pengguna yang tidak berkepentingan. Dalam produksi, semuanya harus dikonfigurasikan menggunakan semacam pengawas (kami menggunakan runit, atau, lebih sering, kubernetes).

Konfigurasi nginx cukup standar, menggunakan server yang mendengarkan pada port 9000, dikonfigurasi untuk permintaan proxy ke server HAProxy, yang mendengarkan pada port 9001 (dalam konfigurasi kami, kami menggunakan soket domain Unix).

Selain itu, server ini memotong permintaan ke titik akhir /ping untuk secara langsung melayani permintaan yang ditujukan untuk memeriksa konektivitas jaringan. nginx , worker_processes 1, nginx โ€” HAProxy Node-. , , , Hypernova, ( ). .

Node.js cluster . HAProxy, cluster , . pool-hall . โ€” , , , cluster , . pool-hall , .

HAProxy , 9001 , 9002 9005. โ€” maxconn 1 , . . HAProxy ( 8999).


HAProxy

HAProxy . , maxconn . static-rr (static round-robin), , , . , round-robin, , , , , . , , . .

, , . ( ). , , , , . , , .

HAProxy


HAProxy. , , , . , , ( ) . , , cluster . , .

ab (Apache Benchmark) 10000 . - . :

 ab -l -c <CONCURRENCY> -n 10000 http://<HOSTNAME>:9000/render 

15 4- -, ab , . ( concurrency=5 ), ( concurrency=13 ), , ( concurrency=20 ). , .

, -, . , . , , , , . , , , .

, โ€” .

maxconn 1 , , .

HTTP TCP , , , . , maxconn , . , , (, , ).

, , , , , , .

โ€” , . option redispatch retries 3 , , , , , , . .

, - , . , . , , . 100 , 10 , , . , . , accept .

, ( backlog ) , . SYN-ACK ( , , , ACK ). , , , , .

, , , , . , , 1. maxconn . 0 , , , , , . , . - , , . abortonclose , . , abortonclose . nginx.

, , . ( ) , , , , , . HAProxy , , ( ). , , , HTML. , , . , , ( , , ). , , . , , , . HAProxy, MAINT HAProxy.

, , , server.close Node.js , HAProxy , , , . , , , , , .

, , balance first , ( worker1 ) 15% , , , balance static-rr . , ยซยป . . (12 ), , , - . , , , ยซยป ยซยป. .

, , Node server.maxconnections , ( , ), , , , . , maxconnection , , , . JavaScript, ( ). , , , . , , , HAProxy Node , . , , .

, , , , .


Node.js . , , , -. Node.js . , , , , , , , nginx HAProxy.

, Airbnb , Node.js .

Pembaca yang budiman! Apakah Anda menggunakan rendering sisi server dalam proyek Anda?

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


All Articles