From e4244bf4feaec76370d609ec05fc1451d1ce47c5 Mon Sep 17 00:00:00 2001 From: Feng Ruohang Date: Tue, 10 Nov 2026 13:47:15 +0800 Subject: [PATCH 1/2] docs: design record for the lifecycle history under old version headers Record why, on 20261109, lifecycle expiry of data whose version headers a MinIO release before RELEASE.2024-04-28 stored is refused for good once part of the drives of its erasure set were healed: the affected deployments and expiries, the root cause in the revision key, the options considered, the fix to versionRevision, its verification, both review rounds and the residual limits. English and Chinese, with the index entries. Co-Authored-By: Claude Fable 5.2 Signed-off-by: Feng Ruohang --- content/blog/design/_index.md | 4 + content/blog/design/_index.zh.md | 4 + .../design/lifecycle-history-old-headers.md | 201 ++++++++++++++++++ .../lifecycle-history-old-headers.zh.md | 201 ++++++++++++++++++ 4 files changed, 410 insertions(+) create mode 100644 content/blog/design/lifecycle-history-old-headers.md create mode 100644 content/blog/design/lifecycle-history-old-headers.zh.md diff --git a/content/blog/design/_index.md b/content/blog/design/_index.md index 3eab24f7..d13b7ae7 100644 --- a/content/blog/design/_index.md +++ b/content/blog/design/_index.md @@ -13,6 +13,10 @@ cascade: Design records document the engineering decisions behind PGSTY SILO: root causes, S3 and IAM protocol invariants, trade-offs against rejected alternatives, operational limits, and regression coverage. +## November 10: Found in Release Review {#november-10-found-in-release-review} + +- [Lifecycle History: One Version Under the Headers of Two Releases](/blog/design/lifecycle-history-old-headers/) ([#332](https://github.com/pgsty/silo/pull/332)): Why lifecycle expiry of data written by a MinIO release before 2024-04-28 is refused for good once some drives of its erasure set were replaced, and why taking two release-dependent header fields out of the revision key is the minimum complete repair. + ## November 9: Console Languages {#november-9-console-languages} - [Adding Multilingual Support to SILO](/blog/design/console-languages/): Ten UI languages shipped in [Console 2.7.0](/blog/release/console-2.7.0/) and embedded in [SILO Server 20261109](/blog/release/silo-20261109/) ([Console 2.6.0](/blog/release/console-2.6.0/) shipped English and Chinese only; includes Portuguese from [#83](https://github.com/pgsty/silo-console/pull/83) and Turkish requested in [silo#277](https://github.com/pgsty/silo/issues/277)), maintenance tiers, English fallback, and Arabic RTL, with measured gzip and cache tradeoffs behind per-language assets and eager loading. diff --git a/content/blog/design/_index.zh.md b/content/blog/design/_index.zh.md index 4c98bdc9..a99ad952 100644 --- a/content/blog/design/_index.zh.md +++ b/content/blog/design/_index.zh.md @@ -13,6 +13,10 @@ cascade: 设计归档记录 PGSTY SILO 的核心工程决策:缺陷根因、S3 与 IAM 协议不变量、备选方案取舍、运维边界与回归测试索引。 +## 2026-11-10 发布复查中发现 {#november-10-found-in-release-review} + +- [生命周期版本历史:同一个版本,两个发行版写的头部](/zh/blog/design/lifecycle-history-old-headers/)([#332](https://github.com/pgsty/silo/pull/332)):2024-04-28 之前的 MinIO 写入的数据,在纠删集里换过一部分盘之后,生命周期过期为什么会被永久拒绝,以及为什么把两个随写入版本而变的头部字段从修订标识里拿掉就是最小而完整的修复。 + ## 2026-11-09 Console 多语言 {#november-9-console-languages} - [为 SILO 提供多语言支持](/zh/blog/design/console-languages/):随 [Console 2.7.0](/zh/blog/release/console-2.7.0/) 发布并内嵌于 [SILO Server 20261109](/zh/blog/release/silo-20261109/) 的十语言界面([Console 2.6.0](/zh/blog/release/console-2.6.0/) 仅含中英文;涵盖 [#83](https://github.com/pgsty/silo-console/pull/83) 葡语贡献与 [silo#277](https://github.com/pgsty/silo/issues/277) 土耳其语需求),记录维护分级、英文回退与阿拉伯语 RTL,以及支持逐语言分包、全量加载决策的 gzip 和缓存实测。 diff --git a/content/blog/design/lifecycle-history-old-headers.md b/content/blog/design/lifecycle-history-old-headers.md new file mode 100644 index 00000000..75828e61 --- /dev/null +++ b/content/blog/design/lifecycle-history-old-headers.md @@ -0,0 +1,201 @@ +--- +title: "Lifecycle History: One Version Under the Headers of Two Releases" +linkTitle: "Lifecycle History and Old Headers" +date: 2026-11-10 +lastmod: 2026-11-10 +author: "PGSTY SILO Maintainers" +summary: > + Why lifecycle expiry of data written by a MinIO release before 2024-04-28 is refused for good once some drives of its erasure set were replaced, and why taking two release-dependent header fields out of the revision key is the minimum complete repair. +tags: [Design, Lifecycle, Erasure Coding, Compatibility] +weight: 10 +draft: false +url: "/blog/design/lifecycle-history-old-headers/" +--- + +This record covers a defect found in a maintainer review of `RELEASE.2026-11-09T00-00-00Z` ([d99829238](https://github.com/pgsty/silo/commit/d998292387ccf1d2907749e0fb1575ef5fb93fda)). That release introduced it with [9f7b49369](https://github.com/pgsty/silo/commit/9f7b49369d4cbc1a5506af4bff05df05c8c8b6d3), part of the multi-pool lifecycle fix ([#278](https://github.com/pgsty/silo/issues/278)). The fix is [PR #332](https://github.com/pgsty/silo/pull/332); there is no separate GitHub issue. + +**Status:** the fix and its regression tests are in [PR #332](https://github.com/pgsty/silo/pull/332), approved by an adversarial review in its second round; the first round changed the fix, see [Review history](#review). Not merged or released. + +## Short answer {#tldr} + +Since 20261109, a lifecycle expiry that depends on the other versions of a key first reads the key's version history from the drives of its erasure set, and accepts a version only when enough drives hold the same revision of it. A revision was identified by the version's header as the drive stores it, plus a digest of its metadata. + +The stored header is not the same on every drive of an old version. A drive keeps the header that the release which wrote the version there stored, and MinIO releases before `RELEASE.2024-04-28` stored a header without erasure-coding fields. When a drive is replaced, heal writes the version to it with a current header and leaves the other drives alone. Reads and heal take both headers for the same version. The history did not: it counted two revisions, and when neither was on enough drives it refused the key. + +The refusal is permanent. Every scanner cycle retries the expiry and fails the same way, GET works, and heal reports nothing to repair. No data is lost or deleted; versions that should expire stay. + +The fix adds three lines to one function, `versionRevision` in `cmd/erasure-server-pool-lifecycle.go`: the revision key takes the two header fields that depend on the writing release as the current release computes them from the version's metadata, instead of from the drive. Upgrading is the whole remedy; nothing on disk changes. + +## Classification {#assessment} + +| Aspect | Assessment | +| -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Nature | Regression in `RELEASE.2026-11-09T00-00-00Z`: the new version history identifies a revision too strictly. It fails closed. | +| Affected deployments | Data taken over from MinIO whose version headers were stored by a release from `RELEASE.2021-11-24` until before `RELEASE.2024-04-28`, in an erasure set where some drives, but not nearly all, were replaced and healed since. | +| Affected expiries | With several pools: every ILM expiry in a versioned or versioning-suspended bucket, except the expiry of a restored copy. With any number of pools, one included: `ExpiredObjectAllVersions`, `DelMarkerExpiration`, and batch expiration that purges all versions. | +| Not affected | Data written by SILO or by MinIO from `RELEASE.2024-04-28` on; sets without a healed drive; on a single pool, the expiry of one version or one delete marker (`Expiration`, `NoncurrentVersionExpiration`, `ExpiredObjectDeleteMarker`); reads, writes, heal and replication. | +| Data safety | Nothing is deleted or hidden. The affected versions stay readable. | +| Severity | High where it applies: the rules silently stop for the affected keys, storage grows and retention periods are not enforced. Low reach: it needs old data and a particular share of healed drives. | +| Effort | Small: one function, no storage RPC, no on-disk format, no configuration. | +{.wide .expand} + +## When it happens {#trigger} + +Three conditions must hold together: + +1. **An old header.** The version's entry in `xl.meta` was stored by a MinIO release from `RELEASE.2021-11-24`, the first with a per-version header, until before `RELEASE.2024-04-28`, the first to put the erasure coding into it. Later releases rewrite `xl.meta` without touching the headers of the versions they do not change, so a version keeps its old header while newer versions are added to the key. +2. **A partial heal.** Some drives of the erasure set were replaced, or lost the key for another reason, and heal wrote the version to them. These drives hold a current header. +3. **Neither group is large enough.** A revision counts when it is on more than half of the drives of the set and on the read quorum of the version: `max(n/2+1, n − parity)` drives. + +| Erasure set | Drives a revision needs | Refused when this many drives were healed | +| --------------- | ----------------------- | ----------------------------------------- | +| 16 drives, EC:4 | 12 | 5 to 11 (reproduced) | +| 12 drives, EC:4 | 8 | 5 to 7 | +| 8 drives, EC:4 | 5 | 4 | +| 4 drives, EC:2 | 3 | 2 | +| 2 drives | 2 | 1 | +{.wide .expand} + +Only the first row was run; the others follow from the same rule. Small sets are hit soonest: one replaced drive in a two-drive set is enough. + +What an operator sees for an affected key: + +- `minio_ilm_expiry_incomplete_total{reason="history_unavailable"}` grows with every scanner cycle; +- the ILM trace reports `result=failed` with that reason, the audit log has an `ILMExpiry` entry tagged `ilm-incomplete-reason=history_unavailable`, and the server log says the expiry was not done because storage resources are insufficient for the write operation; +- GET and HEAD of the version succeed, and a heal of the key reports every drive as healthy. + +## Root cause {#root-cause} + +Line numbers are for [d99829238](https://github.com/pgsty/silo/commit/d998292387ccf1d2907749e0fb1575ef5fb93fda). + +1. `objectVersions` reads `xl.meta` from every drive of the set and passes the version lists to `authoritativeHistory` (`cmd/erasure-server-pool-lifecycle.go:74-133`). +2. `authoritativeHistory` groups the drives of each version by `versionRevision` and accepts the largest group only when it has the majority and the read quorum (`:342`). Any version without such a group refuses the whole key with a read-quorum error. +3. `versionRevision` starts the key with the stored header, unchanged: `key := revisionKey{header: ver.header}` (`:222`). Only a legacy (`xl.json`) version had its signature replaced, because that signature covers per-shard checksums. +4. The version header has seven fields. Two of them depend on which release stored it: + +| Field | What differs between releases | +| ------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `EcN`, `EcM` | Added to the header by upstream [#19534](https://github.com/minio/minio/pull/19534), first released in `RELEASE.2024-04-28`. A header stored before reads as 0/0. | +| `Signature` | A 32-bit hash of the version's metadata. Until upstream [#14125](https://github.com/minio/minio/pull/14125), first released in `RELEASE.2022-01-25`, it counted part ETags that are all empty, and a version written by a plain PUT before `RELEASE.2021-11-24` has such ETags. A header that one of the releases in between stored for such a version carries a signature that no later release computes. | +| `VersionID`, `ModTime`, `Type` | The same on every drive. | +| `Flags` | Derived from the metadata. One flag, whether a transitioned version uses its data directory, also depends on whether a restored copy had expired when the drive wrote the entry. It does not depend on the release and stays in the key as stored; see [Options](#options). | +{.wide .expand} + +5. Heal decides by the decoded metadata, not by the header. It writes the version to the drives that lack it, with the header its own release computes, and finds the other drives healthy. Nothing rewrites their headers afterwards. + +The rest of the server already treats these drives as one version. `matchesNotStrict` ignores the signature, `matchesEC` accepts a header without erasure coding next to one with it, and the listing comparison `metaCacheEntry.matches` compares "without considering EC" when only one side has it. The listing tolerance came with the upstream commit that added the fields, and `matchesEC` followed in upstream [#20351](https://github.com/minio/minio/pull/20351). The revision key added in 20261109 did not carry it over. + +Everything the two fields say is also in the digest: the erasure coding is part of the version's metadata, and the signature is a weaker hash of the same metadata. Leaving them in the key added no information; it only tied the key to the release that wrote the drive. + +## Where the behavior came from {#history} + +### Provenance {#provenance} + +**Introduced by SILO, in `RELEASE.2026-11-09T00-00-00Z`.** No earlier SILO release and no upstream MinIO release has the authoritative history or the revision key. + +| Ingredient | Commit | PR | First release | +| -------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------- | --------------------------------------------------- | ------------------------------ | +| Per-version header in `xl.meta`, with a signature | [faf013ec8](https://github.com/pgsty/silo/commit/faf013ec84051b92ae0f420a658b8d35bb7bb000) (Klaus Post) | [#13573](https://github.com/minio/minio/pull/13573) | `RELEASE.2021-11-24T23-19-33Z` | +| Signature no longer counts empty part ETags | [0012ca8ca](https://github.com/pgsty/silo/commit/0012ca8ca5f816019d83c39b9018dc7992377e3b) (Klaus Post) | [#14125](https://github.com/minio/minio/pull/14125) | `RELEASE.2022-01-25T19-56-04Z` | +| Erasure coding in the header; comparisons tolerate its absence | [5f774951b](https://github.com/pgsty/silo/commit/5f774951b1cc464b3d8ebe39750fde1a2d09f23d) (Klaus Post) | [#19534](https://github.com/minio/minio/pull/19534) | `RELEASE.2024-04-28T17-53-50Z` | +| Revision key that includes the stored header | [9f7b49369](https://github.com/pgsty/silo/commit/9f7b49369d4cbc1a5506af4bff05df05c8c8b6d3) (SILO) | part of the #278 fix | `RELEASE.2026-11-09T00-00-00Z` | +{.wide .expand} + +## Options {#options} + +### Option 1: rebuild the whole header from the metadata {#option-1} + +The first suggestion: compute the header from the decoded version, as `restoreNeutralRevision` does for a version without its restore fields. Rejected. The flag that tells whether a transitioned version uses its data directory is computed from the restore expiry date and the clock. A key rebuilt this way changes when a restored copy expires, and the native restore compares keys it took before a commit with keys it reads after it. `fileInfoRevisions` accepts both values of the stored flag for exactly this reason, and relies on the key carrying the flag as stored. + +### Option 2: take the erasure coding from the metadata only {#option-2} + +Two lines, and it repairs the reported case. Insufficient: a version written by a plain PUT before `RELEASE.2021-11-24`, whose key was rewritten by a release before `RELEASE.2022-01-25` and partly healed later, still has two signatures on its drives and stays refused. The unit test for that header fails with this option alone. + +### Option 3: rewrite old headers on the drives {#option-3} + +Let heal or the scanner bring every drive to the current header. Rejected: it rewrites the metadata of every old object to repair a comparison, it makes heal find work on healthy drives, and until it has finished the expiry stays refused. + +### Option 4: drop the header from the key {#option-4} + +Identify a revision by the digest alone. Rejected: the stored data-directory flag is not in the digest, and the merge that orders the history needs one header for all drives of a revision. It is a larger change for the same result. + +### Option 5: take the signature from the digest {#option-5} + +The first version of this fix: the erasure coding from the metadata, and the signature from the digest for every version, as the key already does for a legacy version. Rejected after review. The signature is also what orders two versions of the same modification time in the merge, and each drive's version list keeps the order the drive gave it. With two such versions, each on 12 of 16 drives, 20261109 accepted all 40 layouts tried and this option refused 21. + +### Option 6: compute both fields as the current release does {#option-6} + +The erasure coding and the signature of an object version, computed from its decoded metadata by the functions that write a header today. For a version that a current release wrote, the result is the stored header, so nothing changes for it. Chosen. + +## Proposed fix {#fix} + +In `versionRevision`, after the digest of a decodable version is computed: + +```go +switch v.Type { +case LegacyType: + copy(key.header.Signature[:], key.digest[:]) +case ObjectType: + key.header.Signature = v.ObjectV2.Signature() + key.header.EcN, key.header.EcM = uint8(v.ObjectV2.ErasureN), uint8(v.ObjectV2.ErasureM) +} +``` + +The legacy case is unchanged; the object case is new. A delete marker keeps its stored header: it has no erasure coding under any release, and no release changed how its signature is computed for the same metadata. A version whose metadata does not decode keeps its stored header and still matches only the same bytes. + +The history that `authoritativeHistory` returns carries the header of the accepted key on every drive, as before. Nothing is written to a drive, and no key is stored anywhere: every read computes its keys again. + +## Why it is sufficient and necessary {#sufficiency} + +- **Sufficient.** Of the header fields, only the erasure coding and the signature depend on the release that stored them, and the key now has both as the current release computes them. The same function serves both readers of revision keys: the lifecycle history, and the generation a native restore selects. With the fix, the history accepts an old version on any number of healed drives, from none to all. +- **Necessary.** With the erasure coding alone, the old-signature header stays refused for 5 to 11 healed drives of 16. With the signature alone, the reported case stays refused. +- **Safe.** The digest covers the erasure coding and everything the signature hashed, so two revisions that differ in either still have two keys; a control test checks another erasure coding under the same header. A real split, such as a metadata update that reached half of the drives, is refused as before. +- **Unchanged for current data.** The key of a version that a current release wrote carries its header exactly as stored; a control in the end-to-end test checks this on written objects. For such data the history decides as 20261109 does. +- **Minimal.** One function and three added lines. No storage RPC, no on-disk format, no configuration, no change to heal or to any read path. + +## Cost and compatibility {#cost} + +- Cost: one more signature for each object version of each drive, a second encoding and a 64-bit hash of metadata the digest already reads. No extra I/O. +- On disk: nothing changes. Old headers stay as they are. +- Rolling upgrade: each node decides with its own code. A node that still runs 20261109 keeps refusing the affected keys; an upgraded node expires them. No coordination is needed. +- After the upgrade, the scanner queues the refused expiries again and they run. A deployment that was affected for some time should expect the delayed deletions to happen then. +- Native restore: `readRestoreGeneration` took such drives for a failed restore-status write and used its fallback, a generation without restore fields. It now finds one generation directly. The expiry of a restored copy refuses a generation found by the fallback, so by the code it no longer refuses these; that expiry was not run. + +## Residual limits {#limits} + +- **A restored copy that expired before a heal.** This is a separate defect of the same shape, and this fix does not repair it. The flag that stays in the key as stored says whether a transitioned version uses its data directory. A drive that wrote the version while its restored copy was on disk has it set; by the code, a drive healed after the copy expired has it clear. With such headers on 8 and 8 of 16 drives, the history is refused and the restore generation is found only by its fallback, which the expiry of the restored copy refuses. So the copy is not removed and the drives do not come together. The headers were crafted for this check; the heal that would produce them was not run. It needs enough drives healed between the expiry of a restored copy and its removal by the scanner, and gets its own record. +- **Two versions with the same time under an old signature.** For a version whose stored signature no current release computes, the key carries another signature than the drive, and the merge orders versions of the same modification time by it. Two such versions of one key, each missing on some drives, can be refused where 20261109 accepted them. Nothing is deleted. +- **Fixtures.** The tests model an old header by clearing the erasure-coding fields in a current `xl.meta`, and compute the old signature with the rule of the releases named above. No `xl.meta` written by those releases was used. + +## Verification {#verification} + +| Case | 20261109 | With the fix | +| --------------------------------------------------------------------------- | ---------------- | ------------------------------------------- | +| Several pools, noncurrent-version expiry, 16 drives, 5 to 11 healed | refused | expires | +| Same, 4 or 12 drives healed (control) | expires | expires | +| One pool, expiry of all versions, 8 of 16 drives healed | refused | expires | +| Header signed by the rule before `RELEASE.2022-01-25`, 5 to 11 of 16 healed | refused | accepted; still refused with Option 2 alone | +| Another erasure coding under the same header (control) | another revision | another revision | +| Restore generation on 8 healed drives of 16 | split | one generation | +| Two versions of one time, each on 12 of 16 drives, 40 layouts (control) | 40 accepted | 40 accepted; 19 with Option 5 | +| Key of a version written by this release (control) | stored header | stored header | +| Data-directory flag differs on 8 and 8 drives ([Residual limits](#limits)) | refused, split | refused, split | +{.wide .expand} + +Two tests are added to `cmd/erasure-server-pool-lifecycle-authority_test.go`. `TestPoolsLifecycleHeaderEras` builds the version lists of 16 drives for every count of healed drives and both old headers. `TestPoolsLifecycleHealedOldHeaders` writes two versions, checks that their keys carry the stored headers, gives them old headers, wipes and heals half of the drives, checks that reads work and heal has nothing left to do, and then expires a noncurrent version with two pools and all versions with one. Both fail on 20261109 and with Option 5, and pass with the fix. The two rows on equal times and on the data-directory flag were run as one-off probes and are not part of the suite. + +The 192 existing tests of the lifecycle, restore, delete-all, tier, migration and `xl.meta` format suites pass with the fix, and `go vet` is clean. The full test suite and a run against data from an old MinIO release were not done. + +## Review history {#review} + +- Adversarial review, round 1, of the first version (Option 5): ran out of budget before a verdict. Its notes held two findings, and both were confirmed by test. + - Taking the signature from the digest changes the order of versions of one time in the merge and refuses histories that 20261109 accepts. Adopted: Option 6 replaces Option 5, and the tests now fail with Option 5. + - A header that differs only in the data-directory flag is left split, and the restored-copy expiry stays refused. This record had said the difference ends by itself; that was wrong and is corrected under [Residual limits](#limits). Not fixed here. +- Adversarial review, round 2, of the revised fix (Option 6): approved. It checked that the signature and erasure coding recomputed after decoding equal the stored ones for every object a current release writes, including the load-time repairs of old compression indexes and replication timestamps; that no two distinct revisions now share a key; that delete markers and free versions have no release-dependent header field; and that the restore readers of the key stay consistent. It could not run the tests in its read-only sandbox. One note on this record was adopted: which single-pool expiries are affected is now stated by rule name. + +## References {#references} + +- [Multi-Pool Lifecycle Expiry: Decide on the Whole Object](/blog/design/multi-pool-lifecycle-expiry/) +- [Lifecycle Delete-All Must Stay on One Key](/blog/design/ilm-delete-all-exact-key/) +- [Native Restore in Two Phases](/blog/design/native-restore-two-phase/) +- Upstream [#13573](https://github.com/minio/minio/pull/13573), [#14125](https://github.com/minio/minio/pull/14125), [#19534](https://github.com/minio/minio/pull/19534) diff --git a/content/blog/design/lifecycle-history-old-headers.zh.md b/content/blog/design/lifecycle-history-old-headers.zh.md new file mode 100644 index 00000000..2552c5e7 --- /dev/null +++ b/content/blog/design/lifecycle-history-old-headers.zh.md @@ -0,0 +1,201 @@ +--- +title: "生命周期版本历史:同一个版本,两个发行版写的头部" +linkTitle: "生命周期历史与旧头部" +date: 2026-11-10 +lastmod: 2026-11-10 +author: "PGSTY SILO Maintainers" +summary: > + 2024-04-28 之前的 MinIO 写入的数据,在纠删集里换过一部分盘之后,生命周期过期为什么会被永久拒绝;以及为什么把两个随写入版本而变的头部字段从修订标识里拿掉,就是最小而完整的修复。 +tags: [设计, 生命周期, 纠删码, 兼容性] +weight: 10 +draft: false +url: "/zh/blog/design/lifecycle-history-old-headers/" +--- + +本记录说明维护者复查 `RELEASE.2026-11-09T00-00-00Z`([d99829238](https://github.com/pgsty/silo/commit/d998292387ccf1d2907749e0fb1575ef5fb93fda))时发现的一个缺陷。它由该版本中的 [9f7b49369](https://github.com/pgsty/silo/commit/9f7b49369d4cbc1a5506af4bff05df05c8c8b6d3) 引入,这个提交是多池生命周期修复([#278](https://github.com/pgsty/silo/issues/278))的一部分。修复见 [PR #332](https://github.com/pgsty/silo/pull/332),没有单独的 GitHub issue。 + +**状态:**修复和回归测试在 [PR #332](https://github.com/pgsty/silo/pull/332) 中,已在第二轮对抗评审中获得通过;第一轮评审改动了修复方案,见[评审记录](#review)。尚未合并或发布。 + +## 简短回答 {#tldr} + +从 20261109 起,凡是要看同一个 key 其他版本才能决定的生命周期过期,都会先从纠删集的各块盘上读出这个 key 的版本历史,并且只接受“足够多的盘持有同一个修订”的版本。修订的标识由两部分组成:盘上存的版本头部,加上元数据的摘要。 + +问题在于,老版本在各块盘上存的头部并不一样。一块盘上存的头部,是当初把这个版本写到这块盘上的那个发行版写的,而 `RELEASE.2024-04-28` 之前的 MinIO 写的头部里没有纠删码字段。换盘之后,自愈会用当前的头部把版本写到新盘上,其他盘不动。读取和自愈都把这两种头部当成同一个版本,版本历史却把它们算成两个修订;哪一个都凑不够盘数时,整个 key 就被拒绝。 + +这种拒绝是永久的:扫描器每一轮都会重试,每一轮都以同样的方式失败;GET 正常,自愈报告没有需要修的东西。数据不会丢失,也不会被删除,只是该过期的版本一直留着。 + +修复只在一个函数里新增三行,即 `cmd/erasure-server-pool-lifecycle.go` 里的 `versionRevision`:修订标识里那两个随写入版本而变的头部字段,改为按当前发行版的算法从版本的元数据计算,不再取盘上存的值。升级即可恢复,盘上的内容不需要任何改动。 + +## 定性 {#assessment} + +| 方面 | 评估 | +| ------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| 性质 | `RELEASE.2026-11-09T00-00-00Z` 引入的回归:新的版本历史对“同一个修订”的判定过严。失败方向是拒绝,不是误删。 | +| 受影响的部署 | 从 MinIO 接手的数据,其版本头部由 `RELEASE.2021-11-24` 到 `RELEASE.2024-04-28` 之前的发行版写入;并且所在纠删集此后换过一部分盘(不是绝大多数)并完成了自愈。 | +| 受影响的过期 | 多池:版本化或暂停版本化的桶里的所有 ILM 过期,恢复副本的过期除外。任意池数(包括单池):`ExpiredObjectAllVersions`、`DelMarkerExpiration`,以及清除全部版本的批量过期。 | +| 不受影响 | SILO 写入的数据,以及 `RELEASE.2024-04-28` 及之后的 MinIO 写入的数据;没有盘被自愈过的纠删集;单池上针对单个版本或单个删除标记的过期(`Expiration`、`NoncurrentVersionExpiration`、`ExpiredObjectDeleteMarker`);读、写、自愈和复制。 | +| 数据安全 | 不删除、不隐藏任何数据。受影响的版本始终可读。 | +| 严重度 | 命中时为高:受影响 key 上的规则悄悄停止生效,空间持续增长,保留期限得不到执行。命中面窄:需要老数据,并且自愈过的盘恰好占到特定比例。 | +| 工作量 | 小:一个函数,不改存储 RPC,不改盘上格式,不改配置。 | +{.wide .expand} + +## 何时发生 {#trigger} + +三个条件必须同时成立: + +1. **旧头部。** 版本在 `xl.meta` 里的条目,是由 `RELEASE.2021-11-24`(第一个带逐版本头部的发行版)到 `RELEASE.2024-04-28`(第一个把纠删码写进头部的发行版)之前的 MinIO 写入的。之后的发行版重写 `xl.meta` 时,不会改动没有变化的版本的头部,所以即使这个 key 后来又加了新版本,老版本仍然保留旧头部。 +2. **部分自愈。** 纠删集里有一部分盘被更换,或者因为别的原因丢了这个 key,自愈把版本重新写了上去。这些盘上是当前的头部。 +3. **两组都不够大。** 一个修订要被接受,必须同时出现在纠删集过半的盘上和该版本读 quorum 数量的盘上,即 `max(n/2+1, n − 校验盘数)` 块盘。 + +| 纠删集 | 一个修订需要的盘数 | 自愈过这么多块盘时被拒绝 | +| ----------- | ------------------ | ------------------------ | +| 16 盘,EC:4 | 12 | 5 到 11(已复现) | +| 12 盘,EC:4 | 8 | 5 到 7 | +| 8 盘,EC:4 | 5 | 4 | +| 4 盘,EC:2 | 3 | 2 | +| 2 盘 | 2 | 1 | +{.wide .expand} + +只有第一行实际跑过,其余几行按同一条规则推算。小纠删集最容易命中:两盘的纠删集换过一块盘就够了。 + +对于受影响的 key,运维能看到的现象是: + +- `minio_ilm_expiry_incomplete_total{reason="history_unavailable"}` 每一轮扫描都在增长; +- ILM trace 报告 `result=failed` 和同样的原因;审计日志里有带 `ilm-incomplete-reason=history_unavailable` 标签的 `ILMExpiry` 条目;服务端日志说这次过期没有完成,原因是存储资源不足以完成写操作; +- 对这个版本的 GET 和 HEAD 正常,对这个 key 做 heal 显示每块盘都健康。 + +## 根因 {#root-cause} + +行号以 [d99829238](https://github.com/pgsty/silo/commit/d998292387ccf1d2907749e0fb1575ef5fb93fda) 为准。 + +1. `objectVersions` 从纠删集的每块盘读出 `xl.meta`,把各盘的版本列表交给 `authoritativeHistory`(`cmd/erasure-server-pool-lifecycle.go:74-133`)。 +2. `authoritativeHistory` 按 `versionRevision` 把每个版本所在的盘分组,只有最大的一组同时达到过半和读 quorum 时才接受(`:342`)。只要有一个版本没有这样的组,整个 key 就以读 quorum 错误被拒绝。 +3. `versionRevision` 把盘上存的头部原样放进标识:`key := revisionKey{header: ver.header}`(`:222`)。只有旧格式(`xl.json`)版本的签名会被替换,因为它的签名覆盖了每个分片各自的校验和。 +4. 版本头部有七个字段,其中两个取决于是哪个发行版写的: + +| 字段 | 不同发行版之间的差异 | +| ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `EcN`、`EcM` | 由上游 [#19534](https://github.com/minio/minio/pull/19534) 加入头部,首次发布于 `RELEASE.2024-04-28`。在此之前写入的头部读出来是 0/0。 | +| `Signature` | 版本元数据的 32 位哈希。在上游 [#14125](https://github.com/minio/minio/pull/14125)(首次发布于 `RELEASE.2022-01-25`)之前,全为空的分片 ETag 也被算进签名,而 `RELEASE.2021-11-24` 之前用普通 PUT 写入的版本正好带着这样的 ETag。这两个发行版之间的版本为这类版本写下的头部,带着一个之后任何发行版都不会再算出来的签名。 | +| `VersionID`、`ModTime`、`Type` | 每块盘上都相同。 | +| `Flags` | 由元数据推出。其中“转储版本是否使用自己的数据目录”这一位,还取决于这块盘写入条目时恢复副本是否已经过期。它与发行版无关,在标识里保留盘上存的值,见[方案](#options)。 | +{.wide .expand} + +5. 自愈按解码后的元数据做判断,不看头部。它把版本写到缺失的盘上,头部由自己这个发行版计算,并认为其余的盘是健康的。此后没有任何流程会去重写那些盘上的头部。 + +服务端的其他部分早已把这些盘当成同一个版本:`matchesNotStrict` 不比较签名;`matchesEC` 接受一边有纠删码、一边没有;列举路径的比较函数 `metaCacheEntry.matches` 在只有一边带纠删码时“不考虑 EC”再比较。列举路径的这种容忍,是和加入这两个字段的上游提交一起来的,`matchesEC` 随后由上游 [#20351](https://github.com/minio/minio/pull/20351) 加入。20261109 新增的修订标识没有把它继承过来。 + +这两个字段表达的内容,摘要里都有:纠删码本身就是版本元数据的一部分,签名则是对同一份元数据的一个更弱的哈希。把它们留在标识里不增加任何信息,只是把标识和“写这块盘的发行版”绑在了一起。 + +## 行为的来源 {#history} + +### 溯源 {#provenance} + +**由 SILO 引入,首次出现在 `RELEASE.2026-11-09T00-00-00Z`。** 更早的 SILO 发行版和上游 MinIO 的任何发行版,都没有权威版本历史和修订标识。 + +| 成分 | 提交 | PR | 首个发行版 | +| ------------------------------ | -------------------------------------------------------------------------------------------------------- | --------------------------------------------------- | ------------------------------ | +| `xl.meta` 的逐版本头部,带签名 | [faf013ec8](https://github.com/pgsty/silo/commit/faf013ec84051b92ae0f420a658b8d35bb7bb000)(Klaus Post) | [#13573](https://github.com/minio/minio/pull/13573) | `RELEASE.2021-11-24T23-19-33Z` | +| 签名不再计入空的分片 ETag | [0012ca8ca](https://github.com/pgsty/silo/commit/0012ca8ca5f816019d83c39b9018dc7992377e3b)(Klaus Post) | [#14125](https://github.com/minio/minio/pull/14125) | `RELEASE.2022-01-25T19-56-04Z` | +| 头部加入纠删码;比较时容忍缺失 | [5f774951b](https://github.com/pgsty/silo/commit/5f774951b1cc464b3d8ebe39750fde1a2d09f23d)(Klaus Post) | [#19534](https://github.com/minio/minio/pull/19534) | `RELEASE.2024-04-28T17-53-50Z` | +| 包含盘上头部的修订标识 | [9f7b49369](https://github.com/pgsty/silo/commit/9f7b49369d4cbc1a5506af4bff05df05c8c8b6d3)(SILO) | #278 修复的一部分 | `RELEASE.2026-11-09T00-00-00Z` | +{.wide .expand} + +## 方案 {#options} + +### 方案 1:用元数据重建整个头部 {#option-1} + +这是最初的建议:像 `restoreNeutralRevision` 对“去掉恢复字段的版本”所做的那样,用解码后的版本重新计算头部。不采纳。“转储版本是否使用自己的数据目录”这一位,是用恢复到期时间和当前时钟算出来的。这样重建的标识会在恢复副本到期的那一刻发生变化,而原生恢复要拿提交前取得的标识和提交后读到的标识做比较。`fileInfoRevisions` 正是因此才同时接受这一位的两种取值,并且依赖标识里带的是盘上存的那个值。 + +### 方案 2:只从元数据取纠删码 {#option-2} + +两行,能修好报告里的情形。但不充分:`RELEASE.2021-11-24` 之前用普通 PUT 写入的版本,如果它的 key 被 `RELEASE.2022-01-25` 之前的发行版重写过,之后又有一部分盘被自愈,各盘上仍然有两种签名,仍然被拒绝。针对这种头部的单元测试,只用这个方案时不通过。 + +### 方案 3:把盘上的旧头部重写掉 {#option-3} + +让自愈或扫描器把每块盘都改成当前的头部。不采纳:为了修一处比较而重写所有老对象的元数据,会让自愈在健康的盘上找出活来干;而且在它跑完之前,过期照样被拒绝。 + +### 方案 4:把头部从标识里去掉 {#option-4} + +只用摘要标识修订。不采纳:盘上存的“数据目录”标志位不在摘要里,而给历史排序的那次合并要求同一修订的各盘带同一个头部。改动更大,结果相同。 + +### 方案 5:签名取自摘要 {#option-5} + +这是本修复的第一版:纠删码取自元数据,签名则对所有版本都取自摘要,标识对旧格式版本本来就是这么做的。评审后不采纳。签名同时也是合并时给“修改时间相同的两个版本”排序的依据,而每块盘的版本列表仍然保持盘上原有的次序。对于各在 16 盘中 12 盘上的两个同时间版本,试过的 40 种布局 20261109 全部接受,这个方案拒绝了其中 21 种。 + +### 方案 6:两个字段都按当前发行版的算法计算 {#option-6} + +对象版本的纠删码和签名,都用今天写头部时用的同一套函数,从解码后的元数据算出来。对于当前发行版写入的版本,算出来的就是盘上存的头部,所以对它们没有任何变化。采纳。 + +## 修复方案 {#fix} + +在 `versionRevision` 里,可解码版本的摘要算出来之后: + +```go +switch v.Type { +case LegacyType: + copy(key.header.Signature[:], key.digest[:]) +case ObjectType: + key.header.Signature = v.ObjectV2.Signature() + key.header.EcN, key.header.EcM = uint8(v.ObjectV2.ErasureN), uint8(v.ObjectV2.ErasureM) +} +``` + +旧格式版本的分支没有变化,对象版本的分支是新增的。删除标记保留盘上存的头部:它在任何发行版下都不带纠删码,也没有哪个发行版改过“同样的元数据算出什么签名”。元数据无法解码的版本保留盘上存的头部,仍然只和字节完全相同的条目算作同一个修订。 + +`authoritativeHistory` 返回的历史里,每块盘上的条目带的都是被接受的那个标识的头部,这一点和以前一样。不向任何盘写入任何东西,标识也不落盘:每次读取都重新计算。 + +## 为什么充分且必要 {#sufficiency} + +- **充分。** 头部字段里,只有纠删码和签名取决于写入它的发行版,现在标识里的这两项都按当前发行版的算法计算。修订标识的两处使用者走的是同一个函数:生命周期的版本历史,以及原生恢复选定的那一代元数据。修复后,无论自愈过多少块盘,从零到全部,版本历史都接受老版本。 +- **必要。** 只改纠删码,16 盘里自愈 5 到 11 块时,旧签名的头部仍被拒绝。只改签名,报告里的情形仍被拒绝。 +- **安全。** 摘要覆盖了纠删码,也覆盖了签名所哈希的全部内容,所以在这两方面有差异的修订仍然是两个标识;有一个对照测试专门检查“同一头部下另一种纠删码”。真正的分裂(比如只到达一半盘的元数据更新)和以前一样被拒绝。 +- **对当前数据无变化。** 当前发行版写入的版本,其标识里的头部与盘上存的完全一致;端到端测试里有一个对照,在实际写入的对象上检查这一点。对这类数据,版本历史的判断与 20261109 相同。 +- **最小。** 一个函数,新增三行。不改存储 RPC,不改盘上格式,不改配置,不改自愈和任何读路径。 + +## 代价与兼容性 {#cost} + +- 开销:每块盘的每个对象版本多算一次签名,即对摘要本来就要读的元数据再编码一次、做一次 64 位哈希。没有额外的 I/O。 +- 盘上:没有任何变化,旧头部保持原样。 +- 滚动升级:每个节点用自己的代码做判断。仍在运行 20261109 的节点继续拒绝受影响的 key,升级后的节点会让它们过期。不需要协调。 +- 升级之后,扫描器会把之前被拒绝的过期重新排队并执行。受影响已有一段时间的部署,应当预期这些被推迟的删除会在升级后集中发生。 +- 原生恢复:`readRestoreGeneration` 原先把这样的盘当成“恢复状态写入失败”,走后备路径,得到一代不含恢复字段的元数据。现在它直接找到同一代。恢复副本的过期会拒绝经后备路径找到的那一代,所以按代码推断,它不再拒绝这类版本;这条过期路径没有实际跑过。 + +## 遗留限制 {#limits} + +- **自愈之前已经到期的恢复副本。** 这是另一个形态相同的独立缺陷,本修复没有解决它。标识里保留盘上原值的那个标志位,表示转储版本是否使用自己的数据目录。恢复副本还在盘上时写入这个版本的盘,这一位是置位的;按代码,恢复副本到期之后才自愈的盘,这一位是清零的。当 16 块盘里这两种头部各占 8 块时,版本历史被拒绝,恢复的元数据代也只能经后备路径找到,而恢复副本的过期会拒绝这样找到的代。于是副本不会被清除,各盘也不会自行趋于一致。这项检查用的头部是手工构造的,没有实际跑过会产生这种头部的自愈。它要求在恢复副本到期之后、扫描器清除它之前,有足够多的盘被自愈;将另立记录处理。 +- **旧签名下同一时间的两个版本。** 对于盘上签名是当前发行版算不出来的那类版本,标识里带的签名与盘上不同,而合并靠它给修改时间相同的版本排序。同一个 key 上两个这样的版本,各自又在一些盘上缺失时,可能在 20261109 接受的情况下被拒绝。不会删除任何东西。 +- **测试夹具。** 测试是在当前格式的 `xl.meta` 里把纠删码字段清零来模拟旧头部,并按上述发行版的规则计算旧签名。没有使用那些发行版实际写出的 `xl.meta`。 + +## 验证 {#verification} + +| 场景 | 20261109 | 修复后 | +| ------------------------------------------------------------------ | ---------- | -------------------------- | +| 多池,非当前版本过期,16 盘,自愈 5 到 11 块 | 拒绝 | 过期 | +| 同上,自愈 4 块或 12 块(对照) | 过期 | 过期 | +| 单池,过期全部版本,16 盘自愈 8 块 | 拒绝 | 过期 | +| 按 `RELEASE.2022-01-25` 之前的规则签名的头部,16 盘自愈 5 到 11 块 | 拒绝 | 接受;只用方案 2 时仍拒绝 | +| 同一头部下另一种纠删码(对照) | 另一个修订 | 另一个修订 | +| 16 盘自愈 8 块时的恢复元数据代 | 分裂 | 同一代 | +| 同一时间的两个版本,各在 16 盘中的 12 盘上,40 种布局(对照) | 40 种接受 | 40 种接受;方案 5 为 19 种 | +| 本发行版写入的版本的标识(对照) | 盘上头部 | 盘上头部 | +| 数据目录标志位在 8 块和 8 块盘上不同(见[遗留限制](#limits)) | 拒绝,分裂 | 拒绝,分裂 | +{.wide .expand} + +`cmd/erasure-server-pool-lifecycle-authority_test.go` 新增两个测试。`TestPoolsLifecycleHeaderEras` 针对两种旧头部、自愈盘数从 0 到 16 的每一种情况,构造 16 块盘的版本列表。`TestPoolsLifecycleHealedOldHeaders` 写入两个版本,先检查它们的标识带的就是盘上的头部,再给它们换上旧头部,清空并自愈一半的盘,先确认读取正常、自愈无事可做,然后在两个池上过期一个非当前版本,在一个池上过期全部版本。两个测试在 20261109 上和方案 5 下都失败,修复后通过。同时间版本和数据目录标志位这两行是一次性的探针,没有并入测试集。 + +生命周期、恢复、delete-all、分层、迁移和 `xl.meta` 格式这几组共 192 个既有测试在修复后通过,`go vet` 无告警。完整测试集没有跑,也没有用旧版 MinIO 的真实数据跑过。 + +## 评审记录 {#review} + +- 对抗评审第 1 轮,针对第一版(方案 5):在给出结论前用完了额度。它的笔记里有两条发现,都已用测试证实。 + - 签名取自摘要会改变合并时同时间版本的次序,使 20261109 能接受的历史被拒绝。已采纳:以方案 6 取代方案 5,测试现在在方案 5 下会失败。 + - 只在数据目录标志位上不同的头部仍然是分裂的,恢复副本的过期也一直被拒绝。本记录原先写的是这种差异会自行消失,这是错的,已在[遗留限制](#limits)中更正。本修复不处理。 +- 对抗评审第 2 轮,针对修订后的修复(方案 6):通过。它核对了以下几点:对当前发行版写入的每一种对象,解码后重算的签名和纠删码与盘上存的一致,包括加载时对旧压缩索引和旧复制时间戳的修补;没有两个不同的修订会共用一个标识;删除标记和 free version 没有随发行版而变的头部字段;使用该标识的恢复路径保持一致。它的只读沙箱无法运行测试。它对本记录提的一条意见已采纳:单池上哪些过期受影响,现在按规则名称写明。 + +## 参考 {#references} + +- [多池生命周期过期:按整个对象决策](/zh/blog/design/multi-pool-lifecycle-expiry/) +- [生命周期“删除全部版本”只能作用于一个 key](/zh/blog/design/ilm-delete-all-exact-key/) +- [两阶段原生恢复](/zh/blog/design/native-restore-two-phase/) +- 上游 [#13573](https://github.com/minio/minio/pull/13573)、[#14125](https://github.com/minio/minio/pull/14125)、[#19534](https://github.com/minio/minio/pull/19534) From 64e58f4557a1573554aa44a7f6aa0343ba7bff5d Mon Sep 17 00:00:00 2001 From: Feng Ruohang Date: Tue, 10 Nov 2026 14:15:51 +0800 Subject: [PATCH 2/2] docs: record that the lifecycle history fix is merged pgsty/silo#332 merged to main as 36c8d1683. Update the status line of the design record in both languages, and note that its CI ran the full cmd suite, the race selection and the cross compile. Not released yet. Co-Authored-By: Claude Fable 5.2 Signed-off-by: Feng Ruohang --- content/blog/design/lifecycle-history-old-headers.md | 4 ++-- content/blog/design/lifecycle-history-old-headers.zh.md | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/content/blog/design/lifecycle-history-old-headers.md b/content/blog/design/lifecycle-history-old-headers.md index 75828e61..614c616b 100644 --- a/content/blog/design/lifecycle-history-old-headers.md +++ b/content/blog/design/lifecycle-history-old-headers.md @@ -14,7 +14,7 @@ url: "/blog/design/lifecycle-history-old-headers/" This record covers a defect found in a maintainer review of `RELEASE.2026-11-09T00-00-00Z` ([d99829238](https://github.com/pgsty/silo/commit/d998292387ccf1d2907749e0fb1575ef5fb93fda)). That release introduced it with [9f7b49369](https://github.com/pgsty/silo/commit/9f7b49369d4cbc1a5506af4bff05df05c8c8b6d3), part of the multi-pool lifecycle fix ([#278](https://github.com/pgsty/silo/issues/278)). The fix is [PR #332](https://github.com/pgsty/silo/pull/332); there is no separate GitHub issue. -**Status:** the fix and its regression tests are in [PR #332](https://github.com/pgsty/silo/pull/332), approved by an adversarial review in its second round; the first round changed the fix, see [Review history](#review). Not merged or released. +**Status:** merged to main as [36c8d1683](https://github.com/pgsty/silo/commit/36c8d1683a6498bbfeb526ce58ae7bdac9f1c373) through [PR #332](https://github.com/pgsty/silo/pull/332), after an adversarial review approved it in its second round; the first round changed the fix, see [Review history](#review). Not released yet: `RELEASE.2026-11-09T00-00-00Z` is affected. ## Short answer {#tldr} @@ -184,7 +184,7 @@ The history that `authoritativeHistory` returns carries the header of the accept Two tests are added to `cmd/erasure-server-pool-lifecycle-authority_test.go`. `TestPoolsLifecycleHeaderEras` builds the version lists of 16 drives for every count of healed drives and both old headers. `TestPoolsLifecycleHealedOldHeaders` writes two versions, checks that their keys carry the stored headers, gives them old headers, wipes and heals half of the drives, checks that reads work and heal has nothing left to do, and then expires a noncurrent version with two pools and all versions with one. Both fail on 20261109 and with Option 5, and pass with the fix. The two rows on equal times and on the data-directory flag were run as one-off probes and are not part of the suite. -The 192 existing tests of the lifecycle, restore, delete-all, tier, migration and `xl.meta` format suites pass with the fix, and `go vet` is clean. The full test suite and a run against data from an old MinIO release were not done. +The 192 existing tests of the lifecycle, restore, delete-all, tier, migration and `xl.meta` format suites pass with the fix, and `go vet` is clean. The CI of PR #332 ran the full `cmd` suite, the race selection and the cross compile, and all passed. A run against data from an old MinIO release was not done. ## Review history {#review} diff --git a/content/blog/design/lifecycle-history-old-headers.zh.md b/content/blog/design/lifecycle-history-old-headers.zh.md index 2552c5e7..ec6cb46f 100644 --- a/content/blog/design/lifecycle-history-old-headers.zh.md +++ b/content/blog/design/lifecycle-history-old-headers.zh.md @@ -14,7 +14,7 @@ url: "/zh/blog/design/lifecycle-history-old-headers/" 本记录说明维护者复查 `RELEASE.2026-11-09T00-00-00Z`([d99829238](https://github.com/pgsty/silo/commit/d998292387ccf1d2907749e0fb1575ef5fb93fda))时发现的一个缺陷。它由该版本中的 [9f7b49369](https://github.com/pgsty/silo/commit/9f7b49369d4cbc1a5506af4bff05df05c8c8b6d3) 引入,这个提交是多池生命周期修复([#278](https://github.com/pgsty/silo/issues/278))的一部分。修复见 [PR #332](https://github.com/pgsty/silo/pull/332),没有单独的 GitHub issue。 -**状态:**修复和回归测试在 [PR #332](https://github.com/pgsty/silo/pull/332) 中,已在第二轮对抗评审中获得通过;第一轮评审改动了修复方案,见[评审记录](#review)。尚未合并或发布。 +**状态:**已通过 [PR #332](https://github.com/pgsty/silo/pull/332) 合并到 main,提交为 [36c8d1683](https://github.com/pgsty/silo/commit/36c8d1683a6498bbfeb526ce58ae7bdac9f1c373);合并前在第二轮对抗评审中获得通过,第一轮评审改动了修复方案,见[评审记录](#review)。尚未发布:`RELEASE.2026-11-09T00-00-00Z` 受影响。 ## 简短回答 {#tldr} @@ -184,7 +184,7 @@ case ObjectType: `cmd/erasure-server-pool-lifecycle-authority_test.go` 新增两个测试。`TestPoolsLifecycleHeaderEras` 针对两种旧头部、自愈盘数从 0 到 16 的每一种情况,构造 16 块盘的版本列表。`TestPoolsLifecycleHealedOldHeaders` 写入两个版本,先检查它们的标识带的就是盘上的头部,再给它们换上旧头部,清空并自愈一半的盘,先确认读取正常、自愈无事可做,然后在两个池上过期一个非当前版本,在一个池上过期全部版本。两个测试在 20261109 上和方案 5 下都失败,修复后通过。同时间版本和数据目录标志位这两行是一次性的探针,没有并入测试集。 -生命周期、恢复、delete-all、分层、迁移和 `xl.meta` 格式这几组共 192 个既有测试在修复后通过,`go vet` 无告警。完整测试集没有跑,也没有用旧版 MinIO 的真实数据跑过。 +生命周期、恢复、delete-all、分层、迁移和 `xl.meta` 格式这几组共 192 个既有测试在修复后通过,`go vet` 无告警。PR #332 的 CI 跑了完整的 `cmd` 测试集、race 选集和交叉编译,全部通过。没有用旧版 MinIO 的真实数据跑过。 ## 评审记录 {#review}