I need to shrink a large ext4 volume, and I would like to do it with as little downtime as possible. With the testing I've done so far it looks like it could be unmounted for the resize for up to a week. Is there any way to defragment the filesystem online ahead of time so that resizefs won't have to move so many blocks around?
Update: It's taken some time to get to this point, moved quite a few TB of data around in preparation for the shrink, and I've been experimenting using the information in the answer below. I finally came up with the following command-line which could be useful to others in a similar situation with only minor modifications. Also note, it should be run as root for the filefrag and e4defrag commands to work properly - it won't affect the file ownership. It does also work properly on files with multiple hard-links, which I have lots of.
find -type f -print0 | xargs -0 filefrag -v | grep '\.\.[34][0-9]\{9\}.*eof' -A 1 | awk '/extents found/ {match($0, /^(.*): [0-9]+ extents found/, res); print res[1]}' | xargs -n 1 -d '\n' e4defrag
A quick explanation to make it easier for others to modify/use:
The first 'find' command builds the list of files to work with. Possibly redundant now or could be done a better way, but while testing I had other filters there and I've left it as a handy place to modify the scope of the rest of the command.
Next pass each file through 'filefrag -v' to get a list of all physical blocks used by each file.
The grep looks for the last block used by each file (line ending in 'eof'), and where that block is a 10-digit number starting with 3 or 4. In my case my new filesystem size will be 2980024320 blocks long so that does a good-enough job of only working on files that are on the area of disk to be removed. Having grep also include the following line (the '-A 1') also includes the filename in the output for the next section. This is where anyone else doing this will have to modify the command depending on the size of their filesystem. It could also probably be done in a much better way but this is working for me now and I'm lazy.
awk pulls just the filenames out of all the other garbage that grep left in the filefrag output.
And finally e4defrag is called - I don't care about the actual fragment count, but it has the side effect of moving the physical blocks around (hopefully into an early part of the drive), and it works against files with multiple hard-links with no extra effort.
If you only want to know which files it would defrag without actually moving any data around, just leave the last piece of the command off.
find -type f -print0 | xargs -0 filefrag -v | grep '\.\.[34][0-9]\{9\}.*eof' -A 1 | awk '/extents found/ {match($0, /^(.*): [0-9]+ extents found/, res); print res[1]}'