Jawaban singkat untuk pertanyaan yang paling sering diajukan pembaca tentang topik ini.
01Bagaimana cara kerja service discovery di microservices?
Setiap instance mendaftarkan alamatnya ke registry, memperbarui pendaftaran dengan heartbeat, dan dihapus saat deregister atau TTL-nya habis. Pemanggil lalu mencari service lewat nama saat dipanggil, bukan memakai IP tetap. Pencariannya dilakukan oleh client sendiri atau oleh router di depan instance.
02Apa beda client-side dan server-side service discovery?
Di client-side discovery, service pemanggil query registry dan memilih instance sendiri, yang menghemat satu network hop tetapi butuh logika discovery di setiap bahasa yang dipakai. Di server-side discovery, client memanggil router atau load balancer yang query registry untuknya, sehingga client sederhana tetapi ada komponen dan hop tambahan.
03Bagaimana service di Docker Compose saling menemukan?
Compose menyambungkan service ke default network dan mendaftarkan setiap nama service ke DNS server internal. Container lalu bisa memanggil http://orders:8080 dan nama itu di-resolve ke IP container saat ini. IP container diberikan secara dinamis, jadi nama adalah bagian yang harus diandalkan.
04Bagaimana service discovery bekerja di Kubernetes?
Service biasa mendapat nama DNS berbentuk my-svc.my-namespace.svc.cluster-domain.example yang me-resolve ke cluster IP Service, dan kubelet mengatur daftar search di setiap Pod sehingga nama pendek bisa dipakai dalam namespace yang sama. CoreDNS adalah implementasi default DNS cluster. Headless Service melewati cluster IP dan me-resolve ke IP semua Pod-nya.
05Apakah saya butuh Consul atau etcd untuk service discovery?
Tidak di satu host, di mana nama service Compose atau reverse proxy sudah cukup. Consul dan etcd layak dipakai ketika Anda memakai beberapa mesin, butuh routing sadar kesehatan yang tidak bisa dilakukan DNS biasa, atau butuh notifikasi perubahan lewat lease dan watch. Sebelum itu, keduanya hanya sistem stateful tambahan yang harus dijalankan.
Bagaimana service discovery bekerja di microservices? Registry, heartbeat, TTL, client-side vs server-side, DNS di Docker Compose dan Kubernetes, Consul.
Service discovery membuat satu microservice menemukan alamat terbaru service lain saat dipanggil, bukan lewat IP yang ditulis permanen. Instance mendaftar ke registry, memperbarui heartbeat ber-TTL, dan hilang saat berhenti atau gagal health check. Pemanggil me-resolve nama lewat DNS seperti di Docker Compose dan Kubernetes, atau lewat registry seperti Consul atau etcd.
Cara tercepat menghubungkan dua service adalah menempelkan IP satu container ke environment variable service lain. Cara ini jalan sampai container restart dan alamatnya tidak lagi sama. Tidak ada yang salah di kode. Alamat yang Anda tulis hanya sudah tidak benar lagi.
Artikel ini menjawab satu pertanyaan: bagaimana service discovery bekerja di microservices? Isinya meliputi alasan alamat statis gagal, cara registry menangani register, heartbeat dan deregister, client-side vs server-side discovery, DNS di Docker Compose dan Kubernetes, serta posisi Consul dan etcd. Output Docker dan output registry TypeScript adalah hasil run nyata yang saya jalankan untuk artikel ini, di Docker 29.8.1 dan Node 26. Selebihnya merujuk ke dokumentasi resmi di bagian akhir.
Mengapa IP statis dan host yang ditulis permanen merusak microservices?
Karena alamat sebuah service bukan properti dari service itu sendiri. Alamat adalah properti dari tempat scheduler menaruhnya kali ini. Dokumentasi networking Docker Compose menyebut IP container diberikan secara dinamis saat container start dan tidak dipertahankan, sedangkan nama service adalah bagian yang stabil. Saya mereproduksi kegagalannya: saya menghentikan container orders, membiarkan container kedua mengambil alamatnya yang baru kosong, lalu menjalankan orders lagi.
$ docker compose up -d orders
before: 172.18.0.2
$ docker compose stop orders
$ docker run -d --network sdemo_default --name squatter busybox sleep 60
squatter took: 172.18.0.2 # something else grabbed the freed address
$ docker compose start orders
after: 172.18.0.3 # same service, new IP
$ docker run --rm --network sdemo_default busybox nslookup orders
Name: orders
Address: 172.18.0.3 # the NAME followed it, a hardcoded IP would not
Satu catatan jujur: force-recreate biasa tanpa container lain di network memberi orders alamat yang sama di run saya, jadi perubahan alamat tidak dijamin terjadi di setiap restart. Perubahan terjadi ketika ada yang mengambil alamat itu di antaranya, misalnya container lain, scale-out, atau deploy. Dengan satu container di satu host, IP yang ditulis permanen bisa aman berbulan-bulan lalu menghabiskan satu sore, yang lebih buruk daripada gagal seketika.
IP yang ditulis permanen gagal secara diam-diam dan terlambat. Service sehat, config tampak benar, tetapi request mengarah ke container lain atau ke ketiadaan. Anggap setiap IP literal di config service sebagai bug yang menunggu restart.
Apa itu service registry, dan bagaimana register, heartbeat dan deregister bekerja?
Service registry adalah tabel berisi nama service ke instance yang masih hidup. Setiap instance mendaftar sendiri saat startup dengan id, host dan port, memperbarui pendaftarannya lewat timer, dan deregister saat shutdown normal. Kalau crash, instance tidak sempat deregister, jadi pendaftaran dibekali TTL dan registry menghapusnya sendiri saat kedaluwarsa. Inilah seluruh mekanisme di balik TTL check Consul dan lease etcd, disederhanakan ke bentuk TypeScript terkecil.
type Instance = { id: string; host: string; port: number; expiresAt: number };
const TTL_MS = 10_000;
export class Registry {
private byService = new Map<string, Map<string, Instance>>();
// register and heartbeat are the same call: upsert, push expiry forward.
register(service: string, id: string, host: string, port: number, now = Date.now()) {
const instances = this.byService.get(service) ?? new Map<string, Instance>();
instances.set(id, { id, host, port, expiresAt: now + TTL_MS });
this.byService.set(service, instances);
}
// Graceful shutdown path: remove immediately instead of waiting out the TTL.
deregister(service: string, id: string) {
this.byService.get(service)?.delete(id);
}
// Lazy expiry: an entry is dead the moment now passes expiresAt,
// whether or not a sweeper has deleted it yet.
lookup(service: string, now = Date.now()): Instance[] {
const instances = this.byService.get(service);
if (!instances) return [];
for (const [id, inst] of instances) {
if (inst.expiresAt <= now) instances.delete(id);
}
return [...instances.values()];
}
}
Saya menjalankannya dengan fake clock supaya timeline-nya tepat. Kedua instance register di t=0 dengan TTL 10 detik. Hanya orders-a yang mengirim heartbeat, pada detik ke-6.
// fake clock, so the run is instant: orders-a and orders-b register at t=0
// orders-a heartbeats at t=6s, orders-b goes silent
t=6s [ 'orders-a', 'orders-b' ]
t=10s [ 'orders-a' ] // orders-b: 0 + 10 s TTL, gone exactly now
t=16s [] // orders-a: 6 s + 10 s TTL
Hitungannya adalah intinya. orders-b terakhir terlihat di 0, jadi kedaluwarsa pada 0 ditambah 10 sama dengan 10 detik. orders-a memperbarui di 6, jadi hidup sampai 6 ditambah 10 sama dengan 16 detik. Perhatikan bahwa register dan heartbeat adalah satu panggilan: upsert yang mendorong expiry maju. Begitu pula instance yang pulih bergabung kembali tanpa jalur khusus.
Kirim heartbeat sekitar sepertiga TTL. Dengan TTL 10 detik, itu setiap 3 detik, sehingga beat jatuh di detik 3, 6 dan 9 sebelum expiry dan satu gangguan jaringan, bahkan dua, tidak menendang instance yang sehat. Rasio ini aturan praktis saya sendiri, bukan angka dari dokumentasi vendor mana pun.
Client-side atau server-side discovery: mana yang sebaiknya dipakai?
Kedua pola dibedakan oleh siapa yang query registry. Di client-side discovery, client bertanya ke registry untuk daftar instance lalu memilih sendiri. Di server-side discovery, client memanggil router atau load balancer yang query registry dan meneruskan request. Halaman pola microservices.io mendaftar trade-off-nya, dan tabel berikut memetakannya.
Aspek
Client-side discovery
Server-side discovery
Siapa yang query registry
Service pemanggil, di kodenya sendiri
Router atau load balancer di depan instance
Jumlah network hop
Lebih sedikit, client terhubung langsung
Lebih banyak, setiap request lewat router
Kode client
Butuh logika discovery per bahasa dan framework
Lebih sederhana, client cukup memanggil satu alamat
Komponen tambahan yang dijalankan
Tidak ada selain registry itu sendiri
Router harus dipasang, dikonfigurasi dan dirawat
Contoh nyata
Aplikasi yang memanggil API Consul atau etcd langsung
AWS Elastic Load Balancer, atau proxy Kubernetes di setiap host
Untuk skala kecil saya condong ke server-side: logikanya ada di satu tempat dan setiap service tidak perlu tahu soal jaringan. Komponennya sudah ada di kebanyakan stack, seperti reverse proxy atau cluster itu sendiri. Saya sudah menulis terpisah tentang API gateway pattern di NestJS dan konfigurasi load balancer production, yang merupakan router server-side yang biasa dipakai. Service mesh memindahkan logika client-side ke sidecar sehingga manfaatnya didapat tanpa kode per bahasa, dan itu perbandingan tersendiri.
Bagaimana DNS-based discovery bekerja di Docker Compose dan Kubernetes?
DNS adalah discovery dengan hambatan paling kecil, karena setiap runtime sudah bisa me-resolve nama. Dokumentasi Compose menyebut setiap service mendaftarkan namanya ke DNS server internal di network aplikasi, sehingga container bisa mencari web atau db dan mendapat IP container yang tepat. Berikut file dua service di mana pos memanggil orders lewat nama. Tidak ada port yang dipublikasikan, dan tidak ada IP di mana pun.
services:
orders:
image: busybox:latest
# One static file on 8080. No published ports: only the Compose network reaches it.
command:
- sh
- -c
- mkdir -p /www && echo 'orders says wash 8841 is done' > /www/index.html && httpd -f -p 8080 -h /www
pos:
image: busybox:latest
depends_on: [orders]
# Resolve and call the other service by NAME, never by IP.
command:
- sh
- -c
- sleep 2; nslookup orders; wget -qO- http://orders:8080/
Output di bawah ditempel dari docker compose up yang benar-benar dijalankan. Resolver-nya 127.0.0.11, DNS tertanam milik Docker, dan nama orders menjadi 172.18.0.2 di network project.
Satu catatan dari dokumentasi bridge Docker: resolusi nama otomatis bekerja di user-defined network, yang dibuatkan Compose untuk Anda, tetapi di default bridge network container tidak bisa saling me-resolve nama. Kubernetes melakukan pekerjaan yang sama dalam skala cluster. Service biasa mendapat record A atau AAAA berbentuk my-svc.my-namespace.svc.cluster-domain.example yang me-resolve ke cluster IP Service, dan kubelet menulis daftar search ke setiap Pod sehingga nama pendek bisa dipakai.
# /etc/resolv.conf inside a Pod (the example from the Kubernetes DNS page)
nameserver 10.32.0.10
search <namespace>.svc.cluster.local svc.cluster.local cluster.local
options ndots:5
# So a Pod in namespace "test" can call:
# http://orders (same namespace, short name)
# http://orders.prod (other namespace)
# http://orders.prod.svc.cluster.local (fully qualified)
CoreDNS adalah implementasi default untuk DNS cluster Kubernetes, dan plugin kubernetes-nya memantau API cluster untuk endpoint. Jawabannya membawa TTL yang defaultnya 5 detik, maksimum 3600, dan nilai 0 berarti tanpa caching. Headless Service, yaitu yang tanpa cluster IP, me-resolve ke IP semua Pod-nya, bukan satu alamat virtual, dan client diharapkan memilih sendiri dari kumpulan itu.
Kapan saya butuh Consul atau etcd, bukan DNS biasa?
DNS biasa menjawab di mana sebuah service berada, tetapi tidak banyak bicara soal apakah instance sehat saat ini, dan tidak memberi tahu ketika kumpulannya berubah. etcd menyediakan primitif untuk membangunnya. Lease-nya punya TTL, key yang terpasang pada lease dihapus saat lease kedaluwarsa atau dicabut, dan client harus mengirim keepalive untuk mempertahankan lease. Setiap key yang kedaluwarsa menghasilkan event delete, dan Watch API menyiarkan event itu, sehingga client bisa bereaksi terhadap perubahan, bukan polling.
Consul membungkus ide yang sama sebagai produk. Service catalog-nya menjadi sumber kebenaran tentang lokasi service, dan health check mengeluarkan instance yang tidak sehat dari catalog sehingga tidak lagi menerima request. Menurut dokumentasi Consul, TTL check menunggu proses eksternal melapor ke endpoint agent check, dan kalau tidak ada update sebelum durasi ttl, service ditandai critical. Definisinya singkat.
service {
name = "orders"
id = "orders-a"
port = 3000
check {
id = "orders-a-ttl"
name = "heartbeat"
ttl = "10s" # no update within 10 s and the check turns critical
}
}
Pendapat saya: jangan pasang Consul atau etcd di satu host. Keduanya layak ketika Anda menjalankan beberapa mesin, butuh routing yang sadar kesehatan yang tidak bisa diungkapkan DNS, atau butuh notifikasi perubahan. Sebelum itu, keduanya hanya satu sistem stateful lagi yang harus dijaga tetap hidup, dan registry yang mati membawa discovery ikut mati.
Apa yang bisa salah dengan health check, entri basi dan caching?
Registry hanya sesegar lapisan paling lambatnya. Empat bentuk kegagalan mencakup sebagian besar insiden.
Entri basi: instance yang crash tetap terdaftar sampai TTL habis, dan pemanggil terus mengirim traffic ke sana selama itu.
Health check dangkal: proses menjawab tetapi koneksi database-nya hilang, sehingga instance melapor sehat dan gagal di setiap request nyata.
Caching di sisi client: pemanggil memakai alamat hasil resolve selama seluruh umur cache, padahal registry sudah menghapusnya.
Flapping: TTL yang terlalu ketat terhadap interval heartbeat mengeluarkan instance sehat saat jeda singkat, lalu menambahkannya lagi, dan itu mengguncang setiap client.
Penundaan ini bertumpuk, jadi jumlahkan. Kalau TTL registry 10 detik dan client menyimpan alamat hasil resolve selama 5 detik, sesuai default CoreDNS, instance yang crash masih bisa menerima traffic selama 10 ditambah 5 sama dengan 15 detik di kasus terburuk. Memperpendek TTL mempersempit jendela itu, tetapi TTL lebih ketat butuh heartbeat lebih cepat, yang berarti beban registry lebih besar.
registry TTL = 10 s (entry survives 10 s after the last heartbeat)
client-side cache lifetime = 5 s (the CoreDNS kubernetes plugin default TTL)
worst case for traffic to a crashed instance
= TTL + cache = 10 + 5 = 15 s
heartbeat every TTL / 3 = 10 / 3 = 3.33 s -> use 3 s
beats at 3, 6, 9 s before the 10 s expiry -> two lost beats are survivable
Jangan menambal entri basi dengan retry tanpa batas. Retry ke alamat mati menghabiskan anggaran timeout pemanggil setiap kali. Padukan discovery dengan connect timeout pendek, dan retry ke instance yang berbeda, bukan yang sama.
Apa yang sebaiknya dipilih untuk satu VPS?
Saya menjalankan Docker di satu VPS, dan itu membentuk saran ini. Ikuti checklist berikut berurutan dan berhenti di langkah pertama yang sudah mencukupi.
Taruh setiap service di satu user-defined network, yang dibuat Compose secara default, dan panggil lewat nama service. Jangan publikasikan port yang tidak perlu.
Pastikan tidak ada IP literal di environment variable atau file config. Cari sebelum deploy.
Tambahkan health check sungguhan yang menguji dependency, lalu biarkan orchestrator atau reverse proxy berhenti mengarahkan traffic ke instance yang gagal.
Pindah ke Kubernetes Services dan CoreDNS hanya saat satu host tidak lagi cukup, dan pertahankan kebiasaan memanggil lewat nama.
Tambahkan Consul atau etcd hanya ketika Anda bisa menyebut kebutuhan yang tidak bisa dipenuhi DNS, seperti routing yang sadar kesehatan lintas mesin atau notifikasi perubahan.
Alamat sebuah service adalah data yang berubah, jadi cari saat dipanggil dan panggil lewat nama. Semua hal lain di artikel ini adalah keputusan tentang seberapa segar pencarian itu dan siapa yang melakukannya. Di satu host, nama service Compose sudah cukup. Lebih dari itu, registry, TTL-nya, dan setiap cache di depannya menjumlah menjadi berapa lama Anda mengarahkan traffic ke instance mati, jadi jumlahkan dulu sebelum menyetel apa pun.