8.3 KiB
Migrare MinIO: local multi-bucket → cluster extern single-bucket
Autor: refactor 2026-04-25
Branch: feat/minio-cluster-single-bucket
Scop: mutarea fișierelor user-uploaded de pe staging-dataLayer-minio (bucket-per-user) pe cluster extern managed (<minio-host>, single bucket didi-prod cu prefix-uri).
De ce single-bucket
Cluster extern (4 noduri MinIO minio1..4.<minio-domain>, EC:2, 466 GiB utilizabili) ne dă credentiale didi-prod cu s3:* doar pe bucket-ul didi-prod. Nu putem crea bucket-uri (testat: 3/3 încercări Access Denied).
Soluția: păstrăm conceptul "namespace per user" dar ca prefix în bucket-ul fix:
ÎNAINTE (local, multi-bucket): DUPĂ (cluster, single-bucket):
user-3/ didi-prod/
images/ users/3/
videos/ images/
audio-files/ videos/
user-19/ audio-files/
images/ users/19/
uploads/ images/
image-files/ didi-prod/uploads/ ← legacy fallback
didi-prod/image-files/ ← legacy fallback
Numele de prefix-uri sistem (uploads, image-files, audio-files, video-files, text-files, document-files, pipeline-artifacts) sunt identice cu numele bucket-urilor vechi — astfel URL-urile vechi rămân interpretabile de proxy fără DB rewrite.
Compatibilitate URL — backward compat
URL-urile vechi din bos_analysis.analysis_session.input_media_url și PG history pointează la formate vechi:
https://didi365.eu/storage/user-3/images/abc.jpg ← format vechi user
https://didi365.eu/api/v3/media/file/user-3/images/abc.jpg ← proxy vechi
http://staging-dataLayer-minio:9000/image-files/foo.jpg ← bucket sistem
Codul rezolvă toate aceste forme la canonic didi-prod/<full-path>:
| URL primit | Resolved bucket | Resolved key |
|---|---|---|
user-3/images/abc.jpg |
didi-prod |
users/3/images/abc.jpg |
image-files/foo.jpg |
didi-prod |
image-files/foo.jpg |
didi-prod/users/3/images/abc.jpg |
didi-prod |
users/3/images/abc.jpg (passthrough) |
Implementat în:
didiFramework/src/config/minio.ts→resolveBucketRequest(bucket, key)agent-v3/src/shared/media/media-service.ts→ inline înproxyFile()
Modificări în cod (branch feat/minio-cluster-single-bucket)
didiFramework
| Fișier | Schimbare |
|---|---|
src/config/minio.ts |
Refactor complet: BUCKET fix, resolveBucketRequest(), userObjectKey(), no-op ensureBucket() în cluster mode, createUserBucket() lazy (doar logging) |
src/routes/auth.ts (endpoint /internal/get-bucket-info) |
Returnează bucketName=didi-prod + folder=users/{id}/{mimeFolder} în loc de bucketName=user-{id} + folder={mimeFolder} |
src/routes/uploads.ts |
Nemodificat — funcționează prin backward compat (caller pasează bucket=image-files, layer-ul îl rezolvă) |
sql/migrations/010_add_user_storage_quota.sql |
NEW — adaugă storage_used_bytes + storage_limit_bytes la internet_user (înlocuiește bucket tags) |
docker-compose.yml |
Env vars cu fallback ${MINIO_*:-default} |
agent-v3
| Fișier | Schimbare |
|---|---|
src/shared/media/media-service.ts |
ensureBucket() no-op în single-bucket mode. proxyFile() rezolvă legacy bucket → cluster prefix. Ownership check acceptă atât bucket=user-{N} cât și prefix users/{N}/. uploadFile() folosește MINIO_BUCKET env ca fallback |
docker-compose.yml |
MINIO_BUCKET, MINIO_USE_SSL, MINIO_PUBLIC_ENDPOINT adăugate la x-worker-env cu fallback empty (= legacy multi-bucket) |
Scripts
| Fișier | Schimbare |
|---|---|
scripts/minio-switch.sh |
NEW — swap între local/cluster. Sourceaza credentiale din .cluster-credentials.env |
scripts/.cluster-credentials.env |
+ DIDI_MINIO_ACCESS_KEY / DIDI_MINIO_SECRET_KEY / DIDI_MINIO_DEV_* |
Cluster extern — date de conectare
S3 API: http://<minio-host>:9000 (VIP 10.11.10.128)
Console: http://<minio-console-host>:9001 (VIP 10.11.10.129)
Region: us-east-1
Path style: REQUIRED (forcePathStyle în SDK)
TLS: false acum (HTTPS în curând prin HAProxy)
User PROD: didi-prod / 627074a6ceb8d8531feffaf4ee9cc45007350c58
User DEV: didi-dev / 6f51811a4a157cb8f8d0d4376877552b780a06b8
Bucket PROD: didi-prod (full s3:* doar pe bucket-ul propriu)
Bucket DEV: didi-dev (idem)
Infrastructură: 4 noduri Proxmox extern (minio1..4.<minio-domain>, IPs 10.11.10.124-127), erasure coding EC:2, HAProxy + keepalived VRRP, 466 GiB utilizabili.
Cutover plan — NEEXECUTAT încă
Codul e gata pe branch, runtime-ul production rămâne pe local MinIO până când executăm cutover-ul deliberat.
Pre-cutover checklist
- ✓ Backup complet creat:
/home/admin365/backups/pre-minio-refactor-20260425-0935/(545 MB cu cod + git bundle + 476 MB MinIO data) - ✓ Branch
feat/minio-cluster-single-bucketcu refactor + tests - ✓ Cluster
didi-devvalidat (write/list/delete cu prefix-uri noi) - ⏳ Apply migration 010 pe PG cluster
- ⏳ Smoke test cu un user fictiv pe
didi-dev(upload + analiză + URL public)
Cutover steps
# 1. Apply DB migration
docker exec didi-framework node -e "
const fs = require('fs');
const { Pool } = require('pg');
const pool = new Pool({ host: '10.11.50.167', port: 5000, user: 'bos_interface', password: 'interface', database: 'DIDI' });
const sql = fs.readFileSync('/app/sql/migrations/010_add_user_storage_quota.sql', 'utf8');
pool.query(sql).then(() => console.log('migration 010 applied')).catch(console.error).finally(() => pool.end());
"
# 2. Mirror data local → cluster (~2 min pe LAN)
mc alias set local http://staging-dataLayer-minio:9000 minioadmin minio123
mc alias set prod http://<minio-host>:9000 didi-prod '<secret>'
# Per-user buckets → users/{id}/ prefix
for u in $(mc ls local/ | grep -oE 'user-[0-9]+/' | tr -d '/'); do
id=${u#user-}
echo "mirroring $u → users/$id/"
mc mirror local/$u prod/didi-prod/users/$id/
done
# Sistem buckets → identical names ca prefix top-level
for b in uploads image-files audio-files video-files text-files document-files pipeline-artifacts backups; do
mc mirror local/$b prod/didi-prod/$b/
done
# 3. Switch env la cluster
./backend/services/orchestration-layer/scripts/minio-switch.sh cluster
# (rescrie .env, restart didi-framework + agent-v3)
# 4. Smoke test
curl -X POST https://didi365.eu/api/v3/pipeline/analyze ...
# 5. Monitor 24-48h. Local stays running ca fallback.
Rollback (30s)
./backend/services/orchestration-layer/scripts/minio-switch.sh local
Local container rămâne pornit și păstrează datele. Singura pierdere: upload-uri făcute între cutover și rollback nu sunt pe local. Acceptabil pentru fereastra de monitoring.
Riscuri cunoscute
-
Quota — la cutover,
storage_used_bytesîn PG e populat din migration 010 doar dacă userul are deja subscription custorage_limit_gb. Pentru calcul real folosit, după cutover rulez:-- Reconciliere usage cu MinIO real (job periodic recomandat) SELECT internet_user_id FROM bos_sysadmin.internet_user WHERE storage_used_bytes = 0; -- Pentru fiecare → mc du didi-prod/users/{id}/ → UPDATE -
TLS în curând — endpoint va deveni
https://<minio-host>(Let's Encrypt prin HAProxy). La acel moment:MINIO_USE_SSL=true+ endpoint nou. -
Cleanup local cluster — local
staging-dataLayer-minioare 7 alte proiecte (lege365-corpus, ml-models, voice-recordings, publisher-assets, rafai-prod). NU le ștergem la cleanup. -
HAProxy failover — VIP
10.11.10.128rulează pecai1. La failover poate fi o pauză de 2-5s. SDK-ul minio-js are retry implicit, dar la upload-uri mari (>100MB) merită setări explicite.
Status build
✓ tsc didiFramework — exit 0
✓ tsc agent-v3 — zero erori noi (doar pre-existente pe `pg` module)
✓ docker compose build — toate imagini OK
✓ minio-switch.sh status — funcțional
✓ Test write/list/delete pe didi-dev cu prefix-uri noi — validat