ZFS filesystem snapshotting #261
Loading…
x
Reference in New Issue
Block a user
No description provided.
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
What I'm trying to do and why
Much like with witten/borgmatic#80, this is a ticket for borgmatic to provide filesystem snapshotting: Create a read-only snapshot before backups, pass that snapshot to Borg, and then remove / cleanup the snapshot afterwards. This ticket is for ZFS support, in particular.
This would be absolutely amazing :) Thanks for considering it! Happy to beta-test it, too.
In the meantime, could that be achieved via hooks?
Yup, hooks would be a totally valid work-around, assuming you know the right snapshotting incantations.
I'll have a stab as soon as I have time, and report here. Should be simple-ish, as long as we do not try to have nice error checking ;)
The only downside of hooks is the redundancy: paths have to be specified both as locations to back up and their ZFS volume has to be specified in the snapshot hook. An auto detection of the volumes from the paths would be nicer. There will be room for automation :)
Done with hooks, it works, but there are some downsides.
Explanations
Here is the setup, using the ZFS terminology:
pool0
containing a datasetdata
./mnt/mirrored/
./mnt/mirrored/
withzfs snapshot pool0/data@snapshot_name
./mnt/mirrored/.zfs/snapshot/snapshot_name
. Note that/mnt/mirrored/.zfs
is a special ZFS directory that does not appear in listings (e.g. not showing inls -a /mnt/mirrored/
), and therefore that borg does not see when browsing the tree, so no need to explicitely exclude.Resulting config.yaml
Upsides
Downsides
.zfs/snapshot/borg-ongoing/
part of the path in the archive. It is ugly, but I could not find a way to process paths in Borg pre-backup. Any idea?pool0/data
) path in the hook, then its mount path insource_directories
. Potentially confusing.I appreciate you doing some useful R&D on this!
Would it be possible to do a bind mount of
/mnt/mirrored/.zfs/snapshot/borg-ongoing
to appear to be mounted at/mnt/mirrored/
, but only within the borgmatic process (so that it doesn't affect other processes on the system)?Here's a contrived example using
unshare
such that a bind mount is only visible to a single process: witten/borgmatic#80 (comment)Meaning that the ZFS path can't be inferred from the mount path? Is there any ZFS command-line utility that would allow us to parse that out at runtime? (I'm thinking ahead to built-in borgmatic ZFS snapshotting, not just doing it with hooks.)
Very happy to help with the R&D: borgmatic is so useful, least I can do :)
Bind mount
Oh, I like this idea. Where would we declare the bind mount in the config file so that it carries to the call to borg? Would it carry from the hooks to the borg call?
ZFS mount path inference
Yup, I'm right there with you, dreaming of that feature :) Easily feasible with
zfs list
:To make it easier to parse, from the official doc:
Hence the easy-to-parse, tab-separated result below:
Update on the hooks
I need to amend the hooks above. The
zfs destroy
in the pre- hook was a bad idea: if there is already a borgmatic/borg instatiation not yet finished,zfs destroy
will ruin it, because the hook (and hence snapshot destruction) will be called beforeborg
fails to obtain a lock on the repository.So no preemptive clean-up should be done. If someone kills borgmatic mid-way without the post- hook being called, that person is responsible for destroying the snapshot too. Otherwise, the pre- hook will just fail and throw an error.
Ideally, a snapshot would have a UUID determined at pre- hook, and the source_directions and the post- hook would use that UUID too, but that would require using variables. Definitely something for the proper borgmatic feature, though!
TLDR:
Today, with hooks, you could make the bind mount in the
before_backup
hook and unmount it in theafter_backup
hook. In the future world where ZFS snapshotting support is built-in to borgmatic, it would hopefully not even require configuration.. borgmatic would just do it transparently for any source directory enabled for snapshotting.I'm glad to hear that there's a way to get the ZFS path from the mount path! Thanks for describing that.
This ticket may be relevant to your interests: witten/borgmatic#250
Note that there is a borgmatic
on_error
hook too. You could put cleanup there.Mount: Mmm, I'm not sure I understand, sorry. How do you arrange that
unshare
in thebefore_backup
hook makes the bind only visible to borgmatic and its called programs. Then how do you deactivate it in theafter_backup
before callingzfs destroy
?Locking: #250 would solve it indeed, thanks! That'll be very useful when it's merged :)
*Cleanup in
on_error
. I think that has the same problem asafter_backup
. Let's say borgmatic instance B1 creates the snapshot and starts a long run. Meanwhile, instance B2 starts, fails to create a snapshot, or even ignores that error and then fails to access the lock on the repo. I then goes into itson_error
hook, and there destroys the snapshot. Then B1 is suddenly left without a source. #250-like locking is really the only mechanism I can think of.Ah, yes.. This approach won't work with hooks as written without making the bind mount also visible to other processes (which would defeat the purpose). It would have to be baked into an eventual borgmatic filesystem snapshotting feature.. such that when borgmatic invokes Borg, it wraps it with an
unshare
call to create any bind mounts.In regards to cleanup: I'm not that familiar with ZFS, but would it be possible to create uniquely named snapshots, so that no borgmatic invocation impacts another?
Thanks for confirming about the hooks, that's what I feared.
For cleanup: yes, totally feasible and what I was suggesting when I mentioned UUIDS above: just generate a unique name (from timestamp or anything, really) and change the name of the snapshot after the
@
in thezfs
calls.However, I cannot not find a way in YAML to generate and store the unique name at runtime in the YAML file so that both before- and after- hooks can use that same unique name. Do you know how to? Otherwise, this also might need to be done in the eventual borgmatic filesystem snapshotting feature :/
Yeah, I think that's getting beyond what you can do easily with YAML. I can imagine that you could call out to a shell script in the
before_backup
hook that writes its unique name to a temporary file, and then another script in theafter_backup
hook that reads from that file. But at that point, might as well implement the feature for real...Makes a lot of sense! How can I help in that regard?
Thanks for asking. :)
Before implementation: Design discussion, as we've been doing here. On that front: Do you have an opinion on how the feature should determine which ZFS volumes should be snapshotted? Options include automatically introspecting
source_directories
to see if they reside on ZFS volumes (and using that fact to determine whether to snapshot), or just requiring the user to explicitly provide a list of volumes to snapshot. Discussed in other contexts here: witten/borgmatic#80 and witten/borgmatic#251.During implementation: I'm more than happy to field PRs. :)
After implementation: Testing and feedback. I don't use ZFS myself, so getting feedback from actual users would be invaluable.
Introspecting source_directories would be great :) But if it is simpler to have the user specify it manually, then I'd vote for having the manual version earlier rather than a dream solution in an unspecified future. Automatic introspection can always be added later. A bird in the hand etc etc.
Happy to test too!
Sorry for the messy/bad code. But here is a small script that till now works fine when i tried it out.
I want to add a few comments about what is going on, i hope that gives you an idea how the a zfs feature could be implemented.
Note: For some reason the delete wont work when the pre job is run twice without an delete.
Hey, anything I can do to progress this? :)
Pull requests are always appreciated. 😄 But short of that, vetting the script that @floriplum posted for suitable "before" and "after" commands would aid development. Does it look like it would work for your use case, or is it missing some edge cases? I don't use ZFS myself, so review by actual ZFS users is helpful.
Yes, this would work for my use case command wise.
Something as simple as:
Pre hook - create zfs snapshot. Store the name in a variable or in addition create a static mount to separate directory
Backup - pass snapshot name to borgmatic using env to help in creating a path in the special . Zfs directory or just specify the new static snapshot mount
Post hook - delete zfs snapshot using variable or unmount and then delete.
Hope this makes sense, I like the variable option better as it means borgmatic can just dynamically use the new snapshot no matter the name but not sure how I would pass this to borgmatic
Hi,
I'm just testing the following setup, which seems to work quite well (Debian 12.2 bookworm, borgmatic 1.7.7, package version 1.7.7-1):
On my (source) machine I have three ZFS pools:
rpool
(root FS/
),bpool
(for/boot
) anddpool
(production data, say/srv
). Each ZFS pool defines a number of datasets. The backup plan is: backup everything from these pools as a single hierarchy of files, as it's seen on the source machine.I have created a bash script, name it
borgmatic-hook
, which gets invoked for every hook (hook name is passed as a positional argument to the script). ZFS snapshoting and mounting is handled bybefore_everything
hook. Snapshot unmounting and deletion is handled byafter_everyghing
hook. Note, that these hooks are invoked only if theborg create
task is included in the list of task to be executed by the given borgmatic run (job). Also, theafter_everything
seems to be invoked even in case of backup error, so there is no special need for cleaning up snapshots in theon_error
hook.Every
borgmatic
run (job run) gets assigned individualjob-id
. It also has itsjob-name
. I use relative source paths +working_directory
. Thejob-id
is generated for every run (based on date-time + a random number). Thejob-name
is defined by admin for given job type (e.g.nightly-backup-of-everything
). During a single job run, each hook receives the assignedjob-id
, so we have a context. All the snapshots for the given job run are named after itsjob-id
, for examplerpool@var/lib@borgmatic-${job-id}
. Theworking_directory
is named afterjob-name
(I've observed, that changing theworking_directory
from one run to another caused the backup to take much longer than when the same name was used in all runs).So, my cron entry looks like this:
The
borgmatic-new-job-id
looks like this:Example ID generated by this script:
Essential parts of
/etc/borgmatic/backup-all.yaml
:The
borgmatic-hook
script, in it's current form, is the following:The script uses
mount -t zfs
to mount snapshots. After everything, they are unmounted recursively withumount -R
. The list of volumes to be snapshoted is determined automatically: all mountable datasets (canmount != "off"
) from the ZFS pool list provided via-z
option get mounted.Snapshots get unique names (as long as
job-id
's are unique), so there is no risk for collision, in case there are several jobs running in same time. The problematic part may be, however with two jobs running with samejob-name
, in which case the second instance shall fail, because theworking_directory
is already used by the first job. The schedule shall assume enough time for single job run to let it finish with some time-margin.Hope, this may help someone.
It could also work something like this (assuming this is turned into a proper hook like the db backups are):
We would essentially change the snapshot contents to be the actual directory contents for borg. Benefits I see to this approach: The original mount namespace is never touched, everything stays the same. The files are in the right place for a restore from the repo.
Nice idea, of course. The question to be answered is, whether all the hooks are run as same process, or rather a sequence of separate processes. If we have the later case, then unshare will probably be scoped to the single call of a single hook (say "before_everything") and the mounts will disappear before the actual backup job starts.
Well, ... in the meantime I've found that, for example, Debian offers to run Borgmatic jobs using systemd facilities instead of the bare crontable. The benefit (say) is that it's configured by default to run in a quite restricted environment. I managed to find settings, where things work similarly to the above scenario, at least mounts seem to be cleaned-up by the runner (systemd? I don't know). Actually, to be able to create snapshots and make mounts, one has to even loose the default restrictions defined by the package and then some kind of magic happens :).
Here is an example override for systemd configuration (one can put it under
/etc/systemd/system/borgmatic.service.d/override.conf
):and the contents of the
borgmatic-run-job
script:Well we kinda have to run the zfs backups as a separate step or document the behavior very well but as of right now (and if @witten ) allows it I would be very interested in a solution that spawns borg in a separate mount namespace as that would be 1. very robust 2. allow for mounting the backup and 3. be expandable to other zfs like snapshot filesystems
It does however have some downsides (These are all I can think of):
I'm a fan of bind mounts for exactly this reason. (See some of my old comments above.) Your general approach makes sense to me.
Sequence of separate processes. Specifically, whenever a borgmatic hook runs an external binary (
borg
,pg_dump
, etc.), that binary runs in its own process. But any of the surrounding code runs in the borgmatic process.I might be missing something, but couldn't it just be scoped to the actual
create
action (and the resulting call to Borg)? Then Borg would "see" those snapshotted files as if they were in their normal locations.If you're running borgmatic in a container, then presumably you're not using zfs in the container as well. Put another way, if you want borgmatic to work with zfs, it probably needs to be run on the host. Or am I missing something here?
Would it be as simple as not including them in the snapshot?
Well what I had in mind would use
unshare()
to create a private mount namespace, unmount the old mount and mount the snapshot (I know this works in zfs at least). But what if someone would like to snapshot/bin
? This would mean we would take a snapshot of the zfs share thats mounted on/bin
, unshare with a new mount namespace, unmount the directory and now replace it with the snapshot. The mounting the new snapshot part will fail now, because/bin/zfs
suddenly does not exists anymore. We should be able to circumvent this with a chroot inside the private namespace where we mount all the snapshots under a single directory tree and spawn borg inside.The interesting question is:
Can we do it all in one call to borg? I have not read all of the code in the repo but I assume we call borg once for every db dump hook. For zfs it would be really nice to call borg and pass the snapshots AND the directories that should be backed up at the same time so they all go into one filesystem and can be mounted/looked at from borg.
Ah, gotcha.
Actually, no! Each database hook dumps its configured database(s) to individual named pipes on the filesystem. Then, a single Borg call backs up all configured source directories and implicitly reads from each of those named pipes.
I believe that should be possible today.
So a bit of a technical question: How will we call
unshare()
? I did not consider that to be a problem but after looking atos
in python where I expected to find it it does not look like python has support at all. We could of cause useutil-linux
and make borgmatic depend on it or use a native python package but I'm honestly not sure what the correct way to do this would be here.What about not unmounting any part of the original FS and mount the whole hierarchy of zfs snapshots in a subdirectory and then backup just that subdirectory using "relative directory" stuff? IMHO the final result (files in backup) is exactly same. You can use original runtime form running OS, including "/bin", and backup "/my/subdir/bin" from shapshot mounted under /my/subdir.
I'm not sure what the correct way is either, but here is one way that calls the underlying C implementation directly (about as "directly" as you can get in Python): #80 (comment)
That's an interesting idea. Are you suggesting using something like borgmatic's existing
working_directory
option and setting it implicitly to the ZFS snapshot directory so that the paths that are stored in the archive omit the containing ZFS snapshot directory? If so, the main challenge I can see with that approach is that relative paths couldn't also be used for any of thesource_directories
when a ZFS hook is in use. Maybe that's an okay limitation though? borgmatic could even error ifworking_directory
is specified explicitly when the ZFS hook is used. EDIT: Also, you'd be limited to backing up a single ZFS snapshot at a time with this approach unless you wanted Borg to run multiple times (and create multiple archives).More or less. In fact I'm describing my setup that I currently have using borgmatic 1.7 and few dozens of lines of code in bash as borgmatic hooks, and it works quite well so far.
I don't think in terms of "snapshot directories", at least not meaning the hidden ".zfs/snapshot/stuff". Rather, I mount the whole snapshot hierarchy recursively under my own directory, thus recreating the original filesystem hierarchy therein, and then backup the content of that directory. Something along these lines:
First: Snapshot creation (recursive):
Then: Snapshot mounting:
After that, I have my whole filesystem available for backup under this
/borgmatic/jobs/nightly-all
directory. The backup is configured like this:Got it, thanks. That makes sense to me. I think there would still be the limitation of a single working directory per configuration file. Also, there would be the same caveats around interactions with relative
source_directories
. But that aside, this sounds like it could potentially be easier than the unshare / bind mount approach. Then again, with either approach, you're doing amount
(in one case it's-t zfs
and in the other it's--bind
), so maybe the two approaches aren't really that different.Well, not necessarily an issue. Finally, it's just a working directory. :)
Relative source dirs is not a must. You can use absolute dirs as well. In any case, we shall take into account the recovery scenario in the first place, and if I understand docs correctly, the borgmatic recovery scenario is "write all the recovered files to their original locations". If it is as said, then, well, how it's supposed to work with backup of "/" as absolute source_directory? Will this always attempt to override currently running OS (rescue OS/live CD)?
With nested path, such as "/borgmatic/nightly-backup" we can use absolute source_directory "/borgmatic/nightly-backup" and the recovery scenario with the files being recovered into "/borgmatic/nightly-backup" directory, while running borgmatic from some rescue OS or live CD mounted to "/". With relative source_dirs, we can recover to any directory, am I right?
That seems reasonable. And, the bind approach may be applicable to other scenarios, not specific to zfs, so a code may be reusable.
I just meant that any user expecting to use a relative source directory in conjunction with their own
working_directory
wouldn't be able to do that, as this proposed ZFS feature would co-opt theworking_directory
. But yes, absolute source directories will continue to work just fine.By default,
borgmatic extract
extracts to the current directory, just likeborg extract
does. (However IIRC, for purposes ofextract
, this might be distinct from borgmatic'sworking_directory
option.) But you can always override theextract
destination, so even if an archive was backed up with in absolute source directory like/
, you can extract it anywhere you like (including/
). The limitation is that the path stored into the Borg archive will influence where it gets restored.For instance, if you backup with
/
insource_directories
and that stores/bin/bash
in the archive, then when you go toextract
, you can restore that file to/bin/bash
or/some/prefix/bin/bash
. You can even strip path components duringextract
withborgmatic extract --strip-components
, but that's a heavy-handed tool for many use cases.Yes, but you don't necessarily need relative source directories to be able to do that. (See above.)
Some relevant discussion for a potential feature in Borg 1.4 that would make backing up ZFS snapshots and extracting them much easier (no
working_directory
needed): https://github.com/borgbackup/borg/discussions/7975#discussioncomment-8293248We could implement this for zfs at least by doing the following for each dataset:
I don't think there is a better option right now so I think this is what I will roll as soon as borg 1.4 is released
That sounds like a good plan, thank you! You may have already meant this, but you don't have to wait for a stable Borg 1.4 release to implement this. The next beta should probably be fine as long as nobody uses it in production.
One caveat is that I'd recommend using Python's tempfile.mkdtemp() or similar to create that temporary directory.
One more thing: We have to check that we do not back up anything twice, so backing up a dataset DATA/test mounted at /test and /test in source_directories should either result in an error or be resolved by removing /test from source_directories.
@witten Architectural question: Where in the code would you like this to be implemented? Afaik the hook system is not really powerful enough to handle this as we have to manipulate which directories get included right? So this has to either be a new hook or become completely integrated into borgmatic.
One approach for that would be to simply disallow anything user-specified in
source_directories
when the ZFS hook is enabled. That way, you shouldn't get any colliding paths stored into the Borg archive.I'd recommend putting the hook itself
borgmatic/hooks/zfs.py
. And then, yes, the internal hook "API" will probably have to be expanded to support manipulation of source directories. Today, borgmatic automatically includes~/.borgmatic
in the set of directories that get backed up withincollect_borgmatic_source_directories()
, but that won't perform the/./
hack. So it might make sense to call out to hooks from that collect function, and then any hooks that implement it can return additional source directories. (In fact that might be a cleaner way for all hooks to work, ultimately.)Well I was thinking of having the zfs hook work the following:
Have a zfs option in the config file (Only a simple on/off)
Have a zfs user property like
com.borgmatic:backup
that the hook checks for on all datasetsNow check for any overlaps between source_directories and the mount paths of the zfs datasets, snapshot all matching datasets, mount them, add the original path to the excludes and the new path to the includes.
That approach generally makes sense to me! (I might suggest
org.torsion.borgmatic:backup
for the user property though.)So I think the general implementation of a snapshot hook should be pretty simple: We need a snapshot function that allows the hook to create snapshots and return a list of paths that are to be added to the
borg
call plus a cleanup function likeremove_data_source_dumps
but for snapshots. AFAIK this will go inborgmatic/actions/create.py
since all the other create only hooks are also there. Would you agree to this general approach?The overall approach sounds good, but here are some suggested specifics:
dump_data_sources()
was originally intended to be the generic interface for preparing all data sources for Borg consumption: dumping databases, taking snapshots, etc. Now I realize in the ZFS snapshot hook case, you don't want to produce asubprocess.Popen
instance for a named pipe to stream data to Borg likedump_data_sources()
does today, because that's not how snapshots work. What you want is to produce a path (or list of paths) to inject into theborg create
paths to backup. So what ifdump_data_sources()
could do that too? E.g., return asubprocess.Popen
instance or a path/paths to backup. Then it would be up to the caller (in this caseborgmatic/actions/create.py:run_create()
to do the appropriate thing with the returned value: Either pass it tocreate_archive()
as extra paths or pass it tocreate_archive()
asstream_processes
.remove_data_source_dumps()
? I think that should already have all the data it needs to find the snapshots, given that theconfig
is already passed in.My main goal here is to avoid having a proliferation of different data source setup/teardown functions for every type of data source (database, filesystem snapshot, etc.) or every data source that has slightly different requirements from the others. I'm of course open to discussion here if you disagree or just want to riff on this further. Thanks!
Seems valid. We might just have the call return a list of either pathlib or Popen objects and check for the type of all of them. That way a hook could just return a list of whoknowswhat in the future and the type can be checked and processed.
Yup, that sounds like a good idea!
@IBims1NicerTobi, is this something you're still working on? Thanks!
Relevant Borg issue (just filed): https://github.com/borgbackup/borg/issues/8537
Implemented in main! This will be part of the next release. Docs will be online here shortly:
https://torsion.org/borgmatic/docs/how-to/snapshot-your-filesystems/
Any and all feedback (on the docs or the actual feature) is welcome.
Released in borgmatic 1.9.3!
FYI some useful updates to the ZFS hook in borgmatic 1.9.4, just released: https://projects.torsion.org/borgmatic-collective/borgmatic/releases/tag/1.9.4