This question is structured as follows:
I first give some background on why I am interested in this topic and how it would solve a problem I am dealing with. Then, I ask the actual standalone question regarding file system caching, so if you are not interested in the motivation (some C++ project build setup), just skip the first section.
The initial problem: Linking shared libraries
I am looking for a way to speed up our project's build times. The setup is as follows: A directory (lets call it
workarea) is located in an NFS share.
It initally only contains source code and makefiles. Then, the build process first creates static libraries in
workarea/lib and then creates shared libraries in
workarea/dll, using the static libraries in
workarea/lib. During the creation of the shared libraries, these are not only written, but also again read using e.g.
nm to verify at link time that no symbols are missing. Using many jobs in parallel, (e.g. make -j 20 or make -j 40), build times are quickly dominated by linking time. In this case, linking performance is limited by file system performance. For example, linking with 20 jobs in parallel roughly takes 35 seconds in the NFS share, but only 5 seconds in a RAM drive. Note that using rsync to copy
dll back to the NFS share takes another 6 seconds, so working in a RAM drive and syncing to NFS afterwards is much faster than directly working in the NFS share. I am looking for a way to achieve the fast performance without explicitly copying / linking files between the NFS share and the RAM drive.
Note that our NFS share already uses a cache, but this cache can only cache read accesses.
AFAIK, NFS requires that any NFS client may not confirm a write until the NFS server confirms the completion of the write, so the client cannot use a local write buffer and write throughput is (even in spikes) limited by network speed. This effectively limits combined write throughput to roughly 80MB/s in our setup.
Read performance, however, is much better, as a read cache is used. If I do linking (create the content of
workarea/lib in NFS and
workarea/dll being a symlink to the RAM drive, performance is still good - roughly 5 seconds. Note that it is required for the build process to finish with
workarea/* residing in the NFS share:
lib needs to be in the share (or any persistent mount) to allow fast incremental builds, and
dll needs to be in NFS to be accessed by compute machines starting jobs using these dlls.
Hence, I would like to apply a solution to the problem below to
workarea/dll and maybe also
workarea/lib (the latter in order to improve compilation times). The requirement of fast setup times below is caused by the necessity to perform fast incremental builds, only copying data if required.
I should probably have been a bit more specific about the build setup. Here are some more details: Compilation units are compiled into .o files in a temporary directory (in /tmp). These are then merged into static libraries in
ar. The complete build process is incremental:
- Compilation units are only recompiled if the compilation unit itself (the .C file) or an included header has changed (using compiler-generated dependency files which are included into
- Static libraries are only updated if one of its compilation units has been recompiled.
- Shared libraries are only relinked if one of its static libraries has changed. Symbols of shared libraries are only re-checked if the symbols provided by the shared libraries it depends on changed of if the shared library itself has been updated.
Still, complete or near-complete rebuilds are necessary quite often, since multiple compilers (
clang), compiler versions, compilation modes (
debug), C++ standards (
C++11) and additional modifications (e.g.
libubsan) may be used. All combinations effectively use different
dll directories, so one can switch between setups and build incrementally based on the last build for that very setup. Also, for incremental builds, often just a few files have to be recompiled, taking very little time, but triggering relinkage of (possibly large) shared libraries, taking much longer.
In the meantime I learned about the
nocto NFS mount option, which apparently could solve my problem on basically all NFS implementations except for Linux's, since Linux flushes write buffers always on
close(), even with
nocto. We already tried several other things: For example, we could use another local NFS server with
async enabled that serves as a write buffer and exports the main NFS mount, but unfortunately the NFS server itself does no write buffering in this case. It seems that
async just means that the server does not force its underlying file system to flush to stable storage, and a write buffer is used implicitly in the case the underlying file system uses a write buffer (as it apparently is the case for the file system on the physical drive).
We even thought about the option to use a non-Linux virtual machine on the same box that mounts the main NFS share using
nocto, providing a write buffer, and providing this buffered mount via another NFS server, but have not tested it and would like to avoid such a solution.
We also found several
FUSE-based file system wrappers serving as caches, but none of these implemented write buffering.
Caching and buffering a directory
Consider some directory, lets call it
orig, which resides in a slow file system, e.g. an NFS share. For a short timespan (e.g. seconds or minutes, but this should not matter anyway), I would like to create a fully cached and buffered view of
orig using a directory
cache, which resides in a fast file system, e.g. a local hard drive or even a RAM drive. The cache should be accessible via e.g. a mount
cached_view and not require root privileges. I assume that for the lifetime of the cache, there are no read or write accesses directly to
orig (beside the cache itself of course).
By fully cached and buffered, I mean the following:
- Read queries are answered by once forwarding the query to the file system of
orig, caching that result and using it from then on, and
- Write queries are written into
cacheand confirmed upon completion directly, i.e. the cache is also a write buffer. This should even happen when
close()is called on the written file. Then, in the background, the writes are forwarded (maybe using a queue) to
orig. Read queries to written data are answered using the data in
cache, of course.
Furthermore, I need:
- The cache provides a function to shut the cache down, which flushes all writes to
orig. Runtime of flushing should only depend on the size of written files, not all files. Afterwards, one could safely access
- Setup time is fast, e.g. initialization of the cache may only depend on the number of files in
orig, but not on the size of files in
orig, so copying
cacheonce is not an option.
Finally, I would also be fine with a solution that does not use another file system as cache, but just caches in main memory (the servers have plenty of RAM). Note that using builtin caches of e.g. NFS is not an option, as AFAIK NFS does not allow write buffers (c.f. first section).
In my setup, I could emulate a slightly worse behavior by symlinking the contents of
cache, then work with
cache (as all write operations actually replace files by new files, in which case the symlinks are replaced by the updated versions), and rsyncing the modified files to
This does not exactly meet the requirements above, e.g. reads are not done only once and files are replaced by symlinks, which of course does make a difference for some applications.
I assume this is not the correct way to solve this (even in my simpler setting), and maybe someone is aware of a cleaner (and faster!) solution.