Prečo je lepšie ukladať obrázky, videá a texty v MinIO ako v PostgreSQL
Prečo je lepšie ukladať obrázky, videá a texty v MinIO ako v PostgreSQL
"Kde mám ukladať súbory? V databáze alebo MinIO?"
Riešil som tento problém už stokrát. Tu je odpoveď.
Prológ: Problém
Moja cesta storage riešeniami bola... poučná.
Projekt 1 (2018): PostgreSQL BYTEA
Manager: "Potrebujeme storage pre screenshoty!"
Ja: "Jednoduché, dám to do DB..."
O mesiac neskôr:
DBA: "Prečo mi databáza narastá o 50GB denne?!"
Ja: "Screenshoty... Chyba."
Projekt 2 (2019): Filesystem
Manager: "Potrebujeme storage!"
Ja: "Filesystem? Jednoduché..."
O 2 mesiace neskôr:
DevOps: "Server má plný disk!"
Ja: "Backup?"
DevOps: "Backup súborov? Nie je automatický..."
Projekt 3 (2020): ownCloud
Manager: "Potrebujeme enterprise storage!"
Ja: "OK, ownCloud..."
3 týždne setup, komplexná konfigurácia. Výsledok: funguje, ale overkill pre automatické uploady.
Projekt 4 (2021): AWS S3 (SAAS)
Manager: "Potrebujeme cloud storage!"
Ja: "AWS S3?"
2 týždne čakania na AWS schválenie.
CFO: "Koľko to stojí?!"
Ja: "Ehm... €500/mesiac..."
Skvelé riešenie, ale náklady sa sčítavajú.
Projekt 5 (2024): MinIO (On-Premise)
Manager: "Potrebujeme storage! On-premise!"
Ja: "MinIO! 5 minút setup, S3-kompatibilný, nulové náklady"
Manager: "Perfektné!"
Dokonalá rovnováha: S3 API + on-premise kontrola!
Reálny scenár
Test automation generuje denne:
- 100 testov/deň
- 5 screenshotov/test = 500 obrázkov/deň (každý 200KB)
- 20 videí/deň (každé 10MB)
- 1000 log súborov/deň (každý 50KB)
| Typ súboru | Denne | Mesačne | Ročne |
|---|---|---|---|
| Obrázky | 100MB | 3GB | 36GB |
| Videá | 200MB | 6GB | 72GB |
| Logy | 50MB | 1.5GB | 18GB |
| Spolu | 350MB | 10GB | 120GB |
Otázka: Kde to ukladať?
Kapitola 1: Tradičné riešenia (a prečo zlyhávajú)
Riešenie 1: PostgreSQL BYTEA
| Aspekt | Hodnotenie |
|---|---|
| Zložitosť setupu | NÍZKA (len ALTER TABLE) |
| Čas setupu | 10 minút |
| Údržba | STREDNÁ |
Výhody:
- Jednoduché (žiadna extra služba)
- ACID transakcie
- Backup zahrnutý (s DB)
- Žiadna separátna autentifikácia
Nevýhody:
- Explózia veľkosti databázy
- Pomalé query (načítavanie blobov)
- Nočná mora s backupmi (obrovské dumpy)
- Problémy s pamäťou
- Neoptimalizované pre veľké súbory
Verdikt: "Zdá sa jednoduché, stáva sa nočnou morou"
Pokušenie je pochopiteľné:
CREATE TABLE test_screenshots (
id SERIAL PRIMARY KEY,
test_id INTEGER,
filename VARCHAR(255),
content BYTEA, -- Ulož obrázok sem! Jednoduché!
content_type VARCHAR(100),
created_at TIMESTAMP DEFAULT NOW()
);
-- Upload screenshotu:
INSERT INTO test_screenshots (test_id, filename, content, content_type)
VALUES (123, 'login.png', '\xDEADBEEF...', 'image/png');
-- Jednoduché! Čo sa môže pokaziť?Riešenie 2: Filesystem
| Aspekt | Hodnotenie |
|---|---|
| Zložitosť setupu | NÍZKA (mkdir /uploads) |
| Čas setupu | 5 minút |
| Údržba | VYSOKÁ (manuálne backupy) |
Výhody:
- Rýchle (priamy prístup k disku)
- Jednoduché (žiadne závislosti)
- Nulové extra náklady
Nevýhody:
- Nie je škálovateľné (jeden server)
- Žiadny backup (ak nie manuálny)
- Žiadna redundancia
- Ťažké zdieľanie medzi servermi
- Peklo s cestami
- Problémy s oprávneniami
Verdikt: "OK pre vývoj, ZLÉ pre produkciu"
Reálne problémy:
# Problém 1: Peklo s cestami
/var/uploads/test-123/screenshot.png # Server 1
/home/jenkins/uploads/test-123/screenshot.png # Server 2
C:\uploads\test-123\screenshot.png # Windows
# Problém 2: Koľko súborov máme?
find /uploads -type f | wc -l
# Výstup: 1,245,678 súborov
# Ako backupujem 1M+ súborov?
rsync -avz /uploads backup:/uploads # 6 hodín!
# Problém 3: Plný disk
df -h /uploads
# Výstup: 100% využité
# Problém 4: Viac serverov za load balancerom
# Server 1 má súbory, Server 2 nemá = náhodné 404Riešenie 3: ownCloud / Nextcloud
| Aspekt | Hodnotenie |
|---|---|
| Zložitosť setupu | VYSOKÁ (docker-compose, redis, mysql, cron) |
| Čas setupu | 2-3 dni |
| Údržba | VYSOKÁ (aktualizácie, backupy, monitoring) |
Výhody:
- Webové UI (dobré pre manuálne uploady)
- Správa používateľov
- Zdieľanie súborov a verziovanie
- Desktop/mobile synchronizácia
Nevýhody:
- Overkill pre automatické uploady
- Komplexná konfigurácia
- Vysoká spotreba zdrojov
- Pomalé API pre hromadné operácie
Verdikt: "Výborné pre tímové zdieľanie, zlý nástroj pre automatizáciu"
Riešenie 4: AWS S3 (SAAS)
| Aspekt | Hodnotenie |
|---|---|
| Zložitosť setupu | STREDNÁ (AWS účet, IAM, politiky) |
| Čas setupu | 2-3 dni (schvaľovací proces) |
| Údržba | NÍZKA (spravovaná služba) |
| Náklady | VYSOKÉ (platba za GB + requesty) |
Výhody:
- Nekonečne škálovateľné
- Vysoká dostupnosť (99.99%)
- Vstavaný backup a CDN
- S3 API štandard
- Nulová údržba
Nevýhody:
- Náklady (hlavne pre dev/test)
- Zložitosť IAM
- Schvaľovací proces
- Problémy so suverenitou dát
- Nedá sa spustiť lokálne
Verdikt: "Perfektné pre produkciu, ale závisí od SAAS vs On-Premise stratégie"
Reálne náklady pre testovacie prostredie:
| Položka | Využitie | Náklady |
|---|---|---|
| Storage | 50GB | $1.15 |
| GET requesty | 1M | $0.40 |
| PUT requesty | 100K | $0.50 |
| Prenos dát | 50GB | $4.50 |
| Mesačne | ~$7 | |
| × 5 prostredí | $35/mesiac | |
| Ročne | $420 |
Za testovacie dáta!
Kapitola 2: PostgreSQL BYTEA (a prečo NIE)
Problém 1: Explózia veľkosti databázy
-- Pred (bez screenshotov):
SELECT pg_size_pretty(pg_database_size('testdb'));
-- Výstup: 2 GB
-- Po 1 mesiaci (so screenshotmi):
SELECT pg_size_pretty(pg_database_size('testdb'));
-- Výstup: 52 GB1000 testov × 30MB priemerne = 30GB len za screenshoty!
Problém 2: Pomalé query
-- Query PRED screenshotmi:
SELECT * FROM test_results WHERE test_id = 123;
-- Čas vykonania: 5ms
-- Query PO (s JOIN načítavajúcim BLOBy):
SELECT tr.*, ts.content -- Načítava 5MB blob!
FROM test_results tr
LEFT JOIN test_screenshots ts ON ts.test_id = tr.test_id;
-- Čas vykonania: 2500msPostgreSQL načíta CELÝ blob do pamäte. 10 screenshotov × 500KB každý = 5MB načítaných!
Problém 3: Nočná mora s backupmi
| Scenár | Pred | Po |
|---|---|---|
| Čas pg_dump | 2 minúty | 2 hodiny |
| Veľkosť dumpu | 100MB | 55GB |
| Čas obnovy | 5 minút | 4 hodiny |
| Týždenný backup storage | 700MB | 385GB |
Problém 4: Problémy s pamäťou a pripojeniami
@Test
public void testLogin() {
TestResult result = testRepository.findById(123);
// Toto načíta VŠETKY screenshoty do pamäte!
List<Screenshot> screenshots = result.getScreenshots();
// Každý screenshot = 500KB
// 10 screenshotov = 5MB
// 100 súbežných testov = 500MB RAM!
}
// OOM Exception prichádza!Vyčerpanie connection poolu:
- Query načíta 5MB blob = drží pripojenie 5+ sekúnd
- Pool s 20 pripojeniami = vyčerpaný za sekundy
- Ostatné query timeoutujú
Čísla
| Metrika | Dopad |
|---|---|
| Veľkosť databázy | +60GB (30× nárast) |
| Čas backupu | 2 min → 2 hodiny (60× pomalšie) |
| Čas query | 5ms → 2500ms (500× pomalšie) |
| Čas VACUUM | 1 min → 2 hodiny (120× pomalšie) |
| Pamäť na operáciu | +500MB |
Verdikt: "Databáza je pre DÁTA, nie SÚBORY!"
Kapitola 3: MinIO riešenie (a prečo ÁNO)
Správna architektúra
MinIO pre súbory:
- Setup: 5 minút (docker run)
- Náklady: $0 (self-hosted, on-premise)
- Škálovateľnosť: Nekonečná (cluster)
- Výkon: Optimalizovaný pre veľké súbory
PostgreSQL pre metadáta:
- screenshot_id, test_id, filename
- minio_path:
s3://bucket/path - size, created_at
┌──────────────────┐
│ PostgreSQL │ ← Len metadáta (maličké!)
│ │
│ id: 123 │
│ path: "s3://test-screenshots/123.png"
│ size: 245KB │
└────────┬─────────┘
│ Referencia (len cesta)
↓
┌──────────────────┐
│ MinIO │ ← Skutočné súbory (veľké!)
│ (On-Premise) │
│ │
│ /test-screenshots/
│ ├─ 123.png │
│ ├─ 124.png │
│ └─ 125.mp4 │
└──────────────────┘Implementácia
@Entity
@Table(name = "test_screenshots")
public class TestScreenshot {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private Long testId;
private String filename;
private String minioPath; // s3://bucket/path - NIE obsah!
private Long fileSize;
private String contentType;
private LocalDateTime createdAt;
// ŽIADNE BYTEA pole!
}
@Service
public class ScreenshotService {
@Autowired
private MinioClient minioClient;
public TestScreenshot saveScreenshot(Long testId, MultipartFile file) {
// 1. Upload do MinIO (file storage)
String filename = generateFilename(testId, file.getOriginalFilename());
String minioPath = uploadToMinio(file, filename);
// 2. Ulož metadáta do PostgreSQL (maličký záznam!)
TestScreenshot screenshot = new TestScreenshot();
screenshot.setTestId(testId);
screenshot.setMinioPath(minioPath); // Len string cesty!
screenshot.setFileSize(file.getSize());
return repository.save(screenshot); // Rýchle! Len metadáta!
}
}Rozdiel
-- Veľkosť tabuľky PRED (s BYTEA):
SELECT pg_size_pretty(pg_total_relation_size('test_screenshots'));
-- Výstup: 48 GB
-- Veľkosť tabuľky PO (len metadáta):
SELECT pg_size_pretty(pg_total_relation_size('test_screenshots'));
-- Výstup: 245 MB
-- To je 196× menšie!| Metrika | Pred (BYTEA) | Po (MinIO) | Zlepšenie |
|---|---|---|---|
| Veľkosť tabuľky | 48 GB | 245 MB | 196× menšie |
| Čas query | 2500ms | 5ms | 500× rýchlejšie |
Kapitola 4: Use Case - Test Automation
Definícia problému
| Denná generácia | Množstvo |
|---|---|
| Testy | 500 |
| Screenshoty | 500 × 7 × 300KB = 1.05GB |
| Videá | 500 × 20MB = 10GB |
| Logy | 500 × 4 × 75KB = 150MB |
| Spolu/deň | ~11.2GB |
| Mesačne | ~336GB |
| Ročne | ~4TB |
Storage potrebný: ÁNO! Databázový storage: NIE!
Postup riešenia
Test začína
Test beží:
- Vytvor screenshoty → Upload do MinIO → Ulož referenciu v PostgreSQL
- Nahraj video → Upload do MinIO → Ulož referenciu v PostgreSQL
- Generuj logy → Upload do MinIO → Ulož referenciu v PostgreSQL
Test končí
Výsledky query:
- Rýchle! (len PostgreSQL metadáta)
- Načítaj súbory on-demand (MinIO streaming cez presigned URL)
Kapitola 5: Porovnanie výkonu
Benchmark: 1000 screenshotov (300MB celkom)
Výkon uploadu:
| Metrika | PostgreSQL BYTEA | MinIO |
|---|---|---|
| Čas | 45 sekúnd | 8 sekúnd |
| Pamäť (špička) | 800MB | 100MB |
| Nárast veľkosti DB | +300MB | +150KB (metadáta) |
Výkon query (100 screenshotov):
| Metrika | PostgreSQL BYTEA | MinIO |
|---|---|---|
| Čas query | 8.5 sekúnd | 12ms + 1.2s (paralelné sťahovanie) |
| Prenesené dáta | 30MB | 15KB (len cesty) |
| Pamäť | 350MB | 2MB |
Výkon backupu:
| Metrika | PostgreSQL BYTEA | MinIO + PostgreSQL |
|---|---|---|
| Čas backupu | 2h 15min | 15 minút |
| Veľkosť dumpu | 52GB | 250MB + 50GB súborov |
To je 9× rýchlejšie!
Porovnanie nákladov za 1 rok
| Metrika | PostgreSQL BYTEA | MinIO + PostgreSQL |
|---|---|---|
| Veľkosť databázy | 4.2 TB | 2 GB |
| File storage | N/A | 4 TB |
| Backup storage | 29.4 TB | 4 TB |
| Čas backupu/deň | 8 hodín | 20 minút |
| Čas query | 2-5 sekúnd | 5-10ms |
| Pamäť/query | 500MB+ | 2-5MB |
| Náklady na hardware | $40,000 | $3,000 |
Úspora: $37,000!
Kapitola 6: Najlepšie praktiky
ČO ROBIŤ
// 1. Ukladaj metadáta do DB, súbory do MinIO
@Entity
public class Document {
private String minioPath; // Referencia na cestu
private Long fileSize;
private String contentType;
// ŽIADNY byte[] content!
}
// 2. Použi presigned URL pre dočasný prístup
String url = minioService.getPresignedUrl(path, 3600);
response.redirect(url); // Klient sťahuje priamo
// 3. Streamuj veľké súbory (nenačítavaj všetko do pamäte)
InputStream stream = minioService.getObject(path);
// Streamuj do odpovede...
// 4. Organizuj súbory do bucketov/priečinkov
String path = String.format("test-screenshots/%d/%s/%s",
testId, dateFolder, filename);
// 5. Nastav lifecycle politiky (automatické mazanie starých súborov)
// mc ilm add --expiry-days 90 local/test-screenshots/tempČO NEROBIŤ
// 1. Neukladaj súbory do databázy
@Column(columnDefinition = "BYTEA")
private byte[] fileContent; // NIE!
// 2. Nenačítavaj všetky súbory do pamäte
List<byte[]> allFiles = new ArrayList<>();
for (Screenshot s : screenshots) {
allFiles.add(s.getContent()); // OOM!
}
// 3. Nepoužívaj filesystem na viacerých serveroch
String path = "/var/uploads/" + filename; // Neškáluje sa!
// 4. Neprehltávaj chyby uploadu
try {
minioService.upload(file);
} catch (Exception e) {
// Súbor stratený navždy!
}Kapitola 7: Migračná stratégia
Krok za krokom migrácia z PostgreSQL BYTEA
-- 1. Pridaj nové stĺpce
ALTER TABLE test_screenshots ADD COLUMN minio_path VARCHAR(500);
ALTER TABLE test_screenshots ADD COLUMN migrated BOOLEAN DEFAULT false;// 2. Migračná služba (dávkové spracovanie)
@Service
public class BlobMigrationService {
public void migrateScreenshots() {
List<Screenshot> batch = repository.findByMigratedFalse(100);
for (Screenshot screenshot : batch) {
// Upload do MinIO
String minioPath = minioService.uploadBytes(
screenshot.getContent(),
String.format("test-screenshots/%d/%s",
screenshot.getTestId(),
screenshot.getFilename())
);
// Aktualizuj záznam
screenshot.setMinioPath(minioPath);
screenshot.setContent(null); // Vymaž BYTEA
screenshot.setMigrated(true);
repository.save(screenshot);
}
}
}-- 3. Po dokončení migrácie
ALTER TABLE test_screenshots DROP COLUMN content;
VACUUM FULL test_screenshots; -- Uvoľni miesto!Záver
Rozhodovacia matica
| Ukladaj do PostgreSQL | Ukladaj do MinIO |
|---|---|
| Malé dáta (<1KB) | Veľké súbory (>100KB) |
| Často dotazované | Binárne dáta |
| Potrebuje transakcie | Mediálne súbory |
| Štruktúrované dáta | Logy, backupy |
SAAS vs On-Premise
| Vyber AWS S3 (SAAS) ak | Vyber MinIO (On-Premise) ak |
|---|---|
| Cloud-first stratégia | Požadovaná suverenita dát |
| Potrebná globálna distribúcia | Nižšie dlhodobé náklady |
| Nechceš spravovať infraštruktúru | Plná kontrola nad infraštruktúrou |
| Rozpočet na opakované náklady | Už máš hardware |
Hybridný prístup: MinIO pre dev/test, AWS S3 pre produkciu. Rovnaké API = rovnaký kód!
Kľúčové poznatky
Databáza = Metadáta (rýchle query, malá veľkosť)
├─ id: 123
├─ filename: "screenshot.png"
├─ path: "s3://bucket/123.png" ← Len referencia!
└─ size: 245KB
MinIO = Súbory (optimalizovaný storage, streaming)
└─ s3://bucket/123.png (245KB) ← Skutočný súborReálne výsledky
| Metrika | Pred (BYTEA) | Po (MinIO) |
|---|---|---|
| Databáza | 52GB | 2GB (26× menšia) |
| Čas backupu | 2 hodiny | 15 min (8× rýchlejšie) |
| Čas query | 2.5 sekundy | 5ms (500× rýchlejšie) |
| Pamäť/operácia | 500MB | 2MB (250× menej) |
| Náklady na hardware | $40,000 | $3,000 (92% lacnejšie) |
ROI: Neoceniteľné!
Tento článok je výsledkom rokov skúseností s rôznymi storage riešeniami. Každý prístup mal svoje miesto v čase, každý naučil niečo nové. MinIO je aktuálne najlepšie on-premise riešenie pre object storage, zatiaľ čo AWS S3 zostáva top voľbou pre SAAS stratégiu.
P.S.: Databáza je pre DÁTA, MinIO/S3 je pre SÚBORY. Zapamätaj si to!
P.P.S.: S3-kompatibilné API znamená flexibilitu - vyvíjaj na MinIO, deployuj na AWS, alebo naopak!