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 kamu 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.

Masalah yang Dipecahkan: Blocking I/O #

Untuk memahami mengapa model event-driven penting, kamu harus terlebih dulu memahami apa yang menjadi masalah.

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:

Solusi blocking model: satu proses per koneksi

Koneksi A datang → Fork Process A → Process A menunggu disk
Koneksi B datang → Fork Process B → Process B menunggu disk
Koneksi C datang → Fork Process C → Process C menunggu disk

OS harus mengelola N proses, masing-masing menggunakan RAM,
dan OS harus terus-menerus berpindah konteks antar proses
(context switching) — ini mahal secara komputasi.

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.

Visualisasi Event Loop Nginx:

Waktu │ Worker Process (1 proses, menangani banyak koneksi)
──────┼────────────────────────────────────────────────────
t=0   │ Terima koneksi A
t=1   │ Mulai baca file untuk A, daftarkan event "file A siap"
t=2   │ Terima koneksi B (A masih menunggu disk!)
t=3   │ Mulai baca file untuk B, daftarkan event "file B siap"
t=4   │ Terima koneksi C
t=5   │ [Event: file B siap] → Kirim respons ke B
t=6   │ [Event: file A siap] → Kirim respons ke A
t=7   │ [Event: koneksi C siap dikirim] → Kirim respons ke C
t=8   │ Semua terselesaikan, tunggu event berikutnya

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 epoll atas mekanisme lama (select/poll):

select/poll (lama):
  - Harus kirim seluruh list file descriptor ke OS setiap kali
  - OS harus cek SEMUA descriptor, bahkan yang tidak aktif
  - Kompleksitas O(n) di mana n = jumlah total koneksi
  - Batas maksimum 1024 fd (select)

epoll (modern):
  - Register sekali, OS menyimpan daftarnya
  - OS hanya return descriptor yang AKTIF
  - Kompleksitas O(1) untuk mendapat events yang siap
  - Tidak ada batas keras jumlah koneksi

kqueue (BSD/macOS) #

kqueue adalah equivalent epoll di sistem BSD (FreeBSD, macOS).
Nginx menggunakan kqueue secara otomatis ketika berjalan di macOS/BSD.

Fungsionalitas sama: monitor banyak event secara efisien.

Deteksi Otomatis Nginx #

Nginx secara otomatis memilih mekanisme terbaik yang tersedia:

# /etc/nginx/nginx.conf
events {
    # Nginx biasanya mendeteksi ini otomatis
    # Kamu bisa set manual jika perlu:
    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:

Proses Nginx saat berjalan:

┌─────────────────────────────────────────────┐
│            Master Process (1 proses)        │
│                                             │
│  - Membaca dan validasi konfigurasi         │
│  - Bind ke port (80, 443)                   │
│  - Spawn dan kelola worker processes        │
│  - Handle signals (reload, stop, upgrade)  │
│  - TIDAK menangani request langsung         │
│  - Berjalan sebagai root (untuk bind port) │
└────────────────────┬────────────────────────┘
                     │ spawn
        ┌────────────┼────────────┐
        ▼            ▼            ▼
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ Worker #1    │ │ Worker #2    │ │ Worker #3    │
│              │ │              │ │              │
│ Menangani    │ │ Menangani    │ │ Menangani    │
│ request HTTP │ │ request HTTP │ │ request HTTP │
│ Event loop   │ │ Event loop   │ │ Event loop   │
│ Non-blocking │ │ Non-blocking │ │ Non-blocking │
│              │ │              │ │              │
│ Berjalan     │ │ Berjalan     │ │ Berjalan     │
│ sebagai user │ │ sebagai user │ │ sebagai user │
│ nginx/www    │ │ nginx/www    │ │ nginx/www    │
└──────────────┘ └──────────────┘ └──────────────┘

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 kamu menjalankan nginx -s reload:

Proses Hot Reload:

1. Master process terima signal HUP
2. Master process baca konfigurasi baru
3. Jika konfigurasi valid:
   a. Master spawn worker-worker BARU dengan config baru
   b. Worker baru mulai terima koneksi
   c. Master kirim signal ke worker LAMA untuk graceful shutdown
   d. Worker lama selesaikan koneksi yang sedang berjalan
   e. Worker lama berhenti setelah semua koneksi selesai
4. Hasil: zero downtime reload!

Jika konfigurasi TIDAK valid:
   - Master process menolak reload
   - Worker lama terus berjalan dengan config lama
   - Tidak ada gangguan layanan

Worker Process: Di Mana Pekerjaan Sesungguhnya Terjadi #

Setiap worker process menjalankan event loop-nya sendiri. Mereka independen satu sama lain:

Jumlah Worker Process yang Disarankan:

worker_processes auto;
# "auto" = jumlah CPU core yang tersedia

Mengapa = jumlah CPU core?
  - Lebih sedikit dari core: beberapa core idle
  - Sama dengan core: setiap worker punya 1 core penuh
  - Lebih banyak dari core: OS harus context switch
    antar worker → overhead tidak perlu

Server 4 core → 4 worker processes
Setiap worker mengelola ribuan koneksi secara async
Total kapasitas: ribuan × 4 = puluhan ribu koneksi

Cache Manager dan Cache Loader #

Selain master dan worker, Nginx juga memiliki proses opsional:

Proses Tambahan (jika caching diaktifkan):

┌─────────────────┐
│  Cache Manager  │ ← Memantau ukuran cache,
│                 │   menghapus entry lama,
│                 │   memastikan cache tidak
│                 │   melebihi batas yang dikonfigurasi
└─────────────────┘

┌─────────────────┐
│  Cache Loader   │ ← Saat startup, memuat metadata
│                 │   cache dari disk ke memori
│                 │   (agar cache "hangat" lebih cepat)
└─────────────────┘

Memory Model: Mengapa Nginx Hemat RAM #

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

1. Sedikit Proses #

Apache (prefork, 1000 koneksi):
  1000 proses × 8-10 MB = 8-10 GB RAM

Nginx (1000 koneksi):
  4 worker processes × ~2-3 MB = ~12 MB RAM
  (memori per koneksi sangat kecil karena state tersimpan
   sebagai struct kecil di heap worker, bukan proses terpisah)

2. Pool Allocator #

Nginx menggunakan custom memory allocator berbasis pool:

Nginx Memory Pool:

Setiap request mendapat "pool" memori sendiri:
  ┌──────────────────┐
  │   Request Pool   │
  │                  │
  │ Header parsing   │
  │ URL storage      │
  │ Response buffer  │
  │ Temp variables   │
  └──────────────────┘
       │
       ▼ Request selesai
  Pool di-dealokasi sekaligus
  (tidak perlu free() satu per satu, tidak ada memory leak)

Keuntungan:
  ✓ Alokasi sangat cepat (hanya increment pointer)
  ✓ Tidak ada memory fragmentation
  ✓ Tidak ada memory leak per-request
  ✓ Deallokasi bulk lebih cepat dari per-item

3. Copy-on-Write untuk Worker Processes #

Ketika master process spawn worker dengan fork(), OS menggunakan copy-on-write:

Master Process: menggunakan 10 MB RAM

Fork() → Worker Process 1
Fork() → Worker Process 2
Fork() → Worker Process 3

Tanpa Copy-on-Write: 4 × 10 MB = 40 MB
Dengan Copy-on-Write: Semua worker berbagi pages memori
yang sama dari master. Hanya page yang DIMODIFIKASI yang
di-copy ke proses individual.

Hasil praktis: 4 worker processes tidak menggunakan 4× memori.

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:

Shared Memory di Nginx:

┌───────────────────────────────────────┐
│         Shared Memory (mmap)          │
│                                       │
│  ┌─────────────┐  ┌─────────────────┐ │
│  │ Rate Limit  │  │  Cache Metadata │ │
│  │ Counters    │  │  (path, size,   │ │
│  │             │  │   expiry time)  │ │
│  │ Worker 1: 5 │  │                 │ │
│  │ Worker 2: 3 │  │  Dibaca semua   │ │
│  │ Worker 3: 7 │  │  worker untuk   │ │
│  │ Worker 4: 2 │  │  cache hit/miss │ │
│  │ Total: 17   │  │                 │ │
│  └─────────────┘  └─────────────────┘ │
└───────────────────────────────────────┘
        ↑                   ↑
   Semua worker         Semua worker
   baca/tulis bersama   baca/tulis bersama

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;
        }
    }
}
Tanpa Thread Pool:
  Worker sedang baca file dari disk yang lambat (HDD, bukan SSD)
  → Worker BLOCK selama operasi disk
  → Semua koneksi lain yang dikelola worker ini IKUT TERHENTI

Dengan Thread Pool:
  Worker mendelegasikan operasi disk ke thread pool
  → Worker kembali ke event loop, tangani koneksi lain
  → Ketika thread pool selesai baca file, ia memberi tahu worker
  → Worker mengirim respons
  → Zero blocking!

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 #

HTTP/1.1 (satu request per koneksi, atau pipeline):
Koneksi 1: GET /page → respons
Koneksi 2: GET /style.css → respons
Koneksi 3: GET /app.js → respons
→ 3 koneksi TCP terpisah

HTTP/2 (multiplexing dalam satu koneksi):
Koneksi 1:
  Stream 1: GET /page → respons
  Stream 3: GET /style.css → respons  (bersamaan!)
  Stream 5: GET /app.js → respons     (bersamaan!)
→ 1 koneksi TCP, multiple streams bersamaan
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, kamu bisa gunakan strace untuk melihat system calls yang dilakukan worker:

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

# Trace system calls (HATI-HATI: overhead tinggi, 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:

Profil waktu sebuah request web (estimasi):

Menerima request headers:    ~0.1ms  (I/O — menunggu jaringan)
Parse headers:               ~0.01ms (CPU)
Baca file dari disk:         ~1-10ms (I/O — menunggu disk)
Kirim ke backend (proxy):    ~5-50ms (I/O — menunggu network+backend)
Kirim respons ke klien:      ~0.5-5ms (I/O — menunggu jaringan klien)

Total CPU time aktual:       ~1-5%
Total I/O wait time:         ~95-99%

Kesimpulan: Web server hampir selalu menunggu I/O.
Model event-driven sangat cocok karena selama menunggu,
worker bebas menangani koneksi lain.

Dibandingkan dengan use case di mana model process-per-request lebih cocok:

Use case di mana model blocking LEBIH sederhana:
  - Pemrosesan data intensif CPU (encoding video, ML inference)
  - Operasi yang benar-benar sequential dan tidak perlu concurrency
  - Script one-off yang tidak perlu scale

Use case di mana event-driven NON-BLOCKING unggul:
  - Web server (banyak I/O, sedikit CPU per request) ← Nginx
  - API gateway
  - Real-time applications (WebSocket, long-polling)
  - Proxy dan load balancer

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.

Tanpa Keep-Alive (HTTP/1.0):
  Request 1: TCP SYN → SYN-ACK → ACK → GET / → respons → FIN
  Request 2: TCP SYN → SYN-ACK → ACK → GET /style.css → respons → FIN
  Request 3: TCP SYN → SYN-ACK → ACK → GET /logo.png → respons → FIN
  → 3 kali TCP handshake, lambat

Dengan Keep-Alive (HTTP/1.1):
  TCP SYN → SYN-ACK → ACK   (hanya SATU kali handshake)
  → GET / → respons
  → GET /style.css → respons
  → GET /logo.png → respons
  → FIN (setelah keepalive_timeout)
  → Jauh lebih cepat

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:

Alur Data dengan Buffering:

Klien (koneksi lambat) ←→ [Nginx Buffer] ←→ Backend (koneksi cepat)

Scenario:
1. Nginx terima request dari klien (mungkin lambat)
2. Nginx buffer request body sepenuhnya
3. Nginx kirim request ke backend sekaligus (cepat)
4. Backend respons langsung selesai
5. Nginx buffer respons backend
6. Nginx kirim respons ke klien (meskipun lambat)

Tanpa buffering:
Backend harus menunggu Nginx selesai kirim ke klien.
Backend "sibuk" menangani satu klien lambat = tidak bisa
layani request lain.

Dengan buffering:
Backend selesai dalam milidetik.
Nginx urus pengiriman lambat ke klien sendiri.
Backend segera tersedia untuk request berikutnya.
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:

Skenario: Worker mengalami crash (sangat jarang, tapi bisa terjadi)

Tanpa multi-worker:
  Nginx crash → Semua request gagal → Downtime

Dengan multi-worker (4 worker):
  Worker #2 crash → Master process deteksi
                  → Master spawn worker #2 baru
                  → Worker #1, #3, #4 tetap melayani request
                  → Recovery dalam < 1 detik
                  → User hampir tidak merasakan gangguan

Ini adalah salah satu alasan Nginx sangat andal di production.

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

# Kamu bisa lihat ini di log Nginx jika terjadi:
# 2024/01/15 10:23:45 [alert] 1234#1234: worker process 5678 exited on signal 11
# 2024/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 #

Tanpa sendfile():
  File di disk
      ↓ read() syscall
  Kernel page cache
      ↓ copy ke user space
  Nginx process memory
      ↓ write() syscall
  Kernel socket buffer
      ↓
  Network card

Total: 2 copy + 2 syscalls

Dengan sendfile():
  File di disk
      ↓ (kernel langsung transfer)
  Kernel socket buffer
      ↓
  Network card

Total: 1 copy + 1 syscall (dan copy ini dilakukan DMA,
bukan CPU!) — jauh lebih efisien
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:

mmap(): Kernel mengelola file caching secara otomatis

File besar (video, ISO, dll.)
    ↓ mmap()
Nginx memetakan file ke address space-nya
    ↓
Kernel mengelola kapan load ke memori (lazy loading)
    ↓
Saat Nginx "baca" bagian file, kernel load dari disk jika belum ada
    ↓
Jika file yang sama diminta lagi, kernel sudah punya di page cache
    ↓
Request berikutnya: langsung dari RAM (very fast)

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
Interpretasi:
Active connections: 291
  → 291 koneksi sedang aktif saat ini

accepts: 16630948
  → Total koneksi yang pernah diterima

handled: 16630948
  → Total koneksi yang berhasil ditangani (sama = tidak ada drop)

requests: 31070465
  → Total request (lebih besar dari connections karena keep-alive)

Reading: 6
  → Worker sedang membaca request headers dari 6 klien

Writing: 179
  → Worker sedang mengirim respons ke 179 klien

Waiting: 106
  → 106 koneksi keep-alive menunggu request berikutnya
     (ini TIDAK menghabiskan banyak sumber daya di Nginx)

Implikasi Praktis untuk Konfigurasi #

Memahami arsitektur ini membantu kamu mengkonfigurasi Nginx dengan benar:

# 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 set worker_connections terlalu tinggi tanpa mempertimbangkan batasan OS. Setiap koneksi menggunakan file descriptor. Pastikan ulimit -n (max open files) cukup besar:

# Cek limit saat ini
ulimit -n

# Nginx mengatur ini otomatis, tapi kamu bisa set manual:
# /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