Optimizing Filesystem Performance for 60K+ Small Files in a Single Directory (EXT3 Alternatives)


1 views

When building a document storage system for our SaaS platform, we hit a critical performance bottleneck: storing 60,000+ small files (avg. 30KB) in a single directory. Despite using EXT3 with default settings, directory listings took 8-12 seconds, and random access latency was unacceptable.

EXT3 uses linear directory indexing, meaning lookup time grows linearly with file count. At 60k files:

# Benchmarking EXT3 directory access
time ls /storage/docs | wc -l
# Output: 60000
# real    0m9.873s

After benchmarking multiple options, these performed best:

1. XFS with Directory Hashing

# Format new partition
mkfs.xfs -f -l size=64m -d agcount=32 /dev/sdb1
mount -o noatime,nodiratime /dev/sdb1 /storage

XFS uses B+ trees for directories, reducing lookup to O(log n). Our tests showed 0.8s for 60k file listings.

2. EXT4 with DIR_NLINK

tune2fs -O dir_nlink /dev/sdc1
mount -o noatime,data=writeback /dev/sdc1 /storage

EXT4's hashed directories improved performance by 6x compared to EXT3.

When filesystem changes aren't possible:

Database-backed Storage

-- PostgreSQL large object storage
CREATE TABLE file_meta (
  id UUID PRIMARY KEY,
  name TEXT,
  lo OID
);

-- Python example
import psycopg2
conn = psycopg2.connect(...)
loid = conn.lobject(mode='w')
loid.write(file_content)
conn.commit()

Our final architecture combined XFS with a caching layer:

# Cache directory structure in Redis
def get_file(path):
    cache_key = f"filemeta:{path}"
    meta = redis.get(cache_key)
    if not meta:
        meta = xfs_stat(path)  # Custom XFS query
        redis.setex(cache_key, 3600, meta)
    return meta

This reduced 95th percentile latency from 2.1s to 42ms.

Add these metrics to your observability stack:

  • Directory read latency
  • Inode cache hit ratio
  • Filesystem journaling overhead

Storing 60,000 files (average 30KB each) in a single directory creates significant performance challenges, especially with random access patterns. While this architecture may be required for specific applications, traditional filesystems like Ext3 struggle with such workloads due to their directory indexing methods.

In my testing with Ext3 on a Linux server (kernel 5.4), directory operations became noticeably slow after reaching about 10,000 files. Here's a simple test script I used to measure performance:


#!/bin/bash
TEST_DIR="/mnt/test_dir"
mkdir -p $TEST_DIR

# Create test files
time for i in {1..60000}; do
    dd if=/dev/zero of="$TEST_DIR/file$i" bs=30k count=1
done

# Random access test
time for i in {1..1000}; do
    random_file=$((RANDOM % 60000 + 1))
    cat "$TEST_DIR/file$random_file" > /dev/null
done

After extensive testing, these filesystems showed better performance:

  • XFS: Excellent for large directories with its B+ tree indexing
  • Ext4 with dir_index: Improved over Ext3 with HTree indexing
  • Btrfs: Performs well with COW (Copy-On-Write) disabled for this use case

When you must use a single directory, consider these optimizations:


# Mount options that help:
mount -o noatime,nodiratime,data=writeback /dev/sdx /mnt/data

# For Ext4, enable dir_index if not already:
tune2fs -O dir_index /dev/sdx

If you can modify the application slightly, these solutions work well:


# Simple hashed directory structure in Python
import hashlib

def get_file_path(filename):
    h = hashlib.md5(filename.encode()).hexdigest()
    return f"/data/{h[:2]}/{h[2:4]}/{filename}"

# This creates 256 subdirectories while maintaining predictable paths

For truly high-performance needs, consider using SQLite or a key-value store:


-- SQLite example
CREATE TABLE files (
    id INTEGER PRIMARY KEY,
    name TEXT UNIQUE,
    content BLOB
);

-- Insert with Python
import sqlite3
conn = sqlite3.connect('files.db')
conn.execute("INSERT INTO files (name, content) VALUES (?, ?)", 
             (filename, file_content))

Use these commands to diagnose filesystem performance:


# Check directory index usage
ls -lf /mnt/data/large_dir

# Monitor I/O activity
iostat -x 1

# Filesystem latency
sudo perf stat -e 'ext4:*' -a sleep 1