原文链接:packagemain.tech/p/database-…
原文作者:Julien Singler
译者:菜小鸟魔王
01 引言
随着微服务和 Kubernetes 的兴起,数据库迁移的管理难度达到了前所未有的高度。以往应用程序启动时执行迁移的传统做法,现已不足以应对。
本文将深入探讨在 Kubernetes 环境下如何有效进行数据库迁移,特别关注基于 Golang 的解决策略。
02 Kubernetes 环境下的迁移难题
Kubernetes 环境下,数据库迁移面临以下挑战:
-
多个副本可能同时启动。
-
需要协调操作,防止迁移过程并发执行。
-
应用程序逻辑与迁移逻辑需要明确划分。
03 Golang 常用的迁移工具
正如其他文章中所提及的,以下是一些可用于管理数据库迁移的 Golang 工具:
golang-migrate
-
使用广泛,支持多种数据库。
-
提供简洁的命令行界面和 API。
-
支持多种迁移源,包括本地文件、S3、Google Storage 等。
goose
-
支持主流的 SQL 数据库。
-
在处理复杂场景时,允许使用 Go 语言编写迁移脚本。
-
灵活的版本控制模式。
atlas
-
一款强大的数据库架构管理工具。
-
支持声明式和版本控制的迁移方式。
-
提供完整性检查和迁移脚本的风格检查。
-
还提供了 GitHub Actions 和 Terraform 的支持。
04 在 Kubernetes 中执行数据库迁移
使用迁移代码直接执行
最简单的方法是在启动服务器之前,直接在主函数中嵌入迁移代码的逻辑。
以下是使用 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
}
但这种做法可能会引发多种问题,例如迁移过程可能耗时较长,导致 Kubernetes 认为容器启动未成功,进而终止该 Pod。如果尝试在 Go 协程中运行迁移,那么遇到失败又该如何应对?
如果同时启动多个 Pod ,还可能遇到并发执行的问题。
同时,这也意味着迁移脚本需要被打包进 Docker 镜像中。
利用 InitContainer 容器
在 Kubernetes 部署中,通过使用 InitContainer,可以在主应用容器启动之前执行迁移操作。这对于尚未面临扩缩容问题的场景来说,是一个不错的初步解决方案。
如果 InitContainer 执行失败,Kubernetes 的蓝/绿部署(blue/green deployment)将不会继续,原有的 Pod 将保持运行状态。这样做可以避免在没有完成计划迁移的情况下,就部署了新版本的代码。
Example:
initContainers:
- name: migrations
image: migrate/migrate:latest
command: ['/migrate']
args: ['-source', 'file:///migrations', '-database','postgres://user:pass@db:5432/dbname', 'up']
独立任务执行
可以创建一个 Kubernetes 任务(Job),用于运行数据库迁移,并在部署流程中,在推出应用之前触发该任务。
Example:
apiVersion: batch/v1
kind: Job
metadata:
name: db-migrate
spec:
template:
spec:
containers:
- name: migrate
image: your-migration-image:latest
command: ['/app/migrate']
还可以将其与 initContainers 结合使用,确保只有在作业成功执行后 pod 才会启动。
initContainers:
- name: migrations-wait
image: ghcr.io/groundnuty/k8s-wait-for:v2.0
args:
- "job"
- "my-migration-job"
Helm hook
如果使用 Helm,可以利用其提供的hook 功能在 chart 安装或升级时执行迁移操作。您可以在 Helm chart 中定义 pre-install(预安装)或 pre-upgrade(预升级)hook。
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"]
Helm 提供了预安装和后安装等多种 hook。
05 Kubernetes 迁移的实践指导原则
- 将迁移与应用程序代码分离
-
为迁移单独构建 Docker 镜像。
-
利用 Atlas 等工具独立进行迁移管理。
- 迁移的版本控制管理
-
将迁移脚本存放在 Git 版本库中。
-
采用顺序编号或时间戳的方式进行版本控制。
- 惰性迁移
- 确保迁移可重复执行而无副作用
- 回滚策略
- 为每一次迁移制定并测试回滚方案。
- 监控与日志记录
- 使用 Atlas Cloud 等工具来追踪迁移历史记录。
06 Conclusion
在 Kubernetes 环境下处理数据库迁移,必须精心策划并精准执行。
借助 golang-migrate、goose、atlas 等工具,并依照业界标准操作,我们可以打造出稳健、可扩展和可维护的迁移方案。
务必将迁移逻辑与应用程序代码分离,坚持版本管理,并且部署有效的监控体系,确保在 Kubernetes 架构下数据库的持续进化不会出现波折。