Cách khởi chạy Database Migrations trong Kubernetes kèm ví dụ
Trong kỷ nguyên của Microservices và Kubernetes, việc quản lý các bản vá cơ sở dữ liệu đã trở nên phức tạp hơn bao giờ hết. Các phương pháp truyền thống thực hiện các bản vá trong quá trình khởi động ứng dụng không còn đủ hiệu quả nữa.
Bài viết này khám phá các phương pháp khác nhau để xử lý các bản vá cơ sở dữ liệu trong môi trường Kubernetes, với trọng tâm là các công cụ Go. Bạn sẽ nhận được nhiều giá trị nhất từ bài viết này nếu bạn đã có một số kinh nghiệm với Go, Kubernetes và cơ sở dữ liệu quan hệ.
Nếu bạn chưa biết Kubernetes là gì, hãy tham khảo bài viết này nhé: Kubernetes (K8s) là gì? Tìm hiểu cơ bản về Kubernetes
Thách thức của Migrations trong Kubernetes
Kubernetes mang đến những thách thức mới đối với việc quản lý các bản vá cơ sở dữ liệu:
- Nhiều bản sao khởi động đồng thời. Điều này có thể dẫn đến việc chạy một bản vá cơ sở dữ liệu hai lần, gây ra tình trạng khóa cơ sở dữ liệu.
- Tách biệt các mối quan tâm giữa logic ứng dụng và logic bản vá. Điều này có nghĩa là sẽ rất hữu ích nếu có thể chạy hoặc hoàn tác các bản vá mà không cần phải triển khai lại ứng dụng của bạn.
Các công cụ quản lý bản vá phổ biến cho Golang
Như tôi đã đề cập trong một bài viết khác, có một số công cụ khác nhau mà bạn có thể sử dụng để quản lý các bản vá cơ sở dữ liệu. Chúng khá giống nhau, vì vậy cá nhân tôi không có sự ưu tiên mạnh mẽ giữa cái này và cái kia. Tôi chỉ muốn cung cấp một vài lựa chọn để bạn biết các công cụ phổ biến là gì.
1. golang-migrate
- Được sử dụng rộng rãi và hỗ trợ nhiều cơ sở dữ liệu.
- CLI và API đơn giản.
- Hỗ trợ nhiều nguồn bản vá (tệp cục bộ, S3, Google Storage).
2. goose
- Hỗ trợ các cơ sở dữ liệu SQL chính.
- Cho phép viết bản vá bằng Go cho các tình huống phức tạp.
- Mô hình phiên bản linh hoạt.
3. atlas
- Công cụ quản lý lược đồ cơ sở dữ liệu mạnh mẽ.
- Hỗ trợ bản vá khai báo và có phiên bản.
- Cung cấp kiểm tra tính toàn vẹn và kiểm tra lỗi bản vá.
- Cung cấp GitHub Actions và Terraform provider.
Chạy bản vá trong ứng dụng
Một cách triển khai đơn giản là chạy mã bản vá trực tiếp trong hàm main trước khi khởi động máy chủ của bạn.
Ví dụ sử dụng golang-migrate:
package main
import (
"database/sql"
"fmt"
"log"
"net/http"
"github.com/golang-migrate/migrate/v4"
"github.com/golang-migrate/migrate/v4/database/postgres"
_ "github.com/golang-migrate/migrate/v4/source/file"
_ "github.com/lib/pq"
)
func main() {
// Database connection parameters
url := "postgres://user:pass@localhost:5432/dbname"
// Connect to the database
db, err := sql.Open("postgres", url)
if err != nil {
log.Fatalf("could not connect to database: %v", err)
}
defer db.Close()
// Run migrations
if err := runMigrations(db); err != nil {
log.Fatalf("could not run migrations: %v", err)
}
// Run the application, for example start the server
if err := http.ListenAndServe(":8080", nil); err != nil {
log.Fatalf("server failed to start: %v", err)
}
}
func runMigrations(db *sql.DB) error {
driver, err := postgres.WithInstance(db, &postgres.Config{})
if err != nil {
return fmt.Errorf("could not create database driver: %w", err)
}
m, err := migrate.NewWithDatabaseInstance(
"file://migrations", // Path to your migration files
"postgres", // Database type
driver,
)
if err != nil {
return fmt.Errorf("could not create migrate instance: %w", err)
}
if err := m.Up(); err != nil && err != migrate.ErrNoChange {
return fmt.Errorf("could not run migrations: %w", err)
}
log.Println("migrations completed successfully")
return nil
}
Tuy nhiên, cách này có thể gây ra một số vấn đề như việc các bản vá của bạn chạy chậm và Kubernetes coi rằng pod không khởi động thành công và do đó sẽ tắt nó. Bạn có thể chạy các bản vá đó trong một Go routine, nhưng làm thế nào để xử lý các lỗi phát sinh?
Trong trường hợp có nhiều pod được tạo ra cùng lúc, bạn sẽ gặp phải vấn đề đồng thời (concurrency).
Điều này cũng có nghĩa là các bản vá của bạn phải được bao gồm trong hình ảnh Docker của bạn.
Mặc dù có một số nhược điểm, phương pháp này có thể hoạt động tốt cho các thay đổi cơ sở dữ liệu nhanh và ổn định, cũng như các dự án nhỏ.
Chạy bản vá trong initContainers
Bằng cách sử dụng initContainers
trong Kubernetes Deployment, bạn có thể thực hiện bản vá trước khi container ứng dụng chính bắt đầu. Đây là một giải pháp tốt ban đầu khi việc mở rộng (scaling) chưa phải là vấn đề.
Nếu initContainer
thất bại, quá trình triển khai blue/green của Kubernetes sẽ không tiến xa hơn và các pod trước đó sẽ giữ nguyên trạng thái. Điều này ngăn chặn việc có phiên bản mã mới mà không có bản vá đã được lên kế hoạch.
Ví dụ:
initContainers:
- name: migrations
image: migrate/migrate:latest
command: ['/migrate']
args: ['-source', 'file:///migrations', '-database','postgres://user:pass@db:5432/dbname', 'up']
Phương pháp này có thể hoạt động tốt cho các thay đổi cơ sở dữ liệu nhanh và ổn định đối với các triển khai với một Pod duy nhất. Nó cũng đã tách biệt được các lớp ứng dụng và bản vá.
Chạy bản vá dưới dạng Kubernetes Job
Bạn có thể tạo một Kubernetes Job để chạy các bản vá của mình, và kích hoạt công việc này trong quá trình triển khai trước khi triển khai ứng dụng.
Ví dụ:
apiVersion: batch/v1
kind: Job
metadata:
name: db-migrate
spec:
template:
spec:
containers:
- name: migrate
image: your-migration-image:latest
command: ['/app/migrate']
Bạn cũng có thể kết hợp nó với initContainers
, đảm bảo rằng pod chỉ khởi động khi công việc (Job) thành công.
initContainers:
- name: migrations-wait
image: ghcr.io/groundnuty/k8s-wait-for:v2.0
args:
- "job"
- "my-migration-job"
Phương pháp này có thể giải quyết các vấn đề liên quan đến nhiều bản sao mà chúng ta đã đề cập ở trên.
Helm Hooks
Nếu bạn sử dụng Helm, nó có các hook mà bạn có thể sử dụng để chạy các bản vá trong quá trình cài đặt/nâng cấp chart. Bạn chỉ cần định nghĩa một hook pre-install hoặc pre-upgrade trong Helm chart của bạn.
pre-install-hook.yaml:
apiVersion: batch/v1
kind: Job
metadata:
name: {{ include "mychart.fullname" . }}-migrations
annotations:
"helm.sh/hook": pre-install,pre-upgrade
"helm.sh/hook-weight": "-5"
"helm.sh/hook-delete-policy": hook-succeeded
spec:
template:
spec:
containers:
- name: migrations
image: your-migrations-image:tag
command: ["./run-migrations.sh"]
Trong ví dụ này, hook pre-install thực thi sau khi các template được render, nhưng trước khi bất kỳ tài nguyên nào được tạo trong Kubernetes.
Điều này tất nhiên chỉ hoạt động khi bạn sử dụng Helm, có nghĩa là bạn cần tìm một phương pháp khác nếu quyết định không sử dụng Helm.
Các Best Practices cho Migrations trong Kubernetes
Tách biệt các bản vá khỏi mã ứng dụng:
- Tạo một hình ảnh Docker riêng biệt cho các bản vá. Điều này đảm bảo rằng logic bản vá được đóng gói riêng biệt và không can thiệp vào mã ứng dụng.
- Sử dụng các công cụ như Atlas để quản lý các bản vá một cách độc lập. Các công cụ như Atlas cung cấp các tính năng tự động hóa quá trình bản vá, lập lịch và hoàn tác.
Sử dụng kiểm soát phiên bản cho các bản vá:
- Lưu trữ các tệp bản vá trong kho Git của bạn. Điều này đảm bảo có một lịch sử đầy đủ của các thay đổi bản vá, giúp dễ dàng theo dõi và hoàn tác thay đổi.
- Sử dụng phiên bản tuần tự hoặc theo dấu thời gian. Phiên bản tuần tự đảm bảo thứ tự chính xác của các bản vá, điều này rất quan trọng đối với cơ sở dữ liệu quan hệ.
Đảm bảo các bản vá là idempotent:
- Đảm bảo rằng các bản vá có thể được chạy nhiều lần mà không gây ra tác dụng phụ. Các bản vá idempotent ngăn chặn sự cố dữ liệu hoặc không nhất quán nếu một bản vá được chạy nhiều lần.
Có chiến lược hoàn tác:
- Triển khai và kiểm tra quy trình hoàn tác cho mỗi bản vá. Có chiến lược hoàn tác giúp bạn có thể quay lại các thay đổi nếu bản vá thất bại hoặc gây ra sự cố không mong muốn.
Thực hiện giám sát và ghi log:
- Sử dụng các công cụ như Atlas Cloud để theo dõi lịch sử bản vá. Atlas Cloud cung cấp các nhật ký chi tiết và lịch sử các bản vá, giúp dễ dàng theo dõi các thay đổi và xử lý sự cố.
Kết luận
Quản lý các bản vá cơ sở dữ liệu trong môi trường Kubernetes yêu cầu kế hoạch và thực thi cẩn thận.
Bằng cách tận dụng các công cụ như golang-migrate, goose hoặc atlas, và làm theo các thực hành tốt nhất, bạn có thể xây dựng các chiến lược bản vá mạnh mẽ, có thể mở rộng và dễ bảo trì.
Hãy nhớ tách biệt các bản vá khỏi mã ứng dụng, sử dụng kiểm soát phiên bản, và triển khai giám sát thích hợp để đảm bảo quá trình phát triển cơ sở dữ liệu suôn sẻ trong kiến trúc dựa trên Kubernetes của bạn.
Cảm ơn các bạn đã theo dõi!
All rights reserved