Kubernetes 備份 & 還原 Operator · Postgres 15–17 · PostGIS · MySQL 8.4
每一秒,牢牢守住。
- 還原到
- 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 前完成封裝,需要時把任意一秒帶回來。
- 01
串流擷取
依排程執行完整備份,同時即時擷取每一次變更:Postgres 用 Tenax 自行開發的 WAL 接收器,MySQL 用 mysqlbinlog。
WAL · binlog - 02
加密
每個位元組都先用 zstd 壓縮,再用 age 依你的公鑰加密,在 Pod 內一次串流完成,不落暫存檔。
zstd → age - 03
儲存
並行分片上傳到任意 S3 相容儲存貯體,每個物件都有 SHA-256,清單最後寫入。
*.zst.age · manifest.json - 04
精確到秒還原
給出一個時間或備份 ID。Tenax 選好基礎備份,並把變更重放到那一秒。
--at 14:02:37 - 05
開啟沙盒
結果落到一個帶 TTL 的私有資料庫和唯讀查詢控制台中。在還原被判定成功之前,你的驗證會先執行。
--ttl 6h
02還原四種目標
還原到任何地方,不碰正式環境。
一次還原就是一個普通的 Kubernetes 資源。指定時間或備份 ID,選好目標。Tenax 規劃基礎備份和要重放的變更,並在判定成功前執行你的驗證。
一次性副本
帶 TTL、憑證和唯讀查詢控制台的私有資料庫,到期自動清理。
--to sandbox --ttl 6h新資料庫
先在暫存沙盒中還原並驗證,再以新名稱複製。絕不覆蓋已有物件。
--to new --database app_copy真正回滾
先做一次安全備份,並要求你輸入目標名稱確認。需要 restorer-admin 角色。
--to in-place --confirm app匯出檔案
解密並壓縮的邏輯匯出,透過一小時後失效的連結提供。全程稽核。
--to download03示範一分鐘
一分鐘,從總覽到還原。
在示範資料目錄上執行的真實命令和真實控制台:列出所有資料庫,檢視夜裡改了什麼,再把它還原到沙盒。
控制台
清楚兩個版本之間改了什麼。
每個備份都帶有表、列數和大小的清單,每個變更區段都帶有它所做操作的摘要。所以在還原之前,你就能看到哪個版本還保留著遺失的列。
- 任意兩個備份之間的差異。 表結構變化、列數和每張表的大小。
- 每個 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 的設計讓這些情況既卡不住它,也冒充不了備份。
- data/part-00001.zst.age
- data/part-00002.zst.age
- data/part-00003.zst.age
- manifest.json
- data/part-00001.zst.age
- data/part-00002.zst.age
- data/part-00003.zst.age
- manifest.json
絕不卡死
不掛載、不用區塊裝置,只透過網路存取資料庫。每條資料流都有進度看門狗,每個工具都執行在獨立的程序群組中。
崩潰即復原
隨時殺掉任何 Pod 都沒關係。備份只有在清單最後寫入後才算存在,混沌測試會在備份中途殺掉 Tenax 來證明這一點。
資料在 Job 中流動,從不經過控制器。
控制器只負責排程。mover 和 streamer 透過一條串流管道把位元組直接送進你的儲存貯體,restorer 再把它們取回。
對正式環境溫和
一個從不寫入的最小權限資料庫使用者。依主機分配槽、限制頻寬,伺服器繁忙時由負載保護延後備份。
變更串流
Postgres 使用複製槽上的 Tenax 自行開發的 WAL 接收器,MySQL 使用 mysqlbinlog。即使資料庫很安靜,時間窗也能延伸到現在。
天生就快
無暫存檔的串流管道、並行分片上傳、多執行緒 zstd,以及支撐毫秒級回應控制台的記憶體資料目錄。
輕到你會忘了它的存在。
Tenax 是一個 Go 二進位檔。控制器和 API 只負責排程和讀取;資料在完成後即退出的短生命週期 Job 中搬運,兩次備份之間叢集裡不會留下任何重負載。
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安裝約十分鐘
從零到可還原。
安裝 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}'指定儲存貯體和你的金鑰
任何相容 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…] # 只放公鑰;私鑰離線儲存
在資料庫旁宣告一個目標
用 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 }
特意還原一次
第一次完整備份會自動開始。然後試試那個你希望永遠用不上的操作。
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 公開原始碼。任何人都可以免費使用、修改和再分發,包括在正式環境和商業公司中,用來保護自己的資料。向第三方出售或作為服務提供則保留給著作權人。