46 Database-Maintenance: Fragmentierung und Reorganisation

Databases degenerieren über Zeit. Writes und Deletes hinterlassen Fragmentierung – Gaps im Storage, ineffiziente Index-Structures, unbalanced Chunks in Sharded-Clusters. Eine fresh Database mit 1 Million Dokumenten auf 500 MB Storage wächst nach Monaten von Insert/Update/Delete-Operations auf 800 MB – 300 MB Overhead durch Fragmentierung. Queries werden langsamer, weil Indexes fragmentiert sind. Sharded-Clusters entwickeln Hotspots, weil Chunks unequal distributed sind. Diese Degradation ist graduell und oft unbemerkt, bis Performance-Problems offensichtlich werden.

Reorganisation ist der Prozess, eine Database zu “defragmentieren” – Speicher zu reclaimen, Indexes zu rebuilden, Chunks zu rebalancen. Dies ist nicht Daily-Maintenance, sondern Periodic-Optimization – etwa quarterly oder when Performance-Degradation detektiert wird. Aber Reorganisation ist nicht kostenlos – sie erfordert I/O, CPU, manchmal Downtime. Die Kunst ist zu wissen wann Reorganisation nötig ist, welche Method zu nutzen, und wie Impact zu minimieren.

Dieses Kapitel behandelt Database-Maintenance systematisch – von Fragmentierung-Detection über Compaction-Strategies bis zu Shard-Balancing. Der Fokus ist auf Production-Safe-Operations mit Minimal-Downtime.

46.1 Fragmentierung: Ursachen und Impact

MongoDB’s WiredTiger-Storage-Engine ist Log-Structured – es schreibt nie in-place. Jedes Update oder Delete markiert alte Daten als “unused”, schreibt neue Version an anderen Platz. Dies ist gut für Concurrency (keine Locks), aber führt zu Fragmentierung.

Example-Scenario:

// Initial Insert: 1M Dokumente
for (let i = 0; i < 1000000; i++) {
  db.logs.insertOne({
    userId: randomUser(),
    message: "Login successful",
    timestamp: new Date()
  })
}

// Storage: 500 MB

// Nach 1 Monat: 50% Dokumente updated
for (let i = 0; i < 500000; i++) {
  db.logs.updateOne(
    { userId: randomUser() },
    { $set: { message: "Updated message" } }
  )
}

// Storage: 750 MB (original 500 MB + 250 MB neue Versionen)
// Aber: Nur 500 MB Data ist "live", 250 MB ist "dead"

Die 250 MB “dead” Data ist reclaimable – aber MongoDB reclaims sie nicht automatisch (außer via normal Reuse für neue Writes). Über Zeit akkumuliert diese Fragmentierung.

Fragmentierung-Detection:

const stats = db.logs.stats()

print(`Storage Size: ${stats.storageSize / (1024**3)} GB`)
print(`Data Size: ${stats.size / (1024**3)} GB`)
print(`Overhead: ${((stats.storageSize - stats.size) / stats.storageSize * 100).toFixed(2)}%`)

Output:

Storage Size: 0.75 GB
Data Size: 0.50 GB
Overhead: 33.33%

Ein 33% Overhead ist signifikant – etwa ein Drittel des Storage ist wasted. Für Production-Systems mit TB-scale kann dies hunderte GB wasted Storage bedeuten.

Performance-Impact:

Fragmentierung beeinträchtigt Performance subtil:

  1. Mehr Disk-I/O: Queries müssen mehr Blocks lesen wegen Fragmentierung
  2. Ineffiziente Caching: RAM cached fragmentierte Blocks statt compact Data
  3. Langsamere Scans: Collection-Scans müssen mehr Storage traversieren

Der Impact ist graduell – nicht catastrophic, aber noticeable. Eine Query, die 50ms dauerte, dauert jetzt 80ms. Nicht dramatisch einzeln, aber bei tausenden Queries summiert es sich.

46.2 compact: In-Place-Defragmentation

Der compact-Command defragmentiert eine Collection in-place – rewrites Data compact, reclaims unused Space.

db.runCommand({ compact: "logs" })

Was compact macht:

  1. Traverses alle Dokumente in der Collection
  2. Rewrites sie sequentiell in neue Data-Files
  3. Rebuilds alle Indexes
  4. Deletes alte fragmentierte Files

Duration:

Compact dauert proportional zur Collection-Size:

Collection-Size Compact-Duration
10 GB ~5-10 minutes
100 GB ~30-60 minutes
1 TB ~4-8 hours

Blocking-Behavior:

Kritisch: compact blocked die Collection während Execution. Reads und Writes auf die Collection sind blocked. Dies ist inakzeptabel für Production-Databases mit 24/7-Uptime-Requirements.

Workaround für Replica-Sets:

Für Replica-Sets kann man compact Rolling durchführen:

# 1. Compact Secondary 1
mongosh --host secondary1:27017 --eval "db.logs.compact()"

# Wait für Completion

# 2. Compact Secondary 2
mongosh --host secondary2:27017 --eval "db.logs.compact()"

# 3. Step-Down Primary
mongosh --host primary:27017 --eval "rs.stepDown()"

# New Primary ist elected

# 4. Compact old Primary (jetzt Secondary)
mongosh --host old-primary:27017 --eval "db.logs.compact()"

Dies compacts alle Nodes ohne Primary-Downtime. Während Secondary-Compaction läuft, handelt der Primary alle Traffic. Nach Step-Down wird ein compacted-Secondary der neue Primary.

Space-Requirements:

compact braucht temporary Disk-Space – etwa 1.5x Collection-Size. Für eine 100 GB Collection braucht man ~150 GB free Space. Ohne genug Space failed compact.

Use-Case-Decision:

Compact ist sinnvoll wenn: - Overhead > 30% - Collection ist read-heavy (weniger Write-Impact während Compact) - Disk-Space ist teuer - Man kann Rolling-Compaction in Replica-Set durchführen

Compact ist NICHT sinnvoll wenn: - Collection ist write-heavy (Fragmentierung kehrt schnell zurück) - Disk-Space ist günstig - Downtime ist inakzeptabel (Standalone-Databases)

46.3 repairDatabase: Full-Database-Reorganization

repairDatabase ist wie compact, aber für die gesamte Database:

db.runCommand({ repairDatabase: 1 })

Dies compacts alle Collections in der Database und rebuilds alle Indexes. Der Impact ist dramatischer als compact – es kann Stunden oder Tage dauern für große Databases, und blocked die gesamte Database.

Use-Case:

repairDatabase ist primär für Disaster-Recovery – wenn eine Database corrupted ist (etwa nach unclean Shutdown). Für normale Maintenance ist es zu invasive. Prefer compact auf einzelne Collections statt repairDatabase.

Deprecation-Note:

MongoDB hat repairDatabase in neueren Versions deprecated für Production-Use. Die empfohlene Alternative: mongodump + drop + mongorestore, oder Rolling-Compact in Replica-Sets.

46.4 Export-Reimport: The Nuclear Option

Für extreme Fragmentierung oder wenn compact nicht ausreicht, ist die Nuclear-Option: Export alle Daten, drop Collection, reimport.

# 1. Export
mongodump --uri="mongodb://host/mydb" --collection=logs --out=/backup/logs-export

# 2. Drop Collection
mongosh --eval "db.logs.drop()"

# 3. Reimport
mongorestore --uri="mongodb://host/mydb" /backup/logs-export/mydb/logs.bson

Der Reimport schreibt Data fresh und compact – zero Fragmentierung. Alle Indexes werden neu created.

Duration:

Export-Reimport ist langsamer als compact:

Collection-Size Export-Reimport-Duration
10 GB ~15-30 minutes
100 GB ~2-4 hours
1 TB ~12-24 hours

Downtime:

Anders als compact kann man Export-Reimport mit zero-downtime durchführen via Replica-Sets:

# 1. Remove Secondary from Replica-Set
rs.remove("secondary1:27017")

# 2. Export-Reimport auf removed Secondary
# (Details wie oben)

# 3. Re-Add Secondary zu Replica-Set
rs.add("secondary1:27017")

# 4. Wait für Initial-Sync (Secondary repliziert from Primary)

# 5. Repeat für andere Secondaries und Primary

Use-Case:

Export-Reimport ist sinnvoll wenn: - Extreme Fragmentierung (> 50% Overhead) - Collection-Schema ändert (Opportunity für Schema-Migration) - Indexes müssen redesigned werden - Man kann Rolling-Process in Replica-Set nutzen

46.5 Index-Maintenance: Rebuild vs. Compact

Indexes fragmentieren ähnlich wie Data. Ein fragmentierter Index ist größer und langsamer – mehr Disk-I/O, mehr RAM-Usage, langsamere Queries.

Index-Fragmentierung-Detection:

const stats = db.logs.stats()

stats.indexSizes  // Object mit Index-Names und Sizes

// Beispiel:
// {
//   _id_: 50000000,
//   userId_1: 80000000,
//   timestamp_1: 60000000
// }

// Compare mit Data-Size
print(`Data Size: ${stats.size / (1024**2)} MB`)
print(`Index Size: ${stats.totalIndexSize / (1024**2)} MB`)
print(`Index-to-Data-Ratio: ${(stats.totalIndexSize / stats.size).toFixed(2)}`)

Ein Index-to-Data-Ratio > 1.0 ist nicht ungewöhnlich (Indexes können größer sein als Data), aber wenn er deutlich wächst über Zeit, deutet das auf Fragmentierung.

Index-Rebuild:

// Drop und Recreate
db.logs.dropIndex("userId_1")
db.logs.createIndex({ userId: 1 })

// Oder: reIndex (rebuilds alle Indexes)
db.logs.reIndex()

reIndex() rebuilds alle Indexes auf der Collection. Dies ist schneller als einzelne Drop/Create für viele Indexes.

Background-Index-Builds:

MongoDB 4.2+ builded Indexes im Background – die Collection bleibt accessible während Build. Pre-4.2 musste man background: true spezifizieren, sonst blocked der Build.

// Pre-4.2: Explicit background
db.logs.createIndex({ userId: 1 }, { background: true })

// 4.2+: Always background (Parameter ignored)
db.logs.createIndex({ userId: 1 })

Rolling-Index-Rebuild in Replica-Sets:

Für Production sollte man Indexes Rolling rebuilden:

# 1. Rebuild auf Secondary 1
mongosh secondary1:27017 --eval "db.logs.reIndex()"

# 2. Rebuild auf Secondary 2
mongosh secondary2:27017 --eval "db.logs.reIndex()"

# 3. Step-Down Primary, rebuild old Primary

Caveat: Index-Rebuild ist I/O-intensive:

Ein Index-Rebuild auf einer 100 GB Collection kann hunderte GB Disk-Writes generieren. Dies kann Production-Performance beeinträchtigen. Best-Practice: Schedule Index-Rebuilds während Low-Traffic-Periods.

46.6 Shard-Balancing: Cluster-Reorganization

In Sharded-Clusters können Chunks unequal über Shards distributed sein – ein Shard hat 70% der Data, andere haben 10% each. Dies führt zu Hotspots – der Heavy-Shard ist overloaded, andere sind underutilized.

Balancer-Basics:

MongoDB’s Balancer ist ein Background-Process, der kontinuierlich Chunks zwischen Shards moved, um Balance zu erhalten:

sh.status()

// Output zeigt Distribution:
// Shard 1: 500 chunks
// Shard 2: 300 chunks
// Shard 3: 200 chunks
// Imbalance detected

Der Balancer würde automatisch Chunks von Shard 1 zu Shard 2 und 3 moven.

Balancer-Schedule:

Per Default läuft der Balancer 24/7. Für Production kann man ihn auf Maintenance-Windows restricten:

// Balancer nur zwischen 2 AM und 6 AM
db.settings.updateOne(
  { _id: "balancer" },
  { 
    $set: {
      activeWindow: {
        start: "02:00",
        stop: "06:00"
      }
    }
  },
  { upsert: true }
)

Manual-Chunk-Moves:

Für sofortige Rebalancing kann man Chunks manuell moven:

// Move Chunk von Shard 1 zu Shard 2
sh.moveChunk(
  "mydb.logs",
  { userId: "USER-12345" },  // Chunk-Range
  "shard2"
)

MongoDB bewegt den Chunk – kopiert Daten zu Destination-Shard, validiert, und updated Metadata.

Chunk-Size-Tuning:

Default-Chunk-Size ist 64 MB. Für große Clusters kann man größere Chunks nutzen (weniger Chunks = weniger Balancing-Overhead):

// Set Chunk-Size zu 128 MB
db.settings.updateOne(
  { _id: "chunksize" },
  { $set: { value: 128 } },
  { upsert: true }
)

Balancer-Impact:

Chunk-Moves sind I/O-intensive – sie kopieren Daten über Network. Ein 64 MB Chunk-Move kann Sekunden bis Minuten dauern. Während des Moves ist der Chunk still accessible (Split-Brain-Prevention), aber Performance kann degradieren.

Monitoring-Balancer-Activity:

// Check if Balancer is Running
sh.isBalancerRunning()

// View recent Balancer-Actions
db.getSiblingDB("config").changelog.find({ what: "moveChunk.start" }).sort({ time: -1 }).limit(10)

Disable-Balancer vor Maintenance:

Vor großen Maintenance-Operations (etwa Backups) sollte man den Balancer stoppen, um konsistente Snapshots zu garantieren:

sh.stopBalancer()

// Perform Maintenance

sh.startBalancer()

46.7 Zone-Sharding: Geographic-Data-Organization

Für Compliance oder Performance kann man Data geografisch organisieren via Zoned-Sharding:

// Tag Shards mit Zones
sh.addShardToZone("shard-eu", "EU")
sh.addShardToZone("shard-us", "US")

// Define Ranges für Zones
sh.updateZoneKeyRange(
  "mydb.users",
  { country: "DE" },
  { country: "GR" },  // DE bis FR (alphabetisch)
  "EU"
)

sh.updateZoneKeyRange(
  "mydb.users",
  { country: "US" },
  { country: "US" },
  "US"
)

Der Balancer respektiert Zones – EU-Data bleibt auf EU-Shards, US-Data auf US-Shards.

Use-Case:

46.8 TTL-Indexes: Automated-Data-Expiration

Für Data mit definierter Lifetime (Sessions, Logs, Cache) sind TTL-Indexes automatische Garbage-Collection:

// Logs expire nach 30 Tagen
db.logs.createIndex(
  { timestamp: 1 },
  { expireAfterSeconds: 30 * 24 * 60 * 60 }
)

MongoDB’s Background-Task deleted automatisch Dokumente, wo timestamp + expireAfterSeconds < now.

Deletion-Frequency:

TTL-Deletes laufen alle 60 Sekunden. Dies bedeutet: Ein Dokument expired nicht exact bei timestamp + 30 days, sondern bis zu 60 Sekunden später.

TTL-Index-Limitation:

TTL-Indexes funktionieren nur auf Date-Fields. Für non-Date-Expiration (etwa “delete nach 1000 accesses”) braucht man Custom-Logic.

TTL-Performance-Impact:

TTL-Deletes sind batch-wise – MongoDB deleted bis zu 100k Dokumente per Batch. Bei sehr hohen Expiration-Rates (Millionen Dokumente per Day) kann dies I/O-Impact haben.

Monitoring TTL-Activity:

db.serverStatus().metrics.ttl

// Output:
// {
//   deletedDocuments: 1500000,
//   passes: 25000
// }

deletedDocuments zeigt Total-Deletes seit Server-Start, passes zeigt wie oft der TTL-Monitor ran.

46.9 Maintenance-Scheduling: Wann und Wie

Database-Maintenance sollte scheduled sein, nicht ad-hoc:

Monthly-Maintenance:

// Fragmentierung-Check
const collections = db.getCollectionNames()

collections.forEach(coll => {
  const stats = db[coll].stats()
  const overhead = ((stats.storageSize - stats.size) / stats.storageSize * 100).toFixed(2)
  
  if (overhead > 30) {
    print(`${coll}: ${overhead}% overhead - COMPACT RECOMMENDED`)
  }
})

Quarterly-Index-Review:

// Unused-Indexes identifizieren
db.getCollectionNames().forEach(coll => {
  const indexStats = db[coll].aggregate([{ $indexStats: {} }]).toArray()
  
  indexStats.forEach(idx => {
    if (idx.accesses.ops < 1000) {
      print(`${coll}.${idx.name}: ${idx.accesses.ops} ops - DROP CANDIDATE`)
    }
  })
})

Pre-Peak-Season-Optimization:

Vor High-Traffic-Periods (etwa Black-Friday für E-Commerce) sollte man: 1. Compact fragmentierte Collections 2. Rebuild Indexes 3. Balance Sharded-Clusters 4. Validate Backups

Dies garantiert optimal-Performance während Peak-Load.

Die folgende Tabelle fasst Maintenance-Operations zusammen:

Operation Impact Duration Downtime Best-For
compact Collection-Level Minutes-Hours Yes (Rolling: No) Moderate Fragmentierung
repairDatabase Database-Level Hours-Days Yes Disaster-Recovery
Export-Reimport Collection-Level Hours-Days Rolling: No Extreme Fragmentierung
Index-Rebuild Index-Level Minutes-Hours No (Background) Index-Fragmentierung
Shard-Balancing Cluster-Level Continuous No Unbalanced-Clusters
TTL-Indexes Automated Continuous No Time-Based-Expiration

Database-Maintenance ist nicht einmalig – “Reorganize und fertig” – sondern ongoing. Databases degenerieren kontinuierlich, Maintenance muss kontinuierlich sein. Die Best-Practice ist scheduled, periodic Maintenance – monatliche Fragmentierung-Checks, quarterly Index-Reviews, pre-peak-season Optimizations. Mit systematischem Maintenance bleibt Performance stabil über Zeit – nicht degrading graduell, sondern maintained proactively. Die Alternative – reaktives Maintenance nur wenn Performance-Problems obvious sind – führt zu Fire-Drills und Emergency-Downtime. Mit proaktivem Approach wird Maintenance von Panic-Response zu Routine-Operation – scheduled, tested, predictable.