Arsitektur Event-Driven

Arsitektur Event-Driven #

Ketika orang mengatakan “Nginx cepat”, mereka sebenarnya berbicara tentang sebuah keputusan arsitektur yang dibuat Igor Sysoev dua dekade lalu. Memahami arsitektur ini bukan sekadar pengetahuan akademis — ia membantu kita membuat keputusan konfigurasi yang lebih baik, memahami mengapa Nginx berperilaku tertentu di bawah beban, dan mendiagnosis masalah performa ketika muncul. Artikel ini membedah cara kerja Nginx dari dalam secara mendalam.

Masalah yang Dipecahkan: Blocking I/O #

Untuk memahami mengapa model event-driven penting, kita harus terlebih dulu memahami apa yang menjadi akar permasalahannya.

Bayangkan sebuah program yang harus membaca file dari disk. Cara paling sederhana:

// Pseudocode: Blocking I/O (cara sederhana)

function handleRequest(connection) {
    data = readFromDisk("file.html")   // ← Program BERHENTI di sini
                                        //   menunggu disk selesai baca
                                        //   Bisa butuh 1-100ms
    sendResponse(connection, data)
}

Selama program menunggu disk membaca file, tidak ada yang terjadi. Program “blok” — ia tidak bisa melakukan apapun selain menunggu. Ini adalah blocking I/O.

Dalam model web server tradisional, solusinya adalah membuat proses atau thread baru untuk setiap koneksi:

flowchart TD
    subgraph BlockingModel ["Model Blocking: Satu Proses Per Koneksi"]
        direction TB
        ConnA["Koneksi A Datang"] --> ForkA["Fork Process A"] --> WaitA["Process A Menunggu Disk (Blocking)"]
        ConnB["Koneksi B Datang"] --> ForkB["Fork Process B"] --> WaitB["Process B Menunggu Disk (Blocking)"]
        ConnC["Koneksi C Datang"] --> ForkC["Fork Process C"] --> WaitC["Process C Menunggu Disk (Blocking)"]
    end
    style BlockingModel stroke:#d32f2f,stroke-width:2px

Sistem operasi harus mengelola $N$ proses secara bersamaan, di mana masing-masing proses mengonsumsi ruang RAM yang signifikan. Selain itu, CPU harus terus-menerus melakukan pergantian konteks (context switching) antar-proses tersebut, sebuah operasi tingkat rendah yang sangat mahal secara komputasi dan memicu overhead overhead yang tinggi.

Masalahnya: web server menghabiskan sebagian besar waktunya menunggu — menunggu disk, menunggu respons dari backend, menunggu data dari klien. Ini adalah penggunaan proses/thread yang sangat tidak efisien.


Non-Blocking I/O: Dasar dari Segalanya #

Solusi Nginx dimulai dari non-blocking I/O. Alih-alih menunggu operasi I/O selesai, Nginx meminta OS untuk memberi tahu ketika operasi selesai:

// Pseudocode: Non-Blocking I/O

function handleRequest(connection) {
    // Minta baca file, tapi JANGAN tunggu
    requestReadFromDisk("file.html", callback: onFileReady)

    // Langsung kembali — proses bebas tangani hal lain
    return
}

function onFileReady(data) {
    // OS memanggil fungsi ini ketika file sudah terbaca
    sendResponse(connection, data)
}

Dengan non-blocking I/O, satu proses bisa memulai banyak operasi I/O dan menangani hasilnya ketika siap, tanpa pernah “memblok” dan menunggu.

Tapi ini menimbulkan pertanyaan: bagaimana satu proses mengetahui kapan setiap operasi I/O selesai? Inilah peran event loop.


Event Loop: Jantung Arsitektur Nginx #

Event loop adalah sebuah loop tak terbatas yang terus berjalan, memeriksa apakah ada event yang perlu ditangani:

Event Loop (konseptual):

while (true) {
    events = checkForReadyEvents()
    // "events" bisa berupa:
    // - Koneksi baru masuk
    // - File selesai dibaca dari disk
    // - Backend mengirim respons
    // - Koneksi klien siap menerima data
    // - Timer habis (untuk timeout)

    for each event in events {
        handleEvent(event)
    }

    // Jika tidak ada event, tunggu sebentar
    // (OS akan membangunkan kita saat ada event baru)
}

Ini terdengar sederhana, tapi implikasinya besar: satu proses bisa mengelola ribuan koneksi sekaligus karena ia tidak pernah benar-benar memblok. Ia selalu melanjutkan ke event berikutnya.

flowchart TD
    subgraph EventLoopTimeline ["Timeline Non-Blocking Event Loop"]
        direction TB
        T0["t=0: Terima Koneksi A"] --> T1["t=1: Mulai baca file A <br> (Daftarkan event 'file A siap')"]
        T1 --> T2["t=2: Terima Koneksi B <br> (A tetap berjalan asinkron)"]
        T2 --> T3["t=3: Mulai baca file B <br> (Daftarkan event 'file B siap')"]
        T3 --> T4["t=4: Terima Koneksi C"]
        T4 --> T5["t=5: Event 'file B siap' dipicu <br> -> Kirim respons ke B"]
        T5 --> T6["t=6: Event 'file A siap' dipicu <br> -> Kirim respons ke A"]
        T6 --> T7["t=7: Koneksi C siap kirim <br> -> Kirim respons ke C"]
        T7 --> T8["t=8: Semua selesai, tunggu event berikutnya"]
    end
    style EventLoopTimeline stroke:#388e3c,stroke-width:2px

Tidak ada siklus tunggu yang terbuang sia-sia. Satu proses worker tunggal dapat terus menyelesaikan pekerjaan nyata secara berkesinambungan.

Tidak ada yang menunggu sia-sia. Satu proses menyelesaikan pekerjaan nyata terus-menerus.


Mekanisme OS: epoll, kqueue, dan IOCP #

Event loop membutuhkan cara yang efisien untuk bertanya kepada OS: “event mana yang sudah siap?” Cara naif adalah memeriksa setiap koneksi satu per satu — tapi ini tidak efisien untuk ribuan koneksi.

OS modern menyediakan mekanisme yang lebih efisien:

epoll (Linux) #

// epoll: mekanisme Linux untuk efisien monitor banyak file descriptor

// Buat epoll instance
int epfd = epoll_create1(0);

// Daftarkan koneksi yang ingin dimonitor
struct epoll_event ev;
ev.events = EPOLLIN | EPOLLET;  // Monitor untuk data masuk, edge-triggered
ev.data.fd = connection_fd;
epoll_ctl(epfd, EPOLL_CTL_ADD, connection_fd, &ev);

// Tunggu events (non-blocking, timeout 1000ms)
int n = epoll_wait(epfd, events, MAX_EVENTS, 1000);

// OS memberikan HANYA file descriptor yang benar-benar siap
// Tidak perlu loop dan cek satu per satu!
for (int i = 0; i < n; i++) {
    handleEvent(events[i].data.fd);
}

Keunggulan mekanisme epoll dibandingkan dengan mekanisme lama (select/poll):

Karakteristikselect / poll (Model Lama)epoll / kqueue (Model Modern)
Pengiriman DescriptorHarus mengirim seluruh daftar file descriptor ke kernel OS di setiap panggilanHanya mendaftarkan descriptor sekali, kernel OS menyimpan status daftarnya
Metode PemeriksaanKernel harus menyusuri (looping) seluruh descriptor satu per satu, meskipun tidak aktifKernel langsung mengembalikan (return) hanya descriptor yang aktif
Kompleksitas Algoritma$\mathcal{O}(n)$, melambat seiring bertambahnya jumlah total koneksi$\mathcal{O}(1)$, performa konstan tidak terpengaruh jumlah total koneksi
Batas Maksimum KoneksiTerbatas (misalnya maks 1024 FD pada select)Tidak ada batasan sistem yang kaku (tergantung limit RAM & file descriptor OS)

kqueue (BSD/macOS) #

kqueue adalah padanan (equivalent) dari epoll di sistem operasi turunan BSD (termasuk FreeBSD dan macOS). Nginx menggunakan mekanisme kqueue secara otomatis ketika kita menjalankannya di lingkungan macOS atau BSD. Fungsionalitas utamanya sama, yaitu memantau banyak event koneksi secara asinkron dengan sangat efisien.

Deteksi Otomatis Nginx #

Nginx secara otomatis memilih mekanisme penanganan event terbaik yang tersedia di sistem operasi:

# /etc/nginx/nginx.conf
events {
    # Nginx biasanya mendeteksi ini secara otomatis.
    # Kita bisa menyetelnya secara manual jika diperlukan:
    use epoll;      # Linux (default di Linux)
    # use kqueue;   # BSD/macOS
    # use select;   # Fallback universal (sangat jarang dibutuhkan)

    worker_connections 1024;
}

Model Master-Worker Process #

Nginx menggunakan arsitektur multi-process dengan dua jenis proses utama:

flowchart TD
    subgraph NginxProcessModel ["Model Proses Master-Worker Nginx"]
        direction TB
        M["Master Process (1 Proses - User: root) <br> - Membaca & validasi konfigurasi <br> - Bind ke privileged ports (80, 443) <br> - Mengelola signals (reload, stop, upgrade) <br> - Spawn worker processes"] 
        
        M -->|"spawn"| W1["Worker Process #1 (User: nginx/www) <br> - Event Loop & Non-blocking I/O <br> - Menangani request HTTP klien"]
        M -->|"spawn"| W2["Worker Process #2 (User: nginx/www) <br> - Event Loop & Non-blocking I/O <br> - Menangani request HTTP klien"]
        M -->|"spawn"| W3["Worker Process #3 (User: nginx/www) <br> - Event Loop & Non-blocking I/O <br> - Menangani request HTTP klien"]
    end
    
    style M stroke:#0288d1,stroke-width:2px
    style W1 stroke:#388e3c,stroke-width:1.5px
    style W2 stroke:#388e3c,stroke-width:1.5px
    style W3 stroke:#388e3c,stroke-width:1.5px

Master Process: Tugas dan Tanggung Jawab #

Master process adalah “manajer” yang tidak pernah langsung menangani koneksi HTTP. Tugasnya:

Membaca konfigurasi. Ketika Nginx pertama start, master process membaca /etc/nginx/nginx.conf, memvalidasinya, dan menyimpan konfigurasi yang sudah diparsing.

Bind ke privileged ports. Port di bawah 1024 (seperti 80 dan 443) membutuhkan hak akses root untuk di-bind. Master process melakukan ini saat startup, lalu menurunkan privilage ke user biasa.

Spawn worker processes. Master process membuat sejumlah worker processes sesuai konfigurasi.

Hot reload tanpa downtime. Ketika kita menjalankan nginx -s reload, alurnya adalah sebagai berikut:

flowchart TD
    subgraph HotReloadProcess ["Alur Kerja Hot Reload Nginx (Signal HUP)"]
        direction TB
        Start["1. Master Process menerima Signal HUP"] --> Read["2. Master Process membaca konfigurasi baru"]
        Read --> Check{"Apakah Konfigurasi Valid?"}
        
        Check -->|"Ya"| Spawn["3a. Master spawn Worker BARU dengan config baru"]
        Spawn --> NewAccept["3b. Worker BARU mulai menerima koneksi masuk"]
        NewAccept --> SignalOld["3c. Master mengirim signal QUIT ke Worker LAMA"]
        SignalOld --> Graceful["3d. Worker LAMA menyelesaikan request yang sedang berjalan"]
        Graceful --> StopOld["3e. Worker LAMA mati setelah semua koneksi selesai"]
        StopOld --> ZeroDowntime["4. Hasil: Zero Downtime Reload!"]
        
        Check -->|"Tidak"| Reject["Reject: Master menolak konfigurasi baru"]
        Reject --> KeepOld["Worker LAMA terus berjalan dengan konfigurasi lama"]
        KeepOld --> NoDowntime["Hasil: Tidak ada gangguan layanan"]
    end
    
    style Check stroke:#f57c00,stroke-width:2px
    style ZeroDowntime stroke:#388e3c,stroke-width:2px
    style NoDowntime stroke:#388e3c,stroke-width:2px

Worker Process: Di Mana Pekerjaan Sesungguhnya Terjadi #

Setiap worker process menjalankan event loop-nya sendiri secara mandiri dan independen satu sama lain. Kita disarankan untuk mencocokkan jumlah worker process dengan jumlah core CPU fisik yang tersedia di server:

# Konfigurasi di nginx.conf
worker_processes auto; # 'auto' mendeteksi jumlah core CPU secara otomatis

Mengapa jumlah worker disarankan sama dengan jumlah core CPU?

  • Lebih sedikit dari core: Beberapa core CPU akan menganggur (idle) dan tidak dimanfaatkan secara optimal.
  • Sama dengan core (Rekomendasi): Setiap worker process diikat secara eksklusif ke satu core CPU penuh, meminimalkan perebutan sumber daya.
  • Lebih banyak dari core: Sistem operasi terpaksa melakukan perpindahan konteks (context switching) antar-worker pada core yang sama, yang memicu overhead CPU yang tidak diperlukan.

Sebagai ilustrasi, jika server kita memiliki 4 core CPU, Nginx akan melahirkan 4 worker process. Jika setiap worker dikonfigurasi untuk menangani hingga 1.024 koneksi simultan, maka total kapasitas server kita adalah $4 \times 1.024 = 4.096$ koneksi aktif secara bersamaan secara asinkron.


Cache Manager dan Cache Loader #

Selain master dan worker, Nginx juga memiliki proses opsional saat caching diaktifkan:

flowchart LR
    subgraph CacheProcesses ["Proses Tambahan Nginx Caching"]
        direction LR
        CM["Cache Manager Process <br> - Memantau ukuran cache di disk <br> - Menghapus entri kedaluwarsa/lama <br> - Menjaga batas ukuran cache maks"]
        CL["Cache Loader Process <br> - Berjalan sekali saat startup <br> - Memuat metadata cache ke Shared Memory <br> - Membuat cache cepat hangat (warm-up)"]
    end
    style CM stroke:#0288d1,stroke-width:1.5px
    style CL stroke:#0288d1,stroke-width:1.5px

Memory Model: Mengapa Nginx Hemat RAM #

Hemat memori bukan kebetulan — ini adalah hasil dari beberapa keputusan desain yang disengaja:

1. Sedikit Proses #

Efisiensi memori yang luar biasa pada Nginx merupakan hasil dari perbedaan manajemen koneksi dibandingkan Apache:

KarakteristikApache (MPM Prefork)Nginx (Event-Driven)
Alokasi Sumber Daya1 Proses per 1 Koneksi Klien1 Worker Process untuk Ribuan Koneksi Klien
Estimasi Memori per Koneksi8 – 10 MB per prosesBeberapa kilobita saja (disimpan sebagai struct di heap)
Total RAM (1.000 Koneksi)~8.000 – 10.000 MB (8 - 10 GB)~12 MB (Total seluruh proses worker)
Efisiensi OverheadSangat boros karena context switchingSangat efisien, hampir tanpa context switching tambahan

2. Pool Allocator #

Nginx menggunakan memory allocator kustom berbasis pool untuk mempercepat alokasi memori dan mencegah kebocoran RAM:

flowchart TD
    subgraph MemoryPoolModel ["Model Alokasi Nginx Memory Pool"]
        direction TB
        ReqStart["Request Masuk"] --> AllocPool["Alokasikan Request Pool (Satu Blok Memori Utuh)"]
        
        subgraph PoolData ["Penggunaan Memori dalam Pool"]
            direction TB
            H["Header Parsing"]
            U["URL Storage"]
            B["Response Buffers"]
            V["Temporary Variables"]
        end
        
        AllocPool --> PoolData
        PoolData --> ReqDone["Request Selesai Diproses"]
        ReqDone --> BulkFree["Bulk Deallocation (Seluruh Pool Dihapus Sekaligus)"]
    end
    
    style AllocPool stroke:#388e3c,stroke-width:1.5px
    style BulkFree stroke:#d32f2f,stroke-width:2px

Keuntungan dari penggunaan model Memory Pool ini adalah:

  • Alokasi sangat cepat: Hanya perlu menaikkan penunjuk (increment pointer) memori tanpa perlu mencari blok memori kosong.
  • Meniadakan fragmentasi memori: Memori dialokasikan dalam satu blok besar yang berkelanjutan.
  • Bebas dari kebocoran memori (memory leak): Seluruh memori yang dialokasikan untuk suatu request langsung didealokasi sekaligus secara massal (bulk) setelah request selesai.
  • Deallokasi efisien: Proses pembebasan memori massal jauh lebih cepat daripada memanggil free() satu per satu untuk setiap variabel kecil.

3. Copy-on-Write untuk Worker Processes #

Ketika master process membuat worker process menggunakan fungsi system call fork(), kernel sistem operasi Linux mengoptimalkan memori menggunakan mekanisme Copy-on-Write (COW):

flowchart TD
    subgraph COWModel ["Mekanisme Copy-on-Write (COW) Nginx Workers"]
        direction TB
        MP["Master Process Memory Page (10 MB RAM)"]
        
        MP -->|"Fork & Share (Read-Only)"| W1["Worker Process 1 (Berbagi Pages)"]
        MP -->|"Fork & Share (Read-Only)"| W2["Worker Process 2 (Berbagi Pages)"]
        MP -->|"Fork & Share (Read-Only)"| W3["Worker Process 3 (Berbagi Pages)"]
        
        W2 -->|"Write Event (Ubah Data)"| ModPage["Duplikasi Halaman Memori Hanya untuk Halaman yang Dimodifikasi"]
    end
    
    style MP stroke:#0288d1,stroke-width:2px
    style ModPage stroke:#f57c00,stroke-width:1.5px

Hasil praktisnya, pembuatan beberapa worker process tidak melipatgandakan kebutuhan memori RAM secara linear karena mereka berbagi halaman memori (memory pages) yang sama dari master process selama datanya tidak berubah. Halaman memori baru hanya disalin secara independen saat ada instruksi penulisan (write operation) pada halaman tersebut.


Timeout dan Timer: Mengelola Koneksi yang Lambat #

Karena Nginx mengelola ribuan koneksi sekaligus, ia harus bisa mendeteksi dan menutup koneksi yang sudah tidak aktif atau terlalu lambat. Ini dilakukan dengan sistem timer yang terintegrasi ke event loop:

http {
    # Berapa lama Nginx menunggu request selanjutnya
    # dari klien yang sama (keep-alive)
    keepalive_timeout 65;

    # Berapa lama Nginx menunggu klien mengirim request header
    client_header_timeout 12;

    # Berapa lama Nginx menunggu klien mengirim request body
    client_body_timeout 12;

    # Berapa lama Nginx menunggu klien menerima respons
    send_timeout 10;
}

Timer ini diimplementasikan sebagai events dalam event loop — tidak ada proses terpisah yang “mengawasi” timeout. Ketika timer habis, event loop memicu event “timeout” dan Nginx menutup koneksi tersebut.


Shared Memory: Koordinasi Antar Worker #

Worker processes berjalan independen, tapi ada beberapa hal yang perlu dibagi antar worker:

flowchart TD
    subgraph SharedMemSpace ["Shared Memory Space (mmap)"]
        direction LR
        RL["Rate Limit Counters <br> (Worker 1: 5, Worker 2: 3...) <br> Akumulasi global aktif"]
        CM["Cache Metadata <br> (Path file, size, expiry) <br> Shared lookup untuk hit/miss"]
    end
    
    W1["Worker 1"] <--> SharedMemSpace
    W2["Worker 2"] <--> SharedMemSpace
    W3["Worker 3"] <--> SharedMemSpace
    W4["Worker 4"] <--> SharedMemSpace
    
    style SharedMemSpace stroke:#f57c00,stroke-width:2px

Konfigurasi shared memory di Nginx:

http {
    # Zone untuk rate limiting — shared memory 10 MB
    limit_req_zone $binary_remote_addr zone=api_limit:10m rate=10r/s;

    # Zone untuk connection limiting
    limit_conn_zone $binary_remote_addr zone=conn_limit:10m;

    # Cache zone — shared memory untuk metadata cache
    proxy_cache_path /var/cache/nginx levels=1:2
                     keys_zone=my_cache:10m
                     max_size=10g;
    #                             ↑
    #                  10m = 10 MB shared memory untuk metadata
    #                  Bukan untuk file cache itu sendiri
}

Thread Pools: Menangani Operasi Blocking #

Meskipun Nginx dirancang untuk non-blocking I/O, ada beberapa operasi yang tidak bisa dilakukan secara non-blocking di semua kondisi — terutama operasi disk tertentu.

Nginx 1.7.11 memperkenalkan thread pools sebagai solusi:

# Konfigurasi thread pool
thread_pool default threads=32 max_queue=65536;

http {
    server {
        location / {
            # Gunakan thread pool untuk membaca file dari disk
            # Berguna jika file sering tidak ada di OS page cache
            aio threads=default;
            sendfile on;
        }
    }
}
flowchart TD
    subgraph ThreadPoolComparison ["Model Eksekusi: Tanpa vs Dengan Thread Pool"]
        direction TB
        
        subgraph NoThreadPool ["Tanpa Thread Pool (Blocking)"]
            direction TB
            W1["Worker Process"] -->|"Operasi Disk I/O"| Disk1[("Disk / HDD (Lambat)")]
            Disk1 -. "Menunggu Data Selesai (Worker BLOCK)" .-> W1
            W1 -. "Koneksi Klien Lain Ikut Terhenti" .-> Drop1["Latency / Stall"]
        end
        
        subgraph WithThreadPool ["Dengan Thread Pool (Non-Blocking)"]
            direction TB
            W2["Worker Process"] -->|"1. Delegasikan I/O"| TP["Thread Pool (Worker Threads)"]
            TP -->|"2. Proses Disk I/O"| Disk2[("Disk / HDD (Lambat)")]
            W2 -->|"3. Kembali ke Event Loop"| Active["4. Menangani Koneksi Lain (Zero Stall)"]
            Disk2 -->|"5. Selesai Baca File"| TP
            TP -->|"6. Notifikasi Worker"| W2
            W2 -->|"7. Kirim Respons Klien"| Done["Respons Sukses"]
        end
    end
    
    style NoThreadPool stroke:#d32f2f,stroke-width:1.5px
    style WithThreadPool stroke:#388e3c,stroke-width:1.5px

Thread pools sangat berguna untuk server dengan HDD (bukan SSD) yang sering mengalami I/O wait, atau untuk kasus di mana file-file besar sering diakses.


HTTP/2 dan HTTP/3: Arsitektur Nginx Modern #

Nginx mendukung HTTP/2 (sejak 1.9.5) dan HTTP/3/QUIC (eksperimental di mainline). Dukungan ini mengubah cara Nginx mengelola koneksi:

HTTP/2 Multiplexing #

flowchart TD
    subgraph ProtocolComparison ["Perbandingan HTTP/1.1 vs HTTP/2 Multiplexing"]
        direction TB
        
        subgraph HTTP1 ["HTTP/1.1 (Satu Request per Koneksi TCP)"]
            direction TB
            C1["Klien"] -->|"Koneksi TCP 1 (GET /page)"| S1["Nginx"]
            C1 -->|"Koneksi TCP 2 (GET /style.css)"| S1
            C1 -->|"Koneksi TCP 3 (GET /app.js)"| S1
            style HTTP1 stroke:#7f8c8d,stroke-width:1.5px
        end
        
        subgraph HTTP2 ["HTTP/2 (Multiplexing dalam Satu Koneksi TCP)"]
            direction TB
            C2["Klien"] -->|"Koneksi TCP Tunggal"| Pipe["Satu Pipa Koneksi"]
            subgraph Streams ["Streams Simultan"]
                direction LR
                st1["Stream 1: GET /page"]
                st2["Stream 3: GET /style.css"]
                st3["Stream 5: GET /app.js"]
            end
            Pipe --> Streams --> S2["Nginx"]
            style HTTP2 stroke:#388e3c,stroke-width:1.5px
        end
    end
server {
    listen 443 ssl;
    http2 on;  # Aktifkan HTTP/2 (Nginx 1.25.1+)
    # Atau: listen 443 ssl http2;  (cara lama)

    ssl_certificate /path/to/cert.pem;
    ssl_certificate_key /path/to/key.pem;

    # HTTP/2 Server Push (opsional)
    location / {
        http2_push /style.css;
        http2_push /app.js;
        # Nginx kirim file CSS dan JS bersamaan dengan HTML
        # tanpa browser perlu request terpisah
    }
}

HTTP/3 / QUIC #

# HTTP/3 (QUIC) — butuh kompilasi dengan quic support
server {
    listen 443 quic reuseport;  # UDP untuk QUIC
    listen 443 ssl;              # TCP fallback untuk HTTP/2

    ssl_certificate /path/to/cert.pem;
    ssl_certificate_key /path/to/key.pem;

    # Informasikan ke browser bahwa QUIC tersedia
    add_header Alt-Svc 'h3=":443"; ma=86400';
}

HTTP/3 menggunakan UDP via protokol QUIC alih-alih TCP, menghilangkan masalah head-of-line blocking di level transport yang masih ada di HTTP/2.


Nginx di Container: Docker dan Kubernetes #

Memahami arsitektur Nginx sangat membantu ketika menjalankannya di container:

Docker #

# Nginx Docker resmi — image sangat kecil (~22 MB)
FROM nginx:1.25-alpine

# Salin konfigurasi custom
COPY nginx.conf /etc/nginx/nginx.conf
COPY default.conf /etc/nginx/conf.d/default.conf

# Salin file statis
COPY dist/ /usr/share/nginx/html/

EXPOSE 80
# docker-compose.yml
version: '3.8'
services:
  nginx:
    image: nginx:1.25-alpine
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf:ro
      - ./ssl:/etc/ssl/nginx:ro
      - nginx_cache:/var/cache/nginx
    depends_on:
      - backend

  backend:
    image: myapp:latest
    expose:
      - "3000"

volumes:
  nginx_cache:

Di container, Nginx biasanya berjalan dengan satu worker process (karena container biasanya mendapat 1 CPU):

# nginx.conf di container
worker_processes 1;  # Atau 'auto' — otomatis deteksi CPU

events {
    worker_connections 1024;
}

Kubernetes Nginx Ingress #

# Kubernetes Ingress menggunakan Nginx Controller
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: myapp-ingress
  annotations:
    nginx.ingress.kubernetes.io/rewrite-target: /
    nginx.ingress.kubernetes.io/ssl-redirect: "true"
    nginx.ingress.kubernetes.io/proxy-body-size: "50m"
spec:
  ingressClassName: nginx
  tls:
    - hosts:
        - example.com
      secretName: example-tls
  rules:
    - host: example.com
      http:
        paths:
          - path: /api
            pathType: Prefix
            backend:
              service:
                name: api-service
                port:
                  number: 80
          - path: /
            pathType: Prefix
            backend:
              service:
                name: frontend-service
                port:
                  number: 80

Nginx Ingress Controller di Kubernetes secara otomatis menerjemahkan konfigurasi Ingress YAML ini menjadi konfigurasi Nginx yang sesuai dan me-reload Nginx ketika ada perubahan — tanpa downtime.


Debugging Performa: Profiling Nginx #

Ketika menghadapi masalah performa di production, ada beberapa teknik debugging yang berguna:

Access Log Analysis #

# Temukan 10 URL paling lambat
awk '{print $NF, $7}' /var/log/nginx/access.log | \
  sort -rn | head -10

# Hitung request per detik dari log
awk '{print $4}' /var/log/nginx/access.log | \
  cut -d: -f1-3 | sort | uniq -c

# Error rate per jam
grep ' 5[0-9][0-9] ' /var/log/nginx/access.log | \
  awk '{print $4}' | cut -d: -f1-2 | sort | uniq -c

Nginx Variables untuk Debug #

# Tambahkan header debug untuk troubleshooting proxy
location / {
    proxy_pass http://backend;

    # Tambahkan headers debug (HAPUS di production!)
    add_header X-Upstream-Addr $upstream_addr;
    add_header X-Upstream-Status $upstream_status;
    add_header X-Upstream-Response-Time $upstream_response_time;
    add_header X-Request-ID $request_id;
}

strace untuk Worker Process #

Dalam situasi ekstrem untuk mendeteksi bottleneck I/O, kita bisa menggunakan utilitas strace untuk melihat panggilan sistem (system calls) yang sedang dilakukan oleh worker process secara real-time:

# Temukan PID worker Nginx
ps aux | grep "nginx: worker"

# Melacak system calls (Peringatan: memicu overhead tinggi, gunakan hanya untuk debug singkat!)
strace -p <PID> -e trace=network,file 2>&1 | head -100

Model event-driven non-blocking cocok untuk web server karena karakteristik beban kerja web server:

Tahapan RequestDurasi EstimasiTipe OperasiKeterangan
Menerima Request Headers~0.1 msI/O JaringanMenunggu data terkirim dari klien melalui soket
Parsing HTTP Headers~0.01 msCPU KomputasiMembaca struktur data header di memori
Membaca File dari Disk~1 – 10 msI/O DiskMenunggu pembacaan piringan magnetis / SSD
Proxying ke Backend~5 – 50 msI/O Jaringan + BackendMenunggu proses komputasi server backend (misal: database)
Mengirim Respons ke Klien~0.5 – 5 msI/O JaringanMengirimkan data kembali melalui jaringan internet ke klien

Persentase Alokasi Waktu Keseluruhan:

  • Total CPU Time (Aktif): ~1% – 5%
  • Total I/O Wait Time (Menunggu): ~95% – 99%

Kesimpulan: Web server menghabiskan hampir seluruh hidupnya hanya untuk menunggu I/O. Model asinkron event-driven Nginx memastikan bahwa selama masa tunggu tersebut, worker process tidak menganggur melainkan langsung dialihkan untuk menangani request klien lainnya.

Dibandingkan dengan skenario di mana model process-per-request tradisional (blocking) lebih cocok digunakan:

Skenario di mana model Blocking (Satu Proses per Koneksi) lebih cocok:

  • Pemrosesan data yang sangat intensif menggunakan CPU (seperti kompresi/encoding video, kalkulasi matematika rumit, atau machine learning inference).
  • Operasi yang benar-benar sekuensial dan tidak memerlukan penanganan banyak koneksi secara bersamaan (low concurrency).
  • Script pengerjaan sekali pakai (one-off scripts) yang tidak membutuhkan skalabilitas tinggi.

Skenario di mana model Event-Driven Non-Blocking (Nginx) sangat unggul:

  • Web server biasa (banyak operasi I/O file dan jaringan, tetapi sangat sedikit pemrosesan CPU per request).
  • API Gateway dan Reverse Proxy (hanya meneruskan request ke backend).
  • Aplikasi real-time yang mempertahankan koneksi terbuka lama (seperti WebSockets atau HTTP long-polling).
  • Load balancer bertrafik tinggi.

Koneksi Keep-Alive dan Pooling #

Salah satu optimasi penting dalam model event-driven Nginx adalah penanganan koneksi keep-alive — baik antara klien dan Nginx, maupun antara Nginx dan backend.

Keep-Alive dengan Klien #

HTTP/1.1 memperkenalkan persistent connections — koneksi TCP yang tetap terbuka setelah satu request selesai, sehingga request berikutnya bisa menggunakan koneksi yang sama tanpa TCP handshake baru.

flowchart TD
    subgraph KeepAliveComparison ["Penanganan Koneksi Keep-Alive"]
        direction TB
        
        subgraph NoKeepAlive ["Tanpa Keep-Alive (HTTP/1.0)"]
            direction TB
            H1["Handshake 1"] --> R1["GET /page"] --> F1["Tutup Koneksi (FIN)"]
            H2["Handshake 2"] --> R2["GET /style.css"] --> F2["Tutup Koneksi (FIN)"]
            H3["Handshake 3"] --> R3["GET /app.js"] --> F3["Tutup Koneksi (FIN)"]
        end
        
        subgraph WithKeepAlive ["Dengan Keep-Alive (HTTP/1.1)"]
            direction TB
            H4["Handshake TCP Tunggal"] --> R4["GET /page"]
            R4 --> R5["GET /style.css"]
            R5 --> R6["GET /app.js"]
            R6 --> Timeout{"keepalive_timeout habis?"}
            Timeout -->|"Ya"| F4["Tutup Koneksi (FIN)"]
        end
    end

Dalam model event-driven Nginx, koneksi keep-alive yang menunggu request berikutnya tidak “menghabiskan” sumber daya seperti di Apache. Koneksi ini hanya terdaftar sebagai event yang menunggu, menggunakan sedikit sekali memori.

http {
    # Berapa lama koneksi keep-alive dipertahankan
    keepalive_timeout 65;

    # Maksimum request per koneksi keep-alive
    keepalive_requests 1000;
}

Keep-Alive dengan Backend (Upstream) #

Ini sering diabaikan tapi sangat penting untuk performa proxy:

upstream backend {
    server 127.0.0.1:3000;

    # Pertahankan N koneksi ke backend agar tidak
    # perlu buka koneksi baru untuk setiap request
    keepalive 32;
}

server {
    location / {
        proxy_pass http://backend;

        # Diperlukan untuk keepalive upstream bekerja
        proxy_http_version 1.1;
        proxy_set_header Connection "";
    }
}

Tanpa keepalive ke backend, setiap request dari Nginx ke backend membutuhkan TCP handshake baru — bisa menambahkan 1-10ms latensi per request. Dengan keepalive, koneksi yang sudah ada digunakan kembali.


Buffering dan Pipelining #

Nginx menggunakan buffering untuk mengoptimalkan transfer data antara berbagai komponen:

flowchart LR
    subgraph BufferingMechanism ["Mekanisme Proxy Buffering Nginx"]
        direction LR
        Client["Klien <br> (Koneksi Lambat/Seluler)"] <-->|"1. Kirim lambat / <br> 6. Terima lambat"| Buffer["Nginx Buffer <br> (RAM / Temp Disk)"]
        Buffer <-->|"2. Kirim instan / <br> 3. Terima instan"| Backend["Backend Server <br> (Fast Loopback/LAN)"]
    end
    
    style Buffer stroke:#0288d1,stroke-width:2px

Skenario alur data buffering:

  1. Nginx menerima request dari klien (bisa jadi sangat lambat karena batasan bandwidth klien).
  2. Nginx menyimpan request body di dalam buffer secara penuh terlebih dahulu.
  3. Nginx mengirimkan request utuh tersebut ke backend sekaligus (sangat cepat melalui inter-process communication atau LAN).
  4. Backend langsung memproses dan mengembalikan respons secara instan.
  5. Nginx menyimpan respons utuh dari backend ke dalam buffer.
  6. Nginx mengirimkan respons dari buffer tersebut ke klien secara bertahap (meskipun koneksi klien sangat lambat).
  • Tanpa buffering: Backend terpaksa harus menunggu Nginx selesai mengirimkan data byte-by-byte ke klien yang lambat. Akibatnya, proses backend tetap dalam kondisi “sibuk” dan tidak dapat melayani request baru lainnya.
  • Dengan buffering: Proses backend selesai dalam hitungan milidetik dan langsung terbebas untuk menangani antrean request berikutnya, sementara Nginx mengurus transmisi data yang lambat ke klien secara mandiri di level event loop.
location / {
    proxy_pass http://backend;

    # Buffer response dari backend
    proxy_buffering on;
    proxy_buffer_size 4k;          # Buffer untuk headers
    proxy_buffers 8 4k;            # 8 buffer × 4k = 32k untuk body
    proxy_busy_buffers_size 8k;    # Max buffer yang sedang dikirim

    # Jika respons lebih besar dari buffer, tulis ke disk
    proxy_temp_path /var/cache/nginx/temp;
    proxy_max_temp_file_size 1024m;
}

Nginx Worker: Isolation dan Fault Tolerance #

Salah satu keuntungan model multi-worker yang jarang dibahas adalah fault isolation:

flowchart TD
    subgraph FaultTolerance ["Isolasi Proses & Pemulihan Mandiri (Fault Tolerance)"]
        direction TB
        Master["Master Process"] -->|"Deteksi crash via SIGCHLD"| CrashEvent{"Worker 2 Crash!"}
        CrashEvent -->|"Ya"| SpawnNew["Master spawn Worker 2 BARU (< 1 detik)"]
        
        subgraph ActiveWorkers ["Worker Lain Tetap Aktif Melayani Trafik"]
            direction LR
            W1["Worker 1"]
            W3["Worker 3"]
            W4["Worker 4"]
        end
        
        CrashEvent --> ActiveWorkers
        SpawnNew -->|"Kembali melayani trafik"| W2New["Worker 2 Baru"]
    end
    
    style Master stroke:#0288d1,stroke-width:2px
    style CrashEvent stroke:#d32f2f,stroke-width:2px
    style ActiveWorkers stroke:#388e3c,stroke-width:1.5px

Skenario penanganan kegagalan (fault isolation):

  • Tanpa model multi-worker: Jika satu-satunya proses web server mengalami crash, seluruh layanan web langsung mati total (downtime).
  • Dengan model multi-worker: Jika salah satu worker process (misalnya Worker 2) crash akibat bug aplikasi atau kehabisan memori, master process akan mendeteksinya secara instan melalui sinyal SIGCHLD. Master process akan langsung men-spawn worker pengganti yang baru dalam waktu kurang dari satu detik. Sementara itu, worker lainnya (Worker 1, 3, dan 4) tetap aktif melayani request masuk tanpa terganggu sama sekali. Pengguna akhir tidak merasakan adanya gangguan layanan.

Master process secara aktif memonitor worker processes. Jika worker mati tidak normal, master langsung melahirkan (spawn) penggantinya secara instan:

# Kita bisa melihat log aktivitas ini di log error Nginx jika kegagalan terjadi:
# 2026/01/15 10:23:45 [alert] 1234#1234: worker process 5678 exited on signal 11
# 2026/01/15 10:23:45 [notice] 1234#1234: start worker process 5679

OS-Level Optimizations: Nginx dan Kernel #

Nginx bekerja erat dengan fitur kernel Linux untuk performa maksimal:

sendfile() — Zero Copy #

flowchart TD
    subgraph ZeroCopyComparison ["Perbandingan Alur I/O: Tradisional vs Zero-Copy (sendfile)"]
        direction TB
        
        subgraph TraditionalIO ["Metode Tradisional (Tanpa sendfile)"]
            direction TB
            D1[("File di Disk")] -->|"1. read() syscall"| KC1["Kernel Page Cache"]
            KC1 -->|"2. Copy data ke User Space"| NP["Nginx Process Memory"]
            NP -->|"3. write() syscall"| SB1["Kernel Socket Buffer"]
            SB1 -->|"4. Kirim data"| Net1["Network Card"]
            style TraditionalIO stroke:#7f8c8d,stroke-width:1.5px
        end
        
        subgraph ZeroCopyIO ["Zero-Copy (Dengan sendfile)"]
            direction TB
            D2[("File di Disk")] -->|"1. sendfile() syscall"| KC2["Kernel Page Cache"]
            KC2 -->|"2. Transfer langsung via DMA"| SB2["Kernel Socket Buffer"]
            SB2 -->|"3. Kirim data"| Net2["Network Card"]
            style ZeroCopyIO stroke:#388e3c,stroke-width:1.5px
        end
    end
http {
    sendfile on;         # Aktifkan zero-copy
    tcp_nopush on;       # Buffer paket TCP hingga penuh sebelum kirim
                         # (berpasangan dengan sendfile)
    tcp_nodelay on;      # Kirim segera untuk koneksi keep-alive
                         # (setelah sendfile/tcp_nopush selesai)
}

mmap() untuk Large File Serving #

Untuk file yang sangat besar, Nginx menggunakan mmap() — memory-mapped file — yang memungkinkan OS mengelola cache file di kernel secara efisien:

flowchart TD
    subgraph MmapServing ["Mekanisme Penyajian Berkas Besar via mmap()"]
        direction TB
        LargeFile[("Berkas Besar (Video / ISO)")] -->|"mmap() syscall"| MapSpace["Nginx memetakan berkas ke Virtual Address Space"]
        MapSpace --> LazyLoad["Kernel mengelola pemuatan halaman (Lazy Loading)"]
        LazyLoad --> ReadReq["Klien meminta membaca byte tertentu"]
        ReadReq --> PageFault{"Apakah halaman ada di RAM?"}
        PageFault -->|"Tidak (Page Fault)"| LoadDisk["Kernel membaca halaman dari disk ke Page Cache RAM"]
        PageFault -->|"Ya (Cache Hit)"| DirectServe["Kernel langsung mengirimkan data dari RAM (Zero-copy)"]
        LoadDisk --> DirectServe
    end
    
    style PageFault stroke:#f57c00,stroke-width:1.5px
    style DirectServe stroke:#388e3c,stroke-width:2px

Diagnosing Performance dengan Nginx Status Module #

Nginx menyediakan modul bawaan untuk melihat statistik real-time:

server {
    listen 8080;
    server_name localhost;

    location /nginx_status {
        stub_status on;
        allow 127.0.0.1;   # Hanya izinkan localhost
        deny all;
    }
}
# Output contoh dari /nginx_status:
curl http://localhost:8080/nginx_status

Active connections: 291
server accepts handled requests
 16630948 16630948 31070465
Reading: 6 Writing: 179 Waiting: 106
Berikut adalah detail interpretasi dari statistik status di atas:
*   **Active connections**: `291` menunjukkan terdapat 291 koneksi klien yang sedang aktif terhubung ke server saat ini.
*   **accepts**: `16630948` menunjukkan jumlah total koneksi yang telah diterima oleh Nginx semenjak server dinyalakan.
*   **handled**: `16630948` menunjukkan jumlah koneksi yang berhasil ditangani. Nilai yang sama dengan `accepts` berarti tidak ada koneksi yang ditolak (*dropped connections*).
*   **requests**: `31070465` menunjukkan jumlah total request yang diproses. Angka ini lebih tinggi dari jumlah koneksi karena adanya fitur *Keep-Alive* yang melayani beberapa request dalam satu koneksi TCP.
*   **Reading**: `6` menunjukkan jumlah koneksi di mana worker process sedang membaca header permintaan dari klien.
*   **Writing**: `179` menunjukkan jumlah koneksi di mana worker sedang menuliskan/mengirimkan kembali data respons ke klien.
*   **Waiting**: `106` menunjukkan jumlah koneksi keep-alive yang sedang menganggur (*idle*) menunggu request berikutnya. Di Nginx, koneksi waiting ini sangat hemat daya dan tidak membebani memori.

---

## Implikasi Praktis untuk Konfigurasi

Memahami arsitektur event-driven ini membantu kita mengonfigurasi Nginx dengan tepat untuk performa produksi:

```nginx
# nginx.conf

# Jumlah worker = jumlah CPU core
worker_processes auto;

# Afinitas CPU: ikat worker ke core tertentu
# untuk menghindari cache thrashing
worker_cpu_affinity auto;

events {
    # Jumlah koneksi per worker
    # Total koneksi = worker_processes × worker_connections
    worker_connections 1024;

    # Izinkan worker terima multiple connections sekaligus
    # (bukan satu per satu)
    multi_accept on;

    # Mekanisme event (auto-detect, tapi bisa di-override)
    use epoll;
}

http {
    # Aktifkan sendfile untuk zero-copy file serving
    sendfile on;

    # tcp_nopush: buffer data sebelum kirim
    # (optimal bersama sendfile)
    tcp_nopush on;

    # tcp_nodelay: kirim segera tanpa buffer
    # (untuk koneksi keep-alive, setelah tcp_nopush)
    tcp_nodelay on;
}

Jangan menyetel worker_connections terlalu tinggi tanpa mempertimbangkan batasan sistem operasi. Setiap koneksi aktif menggunakan satu file descriptor. Kita harus memastikan limit ulimit -n (maksimum berkas terbuka) di sistem operasi diatur cukup besar:

# Memeriksa limit saat ini
ulimit -n

# Nginx mengatur ini secara otomatis, tetapi kita bisa menyetelnya secara manual di limits.conf:
# /etc/security/limits.conf
nginx   soft    nofile  65536
nginx   hard    nofile  65536

Ringkasan #

  • Event-driven non-blocking I/O adalah inti arsitektur Nginx — satu worker process mengelola ribuan koneksi tanpa memblok.
  • Model Master-Worker: master process mengelola konfigurasi dan worker processes; worker processes menangani request aktual.
  • epoll (Linux) / kqueue (BSD) adalah mekanisme OS yang memungkinkan Nginx efisien memonitor ribuan file descriptor sekaligus.
  • Jumlah worker = jumlah CPU core adalah panduan umum — lebih banyak tidak selalu lebih baik karena context switching.
  • Hemat memori karena hanya beberapa proses worker (bukan satu per koneksi) plus custom pool allocator yang efisien.
  • Hot reload tanpa downtime: master spawn worker baru dengan config baru, worker lama selesaikan koneksi yang ada lalu berhenti.
  • Shared memory digunakan untuk koordinasi antar worker: rate limiting counter, cache metadata, dan statistik.
  • Model ini cocok untuk web server karena web server menghabiskan 95%+ waktunya menunggu I/O, bukan komputasi CPU.

← Sebelumnya: Nginx vs Apache   Berikutnya: Instalasi Ubuntu / Debian →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact