From 6c123a1d96cf30f1b72ee54ef3188531ffed58e8 Mon Sep 17 00:00:00 2001 From: pikastech Date: Tue, 21 Jul 2026 09:13:07 +0200 Subject: [PATCH 1/2] =?UTF-8?q?fix:=20=E6=94=B6=E6=95=9B=20NC01=20?= =?UTF-8?q?=E7=89=A9=E7=90=86=E5=86=85=E5=AD=98=E5=9B=9E=E6=94=B6=E8=BE=B9?= =?UTF-8?q?=E7=95=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config/unidesk-cli.yaml | 20 ++++++++----------- docs/reference/gc.md | 9 ++++++--- scripts/src/gc-remote-memory-pressure.test.ts | 2 +- scripts/src/gc-remote-web-observe.py | 20 +++++++++++++++++-- scripts/src/gc-remote.ts | 6 ++++++ 5 files changed, 39 insertions(+), 18 deletions(-) diff --git a/config/unidesk-cli.yaml b/config/unidesk-cli.yaml index 931c9af9..fa0a2d46 100644 --- a/config/unidesk-cli.yaml +++ b/config/unidesk-cli.yaml @@ -224,7 +224,7 @@ gc: processSwapLimit: 4 commandTimeoutSeconds: 20 kubeconfigPath: /etc/rancher/k3s/k3s.yaml - targetMemAvailableBytes: 4294967296 + targetMemAvailableBytes: 5368709120 objectCountResources: - pods - pipelineruns.tekton.dev @@ -262,19 +262,15 @@ gc: - /headless_shell cgroupReclaim: enabled: true - targetMemAvailableBytes: 4294967296 - minimumSwapFreeBytesAfterReclaim: 1073741824 + targetMemAvailableBytes: 5368709120 + minimumSwapFreeBytesAfterReclaim: 34359738368 targets: - - id: k3s-file-pages - path: /sys/fs/cgroup/system.slice/k3s.service - reclaimBytes: 402653184 - swappiness: 0 - priority: 10 - - id: kubepods-file-pages + - id: kubepods-inactive-anon path: /sys/fs/cgroup/kubepods.slice - reclaimBytes: 536870912 - swappiness: 0 - priority: 20 + reclaimBytes: 3221225472 + swappiness: 200 + minimumInactiveAnonBytes: 3221225472 + priority: 10 kubernetesObjectRetention: enabled: true aggregatePreviewLimit: 1 diff --git a/docs/reference/gc.md b/docs/reference/gc.md index d5e34624..de24fbda 100644 --- a/docs/reference/gc.md +++ b/docs/reference/gc.md @@ -73,13 +73,16 @@ - 数量、超时、资源和 namespace 范围全部由 `config/unidesk-cli.yaml` 的 `memoryPressure.attribution` 拥有。 -NC01 的内存压力文件页回收规则: +NC01 的内存压力 cgroup 回收规则: - 只从 YAML 精确允许的 k3s cgroup 请求回收; -- 固定使用 `swappiness=0`; - 不把 swap 当作可用物理内存; - 不终止 workload,也不重启 k3s; -- 执行前由 overview 证明目标 cgroup 存在足够的非匿名页; +- `swappiness=0` 的文件页回收必须由 overview 证明目标 cgroup 存在足够的非匿名页; +- `swappiness>0` 的匿名页回收必须同时满足: + - owning YAML 声明目标 cgroup、回收量、`minimumInactiveAnonBytes` 和 swap 保留线; + - plan 与执行前复核 `inactive_anon` 不低于 YAML 下限; + - 最坏情况下的换出量不会突破 YAML 声明的 `minimumSwapFreeBytesAfterReclaim`; - 按 `plan`、`run --confirm`、`status`、`memory` 的顺序复核实际结果。 默认 `/tmp` GC 只包含 allowlisted 诊断目录和已知低风险路径。非 allowlist 的 stale `/tmp` 一级子项必须显式 `--include-stale-tmp` 才能进入候选;扫描按 `--limit` 有界枚举候选,执行时仍只允许删除 `/tmp` 直接子项,并避开 X11/ICE/font socket、systemd private、tmux、ssh、vscode 等系统/session 前缀。该入口不能递归扩大成通用 `/tmp` 清空器,也不能为了估算全量临时目录而长时间阻塞。 diff --git a/scripts/src/gc-remote-memory-pressure.test.ts b/scripts/src/gc-remote-memory-pressure.test.ts index 0cd2ab8e..7de23b7f 100644 --- a/scripts/src/gc-remote-memory-pressure.test.ts +++ b/scripts/src/gc-remote-memory-pressure.test.ts @@ -137,7 +137,7 @@ MEMORY_CONFIG = { "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, "priority": 20}, + {"id": "inactive-anon", "path": "/sys/fs/cgroup/system.slice/k3s.service", "reclaimBytes": 1342177280, "swappiness": 100, "minimumInactiveAnonBytes": 1342177280, "priority": 20}, ], }, } diff --git a/scripts/src/gc-remote-web-observe.py b/scripts/src/gc-remote-web-observe.py index 4493e8e2..d6aaa37e 100644 --- a/scripts/src/gc-remote-web-observe.py +++ b/scripts/src/gc-remote-web-observe.py @@ -361,6 +361,7 @@ def configured_cgroup_reclaim_targets(): path = item.get("path") 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 [] @@ -373,6 +374,12 @@ def configured_cgroup_reclaim_targets(): if isinstance(swappiness, bool) or not isinstance(swappiness, int) or swappiness < 0 or swappiness > 200: _MEMORY_RECLAIM_CONFIG_ERROR = "yaml-cgroup-reclaim-swappiness-invalid:%s" % target_id return [] + if isinstance(minimum_inactive_anon, bool) or not isinstance(minimum_inactive_anon, int) or minimum_inactive_anon < 0: + _MEMORY_RECLAIM_CONFIG_ERROR = "yaml-cgroup-reclaim-minimum-inactive-anon-invalid:%s" % target_id + return [] + if swappiness > 0 and minimum_inactive_anon < reclaim_bytes: + _MEMORY_RECLAIM_CONFIG_ERROR = "yaml-cgroup-reclaim-minimum-inactive-anon-too-small:%s" % target_id + return [] priority = item.get("priority", index + 1) if isinstance(priority, bool) or not isinstance(priority, int) or priority < 1 or priority > 1000: _MEMORY_RECLAIM_CONFIG_ERROR = "yaml-cgroup-reclaim-priority-invalid:%s" % target_id @@ -383,6 +390,7 @@ def configured_cgroup_reclaim_targets(): "path": path, "reclaimBytes": reclaim_bytes, "swappiness": swappiness, + "minimumInactiveAnonBytes": minimum_inactive_anon, "priority": priority, "targetMemAvailableBytes": target_available, "minimumSwapFreeBytesAfterReclaim": swap_reserve, @@ -416,15 +424,19 @@ def collect_cgroup_reclaim_candidates(): elif safe_int(item.get("swappiness")) > 0 and swap_free - safe_int(item.get("reclaimBytes")) < safe_int(item.get("minimumSwapFreeBytesAfterReclaim")): protected_reason = "minimum-swap-free-reserve" current = read_cgroup_integer(path, "memory.current") + memory_stat = read_cgroup_memory_stat(path) + inactive_anon = safe_int(memory_stat.get("inactive_anon")) + if protected_reason is None and safe_int(item.get("swappiness")) > 0 and inactive_anon < safe_int(item.get("minimumInactiveAnonBytes")): + protected_reason = "minimum-inactive-anon-not-met" if protected_reason is not None: diagnostics["protected"].append({ "id": item.get("id"), "path": path, "reason": protected_reason, "memoryCurrentBytes": current, + "inactiveAnonBytes": inactive_anon, }) continue - memory_stat = read_cgroup_memory_stat(path) reclaim_bytes = safe_int(item.get("reclaimBytes")) candidates.append({ "id": "cgroup-memory-reclaim:%s" % item.get("id"), @@ -437,6 +449,7 @@ def collect_cgroup_reclaim_candidates(): "priority": safe_int(item.get("priority")), "reclaimBytes": reclaim_bytes, "swappiness": safe_int(item.get("swappiness")), + "minimumInactiveAnonBytes": safe_int(item.get("minimumInactiveAnonBytes")), "targetMemAvailableBytes": safe_int(item.get("targetMemAvailableBytes")), "minimumSwapFreeBytesAfterReclaim": safe_int(item.get("minimumSwapFreeBytesAfterReclaim")), "memoryCurrentBytes": current, @@ -667,7 +680,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 ["path", "reclaimBytes", "swappiness", "targetMemAvailableBytes", "minimumSwapFreeBytesAfterReclaim"]: + for key in ["path", "reclaimBytes", "swappiness", "minimumInactiveAnonBytes", "targetMemAvailableBytes", "minimumSwapFreeBytesAfterReclaim"]: if configured.get(key) != candidate.get(key): raise RuntimeError("cgroup reclaim candidate no longer matches YAML: %s" % key) path = configured.get("path") @@ -698,6 +711,9 @@ def execute_cgroup_reclaim_candidate(candidate): required = safe_int(configured.get("reclaimBytes")) + safe_int(configured.get("minimumSwapFreeBytesAfterReclaim")) if swap_free < required: raise RuntimeError("cgroup reclaim would violate YAML minimum SwapFree reserve") + inactive_anon = safe_int(read_cgroup_memory_stat(path).get("inactive_anon")) + if inactive_anon < safe_int(configured.get("minimumInactiveAnonBytes")): + raise RuntimeError("minimum inactive anonymous memory is no longer available") cgroup_before = read_cgroup_integer(path, "memory.current") write_error = None try: diff --git a/scripts/src/gc-remote.ts b/scripts/src/gc-remote.ts index 8014ac93..220a6de7 100644 --- a/scripts/src/gc-remote.ts +++ b/scripts/src/gc-remote.ts @@ -411,6 +411,7 @@ function compactMemoryPressureCandidate(value: unknown): Record path: candidate.path, reclaimBytes: candidate.reclaimBytes, swappiness: candidate.swappiness, + minimumInactiveAnonBytes: candidate.minimumInactiveAnonBytes, targetMemAvailableBytes: candidate.targetMemAvailableBytes, minimumSwapFreeBytesAfterReclaim: candidate.minimumSwapFreeBytesAfterReclaim, memoryCurrentBytes: candidate.memoryCurrentBytes, @@ -446,9 +447,14 @@ function memoryPressureCandidateAllowed(value: unknown): boolean { const action = recordOrEmpty(candidate.action); if (typeof candidate.id !== "string" || candidate.id.length === 0 || positiveInteger(candidate.expectedMemoryRssBytes) === null) return false; if (candidate.kind === "cgroup-memory-reclaim") { + const swappiness = typeof candidate.swappiness === "number" && Number.isInteger(candidate.swappiness) + ? candidate.swappiness + : 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"; } From f7aaa0221fabcf8e5243837779008b3d52e743ae Mon Sep 17 00:00:00 2001 From: pikastech Date: Tue, 21 Jul 2026 09:16:33 +0200 Subject: [PATCH 2/2] fix: align NC01 v03 OpenFGA database secret --- config/platform-db/postgres-nc01.yaml | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/config/platform-db/postgres-nc01.yaml b/config/platform-db/postgres-nc01.yaml index d7f9f9bc..e9b5ce74 100644 --- a/config/platform-db/postgres-nc01.yaml +++ b/config/platform-db/postgres-nc01.yaml @@ -688,6 +688,21 @@ exports: - scope: hwlab-v03 secret: hwlab-cloud-api-v03-db key: DATABASE_URL + - name: hwlab-nc01-v03-openfga-datastore-uri + sourceSecretRef: platform-db/hwlab-nc01-v03-db.env + render: + envKey: DATASTORE_URI + format: postgres://$(HWLAB_NC01_V03_DB_USER):$(HWLAB_NC01_V03_DB_PASSWORD)@$(PGHOST):5432/$(HWLAB_NC01_V03_DB_NAME)?sslmode=require + variables: + PGHOST: 10.42.0.1 + writeToSecretSource: + sourceRef: hwlab/nc01-v03-openfga-db.env + key: DATASTORE_URI + mode: update-or-insert + consumers: + - scope: hwlab-v03 + secret: hwlab-v03-openfga + key: DATASTORE_URI - name: hwlab-nc01-production-cloud-api-database-url sourceSecretRef: platform-db/hwlab-nc01-production-db.env render: