Pada aplikasi React SSR yang kami kelola, trafik normal berada di kisaran 10 request per detik. Aktivitas bot dan crawler bisa menghasilkan lebih dari 100 request per detik, sementara jumlah pengguna juga meningkat ketika ada event tertentu.
Aplikasi ini menggunakan Node.js dengan Koa sebagai HTTP server, dan GraphQL untuk mengambil data yang dibutuhkan dalam proses rendering. Setiap request halaman yang mencapai origin akan diproses oleh server, karena HTML tidak di-cache di CDN.
Untuk mengelola kapasitasnya, kami menggunakan PM2 Cluster Mode pada setiap VM dan cloud autoscaling dengan target CPU utilization sekitar 60%. Konfigurasi tersebut sejauh ini cukup stabil menghadapi variasi trafik yang kami temui di production.
Karakteristik trafik dan beban server-side rendering
Dalam aplikasi yang menggunakan server-side rendering, Node.js ikut menangani proses pembuatan HTML sebelum halaman dikirimkan ke browser.
Pada aplikasi kami, request yang masuk ditangani oleh Koa. Server kemudian mengambil data dari GraphQL API melalui private IP, menggunakan data tersebut untuk merender komponen React, lalu mengirimkan HTML kepada client.
Komunikasi melalui jaringan internal membantu menghindari perjalanan request melalui internet publik. Namun, waktu yang dibutuhkan untuk mendapatkan data tetap bergantung pada pemrosesan GraphQL API dan layanan di belakangnya.
Proses rendering sendiri juga membutuhkan resource CPU. Tingkat penggunaannya bergantung pada kompleksitas komponen React, jumlah data yang diproses, dan pekerjaan JavaScript lain selama request berlangsung.
Karakteristik ini perlu dipahami saat membaca angka RPS.
Sebagai contoh, request untuk halaman sederhana yang hanya membutuhkan sedikit data tentu bisa memiliki biaya pemrosesan yang berbeda dibandingkan halaman dengan banyak komponen dan beberapa operasi GraphQL.
Aktivitas crawler juga memengaruhi pola trafik. Bot bisa mengakses banyak halaman dalam waktu berdekatan, sehingga jumlah request yang diterima server dapat meningkat meskipun aktivitas pengguna biasa relatif stabil.
Dalam kondisi seperti ini, kami perlu memastikan kapasitas aplikasi dapat mengikuti perubahan trafik tersebut.
Menjalankan Node.js dengan PM2 Cluster Mode
Node.js menjalankan JavaScript melalui main thread dalam setiap proses. Pada aplikasi SSR, proses rendering React umumnya berlangsung di thread tersebut.
Ketika sebuah aplikasi dijalankan sebagai satu proses Node.js pada VM dengan beberapa CPU core, eksekusi JavaScript dari proses tersebut tidak otomatis tersebar ke seluruh core.
Kami menggunakan PM2 Cluster Mode untuk menjalankan beberapa proses Node.js dalam satu VM.
Konfigurasi yang digunakan adalah CODE_TOKEN_0 dengan CODE_TOKEN_1.
module.exports = {
apps: [
{
name: 'react-ssr',
script: './server.js',
exec_mode: 'cluster',
instances: 'max'
}
]
};Contoh konfigurasi PM2 untuk menggambarkan pendekatan yang digunakan. Nama aplikasi dan entry point bersifat ilustratif.
Dengan konfigurasi tersebut, PM2 menentukan jumlah worker berdasarkan CPU yang tersedia. Pada VM dengan empat vCPU, misalnya, umumnya akan dijalankan empat worker Node.js.
Setiap worker memiliki proses dan memory sendiri. PM2 membantu membagikan koneksi yang masuk ke worker-worker tersebut, sehingga beberapa request dapat diproses secara bersamaan pada CPU core yang berbeda.
Dalam production, kami menggunakan VM dengan spesifikasi minimal 4 vCPU dan 8 GB RAM.
Penggunaan CODE_TOKEN_0 memudahkan kami menjalankan aplikasi pada VM dengan jumlah CPU yang berbeda tanpa harus menentukan jumlah worker secara manual untuk setiap ukuran mesin.
Namun, penggunaan beberapa worker juga perlu mempertimbangkan konsumsi memory.
Setiap proses Node.js memiliki V8 heap dan alokasi memory sendiri. Untuk aplikasi SSR, penggunaan memory dipengaruhi oleh modul yang dimuat, data yang sedang diproses, serta aktivitas rendering.
Karena itu, konfigurasi worker perlu dievaluasi bersama penggunaan CPU dan RAM, terutama jika aplikasi memiliki kebutuhan memory yang cukup besar.
Menambah kapasitas melalui VM autoscaling
PM2 membantu memanfaatkan kapasitas CPU pada setiap VM. Untuk menangani perubahan trafik dalam skala yang lebih besar, kami menggunakan horizontal autoscaling pada infrastruktur cloud.
Autoscaling dikonfigurasi dengan target CPU utilization sekitar 60%.
Ketika rata-rata penggunaan CPU meningkat, autoscaling dapat menambah VM baru. Setiap VM menjalankan aplikasi yang sama menggunakan PM2 Cluster Mode.
Dengan pendekatan ini, kapasitas aplikasi bertambah melalui dua mekanisme. Jumlah worker pada setiap VM ditentukan oleh PM2, sedangkan jumlah VM disesuaikan oleh autoscaling.
Target CPU 60% kami gunakan sebagai acuan pengelolaan kapasitas. Angka ini memberikan ruang penggunaan CPU sebelum mesin mencapai utilisasi yang lebih tinggi.
Meski demikian, respons autoscaling juga dipengaruhi oleh waktu pengukuran metrik, konfigurasi scaling policy, serta waktu yang dibutuhkan instance baru untuk siap menerima request.
Misalnya, ketika trafik meningkat secara mendadak, penambahan VM tidak langsung menghasilkan kapasitas tambahan pada detik yang sama. Instance baru perlu dinyalakan dan aplikasi harus siap menerima trafik terlebih dahulu.
Hal ini menjadi salah satu pertimbangan ketika memilih target utilisasi CPU.
Pada workload SSR, CPU merupakan metrik yang relevan karena rendering React menggunakan waktu komputasi. Namun, kami juga perlu memahami bahwa sebagian durasi request digunakan untuk menunggu respons dari GraphQL API.
Jika request lebih banyak tertahan oleh latency backend, CPU utilization bisa tetap rendah meskipun waktu respons meningkat.
Karena itu, hubungan antara CPU utilization, request latency, dan kapasitas backend tetap perlu diperhatikan ketika mengevaluasi efektivitas autoscaling.
Peran Akamai dalam distribusi static assets
Selain menjalankan aplikasi pada cloud VM, kami menggunakan Akamai sebagai CDN.
Dalam konfigurasi aplikasi ini, caching di CDN digunakan untuk static assets seperti JavaScript, CSS, dan gambar. HTML hasil server-side rendering tidak di-cache, termasuk untuk pengguna yang belum login.
Artinya, ketika request halaman mencapai origin, aplikasi Node.js tetap menjalankan proses untuk menghasilkan HTML.
Akamai membantu mendistribusikan static assets melalui jaringan edge, sehingga browser dapat mengambil file-file tersebut tanpa selalu meminta ulang ke origin.
Pengaturan caching seperti ini juga perlu mempertimbangkan versi assets. File JavaScript dan CSS dengan nama berbasis content hash, misalnya, dapat menggunakan masa cache yang panjang karena perubahan isi file akan menghasilkan URL baru.
Sementara itu, HTML tetap dikirimkan dari aplikasi untuk setiap request halaman yang diteruskan ke origin.
Dengan konfigurasi tersebut, kapasitas rendering HTML tetap perlu disediakan pada lapisan Node.js, terutama saat aktivitas bot dan pengguna meningkat.
Menggunakan load testing untuk mengevaluasi kapasitas
Dalam mengelola aplikasi ini, kami juga melakukan load testing untuk mengevaluasi kemampuan sistem menghadapi beban request.
Hasil load testing sangat bergantung pada karakteristik aplikasi dan skenario pengujiannya.
Pada React SSR, kompleksitas halaman ikut menentukan kebutuhan komputasi. Halaman yang membutuhkan banyak operasi GraphQL atau pemrosesan data tambahan bisa menghasilkan latency dan penggunaan CPU yang berbeda dari halaman sederhana.
Karena itu, pengujian perlu menggunakan skenario yang cukup mewakili pola trafik aplikasi.
Beberapa metrik yang relevan untuk diamati mencakup:
Throughput, untuk melihat jumlah request yang berhasil diproses per detik.
Response time, terutama persentil seperti p95 dan p99 yang membantu menggambarkan pengalaman request yang lebih lambat.
CPU utilization, untuk memahami penggunaan kapasitas komputasi ketika concurrency meningkat.
Memory utilization, untuk mengamati konsumsi resource pada proses Node.js.
Error rate, untuk melihat apakah peningkatan trafik mulai menyebabkan request gagal.
Pada arsitektur dengan autoscaling, pengujian juga dapat mencakup perubahan trafik secara bertahap maupun lonjakan mendadak.
Skenario ini membantu mengevaluasi bagaimana infrastruktur merespons peningkatan beban, termasuk perilaku sistem selama instance tambahan sedang disiapkan.
Kami tidak menggunakan hasil pengujian sebagai acuan kapasitas yang berlaku untuk semua aplikasi Node.js. Angka yang diperoleh dalam satu lingkungan akan dipengaruhi oleh konfigurasi server, jenis request, dan kondisi backend saat pengujian dilakukan.
Yang lebih relevan adalah menggunakan hasil tersebut untuk memahami karakteristik aplikasi yang sedang dijalankan.
Memperhatikan kapasitas layanan di belakang SSR
Ada satu aspek yang perlu diperhatikan ketika menambah jumlah instance Node.js, yaitu kapasitas layanan yang digunakan oleh aplikasi.
Pada arsitektur kami, server SSR mengambil data melalui GraphQL API. Ketika jumlah instance Node.js bertambah, jumlah request yang dapat dikirimkan secara bersamaan ke GraphQL API juga berpotensi meningkat.
Sebagai ilustrasi, jika setiap request halaman menghasilkan satu atau beberapa operasi GraphQL, peningkatan jumlah request HTML dapat diikuti oleh peningkatan beban pada GraphQL API.
Dampaknya bergantung pada pola data fetching, connection management, caching di lapisan backend, dan kemampuan layanan GraphQL menangani concurrency.
Dalam kondisi tertentu, penambahan kapasitas di lapisan SSR bisa membuat bottleneck berpindah ke backend.
Hal tersebut perlu diperhitungkan dalam perencanaan kapasitas, terutama ketika aplikasi memiliki ketergantungan pada layanan lain.
Untuk aplikasi kami, komunikasi dengan GraphQL dilakukan menggunakan private IP di jaringan internal. Konfigurasi ini membuat jalur komunikasi antarservice lebih langsung, tetapi performa keseluruhan tetap bergantung pada waktu pemrosesan di masing-masing layanan.
Pengalaman menjalankan arsitektur ini
Sejauh ini, kombinasi PM2 Cluster Mode dan cloud autoscaling cukup stabil untuk menangani pola trafik aplikasi kami.
Trafik harian yang relatif kecil, aktivitas bot yang bisa melampaui 100 RPS, serta peningkatan aktivitas pengguna ketika event dapat ditangani dengan kapasitas yang menyesuaikan kebutuhan.
Dari pengalaman tersebut, ada beberapa hal yang terus menjadi perhatian dalam mengelola aplikasi Node.js di production.
Pertama, memahami pekerjaan yang dilakukan server untuk setiap request. Pada React SSR, proses fetching data dan rendering memiliki karakteristik yang perlu diperhitungkan saat mengevaluasi kapasitas.
Kedua, memanfaatkan CPU yang tersedia melalui beberapa worker Node.js. PM2 membantu mengelola proses tersebut tanpa memerlukan perubahan besar pada struktur aplikasi.
Ketiga, menyesuaikan jumlah VM dengan perubahan beban. Autoscaling memungkinkan kapasitas komputasi bertambah ketika diperlukan, dengan tetap memperhatikan waktu yang dibutuhkan instance baru untuk siap menerima trafik.
Terakhir, melakukan pengujian menggunakan karakteristik workload yang sesuai dengan aplikasi. Hasilnya menjadi referensi untuk memahami kapasitas dan perilaku sistem dalam kondisi trafik yang berbeda.
Konfigurasi yang kami gunakan tentu bisa berkembang mengikuti kebutuhan aplikasi. Untuk saat ini, pendekatan tersebut sudah cukup sesuai dengan karakteristik trafik dan arsitektur yang kami jalankan di production.
