XFS Tuning and Internals: Allocation Groups, Log Layout, and Real Workload Tuning
XFS is the quiet giant of Linux filesystems. It has been the default on RHEL since version 7, ships with petabyte-scale guarantees, and is the workhorse under most HPC parallel storage, most high-end NAS, and most large PostgreSQL installations. It does not win benchmarks with marketing; it wins them by not falling over at 3 a.m. when a 40 TB filesystem is 92% full.
This post is for the engineer who has been using XFS by default for years and wants to actually understand what’s happening underneath — the allocation group architecture, the log, the tuning knobs that matter for small-file vs large-file workloads, reflinks, and the handful of pitfalls that still trip people up.
The design in one paragraph
XFS was originally designed at SGI in 1993 for IRIX on machines with many CPUs, large disks, and large files. Its architecture reflects that: the filesystem is partitioned into allocation groups (AGs) that act as nearly-independent filesystems, each with their own free-space B+trees, inode B+trees, and locking. This lets XFS scale concurrent writes across many CPUs without a central bottleneck. The price is that allocation groups are mostly fixed at mkfs time, which is why mkfs.xfs parameters matter more on XFS than on most other filesystems.
Allocation groups
When you run mkfs.xfs /dev/sda1, it picks a default number of allocation groups based on filesystem size. You can see this with:
|
|
The key number is agcount. On that 8 TB filesystem, XFS chose 32 AGs of ~248 GB each. Each AG is a self-contained mini-filesystem with:
- A free-space B+tree indexed by block offset (for locality allocation).
- A free-space B+tree indexed by extent length (for best-fit allocation).
- An inode B+tree tracking where inodes live.
- A reverse-mapping tree (if
rmapbt=1, used by scrub and reflink). - A superblock copy for redundancy.
Why AG count matters
When two processes allocate blocks, XFS tries to place them in different AGs to avoid lock contention. More AGs = more parallelism. Fewer AGs = better locality for sequential workloads.
For a SATA SSD or spinning disk, the default AG sizing is almost always correct. For NVMe at 100+ CPU threads, the default of 32–64 AGs can become a bottleneck if most work concentrates in one directory. XFS places all files created in the same directory in the same AG by default to improve locality — which is great for ls performance but bad for a single-directory ingest workload.
Override with -d agcount=N at mkfs time. A reasonable heuristic for throughput workloads: agcount >= min(nproc * 2, 1024). For archival/bulk storage: leave it alone.
Inode 64-bit mode
On filesystems larger than 1 TB, XFS may allocate 64-bit inodes (inode numbers > 2^32), which some very old applications (32-bit NFS clients, ancient backup agents) cannot handle. The inode32 mount option forces all inodes into the first 1 TB of the filesystem, which is a terrible idea for modern workloads — it makes that portion of the disk a hotspot and can cause premature ENOSPC on metadata.
Always use inode64 (the default on modern kernels). If you have a 32-bit-inode-incompatible client, fix the client.
The log (journal)
XFS is a metadata journaling filesystem. Data blocks are not journaled — they go to their final location before the metadata commits. The log records only the metadata changes.
The log can live in three places:
- Internal log (default): somewhere inside the filesystem’s block range. Chosen by
mkfsfor balance. - External log: a separate device specified with
-l logdev=/dev/nvme1n1. Used when the data device is slow (large arrays) and you have a fast dedicated journal device. - Null log during certain operations (not normally user-visible).
Log size
The log has a maximum size of 2 GB and a minimum of 512 blocks. Default sizing is a small fraction of the filesystem size.
For most workloads, the default is fine. Increase the log size if:
- You do heavy metadata operations (many small-file creates/deletes). A larger log absorbs more transactions before forcing a checkpoint.
- You see
log IO errormessages under heavy load, or long pauses during metadata-intensive bursts.
Set it with -l size=2g at mkfs time. You cannot resize the XFS log after creation — another reason mkfs parameters are consequential.
Log stripe unit
On RAID5/RAID6 arrays, align the log to the stripe size with -l sunit=<blocks>. Misaligned log writes cause read-modify-write cycles and tank performance on parity arrays. mkfs.xfs usually detects this automatically from the underlying device (libblkid topology), but verify:
|
|
If sunit=0 and you’re on a RAID5/6 array, you have misalignment. The fix is mkfs-time only.
Real-time log tuning
Two runtime knobs worth knowing:
/proc/sys/fs/xfs/xfssyncd_centisecs— how often the log is flushed. Default 3000 (30s). Leave alone unless you have a very specific reason./proc/sys/fs/xfs/filestream_centisecs— controls the filestream allocator (see below). Leave alone unless you’ve identified it as a bottleneck.
Allocation strategies
XFS has multiple allocation heuristics:
Delayed allocation
When you write to a file, XFS does not immediately pick blocks. It reserves space in memory and waits, letting multiple writes coalesce into larger contiguous extents. The blocks are chosen at flush time. This is why dd-style benchmarks against XFS show excellent extent sizes — delayed allocation has the full picture by the time it commits.
The downside: crash between write and flush means data loss, but since XFS never journals data, this is expected semantics. Applications that care about durability must fsync(). This also means “how full is the filesystem?” is a slightly squishy question, because delayed-allocated writes have not been accounted against free space yet.
File streams
For parallel ingest into a single directory (think: video recording, log collector), XFS has a filestream allocator invoked with mount -o filestreams. It assigns each concurrently-writing file its own AG, preventing them from interleaving and fragmenting. Only enable on filesystems that see this specific pattern; for general workloads it hurts cache efficiency.
Preallocation
fallocate(2) on XFS is cheap and exact — it marks the extent range as allocated without writing data. Use it when you know the final size ahead of time (download managers, database files, VM images). XFS will place the allocation in one or a few large extents rather than growing incrementally.
Reflinks (reflink=1)
As of kernel 4.9+ and xfsprogs 4.9+, XFS supports reflinks — same mechanism as Btrfs, different implementation. cp --reflink on an XFS filesystem formatted with reflink=1 creates a shared-extent copy in O(1) time and no additional space.
Check with xfs_info:
|
|
On older filesystems formatted before this was the default, reflink=0. There is no online conversion — you must reformat. For greenfield deployments on modern kernels, reflink is now the default.
Reflinks enable:
cp --reflink=alwaysfor build artifacts, container images, and VM clones.xfs_copyandxfsdumpintegration.- Efficient snapshots at the file level (pair with a cron job that reflinks directories).
Unlike Btrfs, XFS does not offer subvolume-level snapshots. For that, layer LVM thin snapshots beneath XFS — covered below.
Small-file vs large-file tuning
The single biggest tuning decision is your workload’s file-size distribution. XFS handles both well, but different AG sizing and mount options apply.
Small-file workloads (mail spools, NFS homes, git repos)
Symptoms: millions of files, average size < 64 KB, heavy metadata churn (create/unlink/rename).
mkfs parameters:
-i size=512— larger inodes (512 bytes) let more file attributes live inline without spilling to separate blocks. This is the default on modernxfsprogsfor this reason.-n size=4096or-n size=16384— directory block size. Larger directory blocks help withreaddiron huge directories but waste space on small ones. 4096 is a good default.-n ftype=1— enables file type in directory entries, needed for overlayfs and used by many tools. Default on modern mkfs.-f finobt=1— free inode B+tree. Speeds up inode allocation in aged filesystems. Default on modern mkfs.
Mount options:
-o inode64(default) — allow inodes anywhere on disk.-o logbsize=256k— larger log buffers absorb more metadata ops per flush cycle. Major win for metadata-heavy workloads.-o logbufs=8— more in-flight log buffers. Default is 2 on small systems; bump to 8 on metadata-heavy servers with enough RAM.
Kernel-side tuning:
vm.dirty_background_ratio,vm.dirty_ratio— how much dirty data the kernel holds before flushing. Lower values = more frequent small flushes, which hurts metadata-heavy workloads. Defaults are usually fine.
Large-file workloads (VM images, scientific data, media)
Symptoms: files measured in GB, mostly sequential I/O, few files per directory.
mkfs parameters:
-d agcount=4or-d agcount=8— fewer AGs means better sequential allocation and less metadata overhead. Default agcount may be too high for pure-large-file usage.-d sunit=<bytes>,swidth=<bytes>— align to underlying RAID stripe. Mkfs usually detects this, but verify.-l size=2g— maximum log size, for large metadata transactions.
Mount options:
-o largeio— prefer large I/O sizes at the page cache layer. Only useful for rare workloads; measure before setting.-o allocsize=1g— pre-allocate in 1 GB increments. Reduces fragmentation of streaming writes.-o swalloc— align stripe-width allocations. For RAID5/6 streaming writes.
Avoid -o noquota unless you are certain — quotas are free to enable and useful for diagnostics.
Mixed workload
Default mkfs and default mount options. Stop tuning. Run for a year. Tune only if you have measured a specific problem.
Quotas that actually work
XFS quotas are real. Unlike Btrfs qgroups, they are fast, cheap, and have worked identically since IRIX. Three types:
- User quotas —
uquota/pquotamount options. - Group quotas —
gquota. - Project quotas — the XFS-specific and most useful type.
Project quotas are per-directory-tree. Assign a project ID to a directory, and all files created under it count against a quota for that ID. This is the mechanism containers use to cap per-container disk usage on XFS overlays (overlay2 + xfs_quota).
|
|
Use xfs_quota -x -c 'report -h' to see current usage. Quotas are enforced at write time with ENOSPC once limits are hit — tools see this as a clean error, not a disk-full condition.
Growing a filesystem
xfs_growfs /mnt/data grows the filesystem to fill the underlying block device after you’ve resized the device (LVM extend, cloud volume resize, etc.). This is online — no unmount needed.
One direction only: XFS cannot be shrunk. If you need to shrink, xfsdump to another volume, mkfs.xfs smaller, xfsrestore. There is no online or offline shrink tool, and this is not a planned feature. Design accordingly — always leave room, and provision LVM such that you can grow the XFS volume instead of needing to shrink some other volume.
LVM + XFS: the production duo
The standard production stack for XFS is:
- mdadm for RAID (below the LVM layer on hardware without HW RAID).
- LVM for volume management — thin provisioning, snapshots, online grow.
- XFS for the filesystem.
This stack gives you:
- Snapshots via LVM (XFS itself doesn’t do subvolume snapshots). Use
lvcreate --snapshot -L 10Gagainst a thin pool to freeze a filesystem for backup. - Online grow by extending the LV and running
xfs_growfs. - Discard/trim passed through to the SSD via
lvm.confissue_discards = 1and XFS mount optiondiscard(or better, scheduledfstrimweekly).
Avoid discard as a mount option on busy filesystems — every unlink triggers a synchronous TRIM, which can stall I/O on some SSDs. Use the fstrim.timer systemd unit instead.
Repair and recovery
xfs_repair is the equivalent of fsck.xfs. Run it only on unmounted filesystems:
|
|
Notable options:
-n— dry run, report what would be fixed without modifying anything.-L— zero the log. Destructive. Use only when mount fails because of a corrupted log, and be prepared to lose the last few seconds of metadata changes.
If xfs_repair reports clean but mount still fails, the log is corrupt. Most often this is because of a failing disk — don’t just run -L and hope, because the next hour of I/O on a failing device will produce more corruption. Image the device first (ddrescue), run repair on the image, recover data, replace the hardware.
xfs_scrub runs online metadata scrubbing and is enabled by default on modern systemd distributions (xfs_scrub_all.service). Let it run. If it starts alerting, investigate before there’s real corruption.
The bits that changed recently
A few modern XFS features that are worth knowing about but not worth the transition if you’re already running stable:
- Reverse mapping B+tree (
rmapbt): tracks which files own which extents. Enables online scrub and verifies reflink consistency. Adds a few percent metadata overhead. Default on recent mkfs. - Sparse inodes (
sparse=1): allocates inode chunks partially, for filesystems that exhaust inodes before disk space. Default on recent mkfs. - Big timestamps (
bigtime=1): extends the inode timestamp range past 2038. Default on recent mkfs. - Large extent counters (
nrext64=1): 64-bit extent counters per inode, for very large sparse files. Not default; enable if you expect multi-TB single files.
None of these require migration for an existing filesystem to keep working. You only get them by reformatting.
Common mistakes
- Running
xfs_repair -Lwithout investigating why the log is dirty. You’ve just committed to losing data that the log had but the filesystem hadn’t. Know what you lost. - Using
discardmount option on busy SSDs. Schedulefstrimweekly instead. - Expecting to shrink. You can’t. Design around growing only.
- Ignoring
inode32. Someone set it years ago, and now you can’t figure out why your 100 TB filesystem has ENOSPC on metadata. Remountinode64. - Forgetting
-d sunit/swidthon RAID5/6. Misaligned XFS on a parity array is a slow-motion performance collapse. - Not using project quotas for multi-tenant container hosts. Overlay2 + project quotas is the clean way to cap per-container disk.
When XFS is the right default
- General-purpose servers > 1 TB. XFS is what RHEL and its derivatives default to for a reason.
- Database hosts. PostgreSQL and MySQL on XFS is a well-trodden path with predictable performance.
- NFS servers. XFS’s metadata performance under parallel load beats ext4 at scale.
- Container hosts using overlay2. Project quotas give you per-container limits cheaply.
- Any filesystem you will grow but never shrink.
When to pick something else
- You need filesystem-level snapshots without an LVM layer. Btrfs or ZFS.
- Root filesystem with rollback on upgrade. Btrfs with snapper.
- RAID5/6 without mdadm. ZFS.
- You need to shrink. ext4, or design differently.
XFS is not exciting. That’s the point. It is the filesystem you pick when you want the storage layer to not be a topic of conversation, and when you learn its handful of quirks — AGs at mkfs time, log size, inode64, LVM for snapshots, project quotas for tenants — it will run for years without asking for attention.
Comments