Skip to content

Instantly share code, notes, and snippets.

@robbat2
Created February 12, 2018 23:10
Show Gist options
  • Select an option

  • Save robbat2/bf2b0515b544d12188d365c8dbf44edc to your computer and use it in GitHub Desktop.

Select an option

Save robbat2/bf2b0515b544d12188d365c8dbf44edc to your computer and use it in GitHub Desktop.
#!/bin/bash
# XFS defrag helper script
# Copyright 2010-2018 Robin H. Johnson <robbat2@gentoo.org>
# Originally written for IsoHunt, later improved for BC Libraries Cooperative & Dreamhost
#
# For each filesystem
# 1. get complete output of inode/fragments
# 2. select all inodes w/ more than 10 excess fragments (actual-ideal)
# 3. just the inode lines, not directories or other stats
# 4. sort inodes by number of fragments, descending
# 5. extract the inode number
# 6. convert inode number to file name
# 7. dump file names to temp file
# 8. defrag files in order from tempfile
#
#
# You must NOT pipe xfs_db & xfs_fsr together
# That WILL cause a deadlock in XFS!
XFS_VOL="$1"
# set the number of fragments past which we fire
: ${FRAGLIMIT:=10}
export TMPDIR=$(mktemp -d /tmp/xfs-defrag.XXXXXX)
export TMPFILE=$(mktemp -p $TMPDIR files-to-defrag.XXXXXX)
trap "rm -rf $TMPDIR" EXIT
echo $TMPDIR
cd $TMPDIR
AWKSCRIPT="${TMPDIR}/xfs_db_parse.awk"
cat >"${AWKSCRIPT}" <<'EOF'
BEGIN {
# Truncate file
printf "" >"inode.frags";
# set limit over excess fragments
if (limit < 1) {
limit = 1;
}
}
# find lines
/^inode [0-9]+ actual [0-9]+ ideal [0-9]+/ {
inode=$2;
actual=$4;
ideal=$6;
if(actual - ideal > limit) {
print inode, actual-ideal >>"inode.frags";
}
next;
}
# stats
/actual.*ideal.*fragmentation.factor/ { print > "stats"; next }
# useless output
/Note, this number is largely meaningless./ { next }
/Files on this filesystem average/ { next }
# other lines
// {
printf "xfs-defrag: unknown xfs_db output: '%s'\n", $0 > "/dev/stderr"
}
EOF
found=0
grep -w xfs /proc/mounts | while read dev mnt fstype rest ; do
if [ -n "$XFS_VOL" ]; then
[ "$XFS_VOL" != "$dev" ] && [ "$XFS_VOL" != "$mnt" ] && continue
found=1
fi
echo Scanning $mnt
# This command will generate a LOT of output, we must pipe
# this will write (inode,fragments) to the file 'inode.frags'.
xfs_db -r -c 'frag -v -f' $dev |
awk -v limit=$FRAGLIMIT -f "${AWKSCRIPT}"
# sort by inode number
sort -k 1b,1 <inode.frags >inode.frags.tmp && mv -f inode.frags.tmp inode.frags
# If there are no fragmented files, skip to next FS
[ -s inode.frags ] || continue
# find the filenames for each inode, print 'inode filename'
# WARNING: filename may contain spaces!
find $mnt \
\( \
-false \
$(awk '{printf "-or -inum %d ",$1}' inode.frags) \
\) \
-printf "%i %h/%f\n" \
>inode.filename
# sort by inode number
# WARNING: filename may contain spaces!
sort -k 1b,1 <inode.filename >inode.filename.tmp && mv inode.filename.tmp -f inode.filename
# Now we need to join the stats, so we can visit files by name, in order of
# how many fragments they have.
join -j 1 inode.frags inode.filename >joined
# 'joined' now contains
# inode numfrag filename
# WARNING: filename may contain spaces!
sort -k +2nr joined |cut -d' ' -f3- >>$TMPFILE
# TMPFILE now contains
# filename
# Do not scan more
[ -n "$XFS_VOL" -a "$found" = "1" ] && break
done
# You must NOT pipe xfs_db & xfs_fsr together
# That WILL cause a deadlock in XFS!
# Run FSR on files listed
if [ -z "$DRYRUN" ] && [ -s "${TMPFILE}" ]; then
# xfs_fsr does not run properly under xargs normally, because it wants an
# interactive stdin.
# if stdin is not interactive [tested with isatty(3)], then xfs_fsr output
# goes to syslog!
# to work around this, we use 'script -c "$CMD" /dev/null' to force the
# command to be in a new interactive shell.
cat $TMPFILE \
| tr '\n' '\0' \
| ionice -c 3 \
xargs -0 --replace \
/usr/bin/script -c 'xfs_fsr -v {}' /dev/null \
| grep --line-buffered -e '^extents ' -e '(skipping)'
fi
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment