1. 概述
在本教程中,我们将探讨如何在 Kubernetes 环境中结合使用 Liquibase、Spring Boot 和 Kubernetes 本身。这些技术可以帮助我们构建在启动时自动配置数据库的应用程序。这在单实例部署中非常方便,但在大规模部署时可能会引发一些问题,特别是在数据库锁和健康检查方面。
2. 服务启动时发生了什么
在 Kubernetes 中部署服务时,我们可以指定副本数(replicas)。如果服务需要处理用户请求,通常我们会部署多个实例。
当使用 Spring Boot 的 Liquibase Starter 时,应用启动时会尝试执行数据库迁移。服务会在迁移完成之后才准备好接收请求。Liquibase 在执行迁移时,会在数据库中插入一个锁记录,以防止多个实例同时运行迁移。如果两个 Spring Boot 实例同时启动 Liquibase,它们之间会存在竞争锁的问题。
✅ 结果是:只有一个实例能获取锁并执行迁移,其他实例会等待锁释放。
迁移完成后,第二个实例会获取锁,但会发现迁移已完成,于是释放锁并继续启动流程。
这在迁移很快完成的情况下问题不大。但如果迁移耗时较长,就会带来风险,尤其是在 Kubernetes 的健康检查机制下。
3. Kubernetes 的探针机制
Kubernetes 推荐使用 Readiness Probe(就绪探针) 和 Liveness Probe(存活探针) 来判断服务是否正常。通常我们会使用 Spring Boot Actuator 的 /health
接口作为健康检查的 endpoint。
以下是典型的配置示例:
apiVersion: apps/v1
kind: Deployment
metadata:
name: api
spec:
replicas: 3
selector:
matchLabels:
app.kubernetes.io/name: api
template:
metadata:
labels:
app.kubernetes.io/name: api
spec:
containers:
- image: myapp:latest
name: api
livenessProbe:
httpGet:
path: /health
port: http
failureThreshold: 1
periodSeconds: 10
startupProbe:
httpGet:
path: /health
port: http
failureThreshold: 12
periodSeconds: 5
ports:
- containerPort: 8080
name: http
上述配置中,服务必须在启动后 60 秒内完成迁移并通过健康检查,否则 Kubernetes 会终止该 Pod。
⚠️ 问题在于:如果迁移耗时超过 readiness probe 的 timeout,Pod 会被终止,但 Liquibase 的锁不会被释放。
这会导致新启动的 Pod 一直等待锁释放,最终也超时被杀,形成“死锁”状态。
4. 如何避免这个问题
在大多数情况下,手动删除数据库锁可以解决问题,但 在生产环境中,这可能会导致数据不一致甚至数据损坏,因此必须非常谨慎。
✅ 建议做法:
- 在类生产环境(Staging)中测试迁移脚本,确保迁移时间可控。
- Staging 环境应尽可能模拟生产数据量和负载,虽然这在现实中可能受限于资源。
- 通常建议:生产环境的迁移时间至少是 Staging 的两倍。
5. 迁移方案选择
为了降低迁移超时的风险,我们可以选择以下几种策略:
5.1. 延长启动探针时间
这是最简单的解决方案,只需调整 startupProbe
的超时时间:
startupProbe:
httpGet:
path: /health
port: http
failureThreshold: 30
periodSeconds: 20
这意味着服务最多可以有 10 分钟完成迁移。
✅ 优点:实现简单。
❌ 缺点:如果迁移失败或卡住,会导致部署失败时间过长。
5.2. 使用生命周期钩子(Lifecycle Hook)
Kubernetes 提供了容器生命周期钩子,可以在容器退出前执行脚本,比如释放 Liquibase 锁:
lifecycle:
preStop:
exec:
command: ["/bin/sh","-c","/stopservice.sh"]
这个脚本可以尝试删除数据库中的锁记录。
✅ 优点:有助于避免锁阻塞后续部署。
❌ 缺点:如果脚本失败,容器可能卡在终止状态,需手动干预。
5.3. 将迁移与应用分离为不同容器
将 Liquibase 迁移作为独立容器运行,可以完全避免迁移影响主应用的健康检查。
apiVersion: v1
kind: Pod
metadata:
name: db-migrationn
spec:
containers:
- name: mymigration:latest
image: migration
resources:
limits:
memory: "200Mi"
cpu: "700m"
requests:
memory: "200Mi"
cpu: "700m"
✅ 优点:迁移和应用完全解耦,互不影响。
❌ 缺点:部署流程变复杂,需维护两个镜像。
5.4. 使用 InitContainer
使用 Kubernetes 的 InitContainer,在主应用容器启动前执行迁移:
spec:
initContainers:
- name: mymigration:latest
image: migration
✅ 优点:无需额外部署,流程清晰。
❌ 缺点:迁移完成前主容器无法启动,耦合度高。
5.5. 完全脱离 Kubernetes 执行迁移
在某些场景下,最好将数据库迁移从 Kubernetes 中完全剥离出来,比如:
- 在 CI/CD 服务器上运行 Liquibase。
- 在云平台临时创建一个 VM,执行完迁移后销毁。
✅ 优点:彻底规避 Kubernetes 的调度和探针问题。
❌ 缺点:需要额外基础设施支持,部署流程更复杂。
6. 总结
在 Kubernetes 中使用 Liquibase 进行数据库迁移是一种常见做法,但需要注意迁移过程中的锁机制和健康检查探针设置。
关键在于:提前评估迁移耗时,合理设置探针超时时间,或采用分离迁移流程的方案。
以下是几种方案的对比总结:
方案 | 难度 | 解耦度 | 风险 | 推荐场景 |
---|---|---|---|---|
延长探针时间 | ⭐ | 低 | 中 | 简单快速,迁移时间可控 |
生命周期钩子 | ⭐⭐ | 中 | 中 | 需要释放锁,防止阻塞 |
分离容器 | ⭐⭐⭐ | 高 | 低 | 大型项目,长期使用 |
InitContainer | ⭐⭐ | 中 | 中 | 迁移必须先于服务启动 |
独立执行迁移 | ⭐⭐⭐⭐ | 高 | 低 | 大型数据库,高风险操作 |
最终选择应基于迁移复杂度、团队运维能力以及对风险的容忍程度。