Skip to main content
System design langkah demi langkah · Pelajaran 2 dari 7 · 13 menit

Cara Scaling Aplikasi Web: Server Stateless sampai CDN

Cara scaling aplikasi web langkah demi langkah: salinan stateless di belakang load balancer, session di store bersama, lalu CDN dan cache penyerap baca.

Oleh Matthews WongDitinjau pada

Poin utama

Scaling aplikasi web berarti menambah kapasitas di tempat yang paling murah lebih dulu: buat server aplikasi stateless agar salinan-salinan identik bisa berjalan di belakang load balancer, pindahkan session dan state lain ke store bersama, lalu biarkan CDN dan cache menjawab pembacaan yang berulang, sehingga origin dan database hanya menerima sebagian kecil trafik.

Setelah pelajaran ini, Anda bisa

  • Menjelaskan apa yang membuat server stateless, dan kenapa sticky session hanya menyembunyikan masalahnya.
  • Menelusuri request melewati load balancer, session store, CDN, dan cache, lalu menentukan lapisan mana yang menjawabnya.
  • Menentukan apa yang boleh disimpan cache bersama, dan memakai URL berversi agar tidak ada yang perlu di-purge.
Lihat cara kerjanya

Ikuti diagramnya langkah demi langkah

Tekan Putar, atau melangkah dengan panah. Setiap langkah juga ditulis di bawah diagram.

Dari satu server ke tier stateless, langkah demi langkah

Salinan app kedua di belakang nginx kehilangan session yang disimpan di memori; memindahkannya ke Redis membuat setiap salinan stateless, CDN menjawab request publik yang berulang, Redis menyimpan cache pembacaan yang sering, dan halaman pribadi selalu sampai ke origin.

Langkah 1 dari 7. Saat ini satu server aplikasi mengerjakan semuanya: merender halaman, menyimpan setiap login di memorinya sendiri, dan membaca database. Kotak putus-putus adalah yang akan ditambahkan oleh scaling.

Tombol: ← → langkah · Home / End · K putar atau jeda

  1. Saat ini satu server aplikasi mengerjakan semuanya: merender halaman, menyimpan setiap login di memorinya sendiri, dan membaca database. Kotak putus-putus adalah yang akan ditambahkan oleh scaling.

  2. Trafik naik, jadi salinan kedua dipasang di belakang nginx, yang kini membagi request secara bergiliran. Ana login di app 1, dan app 1 menyimpan session-nya di memori.

    Tebak langkah berikutnya Request Ana berikutnya masuk ke app 2. Apa yang dilihat app 2? (Ana, masih dalam keadaan login · Orang asing tanpa session)
  3. Round robin mengirim request berikutnya ke app 2, yang belum pernah melihat session-nya. Bagi app 2, Ana orang asing: tidak login, keranjang kosong.

  4. Pindahkan session ke store bersama yang punya masa kedaluwarsa, misalnya Redis. Kini setiap salinan bisa memuat session Ana lewat id-nya, jadi salinan mana pun bisa melayani request apa pun.

    SET session:ana '...' EX 1800
  5. Sebagian besar request sama untuk semua pengunjung: gambar, script, halaman publik. CDN edge di dekat Ana menyimpan salinannya dan menjawab permintaan yang berulang sendiri; origin tidak pernah melihatnya.

    Cache-Control: public, max-age=31536000, immutable
  6. Pembacaan yang sampai ke app juga berulang. Redis bisa menyimpan hasil yang sering dibaca di samping session, jadi database hanya melayani cache miss dan penulisan.

    GET post:42Tebak langkah berikutnya Ana membuka halaman keranjangnya sendiri. Siapa yang harus menjawab? (Salinan bersama di CDN · App di origin, setiap kali)
  7. Keranjang Ana berisi datanya sendiri, jadi tidak boleh disimpan cache bersama mana pun. Respons-nya bertanda private, no-store, dan CDN meneruskan setiap request keranjang ke origin.

    Cache-Control: private, no-store

Apa yang membuat web server stateless, dan kenapa scaling membutuhkannya?

Ada dua cara menambah kapasitas. Scaling up memberi satu mesin CPU dan memori lebih besar, dan kode Anda tidak menyadarinya sama sekali. Scaling out menambah jumlah mesin, dan tiba-tiba setiap request bisa mendarat di proses yang berbeda. Mulailah dari scaling up karena tidak perlu mengubah kode, lalu beralih ke scaling out saat sistem harus bertahan dari matinya satu mesin atau mesin terbesar pun sudah tidak cukup.

Baca selengkapnya (1)

Scaling out hanya berhasil kalau aplikasinya stateless: salinan mana pun bisa melayani request apa pun, karena tidak ada kebutuhan user yang hanya tinggal di satu proses. The Twelve-Factor App merumuskannya sebagai proses yang stateless dan share-nothing, dengan semua data yang harus bertahan disimpan di backing service yang stateful seperti database. State tersembunyi yang biasa ditemui adalah session di memori, counter yang harus sepakat antarsalinan, file yang ditulis ke disk lokal, dan timer di dalam aplikasi, yang akan berjalan sekali per salinan, bukan sekali saja.

Bagaimana load balancer menyebar request, dan kenapa bukan sticky session?

Load balancer berdiri di depan salinan-salinan aplikasi dan memilih satu untuk setiap request. Default nginx adalah round robin, membagi request bergiliran, dan nginx menjalankan health check pasif: server yang responsnya gagal ditandai gagal dan dihindari untuk sementara, diatur lewat max_fails dan fail_timeout. Menyebar request ke beberapa salinan menaikkan throughput dan membuat tier ini tetap hidup saat satu salinan mati.

Baca selengkapnya (2)

ip_hash bisa mengikat satu klien ke satu server, yang disebut sticky session, dan cara ini menggoda selama session masih disimpan di memori. The Twelve-Factor App menyebut sticky session sebagai pelanggaran yang tidak boleh diandalkan: saat salinan yang diikat restart atau dihapus, semua session di dalamnya ikut hilang. Simpan session di datastore yang punya masa kedaluwarsa, seperti Memcached atau Redis, maka load balancer bebas mengirim siapa pun ke mana pun.

Ada satu bagian yang tidak bisa di-scale out dengan cara ini: database. Setiap salinan yang Anda tambahkan tetap berbicara ke database yang sama, dan connection pool tiap salinan ikut membebaninya. Dari situlah modul berikutnya dimulai.

Apa yang boleh di-cache CDN, dan apa yang tidak boleh disimpannya?

CDN adalah jaringan proxy server yang tersebar secara geografis dan menyimpan salinan respons Anda di dekat pengunjung. Setiap edge adalah shared cache, yaitu cache yang menyimpan respons untuk lebih dari satu user, jadi nilainya ada di hit ratio: setiap request yang dijawabnya tidak pernah sampai ke origin. Header Cache-Control memberi tahu apa yang harus dilakukan. s-maxage mengatur masa simpan khusus untuk shared cache, jadi HTML publik bisa mengirim max-age=0 dengan s-maxage yang pendek: edge menyerap trafik, sementara browser tetap melakukan revalidasi.

Baca selengkapnya (2)

Masa simpan panjang paling aman dipasang pada URL yang ikut berubah saat isinya berubah. MDN menyebutnya cache busting: taruh versi atau hash di nama file atau query string, tandai responsnya immutable selama setahun, dan tidak ada yang perlu di-purge, karena isi baru selalu datang lewat URL baru.

Yang tidak boleh berada di shared cache adalah apa pun yang bersifat pribadi: halaman di balik login, atau respons yang memasang cookie. Tandai respons yang dipersonalisasi dengan private, yang melarang shared cache menyimpannya, atau no-store, yang melarang semua cache. MDN mengingatkan bahwa cookie saja tidak membuat respons menjadi private, dan menyarankan private alih-alih Vary: Cookie. Request yang mengubah state, seperti POST, tetap diteruskan ke origin.

Di mana posisi cache aplikasi setelah CDN terpasang?

Request yang lolos dari edge tetap saja berulang: produk yang sama, tulisan yang sama, profil yang sama. Cache-aside menanganinya di aplikasi: cari di cache dulu; kalau miss, baca dari database, simpan hasilnya dengan masa kedaluwarsa, lalu kembalikan. Saat menulis, perbarui database lebih dulu, baru hapus entri cache-nya, supaya pembacaan berikutnya memuat baris yang terbaru.

Baca selengkapnya (1)

Hitungannya menjadi alasan. Dengan hit ratio 80 persen, hanya 20 persen pembacaan yang lolos, jadi 1.000 baca per detik menjadi 200 di database, dan angka itulah, bukan laju mentahnya, yang menjadi dasar ukuran database. Simpan cache di tempat bersama, bukan di memori tiap salinan: panduan Azure mencatat bahwa cache lokal di instance yang berbeda cepat menjadi tidak konsisten. Dan tetap pasang masa kedaluwarsa walaupun Anda menghapus cache saat menulis, karena proses lain bisa saja mengubah baris itu.

Cara situs ini melakukannya

Konfigurasi nyata di balik pelajaran ini

Kutipan dari repositori yang membangun dan men-deploy situs ini, apa adanya.

lib/rate-limit.ts:1-11
/**
 * Simple in-memory rate limiter for API routes
 * In production, consider using Redis (Upstash) for distributed rate limiting
 */

interface RateLimitEntry {
  count: number;
  resetTime: number;
}

const rateLimitMap = new Map<string, RateLimitEntry>();

Rate limiter API situs ini menyimpan hitungannya di sebuah Map di dalam proses Node. Dengan satu container, seperti cara situs ini berjalan sekarang, itu sudah benar. Jalankan dua salinan di belakang load balancer, maka masing-masing menghitung sendiri, sehingga satu klien bisa mendapat hingga dua kali batasnya, dan setiap deploy, yang mengganti container, me-reset semua hitungan. Komentar di file itu sendiri sudah menyebut solusinya: store bersama seperti Redis.

next.config.ts:198-211
        // The films, their posters and captions. A re-render keeps the file
        // names, so only a request carrying the render stamp
        // (lib/film-manifest.ts prints `?v=` on every URL) may be cached as
        // immutable; Cloudflare then serves the films from its edge. A bare
        // URL, from an old link, keeps revalidating against the origin.
        source: "/videos/:path*",
        has: [{ type: "query", key: "v" }],
        headers: [
          {
            key: "Cache-Control",
            value: "public, max-age=31536000, immutable",
          },
        ],

Cache busting di konfigurasi situs ini sendiri. Nama file film tidak berubah saat dirender ulang, jadi hanya URL yang membawa cap render, ?v=, yang ditandai immutable selama setahun, dan Cloudflare menyajikannya dari edge. URL polos dari tautan lama tetap melakukan revalidasi ke origin, jadi tidak ada yang terjebak dengan film versi lama.

Yang sering rusak

Kesalahan yang benar-benar membuang waktu

  • Sticky session sebagai solusi

    Mengikat setiap user ke satu salinan hanya menyembunyikan session di memori, bukan menghilangkannya. Saat salinan itu restart, dihapus, atau gagal, semua user yang terikat padanya ikut logout. Pindahkan session ke store bersama dan lepaskan ikatannya.

  • State yang terlupa bahwa itu state

    Session adalah contoh yang paling jelas. Counter seperti rate limiter, upload yang ditulis ke disk lokal, dan timer di dalam aplikasi rusak dengan cara yang sama: setiap salinan menghitung sendiri, sebuah file hanya ada di satu disk, dan job terjadwal berjalan sekali per salinan.

  • Halaman pribadi di dalam shared cache

    Cache halaman yang menyapa satu user, maka pengunjung berikutnya akan melihat namanya, atau lebih buruk, invoice-nya. Vary: Cookie tidak menyelesaikannya; header itu menyimpan satu salinan per cookie dan tetap menaruh data pribadi di sistem bersama. Tandai dengan private atau no-store.

  • Yang akan saya ubah di sini: immutable pada nama yang bisa berubah

    Situs ini mengirim public, max-age=31536000, immutable untuk semua file di bawah /images/, padahal cover blog dirender ulang dengan nama file yang sama saat template cover berubah. Browser yang sudah menyimpan cover lama bisa terus menampilkannya hingga setahun. Memberi versi pada URL cover, seperti film memakai ?v=, akan menyelesaikannya.

Scaling up atau scaling out
Perubahan kodeTidak ada: aplikasi tidak menyadarinyaAplikasi harus stateless
Kalau mesinnya matiSeluruh kapasitas hilang sekaligusHanya sebagian kapasitas yang hilang
Batas atasMesin terbesar yang bisa Anda sewaJauh lebih tinggi, dibatasi bagian bersama seperti database
Yang dibutuhkanRestart atau migrasi untuk mengubah ukuranLoad balancer dan store bersama
Coba sendiri

Lakukan di komputer Anda sendiri

Anda butuh Node dan Docker. Jalankan dua salinan server kecil yang menghitung kunjungan di memori, pasang nginx di depannya, lihat hitungannya terpecah, lalu pindahkan ke Redis.

  1. Simpan server.mjs. Server ini menghitung kunjungan di memorinya sendiri, persis jenis state yang merusak scaling out.

    // server.mjs: counts visits in this process's memory
    import http from "node:http";
    const port = Number(process.env.PORT);
    let visits = 0;
    http
      .createServer((req, res) => {
        visits += 1;
        res.end(`port ${port}: visit ${visits}\n`);
      })
      .listen(port);
  2. Jalankan dua salinan di port yang berbeda.

    PORT=3001 node server.mjs &
    PORT=3002 node server.mjs &
  3. Simpan nginx.conf lalu jalankan nginx di Docker sebagai load balancer round robin di port 8080. Flag --add-host membuat container bisa menjangkau mesin Anda, termasuk di Linux.

    events {}
    http {
      upstream app {
        server host.docker.internal:3001;
        server host.docker.internal:3002;
      }
      server {
        listen 8080;
        location / { proxy_pass http://app; }
      }
    }
    
    # then:
    docker run --rm -p 8080:8080 --add-host=host.docker.internal:host-gateway \
      -v "$PWD/nginx.conf:/etc/nginx/nginx.conf:ro" nginx:alpine
  4. Di terminal lain, kirim empat request. Setiap salinan menyimpan hitungannya sendiri, jadi visit 1 muncul dua kali: state-nya terpecah di antara salinan.

    for i in 1 2 3 4; do curl -s localhost:8080; done
  5. Hentikan kedua salinan, jalankan Redis, pasang client-nya, lalu ganti counter dengan yang bersama. Jalankan lagi kedua salinan: kini hitungannya naik berurutan di keduanya, 1, 2, 3, 4.

    docker run --rm -d -p 6379:6379 redis:7-alpine
    npm install redis
    
    // server.mjs: the count now lives in Redis, shared by every copy
    import http from "node:http";
    import { createClient } from "redis";
    const redis = await createClient().connect();
    const port = Number(process.env.PORT);
    http
      .createServer(async (req, res) => {
        const visits = await redis.incr("visits");
        res.end(`port ${port}: visit ${visits}\n`);
      })
      .listen(port);
Kuis singkat

Tiga pertanyaan sebelum lanjut

Pilih jawaban untuk melihat kenapa benar atau salah. Tidak ada yang disimpan atau dinilai, jadi tebak saja dengan bebas.

  1. 1.Anda menjalankan tiga salinan di belakang nginx dengan round robin, dan user mengeluh sering logout secara acak. Apa penyebab yang paling mungkin?
  2. 2.Respons mana yang aman disimpan lama di shared cache pada CDN edge?
  3. 3.Cache Redis di depan database punya hit ratio 80 persen, dan 1.000 pembacaan per detik sampai ke app. Berapa yang sampai ke database?
Pikirkan dulu

Jawab dalam hati, lalu cek

  1. 1.Sebutkan dua jenis state, selain session, yang menghalangi aplikasi untuk scaling out.Lihat jawabannya

    Jawaban:Counter di memori seperti rate limiter, dan file yang ditulis ke disk lokal. Timer di dalam aplikasi adalah yang ketiga: dengan tiga salinan, timer itu berjalan tiga kali.

  2. 2.Kenapa HTML publik mengirim max-age=0 bersama s-maxage yang bernilai positif?Lihat jawabannya

    Jawaban:s-maxage hanya berlaku untuk shared cache, jadi CDN edge menyimpan dan menyajikan halaman itu, sementara setiap browser tetap melakukan revalidasi, dan deploy baru cepat sampai ke pembaca.

  3. 3.Kenapa URL berversi lebih baik daripada purge CDN?Lihat jawabannya

    Jawaban:Tidak ada yang perlu dihapus. Isi baru datang lewat URL baru, jadi setiap cache mengalami miss sekali lalu mengambilnya, sementara URL lama boleh tetap tersimpan selama apa pun.

Baca modulnya

Semua artikel di modul ini, berurutan

Pelajaran di atas adalah petanya. Setiap artikel membahas satu bagiannya secara mendalam, lengkap dengan contoh.