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.run/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.run/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 公開原始碼。任何人都可以免費使用、修改和再分發,包括在正式環境和商業公司中,用來保護自己的資料。向第三方出售或作為服務提供則保留給著作權人。

在需要之前,先把還原測一遍。