[PATCH 0/2] mm/shrinker: make per-memcg shrinker units cheaper
From: Konstantin Khorenko
Date: Mon Oct 12 2026 - 15:07:37 EST
Every memory cgroup allocates, per NUMA node, one shrinker_info_unit
per 64 memcg-aware shrinker ids up to shrinker_nr_max, whether or not it
ever charges an object to those shrinkers. Every superblock registers
such a shrinker, so the cost is
memory cgroups x NUMA nodes x shrinker_nr_max / 64 x 1 KiB
because the unit is 520 bytes and comes from kmalloc-1k.
Page cache reclaim does not make this go away:
- Live cgroups keep their units until they are removed.
- The units are not charged to any memcg, so memory.max does not see
them and a delegated subtree creates cgroups at the host's expense.
- A dying cgroup keeps its units until the last reference to it is
dropped, for instance by cgroup writeback, a swap entry or a socket.
Nothing reclaims it for the sake of the units, and MemAvailable
counts them as unreclaimable.
- shrinker_nr_max never comes down: after a burst of mounts every new
cgroup pays for the peak until reboot.
- Both factors grow with the number of workloads on the host.
Patch 1 lets a unit cover BITS_PER_LONG - 1 ids so it fills a 512-byte
slot exactly: half the memory, nothing else changes.
Patch 2 allocates a unit only when the cgroup gets a list on that
shrinker's list_lru, which is where map bits are set: a cgroup pays for
the shrinkers it holds objects of, not for every superblock on the host.
The units stay uncharged, but how many a cgroup makes the host allocate
is now bounded by the shrinkers it uses. The unit pointer array in
shrinker_info still grows with shrinker_nr_max, at 8 bytes per 63 ids
per node. Patch 2 relies on commit fafaeceb89a5 ("mm: switch deferred
split shrinker to list_lru") and is not meant for stable.
On a host running a few hundred containers (45000 memory cgroups, most
of them dying, shrinker_nr_max 52480, 4 nodes) the units took 145 GB of
377 GB.
Reproducer without containers: 48 CPUs, 4 nodes, v7.3-rc6, fresh boot
per kernel. 8000 tmpfs mounts raise shrinker_nr_max to 8064, then 2000
empty memory cgroups are created. Active objects added to the 512-byte
and 1-KiB kmalloc caches, and slab per cgroup:
objects added slab per cgroup
base 1011518 x 1024 B 548 KB
+ patch 1 1023787 x 512 B 289 KB
+ patches 1-2 136 x 512 B 42 KB
The 42 KB left is the same on all three: the memcg's own structures and
8 KB of shrinker_info pointer arrays at this shrinker_nr_max. A cgroup
that looks up names on two filesystems gets 8 units (4 KB) with both
patches, and so does each ancestor that had none. Writing a page of a
file in each cgroup and removing the cgroups keeps the same amounts
pinned by the 2000 dying cgroups until the cache is dropped.
Tested with selftests/cgroup test_kmem and test_memcontrol, a check that
memory.reclaim reaches 140 ext4 superblocks with consecutive shrinker
ids, and unit installs from 64 cgroups racing with 3000 mounts that keep
growing shrinker_nr_max. On a kernel with lockdep, KASAN and DEBUG_VM the
same runs leave nothing in dmesg. test_memcg_sock fails there now and
then, with and without the series: memory.stat's sock counter is not
flushed yet when the test compares it with memory.current.
---
Konstantin Khorenko (2):
mm/shrinker: size shrinker_info_unit to a power of two
mm/shrinker: allocate the per-memcg shrinker units on demand
include/linux/memcontrol.h | 2 +
include/linux/shrinker.h | 9 ++-
mm/list_lru.c | 6 ++
mm/shrinker.c | 161 ++++++++++++++++++++++++++++++++++++---------
4 files changed, 147 insertions(+), 31 deletions(-)
---
base-commit: a5ebb76233b79db01e82e061173cb73c6d2b5c6b
change-id: 20261011-b4-prep-vstor-144715-size-shrinker_info_unit-to-a-power-of-two-9f2e043163af
Best regards,
--
Konstantin Khorenko <khorenko@xxxxxxxxxxxxx>