Created
January 10, 2025 17:45
-
-
Save Uplink03/ad841188d88d860e585087177f8d089a to your computer and use it in GitHub Desktop.
Eat ext4 space slowly with metadata
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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