Microservices Architecture — DevOps

Monolith vs Microservices Monolith ┌────────────────────────┐ │ Monolith App │ │ ┌──────┐ ┌──────┐ │ │ │ Auth │ │ Order│ │ │ └──────┘ └──────┘ │ │ ┌──────┐…

Microservices adalah cara membangun aplikasi sebagai beberapa service kecil yang berdiri sendiri: tiap service punya kode, proses, dan datanya sendiri, di-deploy sendiri, dan bicara dengan service lain lewat jaringan. Lawannya monolith: satu aplikasi, satu proses, satu deploy. Bayangin bedanya warung dan food court. Di warung, satu dapur memasak semua pesananmu, satu kasir, satu stok. Di food court, nasi, sate, dan es teh datang dari tiga stan berbeda, masing-masing dengan dapur dan stoknya sendiri, dan pelayan mengantarkan pesananmu ke stan yang tepat. Pesananmu baru lengkap saat stan paling lambat selesai, dan kalau stan sate tutup, sebagian pesananmu gagal. Dua sifat itu yang paling sering dilupakan orang saat memecah aplikasi.

🧪 Tebakan cepat: catatan-api dipecah jadi empat service. Halaman satu catatan memanggil auth (15 ms), catatan (30 ms), lampiran (40 ms), dan profil (20 ms) berurutan, dan tiap panggilan menambah 2 ms di jaringan. Suatu hari lampiran-svc melambat jadi 1.200 ms. Berapa lama halaman catatan sekarang? Tebak dulu, jawabannya ada di bagian "Satu service lambat, seluruh request lambat".

Monolith: catatan-api hari ini

catatan-api adalah monolith. Satu proses Express di web-1 berisi semua modul: login, catatan, lampiran, ekspor PDF. Semuanya memakai satu database Postgres catatan. Halaman satu catatan dikerjakan di dalam proses yang sama: tiga function dipanggil, satu query JOIN ke Postgres, selesai sekitar 12 ms.

Kalau dipecah: satu request jadi banyak panggilan

Versi microservices catatan-api punya API gateway di depan, pintu masuk tunggal yang meneruskan request ke service yang tepat, dan empat service di belakangnya. Halaman GET /catatan/12 sekarang butuh empat panggilan jaringan:

Urutan Panggilan Latency + jaringan Selesai di
1 auth-svc cek token 15 ms 2 ms 17 ms
2 catatan-svc ambil catatan 12 30 ms 2 ms 49 ms
3 lampiran-svc daftar lampiran 40 ms 2 ms 91 ms
4 profil-svc profil penulis 20 ms 2 ms 113 ms

113 ms, padahal di monolith 12 ms. Tiap panggilan juga bisa gagal sendiri: timeout, service sedang deploy, atau jaringan putus.

Satu service lambat, seluruh request lambat

Dengan panggilan berurutan, total waktu adalah jumlah semua panggilan. Kalau lampiran-svc melambat jadi 1.200 ms, halamannya jadi 17 + 32 + 1.202 + 22 = 1.273 ms. Itu jawaban tebakannya: satu service lambat menyeret seluruh request.

Setelah auth, tiga panggilan lain gak saling menunggu, jadi bisa dijalankan paralel dengan Promise.all. Totalnya jadi auth ditambah panggilan paling lambat:

lampiran-svc Berurutan Paralel
40 ms (normal) 113 ms 17 + 42 = 59 ms
1.200 ms (lambat) 1.273 ms 17 + 1.202 = 1.219 ms

Paralel membantu saat semuanya normal, tapi gak menyelamatkan saat satu service lambat: request tetap menunggu yang paling lambat. Makin banyak service yang disentuh satu request, makin sering ada yang lambat. Kalau tiap service lambat 1% dari waktu, request yang menyentuh 4 service kena lambat 1 - 0,99⁴ ≈ 3,9% dari waktu.

Timeout, fallback, dan retry

Obatnya: jangan menunggu selamanya. Beri tiap panggilan timeout, batas waktu menunggu, dan siapkan fallback, jawaban cadangan kalau batasnya lewat. Ini inti kode di Lab (service-nya palsu, latency-nya sama dengan tabel):

const LATENCY = { auth: 15, catatan: 30, lampiran: 1200, profil: 20 };
const HOP = 2;

function panggil(service) {
  // Di production: fetch ke http://lampiran-svc/... dengan AbortSignal.timeout
  return new Promise((resolve) => setTimeout(() => resolve({ service }), LATENCY[service] + HOP));
}

function denganTimeout(service, ms, cadangan) {
  const batas = new Promise((resolve) => setTimeout(() => resolve(cadangan), ms));
  return Promise.race([panggil(service), batas]);
}

async function halamanCatatan() {
  const user = await panggil("auth");
  const [catatan, lampiran, profil] = await Promise.all([
    panggil("catatan"),
    denganTimeout("lampiran", 300, { fallback: [] }),
    panggil("profil"),
  ]);
  return { user, catatan, lampiran, profil };
}

Retry (mencoba lagi) terdengar aman, tapi hati-hati. Dengan 2 retry, tiap halaman menunggu 17 + 3 × 300 = 917 ms, dan lampiran-svc yang sudah kewalahan menerima 3 kali lipat request. Retry hanya untuk request yang aman diulang (GET, atau yang idempotent), dengan jumlah terbatas dan jeda yang makin panjang (backoff).

Data milik masing-masing service

Di microservices, tiap service memiliki datanya sendiri. Tabel lampiran cuma boleh dibaca dan ditulis lampiran-svc. catatan-svc yang butuh lampiran harus bertanya lewat API, gak boleh membaca database lampiran langsung.

Cara service berkomunikasi

API gateway: satu pintu masuk

Gateway menyimpan tabel prefix path dan alamat service. Untuk tiap request, dia memilih prefix terpanjang yang cocok, lalu meneruskan sisa path-nya:

Request Prefix yang cocok Diteruskan ke
/api/orders/123 /api/orders (bukan /api, karena lebih panjang) http://orders:3002 dengan path /123
/auth/login /auth http://auth:3001 dengan path /login
/unknown gak ada error no route
/authors gak ada: /auth cuma cocok sampai batas segmen error no route

Prefix dicocokkan per segmen path: /auth menangkap /auth dan /auth/login, tapi bukan /authors, jadi cek path === prefix atau path.startsWith(prefix + "/"), bukan startsWith(prefix) saja. Itu persis tugas latihan routeRequest(gateway, path): kembalikan { service, path } dari prefix terpanjang, atau { error: "no route" }.

Kapan memecah, kapan jangan

Hampir semua sistem microservices yang sukses dimulai sebagai monolith (monolith first). Batas antar service baru kelihatan setelah aplikasinya dipakai, dan memindahkan batas yang salah jauh lebih mahal di microservices. Pecah kalau ada alasan nyata: tim sudah banyak dan saling menunggu untuk deploy, satu bagian butuh scaling yang sangat beda, atau domain-nya sudah jelas terpisah.

Untuk catatan-api dengan tim kecil dan satu server, keputusannya: jangan pecah. Rapikan monolith-nya per modul, dan kalau ekspor PDF mulai berat, pindahkan ke background worker lewat antrean. Pembahasan desainnya lebih dalam ada di track System Design, pelajaran Microservices vs Monolith.

⚠️ Jebakan yang sering ketemu

TL;DR: Microservices memecah aplikasi jadi service kecil yang independen, masing-masing dengan kode, proses, dan datanya sendiri, yang berkomunikasi lewat REST, gRPC, atau message queue. Harganya: satu request jadi banyak panggilan jaringan, dan request selalu menunggu service paling lambat. Di Lab, lampiran-svc 1.200 ms membuat halaman 1.273 ms berurutan, 1.219 ms paralel, dan 317 ms dengan timeout 300 ms plus fallback. Retry melipatgandakan beban, jadi batasi dan beri backoff. Tiap service memiliki datanya; berbagi database = distributed monolith. API gateway memilih prefix terpanjang dan meneruskan sisa path. Mulai dari monolith, pecah hanya saat tim, scaling, atau domain benar-benar menuntutnya; catatan-api belum perlu.

Yang akan kamu pelajari