#golang#microservices#service-discovery#consul#backend

Service Discovery dan Health Checking pada Go Microservices

"Panduan implementasi Service Discovery dan Health Checking mandiri di Go microservices menggunakan Consul dan standard library."

By huud
0 views
~5 min read
Service Discovery dan Health Checking pada Go Microservices

Service Discovery dan Health Checking pada Go Microservices

Dalam arsitektur monolith, komunikasi antar modul terjadi di dalam satu memory space. Pemanggilan fungsi bersifat in-process, instan, dan deterministik. Namun saat sistem dipecah menjadi microservices, komunikasi bergeser ke jaringan (network boundaries). Di lingkungan modern seperti Kubernetes, Nomad, atau container cluster, alamat IP dan port service bersifat efemeral: container dapat di-restart, di-reschedule, atau di-scale out kapan saja.

Kondisi ini menuntut adanya mekanisme otomatis untuk melacak di mana suatu service berjalan (Service Discovery) dan memastikan request hanya dikirim ke instance yang sehat (Health Checking). Artikel ini membahas implementasi praktis Service Discovery dan Health Checking di Go menggunakan library native dan integrasi Consul.


1. Konsep Dasar Service Discovery: Client-Side vs Server-Side

Terdapat dua pola utama dalam Service Discovery:

Server-Side Discovery

Client mengirim request ke load balancer (seperti NGINX, AWS ALB, atau Kubernetes ClusterIP). Load balancer mengontak service registry untuk meneruskan traffic ke instance yang tersedia.

  • Kelebihan: Client sederhana, tidak perlu logika discovery.

  • Kekurangan: Menambah network hop ekstra dan potensi bottleneck pada load balancer.

Client-Side Discovery

Client bertanggung jawab langsung menanyakan alamat instance ke service registry (Consul, etcd, Eureka), memilih satu instance menggunakan algoritma load balancing internal (seperti Round Robin atau Random), lalu membuat koneksi langsung.

  • Kelebihan: Tidak ada extra hop, latency lebih rendah, kontrol routing granular di client.

  • Kekurangan: Client harus cerdas dan terintegrasi dengan SDK registry.


2. Merancang Health Check Robust di Go

Health check bukan sekadar HTTP endpoint yang selalu me-return status 200 OK. Health check buruk menyembunyikan masalah deadlock DB pool atau memory leak, sementara health check yang salah desain dapat memicu cascading failure.

Standar industri membagi health check menjadi dua jenis:

  1. Liveness Probe: Memastikan proses Go masih berjalan dan tidak deadlock. Jika gagal, orchestrator akan me-restart container.

  2. Readiness Probe: Memastikan service siap menerima traffic (koneksi DB aktif, cache warming selesai, migrasi DB tuntas). Jika gagal, orchestrator berhenti mengirim traffic tanpa mematikan container.

Implementasi Health Check Handler

Berikut implementasi HTTP health check handler idiomatik menggunakan Go stdlib (net/http) dan database/sql:

go
package health

import (
	"context"
	"database/sql"
	"encoding/json"
	"net/http"
	"time"
)

type Checker interface {
	Check(ctx context.Context) error
}

type DBChecker struct {
	DB *sql.DB
}

func (d *DBChecker) Check(ctx context.Context) error {
	return d.DB.PingContext(ctx)
}

type StatusResponse struct {
	Status    string            `json:"status"`
	Timestamp time.Time         `json:"timestamp"`
	Checks    map[string]string `json:"checks,omitempty"`
}

func LivenessHandler() http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Content-Type", "application/json")
		w.WriteHeader(http.StatusOK)
		_ = json.NewEncoder(w).Encode(StatusResponse{
			Status:    "UP",
			Timestamp: time.Now().UTC(),
		})
	}
}

func ReadinessHandler(checkers map[string]Checker) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		ctx, cancel := context.WithTimeout(r.Context(), 2*time.Second)
		defer cancel()

		w.Header().Set("Content-Type", "application/json")
		results := make(map[string]string)
		isHealthy := true

		for name, checker := range checkers {
			if err := checker.Check(ctx); err != nil {
				results[name] = "DOWN: " + err.Error()
				isHealthy = false
			} else {
				results[name] = "UP"
			}
		}

		if !isHealthy {
			w.WriteHeader(http.StatusServiceUnavailable)
		} else {
			w.WriteHeader(http.StatusOK)
		}

		_ = json.NewEncoder(w).Encode(StatusResponse{
			Status:    map[bool]string{true: "UP", false: "DOWN"}[isHealthy],
			Timestamp: time.Now().UTC(),
			Checks:    results,
		})
	}
}

3. Integrasi Service Registration dengan HashiCorp Consul

HashiCorp Consul adalah salah satu distributed service mesh dan KV store paling populer. Go menyediakan SDK resmi github.com/hashicorp/consul/api.

Implementasi Registrator

go
package discovery

