Tenax
中文
GitHub

Kubernetes 备份与恢复 Operator · Postgres 15–17 · PostGIS · MySQL 8.4

每一秒,牢牢守住。

Tenax 把你在 Kubernetes 中运行的 Postgres 和 MySQL 备份到你自己的 S3 存储桶,并在每次变更发生时实时流式传输。在时间窗口内选中任意一秒,就能在可查询的沙箱中拿回那一刻的数据库。数据离开集群前即已加密;崩溃即恢复的设计,让它永不挂起。

一个二进制,一个 Chart。你的存储桶,你的密钥。

时间点恢复窗口 · team/app · 策略 prod 窗口 7 天 · 链 完整

恢复到
2026-10-10 14:02:37 UTC
基础备份
20261010T020712Z-4c1e
随后重放
114
$ tenax restore team/app --at 2026-10-10T14:02:37Z --wait

看它倒带,或者自己在时间窗口上拖动。橙色刻度是每晚的全量备份,青色条带是两次全量之间的变更流。示意的是 prod 策略:每晚全量,7 天窗口。

  • 9s将 644 MiB 的 Postgres 数据库恢复到沙箱:下载、解密、恢复并运行校验
  • 4.2×存储桶中更小:683 MiB 的物理备份只占 161 MiB
  • 72MiB/s4 vCPU 上逻辑全量备份的吞吐量,流式传输,不落临时文件
  • 20ms1,000 个数据库、10 万个备份时控制台总览页的 p99

在 k3d 测试环境(4 vCPU,集群内 MinIO)中用 make test-perf 和 make test-ui 测得。数据见仓库中的 docs/PERF.md。

01工作原理五个步骤

从实时变更,到可查询的沙箱。

不在数据库里装代理,也不做卷快照。Tenax 通过网络读取变更,在数据离开 Pod 前完成封装,需要时把任意一秒带回来。

  1. 01

    流式捕获

    按计划执行全量备份,同时实时捕获每一次变更:Postgres 用 Tenax 自研的 WAL 接收器,MySQL 用 mysqlbinlog。

    WAL · binlog
  2. 02

    加密

    每个字节都先用 zstd 压缩,再用 age 按你的公钥加密,在 Pod 内一次流式完成,不落临时文件。

    zstd → age
  3. 03

    存储

    并行分片上传到任意 S3 兼容存储桶,每个对象都有 SHA-256,清单最后写入。

    *.zst.age · manifest.json
  4. 04

    精确到秒恢复

    给出一个时间或备份 ID。Tenax 选好基础备份,并把变更重放到那一秒。

    --at 14:02:37
  5. 05

    打开沙箱

    结果落到一个带 TTL 的私有数据库和只读查询控制台中。在恢复被判定成功之前,你的校验会先运行。

    --ttl 6h

02恢复四种目标

恢复到任何地方,不碰生产环境。

一次恢复就是一个普通的 Kubernetes 资源。指定时间或备份 ID,选好目标。Tenax 规划基础备份和要重放的变更,并在判定成功前运行你的校验。

SANDBOX

一次性副本

带 TTL、凭据和只读查询控制台的私有数据库,到期自动清理。

--to sandbox --ttl 6h
NEW

新数据库

先在中转沙箱中恢复并校验,再以新名称复制。绝不覆盖已有对象。

--to new --database app_copy
IN PLACE

真正回滚

先做一次安全备份,并要求你输入目标名称确认。需要 restorer-admin 角色。

--to in-place --confirm app
DOWNLOAD

导出文件

解密并压缩的逻辑导出,通过一小时后失效的链接提供。全程审计。

--to download

03演示一分钟

一分钟,从总览到恢复。

在演示数据目录上运行的真实命令和真实控制台:列出所有数据库,查看夜里改了什么,再把它恢复到沙箱。

0:58 · 有声音 · 1.9 MB

控制台

Tenax 控制台总览:九个数据库的最近全量备份、可恢复窗口、延迟、存储大小和健康状态
所有数据库一目了然:最近全量备份、可恢复窗口、流延迟、大小和健康状态。
Tenax 控制台的数据库页面:可恢复时间线、行数和大小趋势,以及带校验标记的版本列表
单个数据库:可恢复时间线、趋势和每个版本,已校验的恢复会被标出。
Tenax 控制台中两个备份的差异:新增一列、一张表被清空、每张表的行数和大小
两个版本并排对比:应用了什么迁移、哪些列变了、每张表的行数和大小。

清楚两个版本之间改了什么。

每个备份都带有表、行数和大小的清单,每个变更段都带有它所做操作的摘要。所以在恢复之前,你就能看到哪个版本还保留着丢失的行。

  • 任意两个备份之间的差异。 表结构变化、行数和每张表的大小。
  • 每个 WAL 或 binlog 段的变更摘要。 每张表的插入、更新、删除和 DDL,22:48 的一次 TRUNCATE 一眼就能找到。
  • 脱敏预览。 按你指定的列脱敏的示例行,用无法解密数据本身的目录密钥即可读取。

命令行

命令行
$ tenax targets
TARGET              ENGINE    ORG/PROJECT/ENV      LAST FULL             PITR WINDOW                                  STORED    HEALTH
analytics/events    postgres  acme/data/staging    2026-10-07 02:11:47Z  -                                            19.1GiB   ok
auth/users          postgres  acme/platform/prod   2026-10-07 02:11:06Z  2026-09-23 02:11:06Z → 2026-10-07 18:50:52Z  1.5GiB    ok
billing/invoices    mysql     acme/billing/prod    2026-10-07 02:09:03Z  2026-09-23 02:09:03Z → 2026-10-07 18:50:52Z  6.7GiB    ok
billing/ledger      postgres  acme/billing/prod    2026-10-07 02:08:22Z  2026-09-23 02:08:22Z → 2026-10-07 18:50:52Z  14.3GiB   ok
maps/geo            postgres  acme/maps/prod       2026-10-07 02:10:25Z  2026-09-23 02:10:25Z → 2026-10-07 18:50:52Z  23.3GiB   ok
shop-dev/orders-db  postgres  acme/shop/dev        2026-10-07 02:12:28Z  -                                            17.4MiB   ok
shop/catalog-db     postgres  acme/shop/prod       2026-10-07 02:07:41Z  2026-09-23 02:07:41Z → 2026-10-07 18:50:52Z  731.0MiB  ok
shop/orders-db      postgres  acme/shop/prod       2026-10-07 02:07:00Z  2026-09-23 02:07:00Z → 2026-10-07 18:50:52Z  9.6GiB    ok
web/wordpress       mysql     acme/marketing/prod  2026-10-07 02:09:44Z  2026-09-23 02:09:44Z → 2026-10-07 18:50:52Z  607.4MiB  ok
命令行
$ tenax diff 20261006T020700Z-bd79 20261007T020700Z-6da5
from  20261006T020700Z-bd79 (2026-10-06 02:07:00Z)
to    20261007T020700Z-6da5 (2026-10-07 02:07:00Z)

20261007 applied (was 20261006)
column orders.gift_note added
table audit_log +158,175 rows (+2%)
table order_items +62,923 rows (+1%)
table orders +15,655 rows (+1%)
table coupons −3,120 rows (−100%)
table customers +531 rows (+1%)

14 segments in between:
  00000001000000A100000000  18:51:32–19:20:32 · 800 commits · audit_log +3348 / ~1116 / −83 · order_items +1327 / ~442 / −33 · orders +330 / ~110 / −8 · customers +11 / ~3 / −0
  00000001000000A100000001  19:21:32–19:50:32 · 813 commits · audit_log +3352 / ~1116 / −83 · order_items +1328 / ~442 / −33 · orders +330 / ~110 / −8 · customers +13 / ~3 / −0
  00000001000000A100000002  19:51:32–20:20:32 · 826 commits · audit_log +3356 / ~1116 / −83 · order_items +1329 / ~442 / −33 · orders +330 / ~110 / −8 · customers +15 / ~3 / −0
命令行
$ tenax restore shop/orders-db --at 20261006T020700Z-bd79 --ttl 6h --reason "coupons truncated"
restore orders-db-sandbox-60414a15fc created (sandbox from 20261006T020700Z-bd79, 0 segments)
restore  shop/orders-db-sandbox-60414a15fc

$ tenax sandbox list
NAME      ENGINE    SOURCE            OWNER               PHASE         EXPIRES               IN-CLUSTER
sbx-7f3a  postgres  shop/orders-db    maria@acme.example  Ready         2026-10-08 00:37:32Z  tenax-sbx-sbx-7f3a.tenax-sandboxes.svc:5432
sbx-c210  mysql     billing/invoices  dev@acme.example    Provisioning  -

来自仓库中演示服务器的输出:运行 go run ./hack/demo,再把命令行或浏览器指向它。

控制台、命令行和 API

一个用 OpenAPI 描述的 JSON API,角色来自 Cloudflare Access。每次写入和每次查询都会被审计。

原生 GitOps

存储、策略、目标和恢复都是 CRD。保留层级、告警和 Grafana 仪表盘随 Chart 一起提供。

经过验证,而非寄望

定时校验恢复会在真实恢复上运行你的 SQL 检查,并把备份标记为已校验。恢复耗时以指标形式导出。

04可靠性崩溃即恢复

永不挂起,kill -9 也不怕。

备份总在深夜以最无聊的方式失败:卡住的挂载、死掉的 Pod、写了一半的文件。Tenax 的设计让这些情况既卡不住它,也冒充不了备份。

绝不卡死

不挂载、不用块设备,只通过网络访问数据库。每条数据流都有进度看门狗,每个工具都运行在独立的进程组中。

仅崩溃设计

随时杀掉任何 Pod 都没关系。备份只有在清单最后写入后才算存在,混沌测试会在备份中途杀掉 Tenax 来证明这一点。

数据在 Job 中流动,从不经过控制器。

控制器只负责调度。mover 和 streamer 通过一条流式管道把字节直接送进你的存储桶,restorer 再把它们取回。

对生产温和

一个从不写入的最小权限数据库用户。按主机分配槽位、限制带宽,服务器繁忙时由负载保护推迟备份。

变更流

Postgres 使用复制槽上的 Tenax 自研 WAL 接收器,MySQL 使用 mysqlbinlog。即使数据库很安静,窗口也能延伸到现在。

天生就快

无临时文件的流式管道、并行分片上传、多线程 zstd,以及支撑毫秒级响应控制台的内存数据目录。

轻到你会忘了它的存在。

Tenax 是一个 Go 二进制文件。控制器和 API 只负责调度和读取;数据在完成后即退出的短生命周期 Job 中搬运,两次备份之间集群里不会留下任何重负载。

54MB一个静态二进制:控制器、API 与控制台、mover、streamer、restorer 和命令行
18MB压缩下载,无需安装运行时,没有 sidecar
21MiB控制器空闲时内存,同时监视一整个集群的数据库
19MiBAPI 与控制台空闲时内存,数据目录常驻内存

k3d 测试集群(12 个数据库)在端到端和混沌测试之后由 kubelet 报告的工作集;空闲 CPU 只有几毫核。每个启用时间点恢复的数据库还会运行一个 streamer,流式传输时约占 70 MiB。

05安全源头加密

存储桶里只有密文。

加密在 Pod 内、上传之前完成,使用的密钥 Tenax 并不持有。访问按角色划分,每一次写入和查询都会留下审计记录。

存储桶里看到的内容
$ head -c 200 data/part-00001.zst.age
age-encryption.org/v1
-> X25519 Qm9yZWQ/IE5vdGhpbmcgdG8gc2VlIGhlcmUu…
--- 7Hc1kS0zR2QeYc…
ÌÞ#ø§ñ¹t¤¢Ý}ϰë։ÿ]Wœ3Ð÷…

每个数据对象都是按你的接收者加密的 age 文件(X25519)。只有清单是明文 JSON,其中不含任何机密。

  • 离开前就已加密

    每个对象都在客户端用 age 加密到你的密钥。存储桶永远看不到明文,控制台的目录密钥也读不了数据。

  • 读不了数据的目录密钥

    控制台使用独立的目录身份读取清单和脱敏预览,无法解密转储、基础备份、WAL 或 binlog。

  • 端到端校验

    清单记录每个存储对象的 SHA-256,每次读取结束时都会核对;清单本身最后通过条件 PUT 写入。

  • 机密不外露

    数据库凭据通过密封的内存文件交给子进程,绝不出现在 argv 或环境变量中。日志会遮蔽任何看起来像机密的内容。

  • 角色与审计

    通过 Cloudflare Access 登录,分为从 viewer 到 restorer-admin 的四种角色。原地恢复需要最高角色并手动输入确认。

  • 锁定的 Pod

    非 root、只读根文件系统、移除全部 capabilities、seccomp 使用 RuntimeDefault。数据路径上的 Pod 只挂载 emptyDir。

06对比定时任务

远不止存储桶里的一份夜间导出。

当你需要……cron + pg_dump 脚本Tenax
回到 09:41:27昨晚的导出,最多丢失 24 小时窗口内的任意一秒
证明备份能恢复在故障中才发现带你的检查的定时校验恢复
找到还有丢失行的版本恢复好几个逐个查看控制台中的差异和变更摘要
无风险地查看覆盖到共享的预发环境带 TTL 的私有沙箱
让存储桶看不到内容服务端加密(如果配置了)用你的密钥做客户端 age 加密
扛住备份中途的节点故障一个看似完整的半截文件清单最后写入之前一切都不算数
确认它仍在工作毫无动静针对备份逾期、链断裂和延迟的告警

07安装约十分钟

从零到可恢复。

  1. 安装 Chart

    CRD、控制器、API 与控制台、RBAC、告警和仪表盘。

    helm install tenax charts/tenax -n tenax-system --create-namespace \
      --set image.repository=registry.example.com/tenax/tenax --set image.tag=v0.1.0 \
      --set 'targetNamespaces={team-a}'
  2. 指定存储桶和你的密钥

    任何兼容 S3 的存储。用 age-keygen 生成 age 密钥,并离线保存一份副本。

    apiVersion: tenax.republic.global/v1alpha1
    kind: BackupStore
    metadata: { name: main }
    spec:
      s3:
        endpoint: https://s3.example.com
        region: us-east-1
        bucket: db-backups
        credentialsSecret: { namespace: tenax-system, name: tenax-s3 }
      encryption:
        ageRecipients: [age1…]   # 只放公钥;私钥离线保存
  3. 在数据库旁声明一个目标

    用 docs/sql/ 中的 SQL 创建 tenax 用户,再把目标提交到 git。

    apiVersion: tenax.republic.global/v1alpha1
    kind: BackupTarget
    metadata: { name: app, namespace: team-a }
    spec:
      engine: postgres
      endpoint: { host: postgres, port: 5432 }
      databases: [app]
      policy: prod             # 每晚全量,7 天时间点窗口
      credentials:
        secretRef: { name: tenax-postgres }
  4. 特意恢复一次

    第一次全量备份会自动开始。然后试试那个你希望永远用不上的操作。

    tenax targets
    tenax restore team-a/app --at 2026-10-07T09:41:27Z --wait
    tenax query <sandbox> -e "select count(*) from orders"

完整指南、每个 CRD 字段和 REST API 都在文档中(英文)。

08常见问题直接的回答

工程师最先问的问题。

Tenax 支持哪些数据库?

Postgres 15、16、17(含 PostGIS),支持物理或逻辑全量备份以及 WAL 流式传输;MySQL 8.4,支持逻辑全量备份以及 binlog 流式传输。

最远能恢复到什么时候?精度如何?

保留层级中留存的任意全量备份,以及策略所设时间点恢复窗口内的任意一秒。示例 prod 策略保留 7 天窗口,以及 14 个每日、8 个每周、12 个每月和 5 个每年的全量备份。

Tenax 或存储服务商能读到我的数据吗?

存储桶读不到:每个对象在上传前都用 age 按你的接收者加密。控制台用无法解密数据本身的目录密钥读取清单。恢复时使用你提供的 age 身份解密,请保留一份离线副本。

备份进行到一半时 Pod 挂了会怎样?

不会留下任何算数的东西。只有在每个对象都上传并校验完、清单写入之后,备份才存在。半途而废的运行没有清单,因此永远不会被用于恢复。混沌测试套件会在备份中途杀掉 Tenax 来验证这一点。

Tenax 会挂载卷或使用快照吗?

不会。它只通过网络连接数据库,Pod 除了 emptyDir 什么都不挂载:没有 hostPath、FUSE、iSCSI 或块设备,默认也不用 PVC。卡住的存储驱动不会让备份挂起。

Tenax 需要数据库提供什么?

每个数据库一个最小权限、从不写入的 tenax 用户,用 docs/sql/ 中的 SQL 创建。即使没有复制权限,Tenax 仍能做逻辑备份,并告诉你要授予哪些权限才能恢复到任意一秒。

Tenax 是开源的吗?

它以 The Republic Lex Aequa License 1.0 公开源代码。任何人都可以免费使用、修改和再分发,包括在生产环境和商业公司中,用来保护自己的数据。向第三方出售或作为服务提供则保留给版权方。

在需要之前,先把恢复测一遍。