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 ⭐⭐ 迁移必须先于服务启动
独立执行迁移 ⭐⭐⭐⭐ 大型数据库,高风险操作

最终选择应基于迁移复杂度、团队运维能力以及对风险的容忍程度。


原始标题:Using Liquibase in Kubernetes

« 上一篇: Netflix Genie 介绍
» 下一篇: K3s 入门指南