538 lines
24 KiB
Python
538 lines
24 KiB
Python
def memory_distribution_config():
|
|
config = MEMORY_CONFIG.get("attribution")
|
|
if not isinstance(config, dict):
|
|
raise RuntimeError("required YAML memory attribution policy is missing")
|
|
required_integers = {
|
|
"cgroupScanLimit": (1, 1000000),
|
|
"topLevelLimit": (1, 100),
|
|
"systemServiceLimit": (1, 100),
|
|
"podLimit": (1, 100),
|
|
"processLimit": (1, 100),
|
|
"processPssLimit": (1, 50),
|
|
"processSwapLimit": (1, 100),
|
|
"commandTimeoutSeconds": (1, 60),
|
|
"targetMemAvailableBytes": (1, 1 << 50),
|
|
"namespaceCountConcurrency": (1, 32),
|
|
}
|
|
for key, (minimum, maximum) in required_integers.items():
|
|
value = config.get(key)
|
|
if isinstance(value, bool) or not isinstance(value, int) or value < minimum or value > maximum:
|
|
raise RuntimeError("invalid YAML memory attribution integer: %s" % key)
|
|
cgroup_root = config.get("cgroupRoot")
|
|
kubeconfig = config.get("kubeconfigPath")
|
|
resources = config.get("objectCountResources")
|
|
namespaces = config.get("namespaceCountNamespaces")
|
|
if not isinstance(cgroup_root, str) or not cgroup_root.startswith("/") or os.path.normpath(cgroup_root) != cgroup_root:
|
|
raise RuntimeError("invalid YAML memory attribution cgroupRoot")
|
|
if not isinstance(kubeconfig, str) or not kubeconfig.startswith("/") or os.path.normpath(kubeconfig) != kubeconfig:
|
|
raise RuntimeError("invalid YAML memory attribution kubeconfigPath")
|
|
if not isinstance(resources, list) or not resources or any(
|
|
not isinstance(item, str) or not re.match(r"^[a-z0-9.-]{1,100}$", item) for item in resources
|
|
):
|
|
raise RuntimeError("invalid YAML memory attribution objectCountResources")
|
|
if not isinstance(namespaces, list) or not namespaces or any(
|
|
not isinstance(item, str) or not re.match(r"^[a-z0-9]([-a-z0-9]*[a-z0-9])?$", item) for item in namespaces
|
|
):
|
|
raise RuntimeError("invalid YAML memory attribution namespaceCountNamespaces")
|
|
return config
|
|
|
|
def read_memory_distribution_key_values(path, multiplier=1):
|
|
result = {}
|
|
try:
|
|
with open(path, "r", encoding="utf-8") as handle:
|
|
for line in handle:
|
|
parts = line.replace(":", "", 1).split()
|
|
if len(parts) < 2:
|
|
continue
|
|
try:
|
|
result[parts[0]] = int(parts[1]) * multiplier
|
|
except ValueError:
|
|
continue
|
|
except OSError:
|
|
return {}
|
|
return result
|
|
|
|
def read_memory_distribution_integer(path):
|
|
try:
|
|
with open(path, "r", encoding="utf-8") as handle:
|
|
return int(handle.read().strip())
|
|
except (OSError, ValueError):
|
|
return None
|
|
|
|
def memory_distribution_cgroup_row(root, path):
|
|
stat = read_memory_distribution_key_values(os.path.join(path, "memory.stat"))
|
|
file_bytes = safe_int(stat.get("file"))
|
|
slab_reclaimable = safe_int(stat.get("slab_reclaimable"))
|
|
return {
|
|
"path": os.path.relpath(path, root),
|
|
"currentBytes": read_memory_distribution_integer(os.path.join(path, "memory.current")),
|
|
"swapCurrentBytes": read_memory_distribution_integer(os.path.join(path, "memory.swap.current")),
|
|
"anonBytes": stat.get("anon"),
|
|
"fileBytes": stat.get("file"),
|
|
"inactiveAnonBytes": stat.get("inactive_anon"),
|
|
"inactiveFileBytes": stat.get("inactive_file"),
|
|
"reclaimableFileSlabUpperBoundBytes": file_bytes + slab_reclaimable,
|
|
}
|
|
|
|
def memory_distribution_limit(config, key):
|
|
configured = safe_int(config.get(key))
|
|
return min(100, configured * 5) if bool(OPTIONS.get("full")) else configured
|
|
|
|
def memory_distribution_top(rows, limit):
|
|
return sorted(rows, key=lambda item: safe_int(item.get("currentBytes")), reverse=True)[:limit]
|
|
|
|
def memory_distribution_command_summary(result):
|
|
return {
|
|
"ok": result.get("exitCode") == 0,
|
|
"exitCode": result.get("exitCode"),
|
|
"timedOut": result.get("timedOut") is True,
|
|
"durationMs": result.get("durationMs"),
|
|
}
|
|
|
|
def memory_distribution_runtime_maps(config):
|
|
timeout = safe_int(config.get("commandTimeoutSeconds"))
|
|
docker = command(["docker", "ps", "--no-trunc", "--format", "{{.ID}}\t{{.Names}}"], timeout)
|
|
docker_names = {}
|
|
if docker.get("exitCode") == 0:
|
|
for line in docker.get("stdout", "").splitlines():
|
|
parts = line.split("\t", 1)
|
|
if len(parts) == 2 and re.match(r"^[a-f0-9]{64}$", parts[0]):
|
|
docker_names[parts[0]] = parts[1]
|
|
cri = command(["k3s", "crictl", "pods", "-o", "json"], timeout)
|
|
pod_names = {}
|
|
if cri.get("exitCode") == 0:
|
|
try:
|
|
payload = json.loads(cri.get("stdout") or "{}")
|
|
for item in payload.get("items") or []:
|
|
metadata = item.get("metadata") or {}
|
|
uid = str(metadata.get("uid") or "")
|
|
if uid:
|
|
pod_names[uid] = {
|
|
"namespace": metadata.get("namespace"),
|
|
"name": metadata.get("name"),
|
|
"state": item.get("state"),
|
|
}
|
|
except (TypeError, ValueError, json.JSONDecodeError):
|
|
pass
|
|
return docker_names, pod_names, {
|
|
"docker": memory_distribution_command_summary(docker),
|
|
"cri": memory_distribution_command_summary(cri),
|
|
}
|
|
|
|
def collect_memory_distribution_cgroups(config, docker_names, pod_names):
|
|
root = config.get("cgroupRoot")
|
|
scan_limit = safe_int(config.get("cgroupScanLimit"))
|
|
top_level = []
|
|
system_services = []
|
|
pods = []
|
|
scanned = 0
|
|
truncated = False
|
|
pod_pattern = re.compile(r"pod([0-9a-f_]{36})\.slice$")
|
|
docker_pattern = re.compile(r"docker-([a-f0-9]{64})\.scope$")
|
|
for current, directories, files in os.walk(root, topdown=True, followlinks=False):
|
|
directories[:] = sorted(name for name in directories if not os.path.islink(os.path.join(current, name)))
|
|
scanned += 1
|
|
if scanned > scan_limit:
|
|
truncated = True
|
|
break
|
|
if "memory.current" not in files:
|
|
continue
|
|
row = memory_distribution_cgroup_row(root, current)
|
|
parent = os.path.dirname(current)
|
|
if parent == root:
|
|
top_level.append(row)
|
|
if parent == os.path.join(root, "system.slice"):
|
|
match = docker_pattern.search(os.path.basename(current))
|
|
if match:
|
|
row["owner"] = {"kind": "docker", "name": docker_names.get(match.group(1)), "id": match.group(1)}
|
|
system_services.append(row)
|
|
match = pod_pattern.search(os.path.basename(current))
|
|
if match:
|
|
uid = match.group(1).replace("_", "-")
|
|
row["owner"] = {"kind": "kubernetes-pod", "uid": uid, **(pod_names.get(uid) or {})}
|
|
pods.append(row)
|
|
directories[:] = []
|
|
return {
|
|
"scan": {"root": root, "scannedCount": scanned, "limit": scan_limit, "truncated": truncated},
|
|
"topLevel": memory_distribution_top(top_level, memory_distribution_limit(config, "topLevelLimit")),
|
|
"systemServices": memory_distribution_top(system_services, memory_distribution_limit(config, "systemServiceLimit")),
|
|
"pods": memory_distribution_top(pods, memory_distribution_limit(config, "podLimit")),
|
|
"podCgroupCount": len(pods),
|
|
}
|
|
|
|
def memory_distribution_cgroup_for_pid(pid):
|
|
try:
|
|
with open("/proc/%s/cgroup" % int(pid), "r", encoding="utf-8") as handle:
|
|
for line in handle:
|
|
parts = line.rstrip("\n").split(":", 2)
|
|
if len(parts) == 3 and parts[0] == "0" and parts[1] == "":
|
|
return parts[2]
|
|
except (OSError, ValueError):
|
|
pass
|
|
return None
|
|
|
|
def memory_distribution_pss(pid):
|
|
values = read_memory_distribution_key_values("/proc/%s/smaps_rollup" % int(pid), 1024)
|
|
if not values:
|
|
return None
|
|
return {
|
|
"pssBytes": values.get("Pss"),
|
|
"pssAnonBytes": values.get("Pss_Anon"),
|
|
"pssFileBytes": values.get("Pss_File"),
|
|
"swapPssBytes": values.get("SwapPss"),
|
|
}
|
|
|
|
def memory_distribution_process_swap(pid):
|
|
values = read_memory_distribution_key_values("/proc/%s/status" % int(pid), 1024)
|
|
return values.get("VmSwap")
|
|
|
|
def memory_distribution_process_owner(cgroup_path, docker_names, pod_names):
|
|
if not isinstance(cgroup_path, str):
|
|
return None
|
|
docker_match = re.search(r"docker-([a-f0-9]{64})\.scope(?:/|$)", cgroup_path)
|
|
if docker_match is not None:
|
|
container_id = docker_match.group(1)
|
|
return {"kind": "docker", "id": container_id, "name": docker_names.get(container_id)}
|
|
match = re.search(r"pod([0-9a-f_]{36})\.slice(?:/|$)", cgroup_path)
|
|
if match is None:
|
|
return None
|
|
uid = match.group(1).replace("_", "-")
|
|
return {"kind": "kubernetes-pod", "uid": uid, **(pod_names.get(uid) or {})}
|
|
|
|
def collect_memory_distribution_processes(config, docker_names, pod_names):
|
|
result = command(["ps", "-eo", "pid=,ppid=,rss=,stat=,comm=,args="], safe_int(config.get("commandTimeoutSeconds")))
|
|
if result.get("exitCode") != 0:
|
|
return {"ok": False, "command": memory_distribution_command_summary(result), "top": [], "zombies": {}}
|
|
rows = []
|
|
zombie_by_comm = {}
|
|
zombie_by_parent = {}
|
|
for line in result.get("stdout", "").splitlines():
|
|
parts = line.strip().split(None, 5)
|
|
if len(parts) < 5:
|
|
continue
|
|
pid = safe_int(parts[0])
|
|
ppid = safe_int(parts[1])
|
|
rss = safe_int(parts[2]) * 1024
|
|
state = parts[3]
|
|
comm = parts[4]
|
|
args = parts[5] if len(parts) > 5 else comm
|
|
row = {
|
|
"pid": pid,
|
|
"ppid": ppid,
|
|
"comm": comm,
|
|
"state": state,
|
|
"rssBytes": rss,
|
|
"swapBytes": memory_distribution_process_swap(pid),
|
|
"commandPreview": redact_command_preview(args)[:120],
|
|
}
|
|
rows.append(row)
|
|
if state.startswith("Z"):
|
|
zombie_by_comm[comm] = zombie_by_comm.get(comm, 0) + 1
|
|
zombie_by_parent[ppid] = zombie_by_parent.get(ppid, 0) + 1
|
|
by_pid = {item.get("pid"): item for item in rows}
|
|
top = sorted(rows, key=lambda item: safe_int(item.get("rssBytes")), reverse=True)[:memory_distribution_limit(config, "processLimit")]
|
|
top_swap = sorted(rows, key=lambda item: safe_int(item.get("swapBytes")), reverse=True)[:memory_distribution_limit(config, "processSwapLimit")]
|
|
pss_limit = memory_distribution_limit(config, "processPssLimit")
|
|
for index, item in enumerate(top):
|
|
item["cgroupPath"] = memory_distribution_cgroup_for_pid(item.get("pid"))
|
|
item["owner"] = memory_distribution_process_owner(item.get("cgroupPath"), docker_names, pod_names)
|
|
if index < pss_limit:
|
|
item.update(memory_distribution_pss(item.get("pid")) or {})
|
|
for item in top_swap:
|
|
item["cgroupPath"] = memory_distribution_cgroup_for_pid(item.get("pid"))
|
|
item["owner"] = memory_distribution_process_owner(item.get("cgroupPath"), docker_names, pod_names)
|
|
parent_rows = []
|
|
for ppid, count in sorted(zombie_by_parent.items(), key=lambda item: item[1], reverse=True)[:5]:
|
|
parent = by_pid.get(ppid) or {}
|
|
cgroup_path = memory_distribution_cgroup_for_pid(ppid)
|
|
owner = memory_distribution_process_owner(cgroup_path, docker_names, pod_names)
|
|
parent_rows.append({
|
|
"ppid": ppid,
|
|
"parentComm": parent.get("comm"),
|
|
"zombieCount": count,
|
|
"cgroupPath": cgroup_path,
|
|
"owner": owner,
|
|
"classification": "unreaped-child-process-leak",
|
|
"remediation": "fix-child-reaping-and-rollout-owner",
|
|
})
|
|
return {
|
|
"ok": True,
|
|
"command": memory_distribution_command_summary(result),
|
|
"processCount": len(rows),
|
|
"rssTotalBytes": sum(safe_int(item.get("rssBytes")) for item in rows),
|
|
"swapTotalBytes": sum(safe_int(item.get("swapBytes")) for item in rows),
|
|
"top": top,
|
|
"topSwap": top_swap,
|
|
"zombies": {
|
|
"count": sum(zombie_by_comm.values()),
|
|
"byComm": [{"comm": comm, "count": count} for comm, count in sorted(zombie_by_comm.items(), key=lambda item: item[1], reverse=True)[:5]],
|
|
"byParent": parent_rows,
|
|
},
|
|
}
|
|
|
|
def parse_memory_distribution_pressure(path):
|
|
result = {}
|
|
try:
|
|
with open(path, "r", encoding="utf-8") as handle:
|
|
for line in handle:
|
|
parts = line.split()
|
|
if not parts:
|
|
continue
|
|
result[parts[0]] = {
|
|
key: float(value) if key != "total" else safe_int(value)
|
|
for key, value in (item.split("=", 1) for item in parts[1:] if "=" in item)
|
|
}
|
|
except OSError:
|
|
pass
|
|
return result
|
|
|
|
def collect_memory_distribution_metrics(config):
|
|
timeout = safe_int(config.get("commandTimeoutSeconds"))
|
|
kubeconfig = config.get("kubeconfigPath")
|
|
result = command(["kubectl", "--kubeconfig", kubeconfig, "get", "--raw", "/metrics"], timeout)
|
|
selected = set(config.get("objectCountResources") or [])
|
|
objects = {}
|
|
runtime = {}
|
|
if result.get("exitCode") == 0:
|
|
object_pattern = re.compile(r'^apiserver_storage_objects\{resource="([^"]+)"\}\s+([0-9.eE+-]+)$')
|
|
runtime_names = set([
|
|
"process_resident_memory_bytes", "process_virtual_memory_bytes", "go_goroutines",
|
|
"go_memstats_heap_alloc_bytes", "go_memstats_heap_inuse_bytes", "go_memstats_heap_sys_bytes",
|
|
])
|
|
for line in result.get("stdout", "").splitlines():
|
|
match = object_pattern.match(line)
|
|
if match and match.group(1) in selected:
|
|
objects[match.group(1)] = int(float(match.group(2)))
|
|
continue
|
|
parts = line.split()
|
|
if len(parts) == 2 and parts[0] in runtime_names:
|
|
try:
|
|
runtime[parts[0]] = float(parts[1])
|
|
except ValueError:
|
|
pass
|
|
return {"command": memory_distribution_command_summary(result), "objects": objects, "runtime": runtime}
|
|
|
|
def memory_distribution_namespace_resource_path(resource, namespace):
|
|
encoded_namespace = urllib.parse.quote(namespace, safe="")
|
|
paths = {
|
|
"pods": "/api/v1/namespaces/%s/pods?limit=1" % encoded_namespace,
|
|
"events": "/api/v1/namespaces/%s/events?limit=1" % encoded_namespace,
|
|
"secrets": "/api/v1/namespaces/%s/secrets?limit=1" % encoded_namespace,
|
|
"persistentvolumeclaims": "/api/v1/namespaces/%s/persistentvolumeclaims?limit=1" % encoded_namespace,
|
|
"pipelineruns.tekton.dev": "/apis/tekton.dev/v1/namespaces/%s/pipelineruns?limit=1" % encoded_namespace,
|
|
"taskruns.tekton.dev": "/apis/tekton.dev/v1/namespaces/%s/taskruns?limit=1" % encoded_namespace,
|
|
}
|
|
return paths.get(resource)
|
|
|
|
def memory_distribution_namespace_count(config, namespace, resource):
|
|
path = memory_distribution_namespace_resource_path(resource, namespace)
|
|
if path is None:
|
|
return namespace, resource, None, "unsupported-resource"
|
|
result = command([
|
|
"kubectl", "--kubeconfig", config.get("kubeconfigPath"), "get", "--raw", path,
|
|
], safe_int(config.get("commandTimeoutSeconds")))
|
|
if result.get("exitCode") != 0:
|
|
return namespace, resource, None, "read-failed"
|
|
try:
|
|
payload = json.loads(result.get("stdout") or "{}")
|
|
metadata = payload.get("metadata") or {}
|
|
count = len(payload.get("items") or []) + safe_int(metadata.get("remainingItemCount"))
|
|
return namespace, resource, count, None
|
|
except (TypeError, ValueError, json.JSONDecodeError):
|
|
return namespace, resource, None, "parse-failed"
|
|
|
|
def collect_memory_distribution_namespace_counts(config):
|
|
import concurrent.futures
|
|
|
|
namespaces = list(config.get("namespaceCountNamespaces") or [])
|
|
resources = [
|
|
item for item in (config.get("objectCountResources") or [])
|
|
if memory_distribution_namespace_resource_path(item, namespaces[0]) is not None
|
|
]
|
|
jobs = [(namespace, resource) for namespace in namespaces for resource in resources]
|
|
rows = {namespace: {"namespace": namespace} for namespace in namespaces}
|
|
errors = []
|
|
with concurrent.futures.ThreadPoolExecutor(max_workers=safe_int(config.get("namespaceCountConcurrency"))) as pool:
|
|
futures = [pool.submit(memory_distribution_namespace_count, config, namespace, resource) for namespace, resource in jobs]
|
|
for future in concurrent.futures.as_completed(futures):
|
|
namespace, resource, count, error = future.result()
|
|
if error is None:
|
|
rows[namespace][resource] = count
|
|
else:
|
|
errors.append({"namespace": namespace, "resource": resource, "error": error})
|
|
ordered = sorted(
|
|
rows.values(),
|
|
key=lambda item: sum(safe_int(value) for key, value in item.items() if key != "namespace"),
|
|
reverse=True,
|
|
)
|
|
return {"rows": ordered, "errors": errors, "complete": not errors}
|
|
|
|
def compact_memory_distribution_cgroup(item):
|
|
compact = {
|
|
key: item.get(key)
|
|
for key in ["currentBytes", "swapCurrentBytes", "anonBytes"]
|
|
if item.get(key) is not None
|
|
}
|
|
owner = item.get("owner") if isinstance(item.get("owner"), dict) else None
|
|
if owner is not None:
|
|
compact["owner"] = {
|
|
key: owner.get(key)
|
|
for key in ["kind", "namespace", "name"]
|
|
if owner.get(key) is not None
|
|
}
|
|
else:
|
|
compact["path"] = item.get("path")
|
|
return compact
|
|
|
|
def compact_memory_distribution_process(item):
|
|
compact = {
|
|
key: item.get(key)
|
|
for key in ["pid", "comm", "rssBytes", "swapBytes", "pssBytes"]
|
|
if item.get(key) is not None
|
|
}
|
|
owner = item.get("owner") if isinstance(item.get("owner"), dict) else None
|
|
if owner is not None:
|
|
compact["owner"] = {
|
|
key: owner.get(key)
|
|
for key in ["kind", "namespace", "name"]
|
|
if owner.get(key) is not None
|
|
}
|
|
else:
|
|
compact["cgroupPath"] = item.get("cgroupPath")
|
|
return compact
|
|
|
|
def compact_memory_distribution_pressure(pressure):
|
|
return {
|
|
level: {
|
|
key: values.get(key)
|
|
for key in ["avg10", "avg60"]
|
|
if values.get(key) is not None
|
|
}
|
|
for level, values in pressure.items()
|
|
if isinstance(values, dict)
|
|
}
|
|
|
|
def compact_memory_distribution_zombies(zombies):
|
|
by_parent = []
|
|
for item in zombies.get("byParent") or []:
|
|
compact = {
|
|
key: item.get(key)
|
|
for key in ["ppid", "parentComm", "zombieCount", "classification", "remediation"]
|
|
if item.get(key) is not None
|
|
}
|
|
owner = item.get("owner") if isinstance(item.get("owner"), dict) else None
|
|
if owner is not None:
|
|
compact["owner"] = {
|
|
key: owner.get(key)
|
|
for key in ["kind", "namespace", "name"]
|
|
if owner.get(key) is not None
|
|
}
|
|
by_parent.append(compact)
|
|
return {
|
|
"count": zombies.get("count"),
|
|
"byComm": (zombies.get("byComm") or [])[:3],
|
|
"byParent": by_parent[:3],
|
|
}
|
|
|
|
def collect_memory_distribution(observed_at):
|
|
try:
|
|
config = memory_distribution_config()
|
|
except RuntimeError as exc:
|
|
return {"ok": False, "action": "gc remote memory-distribution", "providerId": PROVIDER_ID, "dryRun": True, "mutation": False, "error": str(exc)}
|
|
started = time.time()
|
|
docker_names, pod_names, runtime_commands = memory_distribution_runtime_maps(config)
|
|
host_snapshot = collect_memory_snapshot()
|
|
meminfo = read_memory_distribution_key_values("/proc/meminfo", 1024)
|
|
cgroups = collect_memory_distribution_cgroups(config, docker_names, pod_names)
|
|
processes = collect_memory_distribution_processes(config, docker_names, pod_names)
|
|
metrics = collect_memory_distribution_metrics(config)
|
|
namespace_counts = collect_memory_distribution_namespace_counts(config)
|
|
available = safe_int((host_snapshot.get("memory") or {}).get("availableBytes"))
|
|
target = safe_int(config.get("targetMemAvailableBytes"))
|
|
cached = safe_int(meminfo.get("Cached")) + safe_int(meminfo.get("Buffers"))
|
|
slab_reclaimable = safe_int(meminfo.get("SReclaimable"))
|
|
payload = {
|
|
"ok": cgroups.get("scan", {}).get("truncated") is not True,
|
|
"action": "gc remote memory-distribution",
|
|
"providerId": PROVIDER_ID,
|
|
"dryRun": True,
|
|
"mutation": False,
|
|
"observedAt": observed_at,
|
|
"durationMs": int((time.time() - started) * 1000),
|
|
"summary": {
|
|
"metric": "MemAvailable",
|
|
"source": "/proc/meminfo",
|
|
"swapExcludedFromAvailability": True,
|
|
"availableBytes": available,
|
|
"targetAvailableBytes": target,
|
|
"targetGapBytes": max(0, target - available + 1),
|
|
"anonPagesBytes": meminfo.get("AnonPages"),
|
|
"cachedAndBuffersBytes": cached,
|
|
"slabBytes": meminfo.get("Slab"),
|
|
"reclaimableFileSlabUpperBoundBytes": cached + slab_reclaimable,
|
|
"swapFreeBytes": safe_int((host_snapshot.get("swap") or {}).get("freeBytes")),
|
|
"memoryPsiFullAvg60": (parse_memory_distribution_pressure("/proc/pressure/memory").get("full") or {}).get("avg60"),
|
|
},
|
|
"host": {
|
|
"snapshot": host_snapshot,
|
|
"pages": {key: meminfo.get(key) for key in [
|
|
"MemFree", "Buffers", "Cached", "Active", "Inactive", "Active(anon)", "Inactive(anon)",
|
|
"Active(file)", "Inactive(file)", "AnonPages", "Mapped", "Shmem", "Slab", "SReclaimable",
|
|
"SUnreclaim", "KernelStack", "PageTables", "SwapCached",
|
|
]},
|
|
"pressure": {resource: parse_memory_distribution_pressure("/proc/pressure/%s" % resource) for resource in ["memory", "io", "cpu"]},
|
|
},
|
|
"cgroups": cgroups,
|
|
"processes": processes,
|
|
"kubernetes": {**metrics, "namespaceCounts": namespace_counts},
|
|
"collection": {
|
|
"configSource": "config/unidesk-cli.yaml#gc.remote.targets.%s.memoryPressure.attribution" % PROVIDER_ID,
|
|
"runtimeMaps": runtime_commands,
|
|
"podRuntimeMapCount": len(pod_names),
|
|
"dockerRuntimeMapCount": len(docker_names),
|
|
"full": bool(OPTIONS.get("full")),
|
|
},
|
|
"policy": {
|
|
"analysisOnly": True,
|
|
"next": "Use this distribution before designing or running memory-pressure GC candidates.",
|
|
},
|
|
}
|
|
if bool(OPTIONS.get("full")):
|
|
return payload
|
|
payload["host"] = {
|
|
"pressure": {"memory": compact_memory_distribution_pressure(parse_memory_distribution_pressure("/proc/pressure/memory"))},
|
|
}
|
|
payload["cgroups"] = {
|
|
"scan": cgroups.get("scan"),
|
|
"podCgroupCount": cgroups.get("podCgroupCount"),
|
|
"topLevel": [compact_memory_distribution_cgroup(item) for item in cgroups.get("topLevel") or []],
|
|
"systemServices": [compact_memory_distribution_cgroup(item) for item in cgroups.get("systemServices") or []],
|
|
"pods": [compact_memory_distribution_cgroup(item) for item in cgroups.get("pods") or []],
|
|
}
|
|
payload["processes"] = {
|
|
"ok": processes.get("ok"),
|
|
"processCount": processes.get("processCount"),
|
|
"rssTotalBytes": processes.get("rssTotalBytes"),
|
|
"swapTotalBytes": processes.get("swapTotalBytes"),
|
|
"top": [compact_memory_distribution_process(item) for item in processes.get("top") or []],
|
|
"topSwap": [compact_memory_distribution_process(item) for item in processes.get("topSwap") or []],
|
|
"zombies": compact_memory_distribution_zombies(processes.get("zombies") or {}),
|
|
}
|
|
payload["kubernetes"] = {
|
|
"objects": metrics.get("objects"),
|
|
"namespaceCounts": namespace_counts,
|
|
}
|
|
if (metrics.get("command", {}).get("ok") is not True):
|
|
payload["kubernetes"]["commandError"] = metrics.get("command")
|
|
payload["collection"] = {
|
|
"configSource": payload.get("collection", {}).get("configSource"),
|
|
"podRuntimeMapCount": len(pod_names),
|
|
"dockerRuntimeMapCount": len(docker_names),
|
|
"runtimeMapErrors": {
|
|
key: value for key, value in runtime_commands.items()
|
|
if value.get("ok") is not True
|
|
},
|
|
"full": False,
|
|
}
|
|
return payload
|