Tough yes, preserving data integrity, easy.
In essence, removing a column(s) from a RAID-Zx vDev involves freezing the column(s) from future writes. This prevents the need to deal with normal updates. And stops the RAID-Zx vDev from using the frozen column(s) as stripe member(s). Meaning a 6 disk RAID-Z2 has 2 parity and up to 4 data columns. So, freezing a column would limit that vDev to 3 data columns.
The (excruciating & extremely messy) hard part is breaking up previously full stripe writes, (4 data columns & 2 parity in the example above), into 2 separate stripes. And accounting for any snapshots, clones, block clones, hard links, DeDup, etc…
Any RAID-Zx stripe that does not have to be fragmented, is relatively easy by comparison. (I did say in comparison!) Now this does require moving the block, (thus, block pointer re-write, which is hard but not as hard as fragmenting a block).
Maintaining data integrity is basically the easiest of them all. Regardless if it is a single block move or a 2 block fragmented move, all related data would be in the same ZFS write transaction group. (So both fragmented stripes and all associated metadata for a fragmented block, is written at the same time.) If it fails, (OS crash or power loss), the old data is still their. If the new write succeeds, then still good. Easy peasy.
Such a RAID-Zx vDev shrink task should be restartable. As long as the frozen column(s) state is preserved during export and import.
Yes, for those reading between the lines, I did give this a bunch of thought. Both adding parity to RAID-Z1/2 vDevs, and removing a column from RAID-Zx vDevs. (Note that adding parity to RAID-Z3 is not possible as ZFS does not support RAID-Z4.) However, I have no skills to implement such a feature.