Update the secondary group cache (#1492)
Some checks are pending
CodeQL / Analyze (push) Waiting to run
mkdocs / deploy (push) Waiting to run

* Use concurrent_flat_map to build a global cache rather than per
thread.
* Create global maintanence thread manager for clearing unused cache
entries.
This commit is contained in:
trapexit 2025-07-05 17:05:53 -05:00 • committed by GitHub
parent 9e626228be
commit f852474073
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 342 additions and 246 deletions

View file

@ -56,7 +56,8 @@ SRC_CPP = \
lib/fuse_msgbuf.cpp \ lib/fuse_msgbuf.cpp \
lib/pin_threads.cpp \ lib/pin_threads.cpp \
lib/format.cpp \ lib/format.cpp \
lib/node.cpp lib/node.cpp \
lib/maintenance_thread.cpp
OBJS_C = $(SRC_C:lib/%.c=build/%.o) OBJS_C = $(SRC_C:lib/%.c=build/%.o)
OBJS_CPP = $(SRC_CPP:lib/%.cpp=build/%.o) OBJS_CPP = $(SRC_CPP:lib/%.cpp=build/%.o)

View file

@ -688,8 +688,7 @@ int fuse_is_lib_option(const char *opt);
*/ */
int fuse_main_real(int argc, char *argv[], const struct fuse_operations *op, size_t op_size); int fuse_main_real(int argc, char *argv[], const struct fuse_operations *op, size_t op_size);
int fuse_start_maintenance_thread(struct fuse *fuse); void fuse_populate_maintenance_thread(struct fuse *fuse);
void fuse_stop_maintenance_thread(struct fuse *fuse);
int fuse_log_metrics_get(void); int fuse_log_metrics_get(void);
void fuse_log_metrics_set(int enabled); void fuse_log_metrics_set(int enabled);

View file

@ -0,0 +1,13 @@
#pragma once
#include <functional>
#include <vector>
class MaintenanceThread
{
public:
static void setup();
static void stop();
static void push_job(const std::function<void(int)> &func);
};

View file

@ -27,8 +27,10 @@
#include "fuse_pollhandle.h" #include "fuse_pollhandle.h"
#include "fuse_msgbuf.hpp" #include "fuse_msgbuf.hpp"
#include <vector> #include "maintenance_thread.hpp"
#include <string> #include <string>
#include <vector>
#include <assert.h> #include <assert.h>
#include <dlfcn.h> #include <dlfcn.h>
@ -141,7 +143,6 @@ struct fuse
struct fuse_fs *fs; struct fuse_fs *fs;
struct lock_queue_element *lockq; struct lock_queue_element *lockq;
pthread_t maintenance_thread;
kvec_t(remembered_node_t) remembered_nodes; kvec_t(remembered_node_t) remembered_nodes;
}; };
@ -4032,49 +4033,26 @@ fuse_gc1()
fuse_malloc_trim(); fuse_malloc_trim();
} }
static
void*
fuse_maintenance_loop(void *fuse_)
{
int loops;
int sleep_time;
struct fuse *f = (struct fuse*)fuse_;
pthread_setname_np(pthread_self(),"fuse.maint");
loops = 0;
sleep_time = 60;
while(1)
{
if(remember_nodes(f))
fuse_prune_remembered_nodes(f);
if((loops % 15) == 0)
fuse_gc1();
if(g_LOG_METRICS)
metrics_log_nodes_info_to_tmp_dir(f);
loops++;
sleep(sleep_time);
}
return NULL;
}
int
fuse_start_maintenance_thread(struct fuse *f_)
{
return fuse_start_thread(&f_->maintenance_thread,fuse_maintenance_loop,f_);
}
void void
fuse_stop_maintenance_thread(struct fuse *f_) fuse_populate_maintenance_thread(struct fuse *f_)
{ {
mutex_lock(&f_->lock); MaintenanceThread::push_job([=](int count_)
pthread_cancel(f_->maintenance_thread); {
mutex_unlock(&f_->lock); if(remember_nodes(f_))
pthread_join(f_->maintenance_thread,NULL); fuse_prune_remembered_nodes(f_);
});
MaintenanceThread::push_job([](int count_)
{
if((count_ % 15) == 0)
fuse_gc1();
});
MaintenanceThread::push_job([=](int count_)
{
if(g_LOG_METRICS)
metrics_log_nodes_info_to_tmp_dir(f_);
});
} }
struct fuse* struct fuse*

View file

@ -8,6 +8,7 @@
#include "scope_guard.hpp" #include "scope_guard.hpp"
#include "thread_pool.hpp" #include "thread_pool.hpp"
#include "syslog.hpp" #include "syslog.hpp"
#include "maintenance_thread.hpp"
#include "fuse_i.h" #include "fuse_i.h"
#include "fuse_kernel.h" #include "fuse_kernel.h"
@ -317,7 +318,7 @@ fuse_session_loop_mt(struct fuse_session *se_,
pin_threads_type_); pin_threads_type_);
while(!fuse_session_exited(se_)) while(!fuse_session_exited(se_))
sem_wait(&finished); sem_wait(&finished);
sem_destroy(&finished); sem_destroy(&finished);
@ -325,22 +326,23 @@ fuse_session_loop_mt(struct fuse_session *se_,
} }
int int
fuse_loop_mt(struct fuse *f) fuse_loop_mt(struct fuse *f_)
{ {
if(f == NULL) int res;
if(f_ == NULL)
return -1; return -1;
int res = fuse_start_maintenance_thread(f); MaintenanceThread::setup();
if(res) fuse_populate_maintenance_thread(f_);
return -1;
res = fuse_session_loop_mt(fuse_get_session(f), res = fuse_session_loop_mt(fuse_get_session(f_),
fuse_config_get_read_thread_count(), fuse_config_get_read_thread_count(),
fuse_config_get_process_thread_count(), fuse_config_get_process_thread_count(),
fuse_config_get_process_thread_queue_depth(), fuse_config_get_process_thread_queue_depth(),
fuse_config_get_pin_threads()); fuse_config_get_pin_threads());
fuse_stop_maintenance_thread(f); MaintenanceThread::stop();
return res; return res;
} }

View file

@ -0,0 +1,65 @@
#include "maintenance_thread.hpp"
#include "fmt/core.h"
#include <cassert>
#include <mutex>
#include <vector>
#include <pthread.h>
#include <unistd.h>
pthread_t g_thread;
std::vector<std::function<void(int)>> g_funcs;
std::mutex g_mutex;
static
void*
_thread_loop(void *)
{
int count;
pthread_setname_np(pthread_self(),"fuse.maint");
count = 0;
while(true)
{
pthread_setcancelstate(PTHREAD_CANCEL_DISABLE,NULL);
{
std::lock_guard<std::mutex> lg(g_mutex);
for(auto &func : g_funcs)
func(count);
}
pthread_setcancelstate(PTHREAD_CANCEL_ENABLE,NULL);
count++;
::sleep(60);
}
return NULL;
}
void
MaintenanceThread::setup()
{
int rv;
rv = pthread_create(&g_thread,NULL,_thread_loop,NULL);
assert((rv == 0) && "pthread_create failed");
}
void
MaintenanceThread::push_job(const std::function<void(int)> &func_)
{
std::lock_guard<std::mutex> lg(g_mutex);
g_funcs.emplace_back(func_);
}
void
MaintenanceThread::stop()
{
pthread_cancel(g_thread);
pthread_join(g_thread,NULL);
}

View file

@ -8,7 +8,7 @@ background](../intro_to_filesystems.md) in such things is recommended
for more advanced configurations. for more advanced configurations.
These option names and values are the same regardless of whether you These option names and values are the same regardless of whether you
use them with the `mergerfs` commandline program, in fstab, or in a use them with the `mergerfs` command line program, in fstab, or in a
config file. config file.
@ -204,6 +204,12 @@ config file.
* **[passthrough](passthrough.md)**: Enable [FUSE IO * **[passthrough](passthrough.md)**: Enable [FUSE IO
passthrough](https://kernelnewbies.org/Linux_6.9#Faster_FUSE_I.2FO) passthrough](https://kernelnewbies.org/Linux_6.9#Faster_FUSE_I.2FO)
if available. (default: off) if available. (default: off)
* **gid-cache-expire-timeout**: Number of seconds till supplemental
group data is refreshed in the [GID
cache](../known_issues_bugs.md#supplemental-user-groups). (default:
3600)
* **gid-cache-remove-timeout**: Number of seconds to wait till cached
data is removed due to lack of usage. (default: 43200)
**NOTE:** Options are evaluated in the order listed so if the options **NOTE:** Options are evaluated in the order listed so if the options
are **func.rmdir=rand,category.action=ff** the **action** category are **func.rmdir=rand,category.action=ff** the **action** category

View file

@ -4,25 +4,26 @@
### Supplemental user groups ### Supplemental user groups
#### Supplemental group caching
Due to the overhead of Due to the overhead of
[getgroups/setgroups](http://linux.die.net/man/2/setgroups) mergerfs [getgroups/setgroups](http://linux.die.net/man/2/setgroups) mergerfs
utilizes a cache. This cache is opportunistic and per thread. Each utilizes a cache. As necessary the supplemental group information will
thread will query the supplemental groups for a user when that be queried and cached. That cached list of groups will be used to set
particular thread needs to change credentials and will keep that data the supplement groups as necessary. Due to the high cost of querying
for the lifetime of the thread. This means that if a user is added to the group list the default expiry for said data is 1 hour and after 12
a group it may not be picked up without the restart of hours of no usage will be removed from the cache all together.
mergerfs. In the future this may be improved to allow a periodic or
manual clearing of the cache. #### Host vs Container identity
While not a bug some users have found when using containers that While not a bug some users have found when using containers that
supplemental groups defined inside the container don't work as supplemental groups defined inside the container don't work as
expected. Since mergerfs lives outside the container it is querying expected. Since mergerfs lives outside the container it is querying
the host's group database. Effectively containers have their own user the host's group database. Containers have their own user and group
and group definitions unless setup otherwise just as different systems definitions unless setup otherwise just as different systems would.
would.
Users should mount in the host group file into the containers or use a Users should mount in the host group file into the containers or use a
standard shared user & groups technology like NIS or LDAP. standard shared user and groups technology like NIS or LDAP.
### directory mtime is not being updated ### directory mtime is not being updated

View file

@ -106,6 +106,8 @@ Config::Config()
fsname(), fsname(),
func(), func(),
fuse_msg_size("1M"), fuse_msg_size("1M"),
gid_cache_expire_timeout(60 * 60),
gid_cache_remove_timeout(60 * 60 * 12),
ignorepponrename(false), ignorepponrename(false),
inodecalc("hybrid-hash"), inodecalc("hybrid-hash"),
lazy_umount_mountpoint(false), lazy_umount_mountpoint(false),
@ -184,6 +186,8 @@ Config::Config()
_map["func.unlink"] = &func.unlink; _map["func.unlink"] = &func.unlink;
_map["func.utimens"] = &func.utimens; _map["func.utimens"] = &func.utimens;
_map["fuse_msg_size"] = &fuse_msg_size; _map["fuse_msg_size"] = &fuse_msg_size;
_map["gid-cache-expire-timeout"] = &gid_cache_expire_timeout;
_map["gid-cache-remove-timeout"] = &gid_cache_remove_timeout;
_map["handle-killpriv"] = &handle_killpriv; _map["handle-killpriv"] = &handle_killpriv;
_map["handle-killpriv-v2"] = &handle_killpriv_v2; _map["handle-killpriv-v2"] = &handle_killpriv_v2;
_map["ignorepponrename"] = &ignorepponrename; _map["ignorepponrename"] = &ignorepponrename;

View file

@ -21,6 +21,7 @@
#include "config_cachefiles.hpp" #include "config_cachefiles.hpp"
#include "config_flushonclose.hpp" #include "config_flushonclose.hpp"
#include "config_follow_symlinks.hpp" #include "config_follow_symlinks.hpp"
#include "config_gidcache.hpp"
#include "config_inodecalc.hpp" #include "config_inodecalc.hpp"
#include "config_link_exdev.hpp" #include "config_link_exdev.hpp"
#include "config_log_metrics.hpp" #include "config_log_metrics.hpp"
@ -130,6 +131,8 @@ public:
ConfigSTR fsname; ConfigSTR fsname;
Funcs func; Funcs func;
ConfigPageSize fuse_msg_size; ConfigPageSize fuse_msg_size;
GIDCacheExpireTimeout gid_cache_expire_timeout;
GIDCacheRemoveTimeout gid_cache_remove_timeout;
ConfigBOOL handle_killpriv = true; ConfigBOOL handle_killpriv = true;
ConfigBOOL handle_killpriv_v2 = true; ConfigBOOL handle_killpriv_v2 = true;
ConfigBOOL ignorepponrename; ConfigBOOL ignorepponrename;

65
src/config_gidcache.cpp Normal file
View file

@ -0,0 +1,65 @@
#include "config_gidcache.hpp"
#include "gidcache.hpp"
#include "to_string.hpp"
#include "from_string.hpp"
GIDCacheExpireTimeout::GIDCacheExpireTimeout(const int i_)
{
GIDCache::expire_timeout = i_;
}
GIDCacheExpireTimeout::GIDCacheExpireTimeout(const std::string &s_)
{
from_string(s_);
}
std::string
GIDCacheExpireTimeout::to_string(void) const
{
return str::to(GIDCache::expire_timeout);
}
int
GIDCacheExpireTimeout::from_string(const std::string &s_)
{
int rv;
rv = str::from(s_,&GIDCache::expire_timeout);
if(rv < 0)
return rv;
return 0;
}
GIDCacheRemoveTimeout::GIDCacheRemoveTimeout(const int i_)
{
GIDCache::remove_timeout = i_;
}
GIDCacheRemoveTimeout::GIDCacheRemoveTimeout(const std::string &s_)
{
from_string(s_);
}
std::string
GIDCacheRemoveTimeout::to_string(void) const
{
return str::to(GIDCache::remove_timeout);
}
int
GIDCacheRemoveTimeout::from_string(const std::string &s_)
{
int rv;
rv = str::from(s_,&GIDCache::remove_timeout);
if(rv < 0)
return rv;
return 0;
}

25
src/config_gidcache.hpp Normal file
View file

@ -0,0 +1,25 @@
#pragma once
#include "tofrom_string.hpp"
class GIDCacheExpireTimeout : public ToFromString
{
public:
GIDCacheExpireTimeout(const int i = 60 * 60);
GIDCacheExpireTimeout(const std::string &);
public:
std::string to_string(void) const final;
int from_string(const std::string &) final;
};
class GIDCacheRemoveTimeout : public ToFromString
{
public:
GIDCacheRemoveTimeout(const int = 60 * 60 * 12);
GIDCacheRemoveTimeout(const std::string &);
public:
std::string to_string(void) const final;
int from_string(const std::string &) final;
};

View file

@ -336,7 +336,7 @@ namespace l
fuse_invalidate_all_nodes(); fuse_invalidate_all_nodes();
return 0; return 0;
case IOCTL_INVALIDATE_GID_CACHE: case IOCTL_INVALIDATE_GID_CACHE:
GIDCache::invalidate_all_caches(); GIDCache::invalidate_all();
break; break;
} }

View file

@ -14,7 +14,7 @@
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/ */
#include "rnd.hpp" #include "gidcache.hpp"
#include <grp.h> #include <grp.h>
#include <pwd.h> #include <pwd.h>
@ -28,94 +28,9 @@
# include <sys/param.h> # include <sys/param.h>
#endif #endif
#include <cassert> int GIDCache::expire_timeout = (60 * 60);
#include <cstdlib> int GIDCache::remove_timeout = (60 * 60 * 12);
#include <algorithm> boost::concurrent_flat_map<uid_t,GIDRecord> GIDCache::_records;
#include <unordered_map>
#include <mutex>
#include "gidcache.hpp"
std::mutex g_REGISTERED_CACHES_MUTEX;
std::unordered_map<pthread_t,GIDCache*> g_REGISTERED_CACHES;
inline
bool
GIDRecord::operator<(const struct GIDRecord &b) const
{
return uid < b.uid;
}
GIDCache::GIDCache()
: invalidate(false),
size(0),
recs()
{
std::lock_guard<std::mutex> guard(g_REGISTERED_CACHES_MUTEX);
bool inserted;
pthread_t pthid;
pthid = pthread_self();
inserted = g_REGISTERED_CACHES.emplace(pthid,this).second;
assert(inserted == true);
}
inline
GIDRecord *
GIDCache::begin(void)
{
return &recs[0];
}
inline
GIDRecord *
GIDCache::end(void)
{
return &recs[size];
}
inline
GIDRecord *
GIDCache::allocrec(void)
{
if(size == MAXRECS)
return &recs[RND::rand64(MAXRECS)];
else
return &recs[size++];
}
inline
GIDRecord *
GIDCache::lower_bound(GIDRecord *begin,
GIDRecord *end,
const uid_t uid)
{
int step;
int count;
GIDRecord *iter;
count = std::distance(begin,end);
while(count > 0)
{
iter = begin;
step = count / 2;
std::advance(iter,step);
if(iter->uid < uid)
{
begin = ++iter;
count -= step + 1;
}
else
{
count = step;
}
}
return begin;
}
static static
int int
@ -131,85 +46,111 @@ _getgrouplist(const char *user,
#endif #endif
} }
GIDRecord *
GIDCache::cache(const uid_t uid,
const gid_t gid)
{
int rv;
char buf[4096];
struct passwd pwd;
struct passwd *pwdrv;
GIDRecord *rec;
rec = allocrec();
rec->uid = uid;
rv = ::getpwuid_r(uid,&pwd,buf,sizeof(buf),&pwdrv);
if(pwdrv != NULL && rv == 0)
{
rec->size = 0;
::_getgrouplist(pwd.pw_name,gid,NULL,&rec->size);
rec->size = std::min(MAXGIDS,rec->size);
rv = ::_getgrouplist(pwd.pw_name,gid,rec->gids,&rec->size);
if(rv == -1)
{
rec->gids[0] = gid;
rec->size = 1;
}
}
return rec;
}
static static
inline inline
int int
setgroups(const GIDRecord *rec) _setgroups(const std::vector<gid_t> gids_)
{ {
#if defined __linux__ and UGID_USE_RWLOCK == 0 #if defined __linux__ and UGID_USE_RWLOCK == 0
# if defined SYS_setgroups32 # if defined SYS_setgroups32
return ::syscall(SYS_setgroups32,rec->size,rec->gids); return ::syscall(SYS_setgroups32,gids_.size(),gids_.data());
# else # else
return ::syscall(SYS_setgroups,rec->size,rec->gids); return ::syscall(SYS_setgroups,gids_.size(),gids_.data());
# endif # endif
#else #else
return ::setgroups(rec->size,rec->gids); return ::setgroups(gids_.size(),gids_.data());
#endif #endif
} }
int static
GIDCache::initgroups(const uid_t uid, void
const gid_t gid) _getgroups(const uid_t uid_,
const gid_t gid_,
std::vector<gid_t> &gids_)
{ {
int rv; int rv;
GIDRecord *rec; int ngroups;
char buf[4096];
struct passwd pwd;
struct passwd *pwdrv;
if(invalidate) gids_.clear();
{ rv = ::getpwuid_r(uid_,&pwd,buf,sizeof(buf),&pwdrv);
size = 0; if((rv == -1) || (pwdrv == NULL))
invalidate = false; goto error;
}
rec = lower_bound(begin(),end(),uid); ngroups = 0;
if(rec == end() || rec->uid != uid) rv = ::_getgrouplist(pwd.pw_name,gid_,NULL,&ngroups);
{ gids_.resize(ngroups);
rec = cache(uid,gid);
rv = ::setgroups(rec);
std::sort(begin(),end());
}
else
{
rv = ::setgroups(rec);
}
return rv; rv = ::_getgrouplist(pwd.pw_name,gid_,gids_.data(),&ngroups);
if((size_t)ngroups < gids_.size())
gids_.resize(ngroups);
return;
error:
gids_.clear();
gids_.push_back(gid_);
}
int
GIDCache::initgroups(const uid_t uid_,
const gid_t gid_)
{
auto first_func =
[=](auto &x)
{
x.second.last_update = ::time(NULL);
::_getgroups(uid_,gid_,x.second.gids);
::_setgroups(x.second.gids);
};
auto exists_func =
[=](auto &x)
{
time_t now;
now = ::time(NULL);
if((now - x.second.last_update) > GIDCache::expire_timeout)
{
::_getgroups(uid_,gid_,x.second.gids);
x.second.last_update = now;
}
::_setgroups(x.second.gids);
};
_records.try_emplace_and_visit(uid_,
first_func,
exists_func);
return 0;
} }
void void
GIDCache::invalidate_all_caches() GIDCache::invalidate_all()
{ {
std::lock_guard<std::mutex> guard(g_REGISTERED_CACHES_MUTEX); _records.visit_all([](auto &x)
{
for(auto &p : g_REGISTERED_CACHES) x.second.last_update = 0;
p.second->invalidate = true; });
}
void
GIDCache::clear_all()
{
_records.clear();
}
void
GIDCache::clear_unused()
{
time_t now = ::time(NULL);
auto erase_func =
[=](auto &x)
{
return ((now - x.second.last_update) > GIDCache::remove_timeout);
};
_records.erase_if(erase_func);
} }

View file

@ -16,10 +16,13 @@
#pragma once #pragma once
#include "boost/unordered/concurrent_flat_map.hpp"
#include <sys/types.h> #include <sys/types.h>
#include <unistd.h> #include <unistd.h>
#include <array> #include <array>
#include <vector>
#define MAXGIDS 32 #define MAXGIDS 32
#define MAXRECS 256 #define MAXRECS 256
@ -32,42 +35,28 @@
// told to invalidate the cache on demand. A second instance on the // told to invalidate the cache on demand. A second instance on the
// same thread will cause an assert to be triggered. // same thread will cause an assert to be triggered.
struct GIDRecord struct GIDRecord
{ {
uid_t uid; std::vector<gid_t> gids;
int size; time_t last_update;
gid_t gids[MAXGIDS];
bool
operator<(const struct GIDRecord &b) const;
}; };
struct GIDCache struct GIDCache
{ {
public: public:
GIDCache(); static
public:
bool invalidate;
size_t size;
std::array<GIDRecord,MAXRECS> recs;
private:
GIDRecord *begin(void);
GIDRecord *end(void);
GIDRecord *allocrec(void);
GIDRecord *lower_bound(GIDRecord *begin,
GIDRecord *end,
const uid_t uid);
GIDRecord *cache(const uid_t uid,
const gid_t gid);
public:
int int
initgroups(const uid_t uid, initgroups(const uid_t uid,
const gid_t gid); const gid_t gid);
static void invalidate_all();
static void clear_all();
static void clear_unused();
public: public:
static void invalidate_all_caches(); static int expire_timeout;
static int remove_timeout;
private:
static boost::concurrent_flat_map<uid_t,GIDRecord> _records;
}; };

View file

@ -29,6 +29,7 @@
#include "strvec.hpp" #include "strvec.hpp"
#include "syslog.hpp" #include "syslog.hpp"
#include "version.hpp" #include "version.hpp"
#include "maintenance_thread.hpp"
#include "fuse_access.hpp" #include "fuse_access.hpp"
#include "fuse_bmap.hpp" #include "fuse_bmap.hpp"
@ -246,7 +247,7 @@ namespace l
{ {
SysLog::info("Received SIGUSR2 - triggering thorough gc"); SysLog::info("Received SIGUSR2 - triggering thorough gc");
fuse_gc(); fuse_gc();
GIDCache::invalidate_all_caches(); GIDCache::clear_all();
} }
static static
@ -310,6 +311,11 @@ namespace l
l::warn_if_not_root(); l::warn_if_not_root();
MaintenanceThread::push_job([](int count_)
{
if((count_ % 60) == 0)
GIDCache::clear_unused();
});
l::setup_resources(cfg->scheduling_priority); l::setup_resources(cfg->scheduling_priority);
l::setup_signal_handlers(); l::setup_signal_handlers();
l::get_fuse_operations(ops,cfg->nullrw); l::get_fuse_operations(ops,cfg->nullrw);

View file

@ -28,8 +28,6 @@ namespace ugid
initgroups(const uid_t uid_, initgroups(const uid_t uid_,
const gid_t gid_) const gid_t gid_)
{ {
static thread_local GIDCache cache; GIDCache::initgroups(uid_,gid_);
cache.initgroups(uid_,gid_);
} }
} }