Skip to content

Instantly share code, notes, and snippets.

@Uplink03
Created January 10, 2025 17:45
Show Gist options
  • Save Uplink03/ad841188d88d860e585087177f8d089a to your computer and use it in GitHub Desktop.
Save Uplink03/ad841188d88d860e585087177f8d089a to your computer and use it in GitHub Desktop.
Eat ext4 space slowly with metadata
// Ext4 leaves metadata allocated for directories when files are deleted.
// That isn't usually a problem, as that allocation is reused when new files are created.
// But if you leave lots of abandonded empty directories around, they can slowly eat up disk space.
// Note on "slowly": "very slowly". On my system (8 cores, nvme drive), it takes aproximately 10s per iteration.
// 100 iteration take 15-20 minutes.
// This program creates 200K small files, and then deletes them, after which it repeats the process in a new directory.
// By default, it loops 100 times using the `output_files` directory.
// If you run `du -h` on a directory that has undergone this process, you'll see that it takes a few megabyets of space.
// I ran my test with a 4GB ext4 filesystem. After 100 iterations, I had 534 MB of space wasted by empty directories.
// After 200 iterations, there was more than 1 GB of waste.
// For reference, btrfs doesn't show this behaviour. An empty directory doesn't take up any space.
package main
import (
"flag"
"fmt"
"os"
"os/signal"
"path/filepath"
"sync"
"syscall"
)
const (
totalFiles = 200000
numGoroutines = 200
filesPerGoroutine = totalFiles / numGoroutines
)
func main() {
outputDir := flag.String("outputDir", "output_files", "Directory to store the output files")
first := flag.Int("first", 0, "Index to start at")
count := flag.Int("count", 100, "Count of directories to make")
flag.Parse()
// Handle SIGINT (Control+C)
stop := make(chan os.Signal, 1)
signal.Notify(stop, os.Interrupt, syscall.SIGINT)
// Goroutine to listen for stop signal
go func() {
<-stop
fmt.Println("\nReceived interrupt signal. Exiting.")
os.Exit(1)
}()
for i := *first; i < *first + *count; i++ {
fmt.Printf("%d ", i);
attack(filepath.Join(*outputDir, fmt.Sprintf("%d", i)))
}
}
func attack(outputDir string) {
// Create output directory
if err := os.MkdirAll(outputDir, os.ModePerm); err != nil {
fmt.Printf("Failed to create output directory: %v\n", err)
return
}
// Channel to signal goroutines to start
start := make(chan struct{})
// WaitGroup to wait for all goroutines to finish
var wg sync.WaitGroup
// Start worker goroutines
for i := 0; i < numGoroutines; i++ {
wg.Add(1)
go func() {
defer wg.Done()
<-start
for index := 0; index < filesPerGoroutine; index++ {
filename := filepath.Join(outputDir, fmt.Sprintf("%d.zero", index + i * filesPerGoroutine))
if err := os.WriteFile(filename, []byte("Hello, World!"), 0644); err != nil {
fmt.Printf("Failed to create file %s: %v\n", filename, err)
}
}
}()
}
close(start);
// Wait for all goroutines to complete
wg.Wait()
fmt.Println("All files created successfully.")
// Now delete all the files
start = make(chan struct{})
for i := 0; i < numGoroutines; i++ {
wg.Add(1)
go func() {
defer wg.Done()
<-start
for index := 0; index < filesPerGoroutine; index++ {
filename := filepath.Join(outputDir, fmt.Sprintf("%d.zero", index + i * filesPerGoroutine))
if err := os.Remove(filename); err != nil {
fmt.Printf("Failed to delete file %s: %v\n", filename, err)
}
}
}()
}
close(start);
wg.Wait()
fmt.Println("All files deleted successfully.")
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment