Konfigurasi seperti http://10.0.1.3:8081 yang menempel di kode klien adalah jebakan di dunia microservices. Pod yang direstart berganti alamat, autoscaler menambah dan melepas replika — alamat service berubah sepanjang hari, sedangkan konfigurasi klien diperbarui sebulan sekali. Monolith tidak menghadapi masalah ini; begitu sistem dipecah, “di mana service X berada sekarang?” menjadi pertanyaan yang dijawab berkali-kali per detik. Service discovery menjawabnya dengan registry — buku telepon hidup yang diisi pendaftaran service yang naik, diperbarui lewat detak jantung, dan dibaca klien sebelum memanggil. Chapter ini membangun registry lengkap dengan pengusiran instance yang diam, lalu membedah dua gaya penemuannya.

Registry dan Detak Jantung #

Dua mekanisme dasar bekerja berpasangan. Self-registration: setiap instansi service, begitu naik, mendaftarkan alamatnya sendiri ke registry. Heartbeat: instansi mengirim sinyal “aku masih hidup” berkala; registry yang tidak menerima sinyal dalam jendela waktu mengeluarkan instansi tersebut — instansi yang mati tanpa pamit harus hilang dari buku telepon sendiri. Program berikut membangun keduanya dalam satuan tick (waktu abstrak) agar siklusnya deterministik:

import java.util.ArrayList;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

/** Service registry dengan heartbeat dan penemuan sisi klien (round-robin). */
public class RegistryLayanan {

    /** Satu instansi terdaftar: alamat + tick terakhir kali terlihat hidup. */
    static class Instansi {
        final String alamat;
        int terakhirHidup;

        Instansi(String alamat, int tick) {
            this.alamat = alamat;
            this.terakhirHidup = tick;
        }
    }

    /** Registry: menerima pendaftaran dan detak jantung, mengeluarkan instansi yang diam. */
    static class Registry {
        private final int batasDiam;
        private final Map<String, Instansi> tabel = new LinkedHashMap<>();
        private int tick = 0;

        Registry(int batasDiam) {
            this.batasDiam = batasDiam;
        }

        void daftar(String alamat) {
            tabel.put(alamat, new Instansi(alamat, tick));
        }

        void detak(String alamat) {
            Instansi i = tabel.get(alamat);
            if (i != null) {
                i.terakhirHidup = tick;
            }
        }

        /** Satu satuan waktu berlalu; keluarkan instansi yang diam melampaui batas. */
        void tick() {
            tick++;
            Iterator<Instansi> it = tabel.values().iterator();
            while (it.hasNext()) {
                Instansi i = it.next();
                if (tick - i.terakhirHidup > batasDiam) {
                    System.out.println("  [tick " + tick + "] " + i.alamat
                            + " dikeluarkan: diam " + (tick - i.terakhirHidup)
                            + " tick, batas " + batasDiam);
                    it.remove();
                }
            }
        }

        /** Daftar alamat instansi yang masih dianggap sehat, urut pendaftaran. */
        List<String> alamatSehat() {
            List<String> hasil = new ArrayList<>();
            for (Instansi i : tabel.values()) {
                hasil.add(i.alamat);
            }
            return hasil;
        }
    }

    public static void main(String[] args) {
        Registry registry = new Registry(2); // boleh diam paling banyak 2 tick

        System.out.println("== Tiga replika svc-pembayaran terdaftar ==");
        registry.daftar("10.0.1.1:8081");
        registry.daftar("10.0.1.2:8081");
        registry.daftar("10.0.1.3:8081");
        System.out.println("  sehat: " + registry.alamatSehat());

        System.out.println();
        System.out.println("== Simulasi: 10.0.1.3 berhenti mengirim detak jantung ==");
        for (int t = 1; t <= 3; t++) {
            registry.detak("10.0.1.1:8081");
            registry.detak("10.0.1.2:8081");
            registry.tick();
            System.out.println("  setelah tick " + t + " -> sehat: " + registry.alamatSehat());
        }

        System.out.println();
        System.out.println("== Penemuan sisi klien: round-robin di antara yang sehat ==");
        List<String> sehat = registry.alamatSehat();
        for (int panggilan = 1; panggilan <= 4; panggilan++) {
            String alamat = sehat.get((panggilan - 1) % sehat.size());
            System.out.println("  panggilan " + panggilan + " -> " + alamat);
        }
    }
}

