Craziest thing I ever used SQLite for: partial file deduplication (2022)
sqlite.org
sqlite.org
I had to write a custom tool to do it efficiently during image creation.
It sometimes feels like games are made to thwart this type of thing. They often use packfiles, basically filesystems within files optimized to look up assets quickly. Also perhaps they allowed optimized data layout from when consoles had slow spinning hard drives. The upshot is that a tiny patch inserting a line of code in a script may offset hundreds of megabytes of other data in the packfiles, causing the block hashes to no longer match up. Do any filesystems model inserts in some way? I'm pretty sure Steam updates can handle situations like that. I frequently see updates which download a tiny amount (kilobytes) but write a huge amount to disk (gigabytes), and I can't think of any other cause. (Assuming developers aren't using hilariously un-compressed assets).
> Next - to update the pack file on a client device, SteamPipe builds the new version alongside the old version. When all new files are built, it then "commits" the update by deleting old files and moving the new files in. What this means is that to update a 25 GB pack file, SteamPipe will always build a new, 25GB file. If the update only requires 10 bytes of changes to that file, SteamPipe will need to copy almost the entire 25GB from the old file to the new file. Depending on the client storage hardware, this can be a very slow process.
There are already SQLite clustering technologies that utilize WAL frame shipping in order to replicate data. I don't see why a steam patch couldn't just be a range of WAL frames to be applied to a client's database.
As long as you were potentially seeking around a spinning platter, you needed to have way more control over data locality than was possible with something like SQLite.
Would you be ok to point out ones that do this? Searching online isn't showing up anything mentioning WAL frame shipping for SQLite. :(
Was thinking about exactly this scenario a few days ago, as I have a potential use for it. :)
https://litestream.io/how-it-works/
Awesome. :)With HDD or SDD that might've been feasible but with NVMe you really want as little between raw storage and game engine.
But I'm really talking about ability to just directly load an asse via reading a blob of data vs having to munge it thru SQLite code. Or even just mmap'ing data directly
When compressed packfiles are used they should be built with equivalent of gzip --rsyncable option, which restarts compression stream based on rolling hash values. Otherwise patching methods are defeated. For example UnityFS packfiles with chunked LZ4 compression normally don't do that, which results in huge updates. However it is possible to repack them into patch friendly form while keeping file format compatibility.
(Legacy evolution--all the files of a job were packed into a single .zip. Then a need came along to update some of them. Nothing else would even notice that the files hadn't been compressed, but the one task could very quickly write the changes without replacing the file. Update the relevant byte, recalc the CRC from the in-memory copy, write it to both locations.)
Ie this is how backup tools like Arq or duplicacy work.
These backup tools also don't generally have to optimize random access to parts of a file. They may just store a linear sequence of chunk IDs to represent one file version. To bring this back to an active system that supports random access by a program with patching, I think you'd really need to adapt a copy-on-write filesystem to use content-defined chunking instead of fixed offset chunks. Then, your insertion is likely an operation on some tree structure that represents the list of chunk IDs as the leaves of the tree. But, this tree would now have to encode more byte offset info in the interior tree nodes, since it would vary at the leaves instead of each leaf representing a fixed size chunk.
But doesn't ZFS dedupe eat RAM for breakfast ? Double-digit GB RAM per TB data IIRC ?
However, there are now special devices that can be used stored to store DDT. Typically this is done with two SSDs configured as a mirrored vdev for the DDT metadata. This reduces the overhead on memory, but does cost some performance and still has the same limitation that the DDT size can only be reduced by re-creating the pool.
But I do run disk-wide compression with no problems and have done so on all my datasets for many years now, and it's been a tremendous space saver. Especially on machines where I have a lot of VMs/containers, it's not unusual for me to have a compression ratio of 2 on these with good old lz4, it will be interesting to see what damage zstd will do once I start experimenting with that.
I've yet to compare lz4 to zstd myself, but I've read great things about it.
It will use lots of RAM for ARC (adaptive read cache) but that can be limited.
ZFS RAM usage is greatly overblown in my opinion and experience.
That sounds like a problem specific to the setup you were using. (?)
Saying that because if it was something that commonly happened, then either a) it would have been fixed, or b) people would have stopped using it. :)
Like I mentioned in my sibling comment, I can 100% reproduce something that sounds like what parent mentioned (most recent attempt was like 1-2 months ago); but for my specific case, I can see how ZFS on an external HDD might not be that common.
Thanks!
Then there's encrypted datasets being very slow, especially when copying between two pools.
Then there's having two pools on the same disk corrupting each other. Though in this case the data might have been recoverable using some recovery tool (like a "modified zdb").
Also, I read on some website somewhere (maybe zfsonlinux.org) that USB devices probably shouldn't be used with ZFS.
Honestly I "wish" some bunch of crazy filesystem people would just clean room ZFS...
Steps to reproduce:
* Get an external HDD. I literally bought a new, different external HDD because I thought the problem was the old one (spoiler: nope, the problem still could be reproduced 100% on the new disk).
* Create a zpool for the whole disk.
* Create a dataset.
* Try to rsync several hundreds of GB to the ZFS dataset.
* Wait for a minute or two.
* Notice how it stops transferring data, and gives a weird error complaining the disk is unhealthy, faulty, or something (I don't remember the exact terms).
No amount of `zpool clear` or `zpool scrub` will fix it. I gave up and just formatted it as ext4 like all my other backup disks.
My use case for this was having this external HDD as a backup. The plan was to format this as ZFS, copy data from all my other external HDDs to this one, format the other external HDDs with ZFS, and then start rotating between them.
---
Another way to reproduce this is with torrents. When I downloaded torrents directly to an external HDD, it also hanged and got some errors, but in those cases it could be fixed with a `zpool clear` and scrubs, so it wasn't that bad (it wasn't literally unrecoverable, like the case I mention before).
---
So this leads me to believe there's something weird between ZFS, external HDDs, and trying to write too fast.
The whole point was to be able to run `zpool scrub` on those external HDDs. But like I said, I gave up on that for now. So the current plan is to try to build a NAS and do the same attempt, but with internal HDDs.
An added bonus is that you can use SQL.
Both were valid in his environment since they were both a big Oracle and SQL Server shop while having high Linux usage as well. I must ask him if PowerShell has changed things at all.
There are many of other products which implement "keep track of partial file contents to deduplicate", for example casync borg and restic.
None of them use sqlite for metadata storage, which kinda makes sense - in author's schema, each block uses (fileid, block_index, hash) tuple, which is 2/3 wasted space, as fileids are mostly same (if files are big) and block_index is always increasing. A custom data structure would be both faster and more efficient.
I imagine Dask would be a better tool for this job but I wasn’t familiar with it at the time and it did allow me to deduplicate using Python and no external dependencies.
Is the author implying that APFS or HFS uses this method to calculate the file ID? I am unable to find any information regarding this. From what I understand, w.r.t APFS, the file ID is a combination of the inode OID and genID.
https://lib.rs/crates/dupe-krill#readme-nerding-out-about-th...
(Not (purely) a reference to the “oh hey Mark” meme, I’m vaguely acquainted with Mark from LUG days)
Transact-SQL is also Turing-complete, as proven by a Brainfuck implementation[4].
With that, you can theoretically compute anything :).
[0]: https://wiki.postgresql.org/wiki/Cyclic_Tag_System
[1]: https://blog.coelho.net/database/2013/08/17/turing-sql-1.htm...
[2]: https://wiki.postgresql.org/wiki/Mandelbrot_set
[3]: https://web.archive.org/web/20201111224603/http://assets.en....
[4]: https://stackoverflow.com/questions/900055/is-sql-or-even-ts...
But partial file deduplication is something else...
We also use a SQLite database, and in a manner similar to the OP’s article. We use it to track which content-defined chunks are at which offsets in which files on disk. We deduplicate those chunks on the CDN so you have to download less data when updating the game, but on disk we need to recreate the files as originally uploaded because that’s how games load them.
We’ve expanded the use of this tech quite a bit since the article. For example, my team has started using the patcher internally to manage our 2-million-file vendored code and binaries repo, as a replacement for Git LFS of sorts. Cloning the repo from scratch became 10 times faster, and so did switching branches, especially for remote devs, since the files are served through CloudFront.
Some of the more interesting work we’ve done recently has been optimizing the core content-defined chunking algorithm. The GearHash rolling hash algorithm from FastCDC is decently fast, but we were able to improve chunking speeds by 5x with vectorized implementations and using AES-NI instructions in a clever way to avoid the GearHash table lookups. It can do 11GB/sec per core now using AVX-512 instructions.
Another thing we did recently was build a tool for repacking Unreal Engine Pak files so game assets maintain a consistent layout across versions, similar to what garaetjjte mentioned in a comment above about UnityFS files. This reduced the disk I/O needed to update VALORANT by over 90% and cut update times by half for players. The combination of content-defined chunking with tooling to make game data files friendlier to differential patching can make a huge difference.