fix(storage): verify migration of exports up to one GiB
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
# MinIO → SeaweedFS: betreuter Pilot auf einer Installer-Instanz
|
# MinIO → SeaweedFS: betreuter Pilot auf einer Installer-Instanz
|
||||||
|
|
||||||
Stand: 27. September 2026. Dieser Ablauf ergänzt das
|
Stand: 28. September 2026. Dieser Ablauf ergänzt das
|
||||||
[Austauschkonzept](minio-austausch-konzept.md) um ausführbare Werkzeuge.
|
[Austauschkonzept](minio-austausch-konzept.md) um ausführbare Werkzeuge.
|
||||||
Er übernimmt den Speicher einer vorhandenen Installation, beispielsweise 3.3.0,
|
Er übernimmt den Speicher einer vorhandenen Installation, beispielsweise 3.3.0,
|
||||||
mit unveränderten Anwendungsimages und ohne Datenbankschemamigration.
|
mit unveränderten Anwendungsimages und ohne Datenbankschemamigration.
|
||||||
@@ -8,7 +8,7 @@ mit unveränderten Anwendungsimages und ohne Datenbankschemamigration.
|
|||||||
**Vor dem ersten Wartungsfenster nur `load-images` und `plan` ausführen.**
|
**Vor dem ersten Wartungsfenster nur `load-images` und `plan` ausführen.**
|
||||||
Die Vorprüfung ermittelt die bislang unbekannten Größen und liefert eine
|
Die Vorprüfung ermittelt die bislang unbekannten Größen und liefert eine
|
||||||
kompakte Ausgabe zum gemeinsamen Prüfen. Sie stoppt keine Anwendungscontainer.
|
kompakte Ausgabe zum gemeinsamen Prüfen. Sie stoppt keine Anwendungscontainer.
|
||||||
Ein Zugriff auf die konkrete Kundeninstanz wurde bei der Entwicklung nicht ausgeführt.
|
Die fachliche Abnahme erfolgt zusätzlich auf der konkreten Instanz.
|
||||||
|
|
||||||
## Was der Pilot leistet und wo seine Grenze liegt
|
## Was der Pilot leistet und wo seine Grenze liegt
|
||||||
|
|
||||||
@@ -72,7 +72,7 @@ aus der S3-Liste verbergen, obwohl er per GET erreichbar bleibt. Bei gestopptem
|
|||||||
MinIO muss deshalb die Menge lokaler `xl.meta`-Objektpfade exakt mit dem S3-Inventar
|
MinIO muss deshalb die Menge lokaler `xl.meta`-Objektpfade exakt mit dem S3-Inventar
|
||||||
übereinstimmen. Andere Speicherformate oder Abweichungen blockieren die Umschaltung.
|
übereinstimmen. Andere Speicherformate oder Abweichungen blockieren die Umschaltung.
|
||||||
|
|
||||||
Getestete Pilotgrenzen: **200.000 Objekte, höchstens 256 MiB je Objekt**.
|
Getestete Pilotgrenzen: **200.000 Objekte, höchstens 1 GiB je Objekt**.
|
||||||
Die Kapazitätsabschätzung berücksichtigt Quellsicherung, neues und wiederhergestelltes
|
Die Kapazitätsabschätzung berücksichtigt Quellsicherung, neues und wiederhergestelltes
|
||||||
Ziel, Metadatenreserve pro Objekt, DB-Dump, Offline-Images, Spool und 5 GiB Reserve.
|
Ziel, Metadatenreserve pro Objekt, DB-Dump, Offline-Images, Spool und 5 GiB Reserve.
|
||||||
Nach dem Schreibstopp wird das Inventar erneut ermittelt. Es gibt keine Annahme,
|
Nach dem Schreibstopp wird das Inventar erneut ermittelt. Es gibt keine Annahme,
|
||||||
@@ -211,14 +211,13 @@ nicht automatisch gelöscht.
|
|||||||
## Nachgewiesene Tests und ausstehende Freigaben
|
## Nachgewiesene Tests und ausstehende Freigaben
|
||||||
|
|
||||||
Der lokale S3-Test verwendet das offizielle MinIO-Release-Binary vom 7. September
|
Der lokale S3-Test verwendet das offizielle MinIO-Release-Binary vom 7. September
|
||||||
2025 und SeaweedFS 4.47 mit festem Digest. 1.005 Objekte / 268.446.382 Bytes,
|
2025 und SeaweedFS 4.47 mit festem Digest. 1.005 Objekte / 1.073.752.750 Bytes,
|
||||||
einschließlich 256-MiB-Grenzdatei, Pagination, Sonderzeichen, Metadaten, leeren
|
einschließlich 1-GiB-Grenzdatei, Pagination, Sonderzeichen, Metadaten, leeren
|
||||||
Dateien, Streaming, 24-Stunden-Signaturen, fremdem Bucket und ungültigen Zugängen
|
Dateien, Streaming, 24-Stunden-Signaturen, fremdem Bucket und ungültigen Zugängen
|
||||||
werden geprüft. Beschädigte Zielobjekte werden erkannt und beim Fortsetzen
|
werden geprüft. Beschädigte Zielobjekte werden erkannt und beim Fortsetzen
|
||||||
erneut kopiert; der kalte Restore wird vollständig gelesen und verglichen.
|
erneut kopiert; der kalte Restore wird vollständig gelesen und verglichen.
|
||||||
Ein Lauf der Kopier-/Prüf-/Restore-Sequenz dauerte lokal ungefähr 65 Sekunden;
|
|
||||||
Testhost: Linux, 16 logische CPUs, rund 29 GiB RAM für Docker, lokaler NVMe-Speicher.
|
Testhost: Linux, 16 logische CPUs, rund 29 GiB RAM für Docker, lokaler NVMe-Speicher.
|
||||||
Dies ist keine Laufzeitzusage für die Instanz.
|
Die Laufzeit hängt von Objektanzahl, Datenmenge und Host ab.
|
||||||
|
|
||||||
Das Hilfs-Lockfile enthält den übernommenen vollständigen SDK-Abhängigkeitsbaum
|
Das Hilfs-Lockfile enthält den übernommenen vollständigen SDK-Abhängigkeitsbaum
|
||||||
des Backends, nicht nur dieselbe oberste Versionsnummer. Beim Pilot wird außerdem
|
des Backends, nicht nur dieselbe oberste Versionsnummer. Beim Pilot wird außerdem
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ python3 -B scripts/storage/tests/host_integration.py
|
|||||||
```
|
```
|
||||||
|
|
||||||
Der S3-Test verwendet ausschließlich neu angelegte Testcontainer mit künstlichen
|
Der S3-Test verwendet ausschließlich neu angelegte Testcontainer mit künstlichen
|
||||||
Daten. Er prüft über 1.000 Objekte, Pagination, 256 MiB, leere Dateien,
|
Daten. Er prüft über 1.000 Objekte, Pagination, eine 1-GiB-Datei, leere Dateien,
|
||||||
Sonderzeichen, Metadaten, SigV4, 24-Stunden-Links, Streaming, verbotene Zugriffe,
|
Sonderzeichen, Metadaten, SigV4, 24-Stunden-Links, Streaming, verbotene Zugriffe,
|
||||||
beschädigte Zielobjekte, erneute Kopie und echten Restore in ein frisches
|
beschädigte Zielobjekte, erneute Kopie und echten Restore in ein frisches
|
||||||
Verzeichnis. Die gesicherten Testdaten bleiben unter `/tmp/fedeo-storage-*`
|
Verzeichnis. Die gesicherten Testdaten bleiben unter `/tmp/fedeo-storage-*`
|
||||||
@@ -27,7 +27,7 @@ Der Host-Test verwendet einen echten PostgreSQL-/MinIO-/SeaweedFS-Stack und
|
|||||||
künstliche Anwendungscontainer. Er ersetzt keine fachliche Abnahme von FEDEO
|
künstliche Anwendungscontainer. Er ersetzt keine fachliche Abnahme von FEDEO
|
||||||
(PDF, WebDAV, Exporte, Mandantenrechte) auf der konkreten Instanz.
|
(PDF, WebDAV, Exporte, Mandantenrechte) auf der konkreten Instanz.
|
||||||
|
|
||||||
Die erste Übernahme ist auf 200.000 Objekte und 256 MiB pro Objekt begrenzt.
|
Die erste Übernahme ist auf 200.000 Objekte und 1 GiB pro Objekt begrenzt.
|
||||||
Das größte Objekt wird auf Platte gespult; RAM-Verbrauch wächst nicht mit dessen
|
Das größte Objekt wird auf Platte gespult; RAM-Verbrauch wächst nicht mit dessen
|
||||||
Größe. Multipart-Übernahmen und Sonderkonfigurationen werden abgelehnt.
|
Größe. Multipart-Übernahmen und Sonderkonfigurationen werden abgelehnt.
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ const S3 = requireSdk('@aws-sdk/client-s3');
|
|||||||
const { getSignedUrl } = requireSdk('@aws-sdk/s3-request-presigner');
|
const { getSignedUrl } = requireSdk('@aws-sdk/s3-request-presigner');
|
||||||
|
|
||||||
// The pilot deliberately has a tested bound; no untested multipart conversion.
|
// The pilot deliberately has a tested bound; no untested multipart conversion.
|
||||||
export const MAX_OBJECT_BYTES = 256 * 1024 * 1024;
|
export const MAX_OBJECT_BYTES = 1024 * 1024 * 1024;
|
||||||
export const MAX_OBJECTS = 200_000;
|
export const MAX_OBJECTS = 200_000;
|
||||||
export const client = config => new S3.S3Client({ ...config, forcePathStyle: true, maxAttempts: 3 });
|
export const client = config => new S3.S3Client({ ...config, forcePathStyle: true, maxAttempts: 3 });
|
||||||
export const call = (s3, operation, input) => s3.send(new S3[`${operation}Command`](input));
|
export const call = (s3, operation, input) => s3.send(new S3[`${operation}Command`](input));
|
||||||
@@ -45,7 +45,7 @@ export function validateKeys(objects) {
|
|||||||
requireThat(!/[\x00-\x1f\x7f\\]/.test(key) && segments.every(s => s && s !== '.' && s !== '..'),
|
requireThat(!/[\x00-\x1f\x7f\\]/.test(key) && segments.every(s => s && s !== '.' && s !== '..'),
|
||||||
`Nicht verlustfrei übertragbarer Schlüssel: ${JSON.stringify(key)}`);
|
`Nicht verlustfrei übertragbarer Schlüssel: ${JSON.stringify(key)}`);
|
||||||
requireThat(Number.isSafeInteger(size) && size >= 0 && size <= MAX_OBJECT_BYTES,
|
requireThat(Number.isSafeInteger(size) && size >= 0 && size <= MAX_OBJECT_BYTES,
|
||||||
`Objekt außerhalb der getesteten Größe (256 MiB): ${JSON.stringify(key)}`);
|
`Objekt außerhalb der getesteten Größe (${MAX_OBJECT_BYTES / 1024 / 1024} MiB): ${JSON.stringify(key)}`);
|
||||||
for (let i = 1; i < segments.length; i++) {
|
for (let i = 1; i < segments.length; i++) {
|
||||||
requireThat(!keys.has(segments.slice(0, i).join('/')), `Datei-/Verzeichniskonflikt: ${JSON.stringify(key)}`);
|
requireThat(!keys.has(segments.slice(0, i).join('/')), `Datei-/Verzeichniskonflikt: ${JSON.stringify(key)}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
// Explicit integration run; never discovers or connects to an existing FEDEO stack.
|
// Explicit integration run; never discovers or connects to an existing FEDEO stack.
|
||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
import { execFileSync } from 'node:child_process';
|
import { execFileSync, spawnSync } from 'node:child_process';
|
||||||
import { mkdtemp, mkdir, writeFile, readFile } from 'node:fs/promises';
|
import { mkdtemp, mkdir, writeFile, readFile } from 'node:fs/promises';
|
||||||
import { tmpdir } from 'node:os';
|
import { tmpdir } from 'node:os';
|
||||||
import { join } from 'node:path';
|
import { join } from 'node:path';
|
||||||
@@ -28,6 +28,20 @@ async function ready(s3) {
|
|||||||
}
|
}
|
||||||
throw new Error('S3 did not become ready');
|
throw new Error('S3 did not become ready');
|
||||||
}
|
}
|
||||||
|
async function restoredContentsReady(s3, bucket, report) {
|
||||||
|
// Like the host helper, wait for volume registration after a cold start.
|
||||||
|
// ListBuckets can already succeed while reading object data still returns 500.
|
||||||
|
const deadline = Date.now() + 180_000;
|
||||||
|
for (;;) {
|
||||||
|
try { return await verifyManifest(s3, bucket, report); }
|
||||||
|
catch (error) {
|
||||||
|
const transient = [500, 502, 503, 504].includes(error.$metadata?.httpStatusCode) ||
|
||||||
|
['ECONNREFUSED', 'ECONNRESET', 'ETIMEDOUT', 'EHOSTUNREACH'].includes(error.code);
|
||||||
|
if (!transient || Date.now() >= deadline) throw error;
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
async function weed(name, data) {
|
async function weed(name, data) {
|
||||||
await mkdir(join(data, 'master'), { recursive: true });
|
await mkdir(join(data, 'master'), { recursive: true });
|
||||||
run(name, '-p', '127.0.0.1::9000', '-v', `${data}:/data`, '-v', `${root}/config:/etc/fedeo:ro`,
|
run(name, '-p', '127.0.0.1::9000', '-v', `${data}:/data`, '-v', `${root}/config:/etc/fedeo:ro`,
|
||||||
@@ -73,11 +87,12 @@ try {
|
|||||||
Body: Buffer.from(`fixture-${i}`), ContentType: 'text/plain', Metadata: { custom: 'preserved' } });
|
Body: Buffer.from(`fixture-${i}`), ContentType: 'text/plain', Metadata: { custom: 'preserved' } });
|
||||||
await call(source, 'PutObject', { Bucket, Key: 'empty', Body: Buffer.alloc(0), ContentType: 'application/octet-stream' });
|
await call(source, 'PutObject', { Bucket, Key: 'empty', Body: Buffer.alloc(0), ContentType: 'application/octet-stream' });
|
||||||
await call(source, 'PutObject', { Bucket, Key: 'large.zip', ContentLength: MAX_OBJECT_BYTES,
|
await call(source, 'PutObject', { Bucket, Key: 'large.zip', ContentLength: MAX_OBJECT_BYTES,
|
||||||
Body: Readable.from((function* () { for (let i = 0; i < 256; i++) yield Buffer.alloc(1024 * 1024, i); })()),
|
Body: Readable.from((function* () { for (let i = 0; i < MAX_OBJECT_BYTES / (1024 * 1024); i++) yield Buffer.alloc(1024 * 1024, i % 256); })()),
|
||||||
ContentType: 'application/zip', ContentDisposition: 'attachment; filename="large.zip"',
|
ContentType: 'application/zip', ContentDisposition: 'attachment; filename="large.zip"',
|
||||||
CacheControl: 'private', ContentLanguage: 'de', Expires: new Date('2030-01-01T00:00:00Z') });
|
CacheControl: 'private', ContentLanguage: 'de', Expires: new Date('2030-01-01T00:00:00Z') });
|
||||||
const before = await inventory(source, Bucket);
|
const before = await inventory(source, Bucket);
|
||||||
assert.equal(before.count, 1005);
|
assert.equal(before.count, 1005);
|
||||||
|
assert.equal(before.objects.find(o => o.Key === 'large.zip').Size, MAX_OBJECT_BYTES);
|
||||||
await call(source, 'PutObject', { Bucket, Key: 'unsupported-marker/', Body: '' });
|
await call(source, 'PutObject', { Bucket, Key: 'unsupported-marker/', Body: '' });
|
||||||
await assert.rejects(inventory(source, Bucket), /Schlüssel/);
|
await assert.rejects(inventory(source, Bucket), /Schlüssel/);
|
||||||
await call(source, 'DeleteObject', { Bucket, Key: 'unsupported-marker/' });
|
await call(source, 'DeleteObject', { Bucket, Key: 'unsupported-marker/' });
|
||||||
@@ -95,7 +110,7 @@ try {
|
|||||||
const restoredAdmin = client({ ...restoredConfig, credentials: adminCredentials });
|
const restoredAdmin = client({ ...restoredConfig, credentials: adminCredentials });
|
||||||
try {
|
try {
|
||||||
await ready(restoredAdmin);
|
await ready(restoredAdmin);
|
||||||
await verifyManifest(restored, Bucket, report);
|
await restoredContentsReady(restored, Bucket, report);
|
||||||
await contract(restored, restoredConfig, Bucket, { foreignBucket: 'foreign-private' });
|
await contract(restored, restoredConfig, Bucket, { foreignBucket: 'foreign-private' });
|
||||||
} finally { restored.destroy(); restoredAdmin.destroy(); }
|
} finally { restored.destroy(); restoredAdmin.destroy(); }
|
||||||
console.log(JSON.stringify({ result: 'passed', objects: report.count, bytes: report.bytes,
|
console.log(JSON.stringify({ result: 'passed', objects: report.count, bytes: report.bytes,
|
||||||
@@ -103,7 +118,10 @@ try {
|
|||||||
process.env.FEDEO_MINIO_TEST_IMAGE || 'fedeo-minio-fixture:2025-09-07', '--format', '{{.Id}}'),
|
process.env.FEDEO_MINIO_TEST_IMAGE || 'fedeo-minio-fixture:2025-09-07', '--format', '{{.Id}}'),
|
||||||
evidence: root }, null, 2));
|
evidence: root }, null, 2));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
for (const name of containers) { try { console.error(docker('logs', '--tail', '12', name)); } catch {} }
|
for (const name of containers) {
|
||||||
|
const logs = spawnSync('docker', ['logs', '--tail', '30', name], { encoding: 'utf8' });
|
||||||
|
console.error(logs.stdout + logs.stderr);
|
||||||
|
}
|
||||||
throw error;
|
throw error;
|
||||||
} finally {
|
} finally {
|
||||||
source?.destroy(); target?.destroy(); admin?.destroy();
|
source?.destroy(); target?.destroy(); admin?.destroy();
|
||||||
|
|||||||
Reference in New Issue
Block a user