From 569a151da2fbfeb428f2d8246bc4db5e36191c01 Mon Sep 17 00:00:00 2001 From: trapexit Date: Fri, 26 Sep 2025 12:36:56 -0400 Subject: [PATCH] Add ability to proxy ioprio of client apps (#1534) --- mkdocs/docs/config/func_readdir.md | 12 +++++ mkdocs/docs/config/options.md | 2 + mkdocs/docs/config/proxy-ioprio.md | 53 ++++++++++++++++++++++ mkdocs/mkdocs.yml | 1 + src/config.cpp | 2 + src/config.hpp | 2 + src/config_proxy_ioprio.cpp | 33 ++++++++++++++ src/config_proxy_ioprio.hpp | 13 ++++++ src/error.hpp | 2 +- src/fuse_read.cpp | 2 + src/fuse_write.cpp | 3 ++ src/ioprio.cpp | 73 ++++++++++++++++++++++++++++++ src/ioprio.hpp | 20 ++++++++ 13 files changed, 217 insertions(+), 1 deletion(-) create mode 100644 mkdocs/docs/config/proxy-ioprio.md create mode 100644 src/config_proxy_ioprio.cpp create mode 100644 src/config_proxy_ioprio.hpp create mode 100644 src/ioprio.cpp create mode 100644 src/ioprio.hpp diff --git a/mkdocs/docs/config/func_readdir.md b/mkdocs/docs/config/func_readdir.md index 6ea16b49..02353255 100644 --- a/mkdocs/docs/config/func_readdir.md +++ b/mkdocs/docs/config/func_readdir.md @@ -29,3 +29,15 @@ of the `readdir` call. In v6.16 that was changed to be able to grow to the [fuse_msg_size](fuse_msg_size.md) which allows a lot more data to be sent per request and therefore much better performance in certain situations. + + +## Technical Details + +* On Linux + [getdents](https://man7.org/linux/man-pages/man2/getdents.2.html) is + used instead of + [readdir](https://man7.org/linux/man-pages/man2/readdir.2.html) in + order to leverage larger buffers for better performance on larger + directory reads as well as have more control in concurrent policies. +* [readdir](https://man7.org/linux/man-pages/man2/readdir.2.html) is + used on FreeBSD. diff --git a/mkdocs/docs/config/options.md b/mkdocs/docs/config/options.md index 5695e98e..2c794095 100644 --- a/mkdocs/docs/config/options.md +++ b/mkdocs/docs/config/options.md @@ -162,6 +162,8 @@ config file. * **scheduling-priority=INT**: Set mergerfs' scheduling priority. Valid values range from -20 to 19. See `setpriority` man page for more details. (default: -10) +* **[proxy-ioprio](proxy-ioprio.md)=BOOL**: Use ioprio value from + calling threads when reading and writing. (default: false) * **fsname=STR**: Sets the name of the filesystem as seen in **mount**, **df**, etc. Defaults to a list of the source paths concatenated together with the longest common prefix removed. diff --git a/mkdocs/docs/config/proxy-ioprio.md b/mkdocs/docs/config/proxy-ioprio.md new file mode 100644 index 00000000..550e6786 --- /dev/null +++ b/mkdocs/docs/config/proxy-ioprio.md @@ -0,0 +1,53 @@ +# proxy-ioprio + +* type: `BOOL` +* default: `false` +* example: `proxy-ioprio=true` + +In Linux certain process schedulers have the ability to +[prioritize](https://man7.org/linux/man-pages/man2/ioprio_set.2.html) +based on a per thread [IO +priority](https://www.kernel.org/doc/Documentation/block/ioprio.txt) +assigned to the software by users using tools such as +[ionice](https://man7.org/linux/man-pages/man1/ionice.1.html). Since +such details are not provided by the FUSE protocol, users may not use +schedulers which support IO priority, and the extra overhead to query +the priority and set it within mergerfs it does not by default attempt +to mirror/proxy the value. When enabled however, for all read and +write requests, mergerfs will query the priority of the requesting +process/thread and set the same value on the thread within mergerfs +making the read/write. + +This may be useful in situation where the system has high IO load or +processes are known to have varying priorities. See the [man +page](https://man7.org/linux/man-pages/man2/ioprio_set.2.html) for +specific details. + +Keep in mind that if no IO scheduler has been set for a thread then by +default the IO priority will match the CPU nice value which for +mergerfs is set by [scheduling-priority](options.md). [Nice +value](https://man7.org/linux/man-pages/man2/getpriority.2.html) +proxying may be added in a future release. + + +## Conflicts With Other Options + +If using [IO passthrough](passthrough.md) there is no reason to set +this value to `true`. However, since `passthrough` leads to mergerfs +IO calls being bypassed entirely will have no impact on performance or +behavior regardless. + +When using `passthrough` the IO priority of the client app would be +used directly given the IO is passed through. + + +## Supported Platforms + +Only Linux. This is not supported on FreeBSD. + + +## Performance Impact + +Despite the additional syscalls requires the impact appears +undetectable with larger buffer sizes. At very small buffer sizes +(such as 512 bytes) there is a noticable but small impact (~1.5%). diff --git a/mkdocs/mkdocs.yml b/mkdocs/mkdocs.yml index d018fd42..50804255 100644 --- a/mkdocs/mkdocs.yml +++ b/mkdocs/mkdocs.yml @@ -81,6 +81,7 @@ nav: - config/inodecalc.md - config/threads.md - config/pin-threads.md + - config/proxy-ioprio.md - config/link_cow.md - config/fuse_msg_size.md - config/follow-symlinks.md diff --git a/src/config.cpp b/src/config.cpp index f248c395..67b68fe9 100644 --- a/src/config.cpp +++ b/src/config.cpp @@ -119,6 +119,7 @@ Config::Config() nullrw(false), parallel_direct_writes(true), posix_acl(false), + proxy_ioprio(false), readahead(0), readdir("seq"), readdirplus(false), @@ -207,6 +208,7 @@ Config::Config() _map["parallel-direct-writes"] = ¶llel_direct_writes; _map["pin-threads"] = &fuse_pin_threads; _map["posix_acl"] = &posix_acl; + _map["proxy-ioprio"] = &proxy_ioprio; _map["readahead"] = &readahead; _map["readdirplus"] = &readdirplus; _map["rename-exdev"] = &rename_exdev; diff --git a/src/config.hpp b/src/config.hpp index bcd40f32..ceffd157 100644 --- a/src/config.hpp +++ b/src/config.hpp @@ -30,6 +30,7 @@ #include "config_pagesize.hpp" #include "config_passthrough.hpp" #include "config_pid.hpp" +#include "config_proxy_ioprio.hpp" #include "config_rename_exdev.hpp" #include "config_set.hpp" #include "config_statfs.hpp" @@ -126,6 +127,7 @@ public: ConfigBOOL parallel_direct_writes; ConfigGetPid pid; ConfigBOOL posix_acl; + ProxyIOPrio proxy_ioprio; ConfigUINT64 readahead; FUSE::ReadDir readdir; ConfigBOOL readdirplus; diff --git a/src/config_proxy_ioprio.cpp b/src/config_proxy_ioprio.cpp new file mode 100644 index 00000000..b559a012 --- /dev/null +++ b/src/config_proxy_ioprio.cpp @@ -0,0 +1,33 @@ +#include "config_proxy_ioprio.hpp" + +#include "ioprio.hpp" +#include "from_string.hpp" + + +ProxyIOPrio::ProxyIOPrio(const bool b_) +{ + ioprio::enable(b_); +} + +std::string +ProxyIOPrio::to_string(void) const +{ + if(ioprio::enabled()) + return "true"; + return "false"; +} + +int +ProxyIOPrio::from_string(const std::string_view s_) +{ + int rv; + bool enable; + + rv = str::from(s_,&enable); + if(rv) + return rv; + + ioprio::enable(enable); + + return 0; +} diff --git a/src/config_proxy_ioprio.hpp b/src/config_proxy_ioprio.hpp new file mode 100644 index 00000000..064c0deb --- /dev/null +++ b/src/config_proxy_ioprio.hpp @@ -0,0 +1,13 @@ +#pragma once + +#include "tofrom_string.hpp" + +class ProxyIOPrio : public ToFromString +{ +public: + ProxyIOPrio(const bool); + +public: + std::string to_string(void) const final; + int from_string(const std::string_view) final; +}; diff --git a/src/error.hpp b/src/error.hpp index 627b8bfc..9e496cd9 100644 --- a/src/error.hpp +++ b/src/error.hpp @@ -24,7 +24,7 @@ public: operator=(int v_) { if(!_err.has_value()) - _err = v_; + _err = ((v_ >= 0) ? 0 : v_); else if(v_ >= 0) _err = 0; diff --git a/src/fuse_read.cpp b/src/fuse_read.cpp index dc378426..dc2d0979 100644 --- a/src/fuse_read.cpp +++ b/src/fuse_read.cpp @@ -19,6 +19,7 @@ #include "errno.hpp" #include "fileinfo.hpp" #include "fs_pread.hpp" +#include "ioprio.hpp" #include "fuse.h" @@ -60,6 +61,7 @@ FUSE::read(const fuse_file_info_t *ffi_, size_t size_, off_t offset_) { + ioprio::SetFrom iop(fuse_get_context()->pid); FileInfo *fi = FileInfo::from_fh(ffi_->fh); if(fi->direct_io) diff --git a/src/fuse_write.cpp b/src/fuse_write.cpp index 53252fbc..c7c0c3a9 100644 --- a/src/fuse_write.cpp +++ b/src/fuse_write.cpp @@ -24,6 +24,7 @@ #include "fs_movefile_and_open.hpp" #include "fs_pwrite.hpp" #include "fs_pwriten.hpp" +#include "ioprio.hpp" #include "fuse.h" @@ -187,6 +188,8 @@ FUSE::write(const fuse_file_info_t *ffi_, size_t count_, off_t offset_) { + ioprio::SetFrom iop(fuse_get_context()->pid); + return ::_write(ffi_,buf_,count_,offset_); } diff --git a/src/ioprio.cpp b/src/ioprio.cpp new file mode 100644 index 00000000..f0bd9ab4 --- /dev/null +++ b/src/ioprio.cpp @@ -0,0 +1,73 @@ +#include "ioprio.hpp" + +#ifdef __linux__ +# include +# include +# include +# include +#else +#warning "ioprio not supported on this platform" +#endif + +thread_local int ioprio::SetFrom::thread_prio = -1; +bool _enabled = false; + +int +ioprio::get(const int which_, + const int who_) +{ +#ifdef SYS_ioprio_get + int rv; + + rv = syscall(SYS_ioprio_get,which_,who_); + + return ((rv == -1) ? -errno : rv); +#else + return -ENOSUP; +#endif +} + +int +ioprio::set(const int which_, + const int who_, + const int ioprio_) +{ +#ifdef SYS_ioprio_set + int rv; + + rv = syscall(SYS_ioprio_set,which_,who_,ioprio_); + + return ((rv == -1) ? -errno : rv); +#else + return -ENOSUP; +#endif +} + +void +ioprio::enable(const bool enable_) +{ + _enabled = enable_; +} + +bool +ioprio::enabled() +{ + return _enabled; +} + +ioprio::SetFrom::SetFrom(const pid_t pid_) +{ + int client_prio; + + if(!_enabled) + return; + + client_prio = ioprio::get(IOPRIO_WHO_PROCESS,pid_); + if(client_prio < 0) + return; + if(client_prio == thread_prio) + return; + + thread_prio = client_prio; + ioprio::set(IOPRIO_WHO_PROCESS,0,client_prio); +} diff --git a/src/ioprio.hpp b/src/ioprio.hpp new file mode 100644 index 00000000..b70def19 --- /dev/null +++ b/src/ioprio.hpp @@ -0,0 +1,20 @@ +#pragma once + +#include + + +namespace ioprio +{ + void enable(const bool); + bool enabled(); + + int get(const int which, const int who); + int set(const int which, const int who, const int ioprio); + + struct SetFrom + { + static thread_local int thread_prio; + + SetFrom(const pid_t pid); + }; +};