Service Discovery dan Health Checking pada Go Microservices
"Panduan implementasi Service Discovery dan Health Checking mandiri di Go microservices menggunakan Consul dan standard library."
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:
Liveness Probe: Memastikan proses Go masih berjalan dan tidak deadlock. Jika gagal, orchestrator akan me-restart container.
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:
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
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:
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.
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
Pisahkan Liveness dan Readiness: Liveness hanya cek deadlock/process state. Readiness cek dependensi eksternal (DB, Cache).
Beri Timeout Ketat pada Health Check: Health check tidak boleh memakan waktu lebih dari 1-2 detik.
Graceful Deregister: Selalu deregister service sebelum close database connections dan shutdown server.
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
Systems architect and software engineer building high-performance distributed platforms.