Re: [PATCH REBASED] mm, memcg: Make scan aggression always exclude protection

From: Roman Gushchin
Date: Fri Mar 22 2019 - 18:49:49 EST


On Fri, Mar 22, 2019 at 03:29:10PM -0700, Roman Gushchin wrote:
> On Fri, Mar 22, 2019 at 04:03:07PM +0000, Chris Down wrote:
> > This patch is an incremental improvement on the existing
> > memory.{low,min} relative reclaim work to base its scan pressure
> > calculations on how much protection is available compared to the current
> > usage, rather than how much the current usage is over some protection
> > threshold.
> >
> > Previously the way that memory.low protection works is that if you are
> > 50% over a certain baseline, you get 50% of your normal scan pressure.
> > This is certainly better than the previous cliff-edge behaviour, but it
> > can be improved even further by always considering memory under the
> > currently enforced protection threshold to be out of bounds. This means
> > that we can set relatively low memory.low thresholds for variable or
> > bursty workloads while still getting a reasonable level of protection,
> > whereas with the previous version we may still trivially hit the 100%
> > clamp. The previous 100% clamp is also somewhat arbitrary, whereas this
> > one is more concretely based on the currently enforced protection
> > threshold, which is likely easier to reason about.
> >
> > There is also a subtle issue with the way that proportional reclaim
> > worked previously -- it promotes having no memory.low, since it makes
> > pressure higher during low reclaim. This happens because we base our
> > scan pressure modulation on how far memory.current is between memory.min
> > and memory.low, but if memory.low is unset, we only use the overage
> > method. In most cromulent configurations, this then means that we end up
> > with *more* pressure than with no memory.low at all when we're in low
> > reclaim, which is not really very usable or expected.
> >
> > With this patch, memory.low and memory.min affect reclaim pressure in a
> > more understandable and composable way. For example, from a user
> > standpoint, "protected" memory now remains untouchable from a reclaim
> > aggression standpoint, and users can also have more confidence that
> > bursty workloads will still receive some amount of guaranteed
> > protection.
> >
> > Signed-off-by: Chris Down <chris@xxxxxxxxxxxxxx>
> > Reviewed-by: Roman Gushchin <guro@xxxxxx>
> > Cc: Johannes Weiner <hannes@xxxxxxxxxxx>
> > Cc: Andrew Morton <akpm@xxxxxxxxxxxxxxxxxxxx>
> > Cc: Michal Hocko <mhocko@xxxxxxxxxx>
> > Cc: Tejun Heo <tj@xxxxxxxxxx>
> > Cc: Roman Gushchin <guro@xxxxxx>
> > Cc: Dennis Zhou <dennis@xxxxxxxxxx>
> > Cc: linux-kernel@xxxxxxxxxxxxxxx
> > Cc: cgroups@xxxxxxxxxxxxxxx
> > Cc: linux-mm@xxxxxxxxx
> > Cc: kernel-team@xxxxxx
> > ---
> > include/linux/memcontrol.h | 25 ++++++++--------
> > mm/vmscan.c | 61 +++++++++++++-------------------------
> > 2 files changed, 32 insertions(+), 54 deletions(-)
> >
> > No functional changes, just rebased.
> >
> > diff --git a/include/linux/memcontrol.h b/include/linux/memcontrol.h
> > index b226c4bafc93..799de23edfb7 100644
> > --- a/include/linux/memcontrol.h
> > +++ b/include/linux/memcontrol.h
> > @@ -333,17 +333,17 @@ static inline bool mem_cgroup_disabled(void)
> > return !cgroup_subsys_enabled(memory_cgrp_subsys);
> > }
> >
> > -static inline void mem_cgroup_protection(struct mem_cgroup *memcg,
> > - unsigned long *min, unsigned long *low)
> > +static inline unsigned long mem_cgroup_protection(struct mem_cgroup *memcg,
> > + bool in_low_reclaim)
> > {
> > - if (mem_cgroup_disabled()) {
> > - *min = 0;
> > - *low = 0;
> > - return;
> > - }
> > + if (mem_cgroup_disabled())
> > + return 0;
> > +
> > + if (in_low_reclaim)
> > + return READ_ONCE(memcg->memory.emin);
> >
> > - *min = READ_ONCE(memcg->memory.emin);
> > - *low = READ_ONCE(memcg->memory.elow);
> > + return max(READ_ONCE(memcg->memory.emin),
> > + READ_ONCE(memcg->memory.elow));
> > }
> >
> > enum mem_cgroup_protection mem_cgroup_protected(struct mem_cgroup *root,
> > @@ -845,11 +845,10 @@ static inline void memcg_memory_event_mm(struct mm_struct *mm,
> > {
> > }
> >
> > -static inline void mem_cgroup_protection(struct mem_cgroup *memcg,
> > - unsigned long *min, unsigned long *low)
> > +static inline unsigned long mem_cgroup_protection(struct mem_cgroup *memcg,
> > + bool in_low_reclaim)
> > {
> > - *min = 0;
> > - *low = 0;
> > + return 0;
> > }
> >
> > static inline enum mem_cgroup_protection mem_cgroup_protected(
> > diff --git a/mm/vmscan.c b/mm/vmscan.c
> > index f6b9b45f731d..d5daa224364d 100644
> > --- a/mm/vmscan.c
> > +++ b/mm/vmscan.c
> > @@ -2374,12 +2374,13 @@ static void get_scan_count(struct lruvec *lruvec, struct mem_cgroup *memcg,
> > int file = is_file_lru(lru);
> > unsigned long lruvec_size;
> > unsigned long scan;
> > - unsigned long min, low;
> > + unsigned long protection;
> >
> > lruvec_size = lruvec_lru_size(lruvec, lru, sc->reclaim_idx);
> > - mem_cgroup_protection(memcg, &min, &low);
> > + protection = mem_cgroup_protection(memcg,
> > + sc->memcg_low_reclaim);
> >
> > - if (min || low) {
> > + if (protection) {
> > /*
> > * Scale a cgroup's reclaim pressure by proportioning
> > * its current usage to its memory.low or memory.min
> > @@ -2392,13 +2393,10 @@ static void get_scan_count(struct lruvec *lruvec, struct mem_cgroup *memcg,
> > * setting extremely liberal protection thresholds. It
> > * also means we simply get no protection at all if we
> > * set it too low, which is not ideal.
> > - */
> > - unsigned long cgroup_size = mem_cgroup_size(memcg);
> > -
> > - /*
> > - * If there is any protection in place, we adjust scan
> > - * pressure in proportion to how much a group's current
> > - * usage exceeds that, in percent.
> > + *
> > + * If there is any protection in place, we reduce scan
> > + * pressure by how much of the total memory used is
> > + * within protection thresholds.
> > *
> > * There is one special case: in the first reclaim pass,
> > * we skip over all groups that are within their low
> > @@ -2408,43 +2406,24 @@ static void get_scan_count(struct lruvec *lruvec, struct mem_cgroup *memcg,
> > * ideally want to honor how well-behaved groups are in
> > * that case instead of simply punishing them all
> > * equally. As such, we reclaim them based on how much
> > - * of their best-effort protection they are using. Usage
> > - * below memory.min is excluded from consideration when
> > - * calculating utilisation, as it isn't ever
> > - * reclaimable, so it might as well not exist for our
> > - * purposes.
> > + * memory they are using, reducing the scan pressure
> > + * again by how much of the total memory used is under
> > + * hard protection.
> > */
> > - if (sc->memcg_low_reclaim && low > min) {
> > - /*
> > - * Reclaim according to utilisation between min
> > - * and low
> > - */
> > - scan = lruvec_size * (cgroup_size - min) /
> > - (low - min);
> > - } else {
> > - /* Reclaim according to protection overage */
> > - scan = lruvec_size * cgroup_size /
> > - max(min, low) - lruvec_size;
>
> I've noticed that the old version is just wrong: if cgroup_size is way smaller
> than max(min, low), scan will be set to -lruvec_size.
> Given that it's unsigned long, we'll end up with scanning the whole list
> (due to clamp() below).

Just to clarify: in most cases it works fine because we skip cgroups with
cgroup_size < max(min, low). So we just don't call the code above.

However, we can race with the emin/elow update and end up with negative scan,
especially if cgroup_size is about the effective protection size

The new version looks much more secure.

Thanks!