0

Bài 3: Xây dựng Service Registry với Consul và Golang SDK

Trong các bài trước, chúng ta đã nắm bắt lý thuyết về Service Discovery và cơ chế Health Check. Giờ là lúc hiện thực hóa kiến trúc này bằng một công cụ công nghiệp thực tế: HashiCorp Consul.

Consul là một giải pháp trọn gói cung cấp cả Service Discovery, Health Checking và Key-Value Store. Trong bài học này, chúng ta sẽ viết một ứng dụng Golang hoàn chỉnh thực hiện 3 nhiệm vụ cốt lõi:

  1. Register: Tự động đăng ký một Service Instance lên Consul khi khởi động.

  2. Health Check (HTTP): Cung cấp một endpoint /health để Consul định kỳ "bác sĩ thăm khám".

  3. Discovery: Truy vấn ngược lại Consul để tìm địa chỉ của các service khác.

1. Chuẩn bị môi trường

Trước tiên, bạn cần cài đặt và chạy Consul ở chế độ phát triển (Dev mode) trên máy cá nhân (thường chạy qua Docker):

Bash

docker run -d --name consul -p 8500:8500 hashicorp/consul:latest consul agent -dev -client=0.0.0.0

Sau khi chạy xong, bạn có thể truy cập giao diện Web UI tại http://localhost:8500 để quan sát danh sách các service.

Trong project Go của bạn, hãy cài đặt official Go SDK của Consul:

Bash

go get github.com/hashicorp/consul/api

2. Viết mã nguồn Service Instance (Đăng ký & Health Check)

Dưới đây là mã nguồn chuẩn của một Microservice (ví dụ: Payment-Service) tự động đăng ký với Consul ngay khi khởi động.

Go

package main

import (
	"fmt"
	"log"
	"net/http"
	"os"
	"os/signal"
	"syscall"

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

const (
	serviceID   = "payment-service-01"
	serviceName = "payment-service"
	servicePort = 8080
	serviceHost = "127.0.0.1"
)

func main() {
	// 1. Kết nối đến Consul Agent (Mặc định chạy ở port 8500)
	config := api.DefaultConfig()
	config.Address = "127.0.0.1:8500"
	
	client, err := api.NewClient(config)
	if err != nil {
		log.Fatalf("Không thể kết nối Consul: %v", err)
	}

	// 2. Định nghĩa cấu hình đăng ký Service & Health Check
	registration := &api.AgentServiceRegistration{
		ID:      serviceID,
		Name:    serviceName,
		Port:    servicePort,
		Address: serviceHost,
		// Cấu hình HTTP Health Check (Consul sẽ chủ động gọi endpoint này định kỳ)
		Check: &api.AgentServiceCheck{
			HTTP:                           fmt.Sprintf("http://%s:%d/health", serviceHost, servicePort),
			Interval:                        "5s",  // Kiểm tra mỗi 5 giây
			Timeout:                         "3s",  // Timeout sau 3 giây
			DeregisterCriticalServiceAfter: "10s", // Tự động xóa khỏi danh bạ nếu lỗi liên tục trong 10s
		},
	}

	// 3. Thực hiện đăng ký lên Consul Registry
	err = client.Agent().ServiceRegister(registration)
	if err != nil {
		log.Fatalf("Đăng ký service thất bại: %v", err)
	}
	log.Printf("[%s] Đã đăng ký thành công với Consul!", serviceName)

	// Đảm bảo khi service tắt thì phải tự động Deregister
	defer func() {
		client.Agent().ServiceDeregister(serviceID)
		log.Printf("[%s] Đã hủy đăng ký (Deregister) khỏi Consul.", serviceName)
	}()

	// 4. Khởi ạo HTTP Server cho Service
	mux := http.NewServeMux()
	
	// Endpoint Health Check để Consul "thăm khám"
	mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
		w.WriteHeader(http.StatusOK)
		w.Write([]byte("OK"))
	})

	// Endpoint nghiệp vụ chính
	mux.HandleFunc("/pay", func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprintf(w, "Thanh toán thành công qua instance: %s", serviceID)
	})

	// Lắng nghe port ngầm định
	serverAddr := fmt.Sprintf(":%d", servicePort)
	go func() {
		log.Printf("Server đang chạy tại %s", serverAddr)
		if err := http.ListenAndServe(serverAddr, mux); err != nil && err != http.ErrServerClosed {
			log.Fatalf("HTTP server error: %v", err)
		}
	}()

	// 5. Bắt tín hiệu ngắt (Ctrl+C hoặc Docker Stop) để thực hiện Graceful Shutdown
	quit := make(chan os.Signal, 1)
	signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
	<-quit
	log.Println("Đang tắt service...")
}

Khi bạn chạy chương trình này (go run main.go), truy cập vào http://localhost:8500, bạn sẽ thấy payment-service xuất hiện với một dấu chấm màu xanh lá (Healthy) do Consul định kỳ gọi vào /health thành công.

3. Viết mã nguồn Service Consumer (Khám phá dịch vụ)

Bây giờ, giả sử một service khác (như Order-Service) muốn gọi tới Payment-Service. Thay vì gọi cứng địa chỉ IP, nó sẽ hỏi Consul để lấy danh sách các instance đang sống.

Go

package main

import (
	"fmt"
	"log"

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

func main() {
	// 1. Kết nối Consul client
	config := api.DefaultConfig()
	config.Address = "127.0.0.1:8500"
	
	client, err := api.NewClient(config)
	if err != nil {
		log.Fatalf("Không thể kết nối Consul: %v", err)
	}

	// 2. Truy vấn danh sách các instance KHỎE MẠNH của "payment-service"
	// Tham số thứ 2 (passingOnly = true) cực kỳ quan trọng: 
	// Nó tự động lọc bỏ các service đang bị chết hoặc timeout.
	services, _, err := client.Health().Service("payment-service", "", true, nil)
	if err != nil {
		log.Fatalf("Lỗi truy vấn service discovery: %v", err)
	}

	if len(services) == 0 {
		log.Println("Không tìm thấy instance nào khả dụng!")
		return
	}

	// 3. Chọn một instance (Ví dụ: Lấy instance đầu tiên - Thuật toán Round Robin đơn giản)
	target := services[0].Service
	targetURL := fmt.Sprintf("http://%s:%d/pay", target.Address, target.Port)

	log.Printf("Đã tìm thấy service tại địa chỉ: %s", targetURL)
	
	// Từ đây, bạn có thể dùng http.Get(targetURL) để gọi request bình thường...
}

Tổng kết

Thông qua Consul SDK và Golang, chúng ta đã xây dựng được một hệ thống Service Discovery hoàn chỉnh: Service tự đăng ký khi lên sóng, tự dọn dẹp khi tắt, cung cấp cơ chế Health Check tự động, và cho phép các service khác tra cứu linh hoạt.

Tuy nhiên, Consul là một giải pháp độc lập (External Registry) đòi hỏi phải dựng thêm một cluster chuyên biệt. Trong bài tiếp theo, chúng ta sẽ khám phá một lựa chọn khác vô cùng mạnh mẽ trong thế giới Cloud-Native: Etcd, trái tim đằng sau Kubernetes.


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í