用 StatefulSet 和初始化容器恢复 SQLite 服务

把 SQLite 应用放进 Kubernetes 后,容器重启不应意味着数据库跟着消失。Litestream 官方指南给出了一条清楚的路径:应用与 Litestream sidecar 共用持久卷,应用运行时复制数据库;启动前由 init container 判断是否需要从远端副本恢复。PVC 应对常见的 Pod 重建,对象存储副本应对卷损坏或丢失,二者负责不同层次的恢复。

本文依据 Litestream 文档贡献者的 Running in a Kubernetes cluster 及其完整配套 YAML 翻译整理。覆盖从凭据、ConfigMap、StatefulSet 到两类恢复演练的全部技术流程;不展开 Kubernetes 集群安装、公网服务暴露或多节点读副本配置。文档于 2026 年 10 月 5 日核对,示例未部署、未执行。

Pod 启动时 init container 先从对象存储向 PVC 恢复 SQLite;随后应用写入共享 PVC,Litestream sidecar 将变更复制到对象存储。保留卷重启与丢失卷恢复是不同路径。
启动恢复与运行复制的两个阶段。原创示意图,未完纪整理。

运行前提和单写入者边界

教程假定已经有可工作的 Kubernetes 集群、可供 Litestream 使用的 S3 副本目标,并掌握基本 Litestream 操作。演示应用 myapp 在 8080 端口提供 HTTP 服务,把请求次数存入 /var/lib/myapp/db。真实项目可替换它,但应用与 sidecar 必须对同一数据库路径达成一致。

StatefulSet 设置 replicas: 1,正常情况下生成 myapp-0。这是此部署的单写入者约束,不能当作网络分区、强制删除 Pod 后的故障围栏。旧实例尚未真正停止时启动另一个可写实例,仍可能造成双写;扩容副本数也不是给 SQLite 自动增加多主能力。

原指南结尾将读复制描述为未来能力,这已不能概括整个当前产品:现行 restore 参考页描述了 0.5.9 起的只读 follow 模式。本文保留单个可写应用加备份 sidecar 的范围,不据旧句断言 Litestream 的所有读取能力。

让凭据与普通配置分开

原文通过名为 litestream 的 Kubernetes Secret 提供 LITESTREAM_ACCESS_KEY_ID 和 LITESTREAM_SECRET_ACCESS_KEY,并由 init container 与 sidecar 分别引用;应用容器不需要取得这些对象存储凭据。原页的 kubectl create secret ... --from-literal 只是占位演示,没有真实密钥,但把真实密钥写进命令行可能进入 shell 历史或审计记录。应通过组织批准的 Secret 管理流程注入,在云环境中也可评估受支持的工作负载身份。

Kubernetes Secret 文档明确提醒,Secret 默认并不等于 etcd 中的静态加密。还应限制读取权限、按容器分配访问能力,并将对象存储权限限定到所需桶和前缀。不要把凭据写进 Git、ConfigMap 或示例正文。

普通配置保存为 litestream.yml。下面保持原指南字段:数据库路径为 /var/lib/myapp/db,副本 URL 中的 YOURBUCKET 必须换成专用演练桶,addr 开启 9090 端口的 Prometheus 指标服务。

addr: ":9090"
dbs:
  - path: /var/lib/myapp/db
    replica:
      url: s3://YOURBUCKET/db

这里的 replica 是本次读取的 0.5 系列文档写法。旧版本配置不能只换镜像标签继续使用;升级时应另读迁移指南,尤其已有备份格式和加密方式的兼容性。

原文使用 kubectl create configmap litestream --from-file=litestream.yml 创建 ConfigMap。Secret、ConfigMap、StatefulSet 和 Service 必须处于同一目标命名空间。此命令会修改集群,执行前应确认 kubeconfig 上下文和演练命名空间;本文没有对任何集群执行它。

完整结构:一个 PVC,三个容器角色

下面将原站 YAML 统一缩进并去掉重复注释,保持资源字段、镜像标签、端口和恢复参数不变,便于核对。这是原教程配置,不是生产加固后的清单。:0.5 与 :latest 是可变标签,100Mi 也是演示容量;发布前必须选定经过审核的具体版本与摘要,并根据数据量、WAL、恢复空间和保留策略规划容量。

apiVersion: v1
kind: Service
metadata:
  name: myapp
  labels:
    app: myapp
spec:
  ports:
    - port: 8080
      name: http
    - port: 9090
      name: metrics
  selector:
    app: myapp
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: myapp
spec:
  selector:
    matchLabels:
      app: myapp
  serviceName: myapp
  replicas: 1
  volumeClaimTemplates:
    - metadata:
        name: data
      spec:
        accessModes: ["ReadWriteOnce"]
        resources:
          requests:
            storage: 100Mi
  template:
    metadata:
      labels:
        app: myapp
    spec:
      volumes:
        - name: configmap
          configMap:
            name: litestream
      initContainers:
        - name: init-litestream
          image: litestream/litestream:0.5
          args: ["restore", "-if-db-not-exists", "-if-replica-exists", "/var/lib/myapp/db"]
          volumeMounts:
            - name: data
              mountPath: /var/lib/myapp
            - name: configmap
              mountPath: /etc/litestream.yml
              subPath: litestream.yml
          env:
            - name: LITESTREAM_ACCESS_KEY_ID
              valueFrom:
                secretKeyRef:
                  name: litestream
                  key: LITESTREAM_ACCESS_KEY_ID
            - name: LITESTREAM_SECRET_ACCESS_KEY
              valueFrom:
                secretKeyRef:
                  name: litestream
                  key: LITESTREAM_SECRET_ACCESS_KEY
      containers:
        - name: myapp
          image: benbjohnson/myapp:latest
          ports:
            - name: http
              containerPort: 8080
          volumeMounts:
            - name: data
              mountPath: /var/lib/myapp
        - name: litestream
          image: litestream/litestream:0.5
          args: ["replicate"]
          volumeMounts:
            - name: data
              mountPath: /var/lib/myapp
            - name: configmap
              mountPath: /etc/litestream.yml
              subPath: litestream.yml
          env:
            - name: LITESTREAM_ACCESS_KEY_ID
              valueFrom:
                secretKeyRef:
                  name: litestream
                  key: LITESTREAM_ACCESS_KEY_ID
            - name: LITESTREAM_SECRET_ACCESS_KEY
              valueFrom:
                secretKeyRef:
                  name: litestream
                  key: LITESTREAM_SECRET_ACCESS_KEY
          ports:
            - name: metrics
              containerPort: 9090

volumeClaimTemplates 为实例创建名为 data-myapp-0 的 PVC。应用、init container、sidecar 都将其挂在 /var/lib/myapp,所以看到同一数据库目录。PVC 能减少 Pod 崩溃时尚未复制数据丢失的风险,但不能保证对象存储端永远具有最后一笔事务。

原例的 ReadWriteOnce 表示单节点读写挂载,不是“只能有一个 Pod”的锁;同一节点上的多个 Pod 仍可能访问该卷。Kubernetes 的访问模式说明区分了它与 ReadWriteOncePod。能否采用后者取决于 CSI 驱动和集群支持,也不能替代应用层的故障处理设计。

两个 Litestream 容器将 ConfigMap 中的单个文件挂到 /etc/litestream.yml。原例使用 subPath;运维流程应把配置修改与可控的 Pod 重建关联起来,不要假定修改 ConfigMap 后正在运行的进程已经采用新配置。

init container 决定是否恢复,然后才启动应用

启动阶段执行 litestream restore -if-db-not-exists -if-replica-exists /var/lib/myapp/db。这两个条件分别表达:

  • -if-db-not-exists:数据库已经存在时跳过恢复并成功退出,适合保留 PVC 的普通重启。
  • -if-replica-exists:找不到副本时也允许跳过。演示应用随后会自行建立空数据库,适合明确允许首次初始化的场景。

这也构成一个容易被误解的恢复风险:期待“必须恢复历史数据”的灾后启动,却允许“没有找到备份也成功”,可能把错误桶名或缺失副本表现成全新空库。首次部署与灾难恢复应使用不同的启动策略。

整理版建议,与原 YAML 有明确差异:如果该环境规定无备份就必须阻止应用启动,应去掉 -if-replica-exists。如果还要求恢复后检查结构完整性,可在支持的版本使用下面的参数;当前参考页标明 -integrity-check 自 0.5.10 提供,不能假定任意旧的 0.5 镜像都具备它。

args:
  - restore
  - -if-db-not-exists
  - -integrity-check
  - full
  - /var/lib/myapp/db

full 对应 SQLite PRAGMA integrity_check,检查失败会返回非零退出码。它不检查外键约束,也不能证明数据达到业务要求的恢复点;并且如果本地数据库已存在而恢复被跳过,这不是对旧库的一次无条件复检。真正的验收还应包括独立的外键、关键表、记录时间与业务一致性验证。

运行阶段:应用负责业务,sidecar 负责复制

init container 完成后,应用容器与执行 replicate 的 Litestream 容器运行。两者共享卷,但只给 Litestream 注入对象存储凭据。原文 Service 暴露了内部的 8080 与 9090 端口,9090 由 addr: ":9090" 启用;没有这项配置,指标服务器不会启动。

指标端口可能暴露数据库与复制状态,必须按监控系统的来源限制可达性;声明 containerPort 本身不是防火墙。原 YAML 没有给出 NetworkPolicy、资源限制、探针和容器权限约束,读者应据镜像、存储驱动与业务恢复策略补充,不能把这些缺项视为安全默认值。

原文用 kubectl apply -f litestream-k8s.yml 部署清单。整理时没有代为执行此命令,也没有把示例环境输出当作本次结果。

分层验证,避免把 Running 当成恢复成功

首先查看 Pod 和 PVC 是否正常,再查看 init 与 sidecar 日志。下面只列读取状态和发起一次演示业务请求的命令。请求会增加演示计数,仍然是业务写入,不适合对未知生产目标直接执行。

kubectl get pods,pvc
kubectl logs myapp-0 -c init-litestream
kubectl logs myapp-0 -c litestream
kubectl exec -c myapp myapp-0 -- curl localhost:8080

原文第一次请求显示计数 1,保留 PVC 并重建 Pod 后显示 2,卷丢失并从 S3 恢复后显示 3。这些是原教程的演示数字,本文未复现。它们说明该演示计数可能被保留,不能据此推断所有表、最后事务、索引或外键均已恢复。

场景 验证的能力 不能推出的结论
保留 PVC,正常重建 Pod 数据库跨容器生命周期保存;已有数据库触发跳过恢复 对象存储备份可恢复
把副本恢复到全新、隔离的测试卷 副本可读取、恢复链可用、目标库能接受完整性与业务检查 生产故障期间零数据损失
观察复制状态和目标恢复点 衡量复制滞后与恢复目标 将来任何故障都达到相同 RPO/RTO

原文还通过删除 PVC 再删除 Pod 模拟灾难;这是可能销毁实际存储的破坏性操作,本文不提供可直接复制的删卷命令。只有在专门、可丢弃的演练环境完成备份验证和目标核对后,才应纳入经批准的演练流程。优先将副本恢复到新路径或独立测试卷,以免为验证备份先破坏唯一在线数据。

原指南说恢复成功时日志可能为空,并给出添加完整性检查后出现成功日志的方式。应同时检查退出状态和实际恢复内容;“没有报错日志”“两个容器 Running”或“页面能打开”都不是完整验收。

来源、许可与静态审查说明

主要原作:Litestream 文档贡献者;原文及完整 YAML 链接见文首。中文翻译整理和原创配图:未完纪。本次读取的指南与配套 YAML 未显示独立的文字许可声明,故未将其他代码仓库的软件许可证扩大解释为本文许可。

静态审查覆盖凭据流向、共享卷、启动条件、镜像标签、指标暴露和破坏性演练。未发现源例中的真实密钥或 shell 字符串注入;本文没有运行 kubectl、容器或恢复命令,也没有验证镜像、权限、云存储或存储驱动兼容性。上述审查不能替代目标环境的部署测试与恢复演练。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容