import (
	"fmt"
	"time"

	consulapi "github.com/hashicorp/consul/api"
)

type Registry struct {
	client *consulapi.Client
}

func NewRegistry(consulAddr string) (*Registry, error) {
	config := consulapi.DefaultConfig()
	config.Address = consulAddr
	client, err := consulapi.NewClient(config)
	if err != nil {
		return nil, fmt.Errorf("gagal inisialisasi client consul: %w", err)
	}
	return &Registry{client: client}, nil
}

func (r *Registry) Register(serviceID, serviceName, host string, port int, healthURL string) error {
	registration := &consulapi.AgentServiceRegistration{
		ID:      serviceID,
		Name:    serviceName,
		Address: host,
		Port:    port,
		Check: &consulapi.AgentServiceCheck{
			HTTP:                           healthURL,
			Interval:                       "5s",
			Timeout:                        "2s",
			DeregisterCriticalServiceAfter: "30s",
		},
	}
	return r.client.Agent().ServiceRegister(registration)
}

func (r *Registry) Deregister(serviceID string) error {
	return r.client.Agent().ServiceDeregister(serviceID)
}

4. Client-Side Resolution dan Load Balancing

Ketika Order Service membutuhkan koneksi ke Payment Service, client query daftar endpoint sehat ke Consul lalu memilih instance menggunakan client-side load balancer:

go
package discovery

import (
	"errors"
	"fmt"
	"sync/atomic"

	consulapi "github.com/hashicorp/consul/api"
)

type Resolver struct {
	client *consulapi.Client
	index  uint64
}

func NewResolver(client *consulapi.Client) *Resolver {
	return &Resolver{client: client}
}

func (r *Resolver) Resolve(serviceName string) (string, error) {
	entries, _, err := r.client.Health().Service(serviceName, "", true, nil)
	if err != nil {
		return "", fmt.Errorf("error query consul: %w", err)
	}
	if len(entries) == 0 {
		return "", errors.New("tidak ada instance sehat tersedia")
	}

	// Round-robin load balancer atomic counter
	idx := atomic.AddUint64(&r.index, 1)
	selected := entries[idx%uint64(len(entries))]

	addr := selected.Service.Address
	port := selected.Service.Port
	return fmt.Sprintf("http://%s:%d", addr, port), nil
}

5. Menangani Graceful Shutdown dan Deregister

Saat instance menerima sinyal SIGTERM atau SIGINT, service harus menghapus dirinya dari registry sebelum mematikan HTTP server. Ini mencegah traffic baru masuk ke instance yang sedang proses shutdown.

go
package main

import (
	"context"
	"log"
	"net/http"
	"os"
	"os/signal"
	"syscall"
	"time"

	"discovery"
)

func main() {
	const (
		serviceID   = "order-service-node-01"
		serviceName = "order-service"
		host        = "192.168.1.50"
		port        = 8080
		consulAddr  = "127.0.0.1:8500"
	)

	reg, err := discovery.NewRegistry(consulAddr)
	if err != nil {
		log.Fatalf("Init registry gagal: %v", err)
	}

	mux := http.NewServeMux()
	mux.HandleFunc("/health/live", func(w http.ResponseWriter, r *http.Request) {
		w.WriteHeader(http.StatusOK)
	})

	server := &http.Server{
		Addr:    fmt.Sprintf(":%d", port),
		Handler: mux,
	}

	go func() {
		if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
			log.Fatalf("Listen error: %v", err)
		}
	}()

	healthURL := fmt.Sprintf("http://%s:%d/health/live", host, port)
	if err := reg.Register(serviceID, serviceName, host, port, healthURL); err != nil {
		log.Fatalf("Registrasi gagal: %v", err)
	}
	log.Println("Service terdaftar di Consul.")

	// Menunggu sinyal terminate
	stop := make(chan os.Signal, 1)
	signal.Notify(stop, syscall.SIGINT, syscall.SIGTERM)
	<-stop

	log.Println("Memulai graceful shutdown...")

	// 1. Deregister dari consul
	_ = reg.Deregister(serviceID)

	// 2. Shutdown HTTP server dengan timeout
	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
	defer cancel()
	_ = server.Shutdown(ctx)

	log.Println("Service berhenti dengan aman.")
}

6. Ringkasan Praktik Terbaik

  1. Pisahkan Liveness dan Readiness: Liveness hanya cek deadlock/process state. Readiness cek dependensi eksternal (DB, Cache).

  2. Beri Timeout Ketat pada Health Check: Health check tidak boleh memakan waktu lebih dari 1-2 detik.

  3. Graceful Deregister: Selalu deregister service sebelum close database connections dan shutdown server.

  4. Caching Service Discovery: Client tidak boleh hit Consul setiap 1 request. Gunakan Consul long-polling (Watch) atau cache lokal dengan TTL pendek (1-5 detik).

About the Author

huud

huud

@huud

About →

Systems architect and software engineer building high-performance distributed platforms.