Output yang Dihasilkan:

text
== Tiga replika svc-pembayaran terdaftar ==
  sehat: [10.0.1.1:8081, 10.0.1.2:8081, 10.0.1.3:8081]

== Simulasi: 10.0.1.3 berhenti mengirim detak jantung ==
  setelah tick 1 -> sehat: [10.0.1.1:8081, 10.0.1.2:8081, 10.0.1.3:8081]
  setelah tick 2 -> sehat: [10.0.1.1:8081, 10.0.1.2:8081, 10.0.1.3:8081]
  [tick 3] 10.0.1.3:8081 dikeluarkan: diam 3 tick, batas 2
  setelah tick 3 -> sehat: [10.0.1.1:8081, 10.0.1.2:8081]

== Penemuan sisi klien: round-robin di antara yang sehat ==
  panggilan 1 -> 10.0.1.1:8081
  panggilan 2 -> 10.0.1.2:8081
  panggilan 3 -> 10.0.1.1:8081
  panggilan 4 -> 10.0.1.2:8081

Mari kita bedah bagian-bagiannya:

  1. class Instansi: satu baris buku telepon — alamat plus terakhirHidup, yaitu tick terakhir saat instansi terlihat. Kelas kecil ini mutabel pada satu field karena justru itulah yang berubah setiap detak jantung tiba.
  2. daftar(...) dan detak(...): self-registration dan heartbeat. detak hanya memperbarui terakhirHidup — sinyalnya murah, sesuai kenyataan bahwa heartbeat produksi dikirim tiap beberapa detik.
  3. tick() dengan Iterator: satu satuan waktu berlalu. Baris tick - i.terakhirHidup > batasDiam menghitung berapa lama instansi diam; 10.0.1.3 yang berhenti berdetak pada tick 0 diusir tepat pada tick 3 — dua tick masa tenggang (grace period) memberi ruang bagi jaringan yang sesekali tersendat tanpa langsung menghukum instansi yang sehat.
  4. alamatSehat() dan loop round-robin: klien bertanya sekali, menerima daftar dua alamat, lalu memutar panggilan 1 → 2 → 1 → 2. Load balancing paling sederhana tercipta tanpa satu pun komponen tambahan — hanya modulo terhadap ukuran daftar.

Penemuan Sisi Klien dan Sisi Server #

Loop round-robin di program kita adalah penemuan sisi klien (client-side discovery): klien membaca registry dan memilih alamat sendiri. Jalur lawannya adalah penemuan sisi server: klien memanggil satu alamat tetap — load balancer atau API gateway — dan komponen itulah yang menengahi.

AspekClient-side discoveryServer-side discovery
Yang membaca registrytiap klienload balancer/gateway
Peruteanlangsung klien → instansiklien → balancer → instansi
Tambahan lompatan jaringantidak adasatu (lewat balancer)
Logika di klienpustaka discovery wajib di semua bahasaklien tetap polos
Kegagalan balancertidak berlakujadi titik yang dijaga khusus

Di ekosistem Java, gaya klien diwujudkan Spring Cloud Netflix Eureka (registry + pustaka klien), sedangkan Consul dari HashiCorp umum dipakai lintas bahasa dengan penemuan sisi server. Di Kubernetes, registry dan load balancer menyatu ke dalam platform — Service object memelihara daftar pod sehat, kube-dns menerjemahkan nama svc-pembayaran menjadi alamat, dan kube-proxy memerutekan — mekanisme yang persis kita simulasikan, hanya dijalankan infrastruktur.

Kesehatan Jenis Kedua #

Heartbeat registry menjawab “apakah prosesnya hidup?” — pertanyaan yang lebih tajam adalah “apakah prosesnya sanggup bekerja?” Sebuah instansi bisa berdetak rajin padahal pool database-nya penuh dan setiap permintaannya menggantung. Untuk itu registry modern dan load balancer menambah health check aktif: komponen penengah mengirim permintaan pemeriksaan (GET /actuator/health pada Spring Boot) dan mengukur jawabannya, bukan sekadar menunggu laporan. Pola health-check API dari microservices.io merangkum gradasinya: UP untuk siap melayani, DOWN untuk dikeluarkan dari rotasi, dan status penengah untuk “hidup tapi menolak pekerjaan baru”.

