0

Bài 3: Xây dựng hệ sinh thái Microservices với Docker Compose (Go + PostgreSQL + Redis)

Ở Bài 2, chúng ta đã thiết lập thành công môi trường phát triển độc lập cho ứng dụng Go với tính năng Hot-reload nhờ air. Tuy nhiên, trong thực tế, không có một ứng dụng Backend nào đứng đơn độc một mình. Nó luôn cần một cơ sở dữ liệu để lưu trữ (PostgreSQL) và một bộ nhớ đệm để tối ưu tốc độ/quản lý session (Redis).

Trong bài học này, chúng ta sẽ mở rộng file docker-compose.yml để dựng lên một Hệ sinh thái Microservices toàn diện, nơi ứng dụng Go của bạn có thể giao tiếp mượt mà với PostgreSQL và Redis thông qua mạng nội bộ của Docker.

1. Kiến trúc mạng nội bộ trong Docker Compose

Khi bạn định nghĩa nhiều service trong cùng một file docker-compose.yml, Docker sẽ tự động tạo ra một Default Bridge Network riêng cho ứng dụng đó. Điều kỳ diệu là: Các container có thể gọi nhau thông qua TÊN SERVICE (Service Name) thay vì địa chỉ IP.

  • Ví dụ: Ứng dụng Go của bạn chỉ cần kết nối tới database qua chuỗi host postgres:5432 thay vì 172.17.0.3. Dù container có bị restart và đổi IP, tên service vẫn giữ nguyên tuyệt đối.

2. Hoàn thiện file docker-compose.yml cho toàn bộ hệ sinh thái

Hãy cập nhật lại file docker-compose.yml ở thư mục gốc dự án của bạn với cấu hình chuẩn Production-like cho môi trường dev:

YAML

version: '3.8'

services:
  # 1. Ứng dụng Golang (Chạy với Hot-reload bằng Air)
  app:
    build:
      context: .
      dockerfile: Dockerfile.dev
    container_name: go_app_dev
    ports:
      - "8080:8080"
    volumes:
      - .:/app
      - go_mod_cache:/go/pkg/mod
    environment:
      - PORT=8080
      - DB_HOST=postgres
      - DB_PORT=5432
      - DB_USER=root
      - DB_PASSWORD=secretpassword
      - DB_NAME=gocourse
      - REDIS_HOST=redis
      - REDIS_PORT=6379
    depends_on:
      - postgres
      - redis
    restart: always

  # 2. Cơ sở dữ liệu quan hệ PostgreSQL
  postgres:
    image: postgres:16-alpine
    container_name: postgres_db
    ports:
      - "5432:5432"
    environment:
      POSTGRES_USER: root
      POSTGRES_PASSWORD: secretpassword
      POSTGRES_DB: gocourse
    volumes:
      # Volume cố định dữ liệu PostgreSQL để không bị mất khi container tắt
      - postgres_data:/var/lib/postgresql/data
    restart: always

  # 3. Bộ nhớ đệm Redis
  redis:
    image: redis:7.2-alpine
    container_name: redis_cache
    ports:
      - "6379:6379"
    volumes:
      # Volume lưu trữ dữ liệu bền vững cho Redis (nếu bật tính năng persistence)
      - redis_data:/data
    restart: always

# Định nghĩa các Named Volumes để lưu trữ dữ liệu bền vững trên máy host
volumes:
  go_mod_cache:
  postgres_data:
  redis_data:

3. Kết nối từ Go đến PostgreSQL và Redis

Để kiểm tra hệ thống đã liên thông chưa, bạn có thể cấu hình nhanh kết nối bên trong ứng dụng Go của mình (ví dụ đọc từ biến môi trường đã được truyền qua environment ở trên):

Go

package main

import (
	"database/sql"
	"fmt"
	"log"
	"net/http"
	"os"

	_ "github.com/lib/pq" // Driver PostgreSQL
)

func main() {
	// Đọc cấu hình từ biến môi trường Docker
	dbHost := os.Getenv("DB_HOST")
	dbPort := os.Getenv("DB_PORT")
	dbUser := os.Getenv("DB_USER")
	dbPass := os.Getenv("DB_PASSWORD")
	dbName := os.Getenv("DB_NAME")

	dsn := fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s sslmode=disable",
		dbHost, dbPort, dbUser, dbPass, dbName)

	// Thử kết nối cơ sở dữ liệu
	db, err := sql.Open("postgres", dsn)
	if err != nil {
		log.Fatalf("Lỗi cấu hình kết nối DB: %v", err)
	}
	defer db.Close()

	if err := db.Ping(); err != nil {
		log.Fatalf("Không thể ping tới PostgreSQL: %v", err)
	}
	log.Println("Kết nối thành công tới PostgreSQL container!")

	// Khởi chạy HTTP server đơn giản
	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		w.Write([]byte("Hệ sinh thái Go + PostgreSQL + Redis đang hoạt động hoàn hảo!"))
	})

	log.Println("Server đang lắng nghe tại cổng 8080...")
	http.ListenAndServe(":8080", nil)
}

4. Vấn đề tiềm ẩn: "Race Condition" khi khởi động

Khi bạn chạy lệnh docker compose up, bạn sẽ nhận thấy một hiện tượng: Container app (Golang) có thể khởi động nhanh hơn container postgres.

Vì Go app cố gắng kết nối database ngay giây đầu tiên trong khi PostgreSQL vẫn đang trong quá trình khởi tạo (initializing data directories), ứng dụng Go của bạn sẽ văng lỗi kết nối và bị crash (connection refused). Mặc dù Docker có restart lại ứng dụng, nhưng đây là một trải nghiệm không chuyên nghiệp.

Làm thế nào để ra lệnh cho Docker: "Hãy chắc chắn PostgreSQL đã thực sự sẵn sàng nhận câu lệnh SQL rồi thì app mới được phép khởi động"?

Đó chính là nội dung cốt lõi của Bài 4 tiếp theo.


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í