0

Bài 6: Tự code một Mini-Registry từ con số 0 trong Golang

Trong các bài học trước, chúng ta đã sử dụng các công cụ công nghiệp mạnh mẽ như Consul và Etcd. Tuy nhiên, cách tốt nhất để thực sự thấu hiểu bản chất của một hệ thống phân tán là tự tay xây dựng nó từ con số không (From scratch) mà không cần dựa vào bất kỳ thư viện bên thứ ba nào.

Trong bài học cuối cùng này, chúng ta sẽ viết một "Mini-Registry Server" bằng thư viện chuẩn net/http của Go.

Nó sẽ thực hiện đầy đủ 3 tính năng cốt lõi:

  1. Nhận đăng ký service từ các Provider kèm thời gian sống (TTL).

  2. Chạy một tiến trình ngầm (Background Worker) tự động quét và quét sạch các instance bị quá hạn (Health Check / Expiration).

  3. Cung cấp endpoint trả về danh sách các instance đang sống cho các Consumer.

1. Thiết kế Kiến trúc Dữ liệu

Một Registry server đơn giản chỉ cần lưu trữ trong bộ nhớ RAM (In-memory store) một cấu trúc dữ liệu ánh xạ giữa tên service và danh sách các địa chỉ đang hoạt động.

Vì nhiều Provider sẽ cùng ghi và Consumer sẽ cùng đọc đồng thời, chúng ta bắt buộc phải bảo vệ bộ nhớ này bằng sync.RWMutex để tránh lỗi tranh chấp luồng (Race Condition).

Go

package main

import (
	"encoding/json"
	"fmt"
	"log"
	"net/http"
	"sync"
	"time"
)

// Cấu trúc thông tin của một instance
type Instance struct {
	ID           string    `json:"id"`
	Address      string    `json:"address"`
	LastHeartbeat time.Time `json:"-"` // Dùng để kiểm tra TTL
}

// Registry Server lưu trữ toàn bộ dữ liệu trong RAM
type RegistryServer struct {
	mu        sync.RWMutex
	// Key: Service Name (vd: "payment-service")
	// Value: Map các Instance theo ID
	services  map[string]map[string]*Instance
}

func NewRegistryServer() *RegistryServer {
	return &RegistryServer{
		services: make(map[string]map[string]*Instance),
	}
}

2. Hiện thực hóa các API Endpoints

Chúng ta sẽ xây dựng 3 HTTP endpoints chính cho Mini-Registry:

  • POST /register: Dùng cho Provider đăng ký hoặc gửi heartbeat.

  • POST /deregister: Dùng cho Provider chủ động rút lui khi tắt ứng dụng.

  • GET /discover: Dùng cho Consumer truy vấn danh sách instance.

Go

// 1. API Đăng ký / Heartbeat
func (s *RegistryServer) handleRegister(w http.ResponseWriter, r *http.Request) {
	if r.Method != http.MethodPost {
		http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
		return
	}

	var req struct {
		ServiceName string `json:"service_name"`
		ID          string `json:"id"`
		Address     string `json:"address"`
	}

	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		http.Error(w, err.Error(), http.StatusBadRequest)
		return
	}

	s.mu.Lock()
	defer s.mu.Unlock()

	if _, exists := s.services[req.ServiceName]; !exists {
		s.services[req.ServiceName] = make(map[string]*Instance)
	}

	// Lưu hoặc cập nhật instance kèm thời gian heartbeat hiện tại
	s.services[req.ServiceName][req.ID] = &Instance{
		ID:           req.ID,
		Address:      req.Address,
		LastHeartbeat: time.Now(),
	}

	w.WriteHeader(http.StatusOK)
	fmt.Fprintf(w, "Đăng ký thành công instance %s cho service %s\n", req.ID, req.ServiceName)
}

// 2. API Khám phá dịch vụ cho Consumer
func (s *RegistryServer) handleDiscover(w http.ResponseWriter, r *http.Request) {
	if r.Method != http.MethodGet {
		http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
		return
	}

	serviceName := r.URL.Query().Get("service_name")
	if serviceName == "" {
		http.Error(w, "Thiếu tham số service_name", http.StatusBadRequest)
		return
	}

	s.mu.RLock()
	defer s.mu.RUnlock()

	instancesMap, exists := s.services[serviceName]
	if !exists {
		w.Header().Set("Content-Type", "application/json")
		json.NewEncoder(w).Encode([]Instance{})
		return
	}

	var list []Instance
	for _, inst := range instancesMap {
		list = append(list, *inst)
	}

	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(list)
}

3. Xây dựng "Bác sĩ" quét dọn (TTL Health Check Worker)

Điểm mấu chốt của một Service Discovery là khả năng tự động xóa bỏ các instance đã chết. Ta sẽ chạy một background goroutine thực hiện quét vòng lặp định kỳ (ví dụ mỗi 3 giây): nếu instance nào không gửi heartbeat quá 10 giây, nó sẽ bị khai tử khỏi RAM.

Go

func (s *RegistryServer) StartHealthCheckWorker(ttl time.Duration) {
	ticker := time.NewTicker(3 * time.Second)
	go func() {
		for range ticker.C {
			s.mu.Lock()
			now := time.Now()
			for sName, instances := range s.services {
				for id, inst := range instances {
					// Nếu quá thời gian TTL mà không thấy heartbeat -> Xóa!
					if now.Sub(inst.LastHeartbeat) > ttl {
						log.Printf("[HealthCheck] Instance %s của service %s đã quá hạn TTL. Đang xóa khỏi registry...", id, sName)
						delete(instances, id)
					}
				}
				// Nếu service không còn instance nào, xóa luôn key service đó
				if len(instances) == 0 {
					delete(s.services, sName)
				}
			}
			s.mu.Unlock()
		}
	}()
}

4. Hàm Main hoàn chỉnh của Mini-Registry

Go

func main() {
	registry := NewRegistryServer()

	// Khởi động worker dọn dẹp với TTL là 10 giây
	registry.StartHealthCheckWorker(10 * time.Second)

	mux := http.NewServeMux()
	mux.HandleFunc("/register", registry.handleRegister)
	mux.HandleFunc("/discover", registry.handleDiscover)

	log.Println("Mini-Registry Server đang chạy tại port 8500...")
	if err := http.ListenAndServe(":8500", mux); err != nil {
		log.Fatalf("Server error: %v", err)
	}
}

Tổng kết Series Service Discovery

Chỉ với khoảng chưa đầy 150 dòng code Golang thuần túy bằng net/http, bạn đã tự tay hiện thực hóa trái tim của một hệ thống Service Discovery. Nó vận hành dựa trên đúng các nguyên lý cốt lõi mà Consul hay Etcd đang áp dụng dưới lớp vỏ bọc phức tạp: ghi nhận trạng thái, cung cấp danh bạ, và dùng TTL để tự động dọn dẹp các tiến trình chết.


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí