fix(docker): a container's memory is its processes' own; file cache shown on its page (#6030)
The agent sent Docker's working set (usage less inactive_file), which still counts recently used file cache. A file-heavy container with no limit read 191 GB while its processes held 2.2 GB, and the host's own figure, which already leaves cache out, disagreed with it. Agent 1.23.0 sends mem_used_bytes / mem_used_pct (anon + shmem, so Postgres's shared buffers count; v1: rss + shmem) and mem_cache_bytes apart. The new names keep the two meanings apart in history and in learned normals: docker_013 renames the columns, clears the old values and adds mem_cache_bytes. The Docker page, swarm by-node sums, Busiest Containers, container detail, normals and the MCP tools all read the new figure. File cache is shown only on the container's page and in get_container. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+12
-11
@@ -161,9 +161,10 @@ def usage(c, cpus: int | None, mem_total: int | None, cpu_pct: float | None = No
|
||||
|
||||
Docker's CPU% is per core (100% = one core busy), so it is shown as cores
|
||||
and graded as a share of the host's CPUs when the host's core count is
|
||||
known. Docker's memory limit is the host's RAM when none is set, so memory
|
||||
is shown in bytes, against a limit only when one is actually set; mem_pct
|
||||
is then a share of that limit, otherwise of the host's memory. `cpu_pct`
|
||||
known. Memory is what the container's processes hold, file cache left out
|
||||
(#6030). Docker's memory limit is the host's RAM when none is set, so
|
||||
memory is shown in bytes, against a limit only when one is actually set;
|
||||
mem_pct is then a share of that limit, otherwise of the host's memory. `cpu_pct`
|
||||
replaces the container's last sample (an average, say) when given.
|
||||
|
||||
With `thresholds`, cpu_grade and mem_grade are the grades to show: past
|
||||
@@ -178,9 +179,9 @@ def usage(c, cpus: int | None, mem_total: int | None, cpu_pct: float | None = No
|
||||
"cores": None if cpu is None else cpu / 100.0,
|
||||
"cpu_share": None if cpu is None or not cpus else cpu / cpus,
|
||||
"cpus": cpus,
|
||||
"mem_bytes": getattr(c, "mem_usage_bytes", None),
|
||||
"mem_bytes": getattr(c, "mem_used_bytes", None),
|
||||
"mem_limit": limit if limited else None,
|
||||
"mem_pct": getattr(c, "mem_pct", None),
|
||||
"mem_pct": getattr(c, "mem_used_pct", None),
|
||||
"mem_of": "its limit" if limited else ("the host's memory" if mem_total else None),
|
||||
"cpu_grade": None, "cpu_usual": None, "mem_grade": None, "mem_usual": None,
|
||||
}
|
||||
@@ -191,7 +192,7 @@ def usage(c, cpus: int | None, mem_total: int | None, cpu_pct: float | None = No
|
||||
|
||||
# Container figures graded against their own normal (#5742): the metric that
|
||||
# learns it and its floor's setting (norms.floor.*).
|
||||
NORMED = {"cpu": ("cpu_pct", "container_cpu_pct"), "mem": ("mem_pct", "container_mem_pct")}
|
||||
NORMED = {"cpu": ("cpu_pct", "container_cpu_pct"), "mem": ("mem_used_pct", "container_mem_pct")}
|
||||
|
||||
|
||||
def _grade(u: dict, cpu: float | None, thresholds: dict, norms: dict) -> None:
|
||||
@@ -209,7 +210,7 @@ def _grade(u: dict, cpu: float | None, thresholds: dict, norms: dict) -> None:
|
||||
u["cpu_grade"] = grade
|
||||
if u["mem_of"]:
|
||||
u["mem_grade"], u["mem_usual"] = graded(
|
||||
u["mem_pct"], "mem", thresholds, norms.get("mem_pct"),
|
||||
u["mem_pct"], "mem", thresholds, norms.get("mem_used_pct"),
|
||||
floor_of(thresholds, "container_mem_pct"), "%")
|
||||
else:
|
||||
u["mem_grade"] = "dim"
|
||||
@@ -318,7 +319,7 @@ async def query_logs(db, host_id: str, name: str, stream: str, query: str,
|
||||
return (await db.execute(stmt)).all()
|
||||
|
||||
|
||||
CONTAINER_SERIES = ("cpu_pct", "mem_pct", "mem_usage_bytes")
|
||||
CONTAINER_SERIES = ("cpu_pct", "mem_used_pct", "mem_used_bytes", "mem_cache_bytes")
|
||||
|
||||
|
||||
# A container's series live in the one metric store (#5752): source "docker",
|
||||
@@ -397,14 +398,14 @@ def bar_slots(now: datetime, hours: int, bars: int = BARS) -> tuple[list[datetim
|
||||
|
||||
|
||||
async def recent_usage(db, keys, since: datetime, width: int) -> dict:
|
||||
"""{(host_id, container_name): {bucket start: (cpu_pct, mem_usage_bytes)}}
|
||||
"""{(host_id, container_name): {bucket start: (cpu_pct, mem_used_bytes)}}
|
||||
averaged per bucket since `since`, for the given containers, in one query."""
|
||||
resources = await container_resources(db, keys)
|
||||
if not resources:
|
||||
return {}
|
||||
out: dict = {}
|
||||
for resource, at, metric, value in await _samples(
|
||||
db, resources, ("cpu_pct", "mem_usage_bytes"), since, width):
|
||||
db, resources, ("cpu_pct", "mem_used_bytes"), since, width):
|
||||
cpu, mem = out.setdefault(resources[resource], {}).get(at, (None, None))
|
||||
v = None if value is None else float(value)
|
||||
out[resources[resource]][at] = (v, mem) if metric == "cpu_pct" else (cpu, v)
|
||||
@@ -595,7 +596,7 @@ async def service_history(db, containers, since: datetime) -> list:
|
||||
return []
|
||||
joined: dict[tuple[str, datetime], list] = {}
|
||||
for resource, at, metric, value in await _samples(
|
||||
db, resources, ("cpu_pct", "mem_usage_bytes"), since):
|
||||
db, resources, ("cpu_pct", "mem_used_bytes"), since):
|
||||
pair = joined.setdefault((resource, at), [None, None])
|
||||
pair[0 if metric == "cpu_pct" else 1] = value
|
||||
rows = [(resource, at, cpu, mem) for (resource, at), (cpu, mem) in
|
||||
|
||||
Reference in New Issue
Block a user