Tampilan
Kenapa penting
Di bab sebelumnya gateway meneruskan request ke http://order-service:8080 — alamat yang ditulis tetap. Di production, alamat seperti itu cepat basi: instance bertambah saat traffic naik, berkurang saat sepi, pindah host saat ada deploy atau node yang mati, dan setiap container baru biasanya mendapat IP baru. Jika daftar alamat disimpan di konfigurasi, setiap perubahan butuh edit dan restart — dan selama itu request diarahkan ke instance yang sudah tidak ada. Service discovery menjawab pertanyaan "di mana instance sehat dari service X sekarang?", sedangkan load balancing memutuskan instance mana yang menerima request berikutnya. Kamu akan menemukan keduanya di setiap sistem microservices — kadang lewat Eureka, lebih sering lagi lewat Kubernetes.
Konsep inti
Registry: buku alamat yang hidup
Inti discovery adalah service registry — tabel nama service → daftar instance sehat. Siklus hidup instance:
- Register — saat start, instance mendaftarkan nama, host, port, dan metadata.
- Heartbeat / health check — instance terus membuktikan dirinya hidup (atau registry memeriksanya).
- Lookup — pemanggil menanyakan daftar instance untuk sebuah nama.
- Deregister — saat shutdown normal instance mencabut pendaftarannya; bila mati mendadak, registry menghapusnya setelah heartbeat berhenti melewati batas waktu.
Client-side vs server-side discovery
| Client-side | Server-side | |
|---|---|---|
| Siapa memilih instance | client (library di aplikasi) | komponen di tengah (load balancer, proxy) |
| Client tahu daftar instance | ya | tidak, hanya satu alamat stabil |
| Contoh | Eureka + Spring Cloud LoadBalancer | Kubernetes Service (kube-proxy), AWS ALB |
| Kelebihan | strategi balancing fleksibel, tanpa hop tambahan | client sederhana, lintas bahasa |
| Kekurangan | tiap client butuh library dan konfigurasi | butuh infrastruktur yang dikelola |
Agar alur client-side terasa konkret, berikut simulasi Java murni: registry berbentuk Map, dan balancer round-robin memilih instance dengan counter % jumlahInstance.
java
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
public class RoundRobinDemo {
record ServiceInstance(String id, String host, int port) {
String url() {
return "http://" + host + ":" + port;
}
}
static final class SimpleRegistry {
private final Map<String, List<ServiceInstance>> instances = new ConcurrentHashMap<>();
void register(String serviceId, List<ServiceInstance> list) {
instances.put(serviceId, List.copyOf(list));
}
List<ServiceInstance> lookup(String serviceId) {
return instances.getOrDefault(serviceId, List.of());
}
}
static final class RoundRobinBalancer {
private final AtomicInteger position = new AtomicInteger();
ServiceInstance choose(List<ServiceInstance> candidates) {
if (candidates.isEmpty()) {
throw new IllegalStateException("No instances available");
}
int index = Math.floorMod(position.getAndIncrement(), candidates.size());
return candidates.get(index);
}
}
public static void main(String[] args) {
SimpleRegistry registry = new SimpleRegistry();
RoundRobinBalancer balancer = new RoundRobinBalancer();
registry.register("inventory-service", List.of(
new ServiceInstance("inv-1", "10.0.1.11", 8080),
new ServiceInstance("inv-2", "10.0.1.12", 8080),
new ServiceInstance("inv-3", "10.0.1.13", 8080)));
for (int i = 1; i <= 4; i++) {
System.out.println("call " + i + " -> " + balancer.choose(registry.lookup("inventory-service")).url());
}
// inv-2 gagal health check dan dikeluarkan dari registry
registry.register("inventory-service", List.of(
new ServiceInstance("inv-1", "10.0.1.11", 8080),
new ServiceInstance("inv-3", "10.0.1.13", 8080)));
for (int i = 5; i <= 7; i++) {
System.out.println("call " + i + " -> " + balancer.choose(registry.lookup("inventory-service")).url());
}
try {
balancer.choose(registry.lookup("shipping-service"));
} catch (IllegalStateException e) {
System.out.println("shipping-service: " + e.getMessage());
}
}
}text
call 1 -> http://10.0.1.11:8080
call 2 -> http://10.0.1.12:8080
call 3 -> http://10.0.1.13:8080
call 4 -> http://10.0.1.11:8080
call 5 -> http://10.0.1.11:8080
call 6 -> http://10.0.1.13:8080
call 7 -> http://10.0.1.11:8080
shipping-service: No instances availableMath.floorMod menjaga indeks tetap valid walau counter suatu saat overflow menjadi negatif. Begitu inv-2 dikeluarkan, panggilan berikutnya otomatis hanya mengenai instance yang tersisa — tanpa ada konfigurasi yang diedit. Spring Cloud LoadBalancer bekerja dengan ide yang sama, hanya daftar instance-nya diambil dari registry sungguhan.
Eureka server
Eureka (dari Spring Cloud Netflix) adalah registry yang berjalan sebagai aplikasi Spring Boot biasa.
xml
<!-- Potongan pom.xml discovery-server; versi dari BOM spring-cloud-dependencies 2025.1.x -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>java
// Potongan: aplikasi registry
@SpringBootApplication
@EnableEurekaServer
public class DiscoveryServerApplication {
public static void main(String[] args) {
SpringApplication.run(DiscoveryServerApplication.class, args);
}
}yaml
# Potongan application.yml discovery-server (mode standalone untuk lokal)
server:
port: 8761
eureka:
client:
register-with-eureka: false
fetch-registry: falseDi production, Eureka dijalankan lebih dari satu replika yang saling mereplikasi agar registry tidak menjadi single point of failure.
Eureka client
Setiap service menambahkan spring-cloud-starter-netflix-eureka-client, memberi nama lewat spring.application.name, dan menunjuk alamat registry:
yaml
# Potongan application.yml inventory-service
spring:
application:
name: inventory-service
eureka:
client:
service-url:
defaultZone: http://discovery-server:8761/eureka/
instance:
prefer-ip-address: trueSecara default client mengirim heartbeat tiap 30 detik, dan registry menghapus instance yang tidak memperbarui lease selama 90 detik. Client juga menyimpan cache daftar instance dan memperbaruinya berkala, jadi informasi di sisi pemanggil selalu sedikit terlambat. Konsekuensinya: discovery mengurangi request ke instance mati, tetapi tidak menghilangkannya — timeout dan retry (dibahas di bab resilience terdistribusi) tetap wajib.
Self-preservation
Bila Eureka tiba-tiba kehilangan banyak heartbeat sekaligus, ia menganggap masalahnya ada di jaringan dan berhenti menghapus instance untuk sementara. Ini mencegah registry kosong saat jaringan bermasalah, tetapi berarti instance yang benar-benar mati bisa tetap tercantum lebih lama.
Spring Cloud LoadBalancer dan @LoadBalanced
Starter Eureka client sudah membawa Spring Cloud LoadBalancer. Untuk memanggil service berdasarkan nama, definisikan RestClient.Builder dengan anotasi @LoadBalanced:
java
// Potongan konfigurasi order-service
@Configuration
class HttpClientConfig {
@Bean
@LoadBalanced
RestClient.Builder loadBalancedRestClientBuilder() {
return RestClient.builder();
}
}URL lalu memakai nama service sebagai host: http://inventory-service/stock/SKU-1. Interceptor LoadBalancer mengganti host itu dengan alamat instance pilihan (default round-robin). Di gateway, cukup ganti uri route menjadi lb://inventory-service.
Discovery di Kubernetes
Kubernetes punya discovery bawaan bersifat server-side. Objek Service memberi nama dan IP virtual yang stabil; daftar pod di belakangnya diperbarui otomatis berdasarkan label dan readiness probe; DNS cluster membuat nama inventory-service (lengkapnya inventory-service.<namespace>.svc.cluster.local) dapat di-resolve dari pod lain.
yaml
# Potongan manifest Kubernetes
apiVersion: v1
kind: Service
metadata:
name: inventory-service
spec:
selector:
app: inventory-service
ports:
- port: 8080
targetPort: 8080Aplikasi cukup memanggil http://inventory-service:8080 dengan RestClient biasa — tanpa Eureka, tanpa @LoadBalanced. Pod yang gagal readiness probe dikeluarkan dari endpoint Service sehingga tidak menerima traffic.
Kapan memakai yang mana
| Situasi | Pilihan |
|---|---|
| Semua service berjalan di Kubernetes | Service + DNS bawaan |
| VM/bare metal atau Docker tanpa orkestrator | Eureka + Spring Cloud LoadBalancer |
| Campuran Kubernetes dan VM selama migrasi | Eureka sementara, atau registry netral lain; rencanakan pindah ke satu sistem |
| Butuh strategi balancing khusus per client (zona, weighted) | client-side (LoadBalancer) atau service mesh |
| Pembelajaran lokal dengan Docker Compose | nama service Compose sebagai DNS sudah cukup; Eureka untuk memahami konsepnya |
Contoh di dunia backend
Saat checkout, order-service perlu memeriksa stok ke inventory-service yang berjalan tiga instance dan terdaftar di Eureka.
java
// Potongan order-service (Spring Boot 4.1, Spring Cloud 2025.1)
public record StockResponse(String sku, int available) {}
@Service
class InventoryClient {
private final RestClient restClient;
InventoryClient(@LoadBalanced RestClient.Builder builder) {
this.restClient = builder
.baseUrl("http://inventory-service")
.build();
}
boolean isAvailable(String sku, int quantity) {
StockResponse stock = restClient.get()
.uri("/stock/{sku}", sku)
.retrieve()
.body(StockResponse.class);
return stock != null && stock.available() >= quantity;
}
}yaml
# Potongan application.yml order-service
spring:
application:
name: order-service
lifecycle:
timeout-per-shutdown-phase: 20s
server:
shutdown: graceful
eureka:
client:
service-url:
defaultZone: http://discovery-server:8761/eureka/Graceful shutdown memberi kesempatan request yang sedang berjalan selesai dan instance mencabut pendaftarannya dari Eureka sebelum proses berhenti. Di gateway, route checkout cukup diubah menjadi uri: lb://order-service — gateway pun ikut memakai registry, dan menambah instance order-service tidak memerlukan perubahan konfigurasi apa pun.
Pertanyaan interview
1. Apa beda client-side dan server-side service discovery?
Pada client-side discovery, client menanyakan daftar instance ke registry lalu memilih sendiri instance tujuan, seperti Eureka bersama Spring Cloud LoadBalancer. Pada server-side discovery, client memanggil satu alamat stabil dan komponen di tengah — load balancer atau kube-proxy di Kubernetes — yang memilih instance. Client-side lebih fleksibel tanpa hop tambahan tetapi setiap client butuh library; server-side membuat client sederhana dan lintas bahasa, dengan biaya infrastruktur yang harus dikelola.
2. Kenapa request kadang masih dikirim ke instance yang sudah mati walau memakai Eureka?
Bila instance mati mendadak, ia tidak sempat deregistrasi. Eureka baru menghapusnya setelah lease kedaluwarsa (default 90 detik tanpa heartbeat), self-preservation bisa menundanya lebih lama, dan client juga memakai cache registry yang diperbarui berkala. Karena itu discovery harus dipadukan dengan timeout, retry ke instance lain, dan graceful shutdown agar deregistrasi terjadi saat deploy normal.
3. Apakah masih perlu Eureka bila aplikasi berjalan di Kubernetes?
Umumnya tidak. Kubernetes Service dan DNS cluster sudah menyediakan nama stabil, daftar endpoint yang mengikuti readiness probe, dan load balancing. Eureka masih masuk akal untuk lingkungan tanpa orkestrator atau masa transisi saat sebagian service masih di VM.
Jebakan umum
- Memakai
RestClient.Buildertanpa@LoadBalancedlalu memanggilhttp://nama-service— hasilnyaUnknownHostExceptionkarena nama itu dikirim ke DNS biasa. - Memakai builder
@LoadBalanceduntuk memanggil API eksternal (mis. payment gateway pihak ketiga); hostname-nya dicari di registry dan gagal. Siapkan builder terpisah untuk panggilan keluar. spring.application.nameberbeda antar-replika atau berubah tanpa koordinasi — pemanggil mencari nama lama dan tidak menemukan instance.- Menganggap discovery menjamin instance sehat; tanpa timeout dan retry, cache yang basi tetap mengirim request ke instance mati.
- Menjalankan Eureka dan mengandalkan
ServiceKubernetes sekaligus tanpa alasan — dua sumber kebenaran untuk alamat yang bisa saling bertentangan. - Readiness probe yang selalu
UPdi Kubernetes, sehingga pod yang belum siap langsung menerima traffic.
Ringkasan
- Alamat instance berubah terus; registry menyimpan
nama service → instance sehatlewat register, heartbeat, lookup, dan deregister. - Client-side discovery: client memilih instance (Eureka + Spring Cloud LoadBalancer); server-side: komponen di tengah yang memilih (Kubernetes
Service). - Eureka server memakai
@EnableEurekaServer; client cukupspring.application.namedandefaultZone. @LoadBalanced RestClient.Buildermemungkinkan URLhttp://nama-service/...; di gateway pakailb://nama-service.- Discovery selalu sedikit terlambat — padukan dengan graceful shutdown, timeout, dan retry.
- Di Kubernetes,
Service+ DNS biasanya sudah cukup; Eureka untuk lingkungan tanpa orkestrator.