From b3fa6c9d765aa6b33f0f2b988fc6f4a1bae2092e Mon Sep 17 00:00:00 2001 From: pikastech Date: Tue, 21 Jul 2026 17:11:48 +0200 Subject: [PATCH 1/2] =?UTF-8?q?fix:=20=E4=B8=BA=20GC=20=E5=A2=9E=E5=8A=A0?= =?UTF-8?q?=20systemd=20cgroup=20=E7=B2=BE=E7=A1=AE=E5=9B=9E=E6=94=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config/unidesk-cli.yaml | 11 ++- .../specs/PJ2026-010603-yaml-first-ops.md | 6 +- scripts/src/gc-remote-memory-pressure.test.ts | 36 ++++++-- scripts/src/gc-remote-web-observe.py | 85 ++++++++++++++++--- scripts/src/gc-remote.ts | 50 ++++++++++- 5 files changed, 161 insertions(+), 27 deletions(-) diff --git a/config/unidesk-cli.yaml b/config/unidesk-cli.yaml index af1015b1..57625632 100644 --- a/config/unidesk-cli.yaml +++ b/config/unidesk-cli.yaml @@ -250,7 +250,7 @@ gc: processSwapLimit: 4 commandTimeoutSeconds: 20 kubeconfigPath: /etc/rancher/k3s/k3s.yaml - targetMemAvailableBytes: 5368709120 + targetMemAvailableBytes: 3221225472 objectCountResources: - pods - pipelineruns.tekton.dev @@ -288,9 +288,16 @@ gc: - /headless_shell cgroupReclaim: enabled: true - targetMemAvailableBytes: 5368709120 + targetMemAvailableBytes: 3221225472 minimumSwapFreeBytesAfterReclaim: 34359738368 targets: + - id: k3s-service-inactive-anon + systemdUnitSelector: + unit: k3s.service + reclaimBytes: 805306368 + swappiness: 200 + minimumInactiveAnonBytes: 805306368 + priority: 1 - id: kafka-broker-inactive-anon workloadSelector: namespace: platform-infra diff --git a/project-management/PJ2026-01/specs/PJ2026-010603-yaml-first-ops.md b/project-management/PJ2026-01/specs/PJ2026-010603-yaml-first-ops.md index 281147bb..ea739657 100644 --- a/project-management/PJ2026-01/specs/PJ2026-010603-yaml-first-ops.md +++ b/project-management/PJ2026-01/specs/PJ2026-010603-yaml-first-ops.md @@ -249,8 +249,8 @@ YAML运维状态输出应区分三类依赖:必须保留的 host bootstrap 面 | --- | --- | --- | --- | | OPS-YAML-REQ-012 | 精确工作负载回收 | PJ2026-01060315 精确工作负载回收 | [平台运维](PJ2026-0106-platform-ops.md)、[NC01 CI资源治理](PJ2026-01060107-nc01-ci-resource-governance.md) | -平台 GC 应允许 owning YAML 使用稳定工作负载选择器声明精确 cgroup 回收目标。选择器至少包含 namespace、非空 labels 和最大匹配 Pod 数;目标可用内存、单 Pod 回收上限、swappiness、inactive anonymous memory 下限和 swap 保留线继续由 YAML 声明。YAML 不得保存 Pod UID、容器 ID、当前 cgroup 路径或从运行面反解出的可调数值。 +平台 GC 应允许 owning YAML 使用稳定工作负载选择器声明精确 cgroup 回收目标。Kubernetes workload selector 至少包含 namespace、非空 labels 和最大匹配 Pod 数;host service 使用仅包含稳定 unit 名的 `systemdUnitSelector`。目标可用内存、单目标回收上限、swappiness、inactive anonymous memory 下限、swap 保留线和优先级继续由 YAML 声明。YAML 不得保存 Pod UID、容器 ID、MainPID、进程 startTicks、当前 cgroup 路径、cgroup inode 或从运行面反解出的可调数值。 -受控 plan 只选择 Running Pod,并披露 selector、解析出的 namespace、Pod、UID、cgroup、当前内存、inactive anonymous memory 和保护原因。零匹配、超过最大匹配数、API 不可读、cgroup 缺失、symlink、内存下限不足或 swap 保留线不足时必须保护,禁止回退到父 cgroup、按名称猜测或扩大 selector。 +受控 plan 对 Kubernetes selector 只选择 Running Pod,并披露 selector、解析出的 namespace、Pod、UID、cgroup、当前内存、inactive anonymous memory 和保护原因。对 `systemdUnitSelector` 必须通过 `systemctl show` 读取 `LoadState`、`ActiveState`、`SubState`、`ControlGroup` 和 `MainPID`,只接受 loaded、active、running 的 service;ControlGroup 必须唯一、绝对、位于 `/system.slice`,映射到 `/sys/fs/cgroup/system.slice` 下的真实非 symlink 目录,且 MainPID 当前 cgroup 必须与 ControlGroup 一致。plan 同时记录 MainPID 的 `/proc//stat` startTicks,禁止从 unit 名猜测路径或回退到父 cgroup。 -执行前必须使用同一 YAML selector 重新解析当前 Running Pod,并逐项核对 Pod UID、cgroup 路径和全部回收参数。身份变化、目标已满足或候选不再合格时跳过且结构化披露;只有身份闭合的当前 cgroup 才能写入有界 `memory.reclaim`。执行不得停止、重启、驱逐或迁移 workload,也不得把内核请求上限宣称为实际收益;终态继续分别披露 cgroup 下降、宿主 `MemAvailable` 变化、目标缺口和 swap 排除状态。 +执行前必须使用同一 YAML selector 重新解析当前目标。Kubernetes workload 逐项核对 Pod UID 与 cgroup 路径;systemd service 逐项核对 unit、`LoadState`、`ActiveState`、`SubState`、`ControlGroup`、MainPID 与 startTicks;两类目标都必须重新核对全部回收参数。身份变化、目标已满足或候选不再合格时跳过且结构化披露;只有身份闭合的当前 cgroup 才能写入有界 `memory.reclaim`。执行不得停止、重启、驱逐或迁移 workload,也不得把内核请求上限宣称为实际收益;终态继续分别披露执行前后 cgroup current、宿主 `MemAvailable` 变化、真实收益、目标缺口和 swap 排除状态。 diff --git a/scripts/src/gc-remote-memory-pressure.test.ts b/scripts/src/gc-remote-memory-pressure.test.ts index 0a0c41ea..538a3e72 100644 --- a/scripts/src/gc-remote-memory-pressure.test.ts +++ b/scripts/src/gc-remote-memory-pressure.test.ts @@ -123,11 +123,14 @@ test("memory-pressure async runner uses a parent-child barrier and locked worker assert.match(runnerSource, /worker-identity-unavailable|worker-exited-without-terminal-state/u); }); -test("workload-selected cgroup reclaim re-resolves the Pod UID and path before execution", () => { +test("cgroup reclaim re-resolves workload and systemd identities before execution", () => { assert.match(webObserveSource, /_CGROUP_RECLAIM_RUNTIME_INVENTORY = None/u); assert.match(webObserveSource, /cgroup reclaim candidate identity changed before execution/u); assert.match(webObserveSource, /item\.get\("path"\) == path/u); assert.match(webObserveSource, /item\.get\("pod"\).*get\("uid"\).*resolved_uid/u); + assert.match(webObserveSource, /item\.get\("systemdUnit"\) == resolved_systemd_unit/u); + assert.match(webObserveSource, /systemd-main-pid-cgroup-mismatch/u); + assert.match(webObserveSource, /systemd-main-pid-start-ticks-unavailable/u); }); test("memory-pressure plan includes only YAML allowlisted cgroup reclaim stages with swap reserve", () => { @@ -143,10 +146,11 @@ MEMORY_CONFIG = { "enabled": True, "targetMemAvailableBytes": 4294967296, "minimumSwapFreeBytesAfterReclaim": 402653184, "targets": [ - {"id": "file-pages", "path": "/sys/fs/cgroup/system.slice/k3s.service", "reclaimBytes": 402653184, "swappiness": 0, "priority": 10}, - {"id": "inactive-anon", "path": "/sys/fs/cgroup/system.slice/k3s.service", "reclaimBytes": 1342177280, "swappiness": 100, "minimumInactiveAnonBytes": 1342177280, "priority": 20}, + {"id": "file-pages", "systemdUnitSelector": {"unit": "k3s.service"}, "reclaimBytes": 402653184, "swappiness": 0, "priority": 10}, + {"id": "inactive-anon", "systemdUnitSelector": {"unit": "k3s.service"}, "reclaimBytes": 1342177280, "swappiness": 100, "minimumInactiveAnonBytes": 1342177280, "priority": 20}, ], }, + "attribution": {"commandTimeoutSeconds": 20}, } read_proc_process_table = lambda: {1: {"pid": 1, "ppid": 0, "sid": 1, "startTicks": 1, "ageSeconds": 1, "rssBytes": 1, "commandLine": "init", "commandPreview": "init"}} observer_run_records = lambda: [] @@ -156,13 +160,25 @@ os.path.isdir = lambda path: True os.path.islink = lambda path: False os.path.exists = lambda path: True os.access = lambda path, mode: True +command = lambda argv, timeout: {"exitCode": 0, "stdout": "LoadState=loaded\nActiveState=active\nSubState=running\nControlGroup=/system.slice/k3s.service\nMainPID=321\n", "stderr": ""} +cgroup_path_for_pid = lambda pid: "/system.slice/k3s.service" +process_start_ticks = lambda pid: 987654 read_cgroup_integer = lambda path, name: 6 * 1024 * 1024 * 1024 read_cgroup_memory_stat = lambda path: {"inactive_anon": 2 * 1024 * 1024 * 1024} candidates = collect_memory_reclaim_candidates() -print(json.dumps({"ids": [item["id"] for item in candidates], "swappiness": [item["swappiness"] for item in candidates], "diagnostics": memory_reclaim_scan_diagnostics()})) +process_start_ticks = lambda pid: 987655 +try: + execute_cgroup_reclaim_candidate(candidates[0]) + revalidation_error = None +except RuntimeError as error: + revalidation_error = str(error) +print(json.dumps({"ids": [item["id"] for item in candidates], "swappiness": [item["swappiness"] for item in candidates], "systemd": [item["resolvedSystemdUnit"] for item in candidates], "revalidationError": revalidation_error, "diagnostics": memory_reclaim_scan_diagnostics()})) `); assert.deepEqual(result.ids, ["cgroup-memory-reclaim:file-pages", "cgroup-memory-reclaim:inactive-anon"]); assert.deepEqual(result.swappiness, [0, 100]); + assert.deepEqual((result.systemd as Array>).map((item) => item.unit), ["k3s.service", "k3s.service"]); + assert.deepEqual((result.systemd as Array>).map((item) => item.startTicks), [987654, 987654]); + assert.equal(result.revalidationError, "cgroup reclaim candidate identity changed before execution"); assert.equal(((result.diagnostics as Record).cgroupReclaim as Record).eligibleCount, 2); assert.match(webObserveSource, /minimum SwapFree reserve/u); assert.match(webObserveSource, /memory\.reclaim/u); @@ -199,6 +215,16 @@ test("default memory-pressure plan projects an actionable YAML-bounded preview b risk: "medium", reason: "host-memavailable-below-yaml-target", path: "/sys/fs/cgroup/system.slice/k3s.service", + systemdUnitSelector: { unit: "k3s.service" }, + resolvedSystemdUnit: { + unit: "k3s.service", + loadState: "loaded", + activeState: "active", + subState: "running", + controlGroup: "/system.slice/k3s.service", + mainPid: 321, + startTicks: 987654, + }, reclaimBytes: 256 * 1024 * 1024, swappiness: 0, targetMemAvailableBytes: 4 * 1024 * 1024 * 1024, @@ -207,7 +233,7 @@ test("default memory-pressure plan projects an actionable YAML-bounded preview b memorySwapCurrentBytes: 0, expectedMemoryRssBytes: 256 * 1024 * 1024, expectedMemoryRss: "256 MiB", - action: { op: "cgroup-v2-memory-reclaim", allowlist: "yaml-exact-cgroup-path" }, + action: { op: "cgroup-v2-memory-reclaim", allowlist: "yaml-stable-cgroup-selector" }, }); const candidates = [cgroupCandidate(1), cgroupCandidate(2), ...Array.from({ length: 4 }, (_, index) => processCandidate(index))]; const payload = { diff --git a/scripts/src/gc-remote-web-observe.py b/scripts/src/gc-remote-web-observe.py index 8f0c1ce3..72695061 100644 --- a/scripts/src/gc-remote-web-observe.py +++ b/scripts/src/gc-remote-web-observe.py @@ -360,18 +360,26 @@ def configured_cgroup_reclaim_targets(): target_id = item.get("id") path = item.get("path") workload_selector = item.get("workloadSelector") + systemd_unit_selector = item.get("systemdUnitSelector") reclaim_bytes = item.get("reclaimBytes") swappiness = item.get("swappiness") minimum_inactive_anon = item.get("minimumInactiveAnonBytes", 0) if not isinstance(target_id, str) or not re.match(r"^[A-Za-z0-9._-]{1,80}$", target_id) or target_id in seen_ids: _MEMORY_RECLAIM_CONFIG_ERROR = "yaml-cgroup-reclaim-target-id-invalid:%s" % index return [] - if (path is None) == (workload_selector is None): + if path is not None: + _MEMORY_RECLAIM_CONFIG_ERROR = "yaml-cgroup-reclaim-path-selector-unsupported:%s" % target_id + return [] + if (workload_selector is None) == (systemd_unit_selector is None): _MEMORY_RECLAIM_CONFIG_ERROR = "yaml-cgroup-reclaim-target-identity-invalid:%s" % target_id return [] - if path is not None: - if not isinstance(path, str) or (path != "/sys/fs/cgroup" and not path.startswith("/sys/fs/cgroup/")) or os.path.normpath(path) != path: - _MEMORY_RECLAIM_CONFIG_ERROR = "yaml-cgroup-reclaim-target-path-invalid:%s" % target_id + if systemd_unit_selector is not None: + if not isinstance(systemd_unit_selector, dict) or set(systemd_unit_selector.keys()) != set(["unit"]): + _MEMORY_RECLAIM_CONFIG_ERROR = "yaml-cgroup-reclaim-systemd-selector-invalid:%s" % target_id + return [] + unit = systemd_unit_selector.get("unit") + if not isinstance(unit, str) or not re.match(r"^[A-Za-z0-9_.@-]{1,120}\.service$", unit): + _MEMORY_RECLAIM_CONFIG_ERROR = "yaml-cgroup-reclaim-systemd-unit-invalid:%s" % target_id return [] if workload_selector is not None: if not isinstance(workload_selector, dict): @@ -412,8 +420,8 @@ def configured_cgroup_reclaim_targets(): seen_ids.add(target_id) validated.append({ "id": target_id, - "path": path, "workloadSelector": workload_selector, + "systemdUnitSelector": systemd_unit_selector, "reclaimBytes": reclaim_bytes, "swappiness": swappiness, "minimumInactiveAnonBytes": minimum_inactive_anon, @@ -479,9 +487,48 @@ def cgroup_reclaim_runtime_inventory(): return _CGROUP_RECLAIM_RUNTIME_INVENTORY def resolve_cgroup_reclaim_target(item): - path = item.get("path") - if isinstance(path, str): - return {"ok": True, "matches": [{"path": path, "pod": None}]} + systemd_selector = item.get("systemdUnitSelector") + if isinstance(systemd_selector, dict): + unit = systemd_selector.get("unit") + timeout = safe_int((MEMORY_CONFIG.get("attribution") or {}).get("commandTimeoutSeconds")) + result = command([ + "systemctl", "show", unit, "--no-pager", + "--property=LoadState", "--property=ActiveState", "--property=SubState", + "--property=ControlGroup", "--property=MainPID", + ], timeout) + if result.get("exitCode") != 0: + return {"ok": False, "reason": "systemd-unit-show-failed", "command": bounded(result), "matches": []} + fields = {} + for line in str(result.get("stdout") or "").splitlines(): + key, separator, value = line.partition("=") + if separator: + fields[key] = value + if fields.get("LoadState") != "loaded" or fields.get("ActiveState") != "active" or fields.get("SubState") != "running": + return {"ok": False, "reason": "systemd-unit-not-active-running", "systemdState": fields, "matches": []} + control_group = fields.get("ControlGroup") + main_pid = safe_int(fields.get("MainPID"), None) + if not isinstance(control_group, str) or not control_group.startswith("/system.slice/") or os.path.normpath(control_group) != control_group: + return {"ok": False, "reason": "systemd-control-group-invalid", "matches": []} + if main_pid is None or main_pid <= 0: + return {"ok": False, "reason": "systemd-main-pid-invalid", "matches": []} + if cgroup_path_for_pid(main_pid) != control_group: + return {"ok": False, "reason": "systemd-main-pid-cgroup-mismatch", "matches": []} + start_ticks = process_start_ticks(main_pid) + if start_ticks is None or safe_int(start_ticks) <= 0: + return {"ok": False, "reason": "systemd-main-pid-start-ticks-unavailable", "matches": []} + path = os.path.join("/sys/fs/cgroup", control_group.lstrip("/")) + if os.path.realpath(path) != path or not os.path.isdir(path) or os.path.islink(path): + return {"ok": False, "reason": "systemd-cgroup-path-unavailable-or-redirected", "matches": []} + identity = { + "unit": unit, + "loadState": fields.get("LoadState"), + "activeState": fields.get("ActiveState"), + "subState": fields.get("SubState"), + "controlGroup": control_group, + "mainPid": main_pid, + "startTicks": safe_int(start_ticks), + } + return {"ok": True, "matches": [{"path": path, "pod": None, "systemdUnit": identity}]} selector = item.get("workloadSelector") or {} labels = selector.get("matchLabels") or {} label_selector = ",".join("%s=%s" % pair for pair in sorted(labels.items())) @@ -506,6 +553,7 @@ def resolve_cgroup_reclaim_target(item): matches.append({ "path": resolved_path, "pod": {"namespace": selector.get("namespace"), "name": name, "uid": uid, "labelSelector": label_selector}, + "systemdUnit": None, }) return {"ok": True, "matches": matches} @@ -529,6 +577,7 @@ def collect_cgroup_reclaim_candidates(): diagnostics["protected"].append({ "id": item.get("id"), "workloadSelector": item.get("workloadSelector"), + "systemdUnitSelector": item.get("systemdUnitSelector"), "reason": resolution.get("reason"), "matchedPodCount": resolution.get("matchedPodCount"), }) @@ -536,6 +585,7 @@ def collect_cgroup_reclaim_candidates(): for resolved in resolution.get("matches") or []: path = resolved.get("path") pod = resolved.get("pod") + systemd_unit = resolved.get("systemdUnit") reclaim_path = os.path.join(path, "memory.reclaim") protected_reason = None if os.path.realpath(path) != path or not os.path.isdir(path) or os.path.islink(path): @@ -556,7 +606,9 @@ def collect_cgroup_reclaim_candidates(): "id": item.get("id"), "path": path, "workloadSelector": item.get("workloadSelector"), + "systemdUnitSelector": item.get("systemdUnitSelector"), "resolvedPod": pod, + "resolvedSystemdUnit": systemd_unit, "reason": protected_reason, "memoryCurrentBytes": current, "inactiveAnonBytes": inactive_anon, @@ -572,11 +624,13 @@ def collect_cgroup_reclaim_candidates(): "configId": item.get("id"), "kind": "cgroup-memory-reclaim", "risk": "medium", - "description": "Request bounded cgroup v2 memory reclaim from a YAML-resolved workload", + "description": "Request bounded cgroup v2 memory reclaim from a YAML-resolved target", "reason": "host-memavailable-below-yaml-target", "path": path, "workloadSelector": item.get("workloadSelector"), + "systemdUnitSelector": item.get("systemdUnitSelector"), "resolvedPod": pod, + "resolvedSystemdUnit": systemd_unit, "priority": safe_int(item.get("priority")), "reclaimBytes": reclaim_bytes, "swappiness": safe_int(item.get("swappiness")), @@ -593,7 +647,7 @@ def collect_cgroup_reclaim_candidates(): "expectedMemoryRss": fmt_bytes(reclaim_bytes), "estimatedDiskBytes": 0, "estimatedReclaimBytes": 0, - "action": {"op": "cgroup-v2-memory-reclaim", "allowlist": "yaml-workload-selector-or-exact-path"}, + "action": {"op": "cgroup-v2-memory-reclaim", "allowlist": "yaml-stable-cgroup-selector"}, }) diagnostics["eligibleCount"] = len(candidates) return candidates @@ -815,16 +869,21 @@ def execute_cgroup_reclaim_candidate(candidate): configured = next((item for item in configured_cgroup_reclaim_targets() if item.get("id") == config_id), None) if configured is None: raise RuntimeError("cgroup reclaim candidate is no longer present in YAML allowlist") - for key in ["workloadSelector", "reclaimBytes", "swappiness", "minimumInactiveAnonBytes", "targetMemAvailableBytes", "minimumSwapFreeBytesAfterReclaim"]: + for key in ["workloadSelector", "systemdUnitSelector", "reclaimBytes", "swappiness", "minimumInactiveAnonBytes", "targetMemAvailableBytes", "minimumSwapFreeBytesAfterReclaim"]: if configured.get(key) != candidate.get(key): raise RuntimeError("cgroup reclaim candidate no longer matches YAML: %s" % key) _CGROUP_RECLAIM_RUNTIME_INVENTORY = None resolution = resolve_cgroup_reclaim_target(configured) if resolution.get("ok") is not True: - raise RuntimeError("cgroup reclaim workload selector no longer resolves: %s" % resolution.get("reason")) + raise RuntimeError("cgroup reclaim stable selector no longer resolves: %s" % resolution.get("reason")) path = candidate.get("path") resolved_uid = ((candidate.get("resolvedPod") or {}).get("uid")) - current_match = next((item for item in resolution.get("matches") or [] if item.get("path") == path and ((item.get("pod") or {}).get("uid")) == resolved_uid), None) + resolved_systemd_unit = candidate.get("resolvedSystemdUnit") + current_match = next((item for item in resolution.get("matches") or [] + if item.get("path") == path + and ((item.get("pod") or {}).get("uid")) == resolved_uid + and item.get("systemdUnit") == resolved_systemd_unit + ), None) if current_match is None: raise RuntimeError("cgroup reclaim candidate identity changed before execution") reclaim_path = os.path.join(path, "memory.reclaim") diff --git a/scripts/src/gc-remote.ts b/scripts/src/gc-remote.ts index a22b775b..c6e7d368 100644 --- a/scripts/src/gc-remote.ts +++ b/scripts/src/gc-remote.ts @@ -428,6 +428,31 @@ function compactResolvedPod(value: unknown): Record | undefined return { name: `${pod.namespace}/${pod.name}`, uid: pod.uid }; } +function compactSystemdUnitSelector(value: unknown): string | undefined { + const selector = recordOrEmpty(value); + return typeof selector.unit === "string" ? selector.unit : undefined; +} + +function compactResolvedSystemdUnit(value: unknown): Record | undefined { + const unit = recordOrEmpty(value); + if (typeof unit.unit !== "string" + || typeof unit.loadState !== "string" + || typeof unit.activeState !== "string" + || typeof unit.subState !== "string" + || typeof unit.controlGroup !== "string" + || positiveInteger(unit.mainPid) === null + || positiveInteger(unit.startTicks) === null) return undefined; + return { + unit: unit.unit, + loadState: unit.loadState, + activeState: unit.activeState, + subState: unit.subState, + controlGroup: unit.controlGroup, + mainPid: unit.mainPid, + startTicks: unit.startTicks, + }; +} + function compactMemoryPressureCandidate(value: unknown): Record { const candidate = recordOrEmpty(value); const action = recordOrEmpty(candidate.action); @@ -449,6 +474,8 @@ function compactMemoryPressureCandidate(value: unknown): Record path: candidate.path, workloadSelector: compactWorkloadSelector(candidate.workloadSelector), resolvedPod: compactResolvedPod(candidate.resolvedPod), + systemdUnitSelector: compactSystemdUnitSelector(candidate.systemdUnitSelector), + resolvedSystemdUnit: compactResolvedSystemdUnit(candidate.resolvedSystemdUnit), requestedMemoryReclaimBytes: candidate.requestedMemoryReclaimBytes, requestedMemoryReclaim: candidate.requestedMemoryReclaim, estimateKind: candidate.estimateKind, @@ -493,16 +520,29 @@ function memoryPressureCandidateAllowed(value: unknown): boolean { const swappiness = typeof candidate.swappiness === "number" && Number.isInteger(candidate.swappiness) ? candidate.swappiness : null; + const workloadSelector = recordOrEmpty(candidate.workloadSelector); + const resolvedPod = recordOrEmpty(candidate.resolvedPod); + const systemdUnitSelector = recordOrEmpty(candidate.systemdUnitSelector); + const resolvedSystemdUnit = recordOrEmpty(candidate.resolvedSystemdUnit); + const workloadIdentityClosed = typeof workloadSelector.namespace === "string" && typeof resolvedPod.uid === "string"; + const systemdIdentityClosed = typeof systemdUnitSelector.unit === "string" + && resolvedSystemdUnit.unit === systemdUnitSelector.unit + && resolvedSystemdUnit.loadState === "loaded" + && resolvedSystemdUnit.activeState === "active" + && resolvedSystemdUnit.subState === "running" + && typeof resolvedSystemdUnit.controlGroup === "string" + && resolvedSystemdUnit.controlGroup.startsWith("/system.slice/") + && candidate.path === `/sys/fs/cgroup${resolvedSystemdUnit.controlGroup}` + && positiveInteger(resolvedSystemdUnit.mainPid) !== null + && positiveInteger(resolvedSystemdUnit.startTicks) !== null; return typeof candidate.configId === "string" && candidate.configId.length > 0 && typeof candidate.path === "string" && candidate.path.startsWith("/") && positiveInteger(candidate.reclaimBytes) !== null && swappiness !== null && swappiness >= 0 && swappiness <= 200 && (swappiness === 0 || positiveInteger(candidate.minimumInactiveAnonBytes) !== null) && action.op === "cgroup-v2-memory-reclaim" - && (action.allowlist === "yaml-exact-cgroup-path" - || (action.allowlist === "yaml-workload-selector-or-exact-path" - && typeof recordOrEmpty(candidate.workloadSelector).namespace === "string" - && typeof recordOrEmpty(candidate.resolvedPod).uid === "string")); + && action.allowlist === "yaml-stable-cgroup-selector" + && workloadIdentityClosed !== systemdIdentityClosed; } if (candidate.kind !== "browser-orphan-process-tree" && candidate.kind !== "stale-web-observer-process-tree") return false; const compact = compactMemoryPressureCandidate(candidate); @@ -558,6 +598,8 @@ function compactMemoryPressureCandidateScan(value: unknown): Record Date: Tue, 21 Jul 2026 17:23:00 +0200 Subject: [PATCH 2/2] =?UTF-8?q?fix:=20=E5=A4=8D=E6=A0=B8=20cgroup=20?= =?UTF-8?q?=E5=9B=9E=E6=94=B6=E4=BC=98=E5=85=88=E7=BA=A7=E6=BC=82=E7=A7=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scripts/src/gc-remote-memory-pressure.test.ts | 18 +++++++++++++++++- scripts/src/gc-remote-web-observe.py | 2 +- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/scripts/src/gc-remote-memory-pressure.test.ts b/scripts/src/gc-remote-memory-pressure.test.ts index 538a3e72..d5251d65 100644 --- a/scripts/src/gc-remote-memory-pressure.test.ts +++ b/scripts/src/gc-remote-memory-pressure.test.ts @@ -166,18 +166,34 @@ process_start_ticks = lambda pid: 987654 read_cgroup_integer = lambda path, name: 6 * 1024 * 1024 * 1024 read_cgroup_memory_stat = lambda path: {"inactive_anon": 2 * 1024 * 1024 * 1024} candidates = collect_memory_reclaim_candidates() +memory_reclaim_write_attempts = [] +original_open = open +def tracked_open(path, *args, **kwargs): + if str(path).endswith("/memory.reclaim"): + memory_reclaim_write_attempts.append(path) + return original_open(path, *args, **kwargs) +open = tracked_open +MEMORY_CONFIG["cgroupReclaim"]["targets"][0]["priority"] = 11 +try: + execute_cgroup_reclaim_candidate(candidates[0]) + priority_revalidation_error = None +except RuntimeError as error: + priority_revalidation_error = str(error) +MEMORY_CONFIG["cgroupReclaim"]["targets"][0]["priority"] = 10 process_start_ticks = lambda pid: 987655 try: execute_cgroup_reclaim_candidate(candidates[0]) revalidation_error = None except RuntimeError as error: revalidation_error = str(error) -print(json.dumps({"ids": [item["id"] for item in candidates], "swappiness": [item["swappiness"] for item in candidates], "systemd": [item["resolvedSystemdUnit"] for item in candidates], "revalidationError": revalidation_error, "diagnostics": memory_reclaim_scan_diagnostics()})) +print(json.dumps({"ids": [item["id"] for item in candidates], "swappiness": [item["swappiness"] for item in candidates], "systemd": [item["resolvedSystemdUnit"] for item in candidates], "priorityRevalidationError": priority_revalidation_error, "memoryReclaimWriteAttempts": memory_reclaim_write_attempts, "revalidationError": revalidation_error, "diagnostics": memory_reclaim_scan_diagnostics()})) `); assert.deepEqual(result.ids, ["cgroup-memory-reclaim:file-pages", "cgroup-memory-reclaim:inactive-anon"]); assert.deepEqual(result.swappiness, [0, 100]); assert.deepEqual((result.systemd as Array>).map((item) => item.unit), ["k3s.service", "k3s.service"]); assert.deepEqual((result.systemd as Array>).map((item) => item.startTicks), [987654, 987654]); + assert.equal(result.priorityRevalidationError, "cgroup reclaim candidate no longer matches YAML: priority"); + assert.deepEqual(result.memoryReclaimWriteAttempts, []); assert.equal(result.revalidationError, "cgroup reclaim candidate identity changed before execution"); assert.equal(((result.diagnostics as Record).cgroupReclaim as Record).eligibleCount, 2); assert.match(webObserveSource, /minimum SwapFree reserve/u); diff --git a/scripts/src/gc-remote-web-observe.py b/scripts/src/gc-remote-web-observe.py index 72695061..aa8d926a 100644 --- a/scripts/src/gc-remote-web-observe.py +++ b/scripts/src/gc-remote-web-observe.py @@ -869,7 +869,7 @@ def execute_cgroup_reclaim_candidate(candidate): configured = next((item for item in configured_cgroup_reclaim_targets() if item.get("id") == config_id), None) if configured is None: raise RuntimeError("cgroup reclaim candidate is no longer present in YAML allowlist") - for key in ["workloadSelector", "systemdUnitSelector", "reclaimBytes", "swappiness", "minimumInactiveAnonBytes", "targetMemAvailableBytes", "minimumSwapFreeBytesAfterReclaim"]: + for key in ["workloadSelector", "systemdUnitSelector", "reclaimBytes", "swappiness", "minimumInactiveAnonBytes", "targetMemAvailableBytes", "minimumSwapFreeBytesAfterReclaim", "priority"]: if configured.get(key) != candidate.get(key): raise RuntimeError("cgroup reclaim candidate no longer matches YAML: %s" % key) _CGROUP_RECLAIM_RUNTIME_INVENTORY = None