Health check juga mengatur graceful shutdown yang rapi: instansi yang akan dimatikan pertama-tama melaporkan diri DOWN, menunggu sampai keluar dari rotasi registry, menuntaskan pekerjaan yang sedang menggantung, baru prosesnya ditutup — urutan yang mencegah permintaan hidup-mati di tengah jalan.

Dengan alamat yang bisa ditemukan dan kesehatan yang terpantau, tugas operasional terakhir: saat sesuatu melambat, bagaimana kita tahu di service mana dan karena panggilan apa? Jawabannya observability — log, metrik, dan distributed tracing yang menyatukan jejak lintas proses. Chapter berikutnya membangun ketiganya, lengkap dengan propagasi trace context lewat thread-local.

Referensi #

Berkas Sumber

Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.

RegistryLayanan.java

Unduh berkas
java
import java.util.ArrayList;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

/** Service registry dengan heartbeat dan penemuan sisi klien (round-robin). */
public class RegistryLayanan {

    /** Satu instansi terdaftar: alamat + tick terakhir kali terlihat hidup. */
    static class Instansi {
        final String alamat;
        int terakhirHidup;

        Instansi(String alamat, int tick) {
            this.alamat = alamat;
            this.terakhirHidup = tick;
        }
    }

    /** Registry: menerima pendaftaran dan detak jantung, mengeluarkan instansi yang diam. */
    static class Registry {
        private final int batasDiam;
        private final Map<String, Instansi> tabel = new LinkedHashMap<>();
        private int tick = 0;

        Registry(int batasDiam) {
            this.batasDiam = batasDiam;
        }

        void daftar(String alamat) {
            tabel.put(alamat, new Instansi(alamat, tick));
        }

        void detak(String alamat) {
            Instansi i = tabel.get(alamat);
            if (i != null) {
                i.terakhirHidup = tick;
            }
        }

        /** Satu satuan waktu berlalu; keluarkan instansi yang diam melampaui batas. */
        void tick() {
            tick++;
            Iterator<Instansi> it = tabel.values().iterator();
            while (it.hasNext()) {
                Instansi i = it.next();
                if (tick - i.terakhirHidup > batasDiam) {
                    System.out.println("  [tick " + tick + "] " + i.alamat
                            + " dikeluarkan: diam " + (tick - i.terakhirHidup)
                            + " tick, batas " + batasDiam);
                    it.remove();
                }
            }
        }

        /** Daftar alamat instansi yang masih dianggap sehat, urut pendaftaran. */
        List<String> alamatSehat() {
            List<String> hasil = new ArrayList<>();
            for (Instansi i : tabel.values()) {
                hasil.add(i.alamat);
            }
            return hasil;
        }
    }

    public static void main(String[] args) {
        Registry registry = new Registry(2); // boleh diam paling banyak 2 tick

        System.out.println("== Tiga replika svc-pembayaran terdaftar ==");
        registry.daftar("10.0.1.1:8081");
        registry.daftar("10.0.1.2:8081");
        registry.daftar("10.0.1.3:8081");
        System.out.println("  sehat: " + registry.alamatSehat());

        System.out.println();
        System.out.println("== Simulasi: 10.0.1.3 berhenti mengirim detak jantung ==");
        for (int t = 1; t <= 3; t++) {
            registry.detak("10.0.1.1:8081");
            registry.detak("10.0.1.2:8081");
            registry.tick();
            System.out.println("  setelah tick " + t + " -> sehat: " + registry.alamatSehat());
        }

        System.out.println();
        System.out.println("== Penemuan sisi klien: round-robin di antara yang sehat ==");
        List<String> sehat = registry.alamatSehat();
        for (int panggilan = 1; panggilan <= 4; panggilan++) {
            String alamat = sehat.get((panggilan - 1) % sehat.size());
            System.out.println("  panggilan " + panggilan + " -> " + alamat);
        }
    }
}