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:5432thay 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