fix: 精确回收 NC01 内存并修复 zombie 泄露

This commit is contained in:
pikastech
2026-07-21 12:42:59 +02:00
parent ce6b6f041f
commit 63a33dece3
11 changed files with 347 additions and 68 deletions
+8 -4
View File
@@ -553,10 +553,11 @@ targets:
- COPY --from=golang-toolchain /usr/local/go /usr/local/go - COPY --from=golang-toolchain /usr/local/go /usr/local/go
- COPY --from=docker-cli /usr/bin/kubectl /usr/local/bin/kubectl - COPY --from=docker-cli /usr/bin/kubectl /usr/local/bin/kubectl
- ENV PATH=/usr/local/go/bin:$PATH - ENV PATH=/usr/local/go/bin:$PATH
- RUN apt-get update && apt-get install -y --no-install-recommends tini && rm -rf /var/lib/apt/lists/*
- RUN ln -sf /usr/local/bin/bun /usr/local/bin/bunx - RUN ln -sf /usr/local/bin/bun /usr/local/bin/bunx
- ENV HWLAB_CI_NODE_DEPS=/opt/hwlab-ci-node-deps/node_modules - ENV HWLAB_CI_NODE_DEPS=/opt/hwlab-ci-node-deps/node_modules
- RUN set -eu; export HTTP_PROXY="${HTTP_PROXY:-${http_proxy:-}}"; export HTTPS_PROXY="${HTTPS_PROXY:-${https_proxy:-$HTTP_PROXY}}"; export ALL_PROXY="${ALL_PROXY:-${all_proxy:-}}"; export NO_PROXY="${NO_PROXY:-${no_proxy:-}}"; export http_proxy="$HTTP_PROXY"; export https_proxy="$HTTPS_PROXY"; export all_proxy="$ALL_PROXY"; export no_proxy="$NO_PROXY"; export npm_config_registry="https://registry.npmmirror.com/"; export BUN_CONFIG_REGISTRY="https://registry.npmmirror.com/"; export npm_config_noproxy="$NO_PROXY"; if [ -n "$HTTP_PROXY" ]; then export npm_config_proxy="$HTTP_PROXY"; fi; if [ -n "$HTTPS_PROXY" ]; then export npm_config_https_proxy="$HTTPS_PROXY"; fi; export npm_config_fetch_retries=2; export npm_config_fetch_retry_mintimeout=2000; export npm_config_fetch_retry_maxtimeout=16000; export npm_config_fetch_timeout=120000; proxy_label="${HTTP_PROXY:+HTTP_PROXY}"; proxy_label="${proxy_label:-none}"; mkdir -p /opt/hwlab-ci-node-deps; cd /opt/hwlab-ci-node-deps; printf '{"private":true,"dependencies":{}}\n' > package.json; ok=0; delay=2; for attempt in 1 2 3 4 5; do echo "{\"event\":\"tools-yaml-node-npm-install\",\"attempt\":\"$attempt/5\",\"registry\":\"$npm_config_registry\",\"proxy\":\"$proxy_label\"}" >&2; if timeout 180s npm install --package-lock=false --no-save --ignore-scripts --no-audit --no-fund --omit=dev yaml@2.8.3; then ok=1; break; fi; if [ "$attempt" = 5 ]; then break; fi; echo "{\"event\":\"tools-yaml-node-npm-install\",\"status\":\"retrying\",\"attempt\":\"$attempt/5\",\"sleepSeconds\":$delay}" >&2; sleep "$delay"; delay=$((delay * 2)); done; test "$ok" = 1; node --input-type=module -e 'import("/opt/hwlab-ci-node-deps/node_modules/yaml/browser/dist/index.js").then((yaml)=>console.log("yaml-ok", typeof yaml.parse))' - RUN set -eu; export HTTP_PROXY="${HTTP_PROXY:-${http_proxy:-}}"; export HTTPS_PROXY="${HTTPS_PROXY:-${https_proxy:-$HTTP_PROXY}}"; export ALL_PROXY="${ALL_PROXY:-${all_proxy:-}}"; export NO_PROXY="${NO_PROXY:-${no_proxy:-}}"; export http_proxy="$HTTP_PROXY"; export https_proxy="$HTTPS_PROXY"; export all_proxy="$ALL_PROXY"; export no_proxy="$NO_PROXY"; export npm_config_registry="https://registry.npmmirror.com/"; export BUN_CONFIG_REGISTRY="https://registry.npmmirror.com/"; export npm_config_noproxy="$NO_PROXY"; if [ -n "$HTTP_PROXY" ]; then export npm_config_proxy="$HTTP_PROXY"; fi; if [ -n "$HTTPS_PROXY" ]; then export npm_config_https_proxy="$HTTPS_PROXY"; fi; export npm_config_fetch_retries=2; export npm_config_fetch_retry_mintimeout=2000; export npm_config_fetch_retry_maxtimeout=16000; export npm_config_fetch_timeout=120000; proxy_label="${HTTP_PROXY:+HTTP_PROXY}"; proxy_label="${proxy_label:-none}"; mkdir -p /opt/hwlab-ci-node-deps; cd /opt/hwlab-ci-node-deps; printf '{"private":true,"dependencies":{}}\n' > package.json; ok=0; delay=2; for attempt in 1 2 3 4 5; do echo "{\"event\":\"tools-yaml-node-npm-install\",\"attempt\":\"$attempt/5\",\"registry\":\"$npm_config_registry\",\"proxy\":\"$proxy_label\"}" >&2; if timeout 180s npm install --package-lock=false --no-save --ignore-scripts --no-audit --no-fund --omit=dev yaml@2.8.3; then ok=1; break; fi; if [ "$attempt" = 5 ]; then break; fi; echo "{\"event\":\"tools-yaml-node-npm-install\",\"status\":\"retrying\",\"attempt\":\"$attempt/5\",\"sleepSeconds\":$delay}" >&2; sleep "$delay"; delay=$((delay * 2)); done; test "$ok" = 1; node --input-type=module -e 'import("/opt/hwlab-ci-node-deps/node_modules/yaml/browser/dist/index.js").then((yaml)=>console.log("yaml-ok", typeof yaml.parse))'
- RUN node --version && npm --version && bun --version && git --version && python3 --version && docker --version && kubectl version --client=true && ssh -V && go version - RUN node --version && npm --version && bun --version && git --version && python3 --version && docker --version && kubectl version --client=true && ssh -V && go version && tini --version
buildArgs: {} buildArgs: {}
buildNetwork: host buildNetwork: host
publicBaseImages: publicBaseImages:
@@ -771,7 +772,8 @@ targets:
- RUN ln -sf /usr/local/bin/bun /usr/local/bin/bunx - RUN ln -sf /usr/local/bin/bun /usr/local/bin/bunx
- ENV HWLAB_CI_NODE_DEPS=/opt/hwlab-ci-node-deps/node_modules - ENV HWLAB_CI_NODE_DEPS=/opt/hwlab-ci-node-deps/node_modules
- RUN set -eu; export HTTP_PROXY="${HTTP_PROXY:-${http_proxy:-}}"; export HTTPS_PROXY="${HTTPS_PROXY:-${https_proxy:-$HTTP_PROXY}}"; export ALL_PROXY="${ALL_PROXY:-${all_proxy:-}}"; export NO_PROXY="${NO_PROXY:-${no_proxy:-}}"; export http_proxy="$HTTP_PROXY"; export https_proxy="$HTTPS_PROXY"; export all_proxy="$ALL_PROXY"; export no_proxy="$NO_PROXY"; export npm_config_registry="https://registry.npmmirror.com/"; export BUN_CONFIG_REGISTRY="https://registry.npmmirror.com/"; export npm_config_noproxy="$NO_PROXY"; if [ -n "$HTTP_PROXY" ]; then export npm_config_proxy="$HTTP_PROXY"; fi; if [ -n "$HTTPS_PROXY" ]; then export npm_config_https_proxy="$HTTPS_PROXY"; fi; export npm_config_fetch_retries=2; export npm_config_fetch_retry_mintimeout=2000; export npm_config_fetch_retry_maxtimeout=16000; export npm_config_fetch_timeout=120000; proxy_label="${HTTP_PROXY:+HTTP_PROXY}"; proxy_label="${proxy_label:-none}"; mkdir -p /opt/hwlab-ci-node-deps; cd /opt/hwlab-ci-node-deps; printf '{"private":true,"dependencies":{}}\n' > package.json; ok=0; delay=2; for attempt in 1 2 3 4 5; do echo "{\"event\":\"tools-yaml-node-npm-install\",\"attempt\":\"$attempt/5\",\"registry\":\"$npm_config_registry\",\"proxy\":\"$proxy_label\"}" >&2; if timeout 180s npm install --package-lock=false --no-save --ignore-scripts --no-audit --no-fund --omit=dev yaml@2.8.3; then ok=1; break; fi; if [ "$attempt" = 5 ]; then break; fi; echo "{\"event\":\"tools-yaml-node-npm-install\",\"status\":\"retrying\",\"attempt\":\"$attempt/5\",\"sleepSeconds\":$delay}" >&2; sleep "$delay"; delay=$((delay * 2)); done; test "$ok" = 1; node --input-type=module -e 'import("/opt/hwlab-ci-node-deps/node_modules/yaml/browser/dist/index.js").then((yaml)=>console.log("yaml-ok", typeof yaml.parse))' - RUN set -eu; export HTTP_PROXY="${HTTP_PROXY:-${http_proxy:-}}"; export HTTPS_PROXY="${HTTPS_PROXY:-${https_proxy:-$HTTP_PROXY}}"; export ALL_PROXY="${ALL_PROXY:-${all_proxy:-}}"; export NO_PROXY="${NO_PROXY:-${no_proxy:-}}"; export http_proxy="$HTTP_PROXY"; export https_proxy="$HTTPS_PROXY"; export all_proxy="$ALL_PROXY"; export no_proxy="$NO_PROXY"; export npm_config_registry="https://registry.npmmirror.com/"; export BUN_CONFIG_REGISTRY="https://registry.npmmirror.com/"; export npm_config_noproxy="$NO_PROXY"; if [ -n "$HTTP_PROXY" ]; then export npm_config_proxy="$HTTP_PROXY"; fi; if [ -n "$HTTPS_PROXY" ]; then export npm_config_https_proxy="$HTTPS_PROXY"; fi; export npm_config_fetch_retries=2; export npm_config_fetch_retry_mintimeout=2000; export npm_config_fetch_retry_maxtimeout=16000; export npm_config_fetch_timeout=120000; proxy_label="${HTTP_PROXY:+HTTP_PROXY}"; proxy_label="${proxy_label:-none}"; mkdir -p /opt/hwlab-ci-node-deps; cd /opt/hwlab-ci-node-deps; printf '{"private":true,"dependencies":{}}\n' > package.json; ok=0; delay=2; for attempt in 1 2 3 4 5; do echo "{\"event\":\"tools-yaml-node-npm-install\",\"attempt\":\"$attempt/5\",\"registry\":\"$npm_config_registry\",\"proxy\":\"$proxy_label\"}" >&2; if timeout 180s npm install --package-lock=false --no-save --ignore-scripts --no-audit --no-fund --omit=dev yaml@2.8.3; then ok=1; break; fi; if [ "$attempt" = 5 ]; then break; fi; echo "{\"event\":\"tools-yaml-node-npm-install\",\"status\":\"retrying\",\"attempt\":\"$attempt/5\",\"sleepSeconds\":$delay}" >&2; sleep "$delay"; delay=$((delay * 2)); done; test "$ok" = 1; node --input-type=module -e 'import("/opt/hwlab-ci-node-deps/node_modules/yaml/browser/dist/index.js").then((yaml)=>console.log("yaml-ok", typeof yaml.parse))'
- RUN node --version && npm --version && bun --version && git --version && python3 --version && docker --version && kubectl version --client=true && ssh -V && go version - RUN apt-get update && apt-get install -y --no-install-recommends tini && rm -rf /var/lib/apt/lists/*
- RUN node --version && npm --version && bun --version && git --version && python3 --version && docker --version && kubectl version --client=true && ssh -V && go version && tini --version
buildArgs: {} buildArgs: {}
buildNetwork: host buildNetwork: host
publicBaseImages: publicBaseImages:
@@ -991,7 +993,8 @@ targets:
- RUN ln -sf /usr/local/bin/bun /usr/local/bin/bunx - RUN ln -sf /usr/local/bin/bun /usr/local/bin/bunx
- ENV HWLAB_CI_NODE_DEPS=/opt/hwlab-ci-node-deps/node_modules - ENV HWLAB_CI_NODE_DEPS=/opt/hwlab-ci-node-deps/node_modules
- RUN set -eu; export HTTP_PROXY="${HTTP_PROXY:-${http_proxy:-}}"; export HTTPS_PROXY="${HTTPS_PROXY:-${https_proxy:-$HTTP_PROXY}}"; export ALL_PROXY="${ALL_PROXY:-${all_proxy:-}}"; export NO_PROXY="${NO_PROXY:-${no_proxy:-}}"; export http_proxy="$HTTP_PROXY"; export https_proxy="$HTTPS_PROXY"; export all_proxy="$ALL_PROXY"; export no_proxy="$NO_PROXY"; export npm_config_registry="https://registry.npmmirror.com/"; export BUN_CONFIG_REGISTRY="https://registry.npmmirror.com/"; export npm_config_noproxy="$NO_PROXY"; if [ -n "$HTTP_PROXY" ]; then export npm_config_proxy="$HTTP_PROXY"; fi; if [ -n "$HTTPS_PROXY" ]; then export npm_config_https_proxy="$HTTPS_PROXY"; fi; export npm_config_fetch_retries=2; export npm_config_fetch_retry_mintimeout=2000; export npm_config_fetch_retry_maxtimeout=16000; export npm_config_fetch_timeout=120000; proxy_label="${HTTP_PROXY:+HTTP_PROXY}"; proxy_label="${proxy_label:-none}"; mkdir -p /opt/hwlab-ci-node-deps; cd /opt/hwlab-ci-node-deps; printf '{"private":true,"dependencies":{}}\n' > package.json; ok=0; delay=2; for attempt in 1 2 3 4 5; do echo "{\"event\":\"tools-yaml-node-npm-install\",\"attempt\":\"$attempt/5\",\"registry\":\"$npm_config_registry\",\"proxy\":\"$proxy_label\"}" >&2; if timeout 180s npm install --package-lock=false --no-save --ignore-scripts --no-audit --no-fund --omit=dev yaml@2.8.3; then ok=1; break; fi; if [ "$attempt" = 5 ]; then break; fi; echo "{\"event\":\"tools-yaml-node-npm-install\",\"status\":\"retrying\",\"attempt\":\"$attempt/5\",\"sleepSeconds\":$delay}" >&2; sleep "$delay"; delay=$((delay * 2)); done; test "$ok" = 1; node --input-type=module -e 'import("/opt/hwlab-ci-node-deps/node_modules/yaml/browser/dist/index.js").then((yaml)=>console.log("yaml-ok", typeof yaml.parse))' - RUN set -eu; export HTTP_PROXY="${HTTP_PROXY:-${http_proxy:-}}"; export HTTPS_PROXY="${HTTPS_PROXY:-${https_proxy:-$HTTP_PROXY}}"; export ALL_PROXY="${ALL_PROXY:-${all_proxy:-}}"; export NO_PROXY="${NO_PROXY:-${no_proxy:-}}"; export http_proxy="$HTTP_PROXY"; export https_proxy="$HTTPS_PROXY"; export all_proxy="$ALL_PROXY"; export no_proxy="$NO_PROXY"; export npm_config_registry="https://registry.npmmirror.com/"; export BUN_CONFIG_REGISTRY="https://registry.npmmirror.com/"; export npm_config_noproxy="$NO_PROXY"; if [ -n "$HTTP_PROXY" ]; then export npm_config_proxy="$HTTP_PROXY"; fi; if [ -n "$HTTPS_PROXY" ]; then export npm_config_https_proxy="$HTTPS_PROXY"; fi; export npm_config_fetch_retries=2; export npm_config_fetch_retry_mintimeout=2000; export npm_config_fetch_retry_maxtimeout=16000; export npm_config_fetch_timeout=120000; proxy_label="${HTTP_PROXY:+HTTP_PROXY}"; proxy_label="${proxy_label:-none}"; mkdir -p /opt/hwlab-ci-node-deps; cd /opt/hwlab-ci-node-deps; printf '{"private":true,"dependencies":{}}\n' > package.json; ok=0; delay=2; for attempt in 1 2 3 4 5; do echo "{\"event\":\"tools-yaml-node-npm-install\",\"attempt\":\"$attempt/5\",\"registry\":\"$npm_config_registry\",\"proxy\":\"$proxy_label\"}" >&2; if timeout 180s npm install --package-lock=false --no-save --ignore-scripts --no-audit --no-fund --omit=dev yaml@2.8.3; then ok=1; break; fi; if [ "$attempt" = 5 ]; then break; fi; echo "{\"event\":\"tools-yaml-node-npm-install\",\"status\":\"retrying\",\"attempt\":\"$attempt/5\",\"sleepSeconds\":$delay}" >&2; sleep "$delay"; delay=$((delay * 2)); done; test "$ok" = 1; node --input-type=module -e 'import("/opt/hwlab-ci-node-deps/node_modules/yaml/browser/dist/index.js").then((yaml)=>console.log("yaml-ok", typeof yaml.parse))'
- RUN node --version && npm --version && bun --version && git --version && python3 --version && docker --version && kubectl version --client=true && ssh -V && go version - RUN apt-get update && apt-get install -y --no-install-recommends tini && rm -rf /var/lib/apt/lists/*
- RUN node --version && npm --version && bun --version && git --version && python3 --version && docker --version && kubectl version --client=true && ssh -V && go version && tini --version
buildArgs: {} buildArgs: {}
buildNetwork: host buildNetwork: host
publicBaseImages: publicBaseImages:
@@ -1252,7 +1255,8 @@ targets:
- RUN ln -sf /usr/local/bin/bun /usr/local/bin/bunx - RUN ln -sf /usr/local/bin/bun /usr/local/bin/bunx
- ENV HWLAB_CI_NODE_DEPS=/opt/hwlab-ci-node-deps/node_modules - ENV HWLAB_CI_NODE_DEPS=/opt/hwlab-ci-node-deps/node_modules
- RUN set -eu; unset HTTP_PROXY HTTPS_PROXY ALL_PROXY http_proxy https_proxy all_proxy; export npm_config_registry="https://registry.npmmirror.com/"; export BUN_CONFIG_REGISTRY="https://registry.npmmirror.com/"; export npm_config_fetch_retries=2; export npm_config_fetch_retry_mintimeout=2000; export npm_config_fetch_retry_maxtimeout=16000; export npm_config_fetch_timeout=120000; mkdir -p /opt/hwlab-ci-node-deps; cd /opt/hwlab-ci-node-deps; printf '{"private":true,"dependencies":{}}\n' > package.json; ok=0; delay=2; for attempt in 1 2 3 4 5; do echo "{\"event\":\"tools-yaml-node-npm-install\",\"attempt\":\"$attempt/5\",\"registry\":\"$npm_config_registry\",\"proxy\":\"none\"}" >&2; if timeout 180s npm install --package-lock=false --no-save --ignore-scripts --no-audit --no-fund --omit=dev yaml@2.8.3; then ok=1; break; fi; if [ "$attempt" = 5 ]; then break; fi; echo "{\"event\":\"tools-yaml-node-npm-install\",\"status\":\"retrying\",\"attempt\":\"$attempt/5\",\"sleepSeconds\":$delay}" >&2; sleep "$delay"; delay=$((delay * 2)); done; test "$ok" = 1; node --input-type=module -e 'import("/opt/hwlab-ci-node-deps/node_modules/yaml/browser/dist/index.js").then((yaml)=>console.log("yaml-ok", typeof yaml.parse))' - RUN set -eu; unset HTTP_PROXY HTTPS_PROXY ALL_PROXY http_proxy https_proxy all_proxy; export npm_config_registry="https://registry.npmmirror.com/"; export BUN_CONFIG_REGISTRY="https://registry.npmmirror.com/"; export npm_config_fetch_retries=2; export npm_config_fetch_retry_mintimeout=2000; export npm_config_fetch_retry_maxtimeout=16000; export npm_config_fetch_timeout=120000; mkdir -p /opt/hwlab-ci-node-deps; cd /opt/hwlab-ci-node-deps; printf '{"private":true,"dependencies":{}}\n' > package.json; ok=0; delay=2; for attempt in 1 2 3 4 5; do echo "{\"event\":\"tools-yaml-node-npm-install\",\"attempt\":\"$attempt/5\",\"registry\":\"$npm_config_registry\",\"proxy\":\"none\"}" >&2; if timeout 180s npm install --package-lock=false --no-save --ignore-scripts --no-audit --no-fund --omit=dev yaml@2.8.3; then ok=1; break; fi; if [ "$attempt" = 5 ]; then break; fi; echo "{\"event\":\"tools-yaml-node-npm-install\",\"status\":\"retrying\",\"attempt\":\"$attempt/5\",\"sleepSeconds\":$delay}" >&2; sleep "$delay"; delay=$((delay * 2)); done; test "$ok" = 1; node --input-type=module -e 'import("/opt/hwlab-ci-node-deps/node_modules/yaml/browser/dist/index.js").then((yaml)=>console.log("yaml-ok", typeof yaml.parse))'
- RUN node --version && npm --version && bun --version && git --version && python3 --version && docker --version && kubectl version --client=true && ssh -V && go version - RUN apt-get update && apt-get install -y --no-install-recommends tini && rm -rf /var/lib/apt/lists/*
- RUN node --version && npm --version && bun --version && git --version && python3 --version && docker --version && kubectl version --client=true && ssh -V && go version && tini --version
buildArgs: {} buildArgs: {}
buildNetwork: host buildNetwork: host
publicBaseImages: publicBaseImages:
+79 -5
View File
@@ -223,7 +223,7 @@ gc:
minAgeHours: 336 minAgeHours: 336
maxDeletePerRun: 5 maxDeletePerRun: 5
memoryPressure: memoryPressure:
planPreviewLimit: 6 planPreviewLimit: 4
processPatterns: processPatterns:
- k3s-server - k3s-server
- containerd - containerd
@@ -291,12 +291,86 @@ gc:
targetMemAvailableBytes: 5368709120 targetMemAvailableBytes: 5368709120
minimumSwapFreeBytesAfterReclaim: 34359738368 minimumSwapFreeBytesAfterReclaim: 34359738368
targets: targets:
- id: kubepods-inactive-anon - id: kafka-broker-inactive-anon
path: /sys/fs/cgroup/kubepods.slice workloadSelector:
reclaimBytes: 3221225472 namespace: platform-infra
matchLabels:
strimzi.io/name: platform-infra-kafka-kafka
maxMatchedPods: 1
reclaimBytes: 268435456
swappiness: 200 swappiness: 200
minimumInactiveAnonBytes: 3221225472 minimumInactiveAnonBytes: 268435456
priority: 10 priority: 10
- id: kafka-entity-operator-inactive-anon
workloadSelector:
namespace: platform-infra
matchLabels:
strimzi.io/name: platform-infra-kafka-entity-operator
maxMatchedPods: 1
reclaimBytes: 134217728
swappiness: 200
minimumInactiveAnonBytes: 134217728
priority: 20
- id: argocd-controller-inactive-anon
workloadSelector:
namespace: argocd
matchLabels:
app.kubernetes.io/name: argocd-application-controller
maxMatchedPods: 1
reclaimBytes: 134217728
swappiness: 200
minimumInactiveAnonBytes: 134217728
priority: 30
- id: gitea-inactive-anon
workloadSelector:
namespace: devops-infra
matchLabels:
app.kubernetes.io/name: gitea
maxMatchedPods: 1
reclaimBytes: 134217728
swappiness: 200
minimumInactiveAnonBytes: 134217728
priority: 40
- id: hwlab-v03-hwpod-worker-inactive-anon
workloadSelector:
namespace: hwlab-v03
matchLabels:
app.kubernetes.io/name: hwlab-hwpod-worker
maxMatchedPods: 2
reclaimBytes: 134217728
swappiness: 200
minimumInactiveAnonBytes: 134217728
priority: 50
- id: sub2rank-development-worker-inactive-anon
workloadSelector:
namespace: platform-infra-development
matchLabels:
app.kubernetes.io/name: sub2rank-worker
maxMatchedPods: 1
reclaimBytes: 134217728
swappiness: 200
minimumInactiveAnonBytes: 134217728
priority: 60
- id: hwlab-v03-harnessrl-worker-inactive-anon
workloadSelector:
namespace: hwlab-v03
matchLabels:
app.kubernetes.io/name: hwlab-harnessrl-worker
maxMatchedPods: 2
reclaimBytes: 134217728
swappiness: 200
minimumInactiveAnonBytes: 134217728
priority: 70
- id: hwlab-v03-workbench-worker-inactive-anon
workloadSelector:
namespace: hwlab-v03
matchLabels:
app.kubernetes.io/name: hwlab-workbench-worker
maxMatchedPods: 2
reclaimBytes: 134217728
swappiness: 200
minimumInactiveAnonBytes: 134217728
priority: 80
kubernetesObjectRetention: kubernetesObjectRetention:
enabled: true enabled: true
aggregatePreviewLimit: 1 aggregatePreviewLimit: 1
@@ -96,10 +96,17 @@ CoreDNS 和 k3s 控制面使用 Kubernetes 系统关键优先级。Sub2API 和
中心 PaC owning YAML 必须声明 `deliveryTrigger.mode=manual-plan-confirm``deliveryTrigger.mechanism=pac-webhook`。PaC webhook 接收端继续作为唯一 PipelineRun 创建入口,但 Gitea push hook、PR merge callback、branch follower、poller 和其他自动发送方不得调用它;PR merge 后只允许 source mirror 或 authority 更新。受控 apply 必须删除或禁用 Gitea 自动 hook,并在 status 中把自动发送方启用视为配置漂移。 中心 PaC owning YAML 必须声明 `deliveryTrigger.mode=manual-plan-confirm``deliveryTrigger.mechanism=pac-webhook`。PaC webhook 接收端继续作为唯一 PipelineRun 创建入口,但 Gitea push hook、PR merge callback、branch follower、poller 和其他自动发送方不得调用它;PR merge 后只允许 source mirror 或 authority 更新。受控 apply 必须删除或禁用 Gitea 自动 hook,并在 status 中把自动发送方启用视为配置漂移。
### 5.7 CI-RESOURCE-REQ-007 子进程回收
长期运行且会启动 Git、SSH、编译器或其他子进程的容器必须具有明确的子进程回收者。业务进程不得在没有 init/reaper 的情况下直接作为容器 PID 1;已经正确等待直接子进程的实现仍须处理其退出后被 PID 1 接管的孤儿后代。init/reaper 必须只负责信号转发和子进程回收,不得成为第二业务生命周期 authority。
运行面诊断必须按父 PID 聚合 zombie,并映射到 namespace、Pod、稳定 workload owner 和修复类别。修复通过正常自动交付滚动 owner;直接终止 zombie、批量杀进程或只重启 Pod 不能作为根因修复。
## 6. 验收 ## 6. 验收
- L0:配置解析、资源 quantity、requests/limits 关系、matrix 收敛、有界 worker 队列和依赖保持通过轻量验证。 - L0:配置解析、资源 quantity、requests/limits 关系、matrix 收敛、有界 worker 队列和依赖保持通过轻量验证。
- L1:受控 `plan/status/diagnosis` 显示 YAML 来源、并发预算、批次、资源和保护状态,不依赖裸 Kubernetes 命令。 - L1:受控 `plan/status/diagnosis` 显示 YAML 来源、并发预算、批次、资源和保护状态,不依赖裸 Kubernetes 命令。
- L1:所有会启动子进程的常驻 Pod 均显示 init/reaper 为 PID 1;在真实 Git/SSH 操作前后,owner 的 zombie 数不增长。
- L2:正常 HWLAB source PR merge 后不产生 PipelineRun。受控 `plan` 显示 env reuse、镜像构建数量、精确构建与 rollout 范围且没有非预期扩大;随后使用手动 `trigger` 向 PaC 发送一次 webhook,并由 PaC 创建一次 PipelineRun。观察到同一时刻构建 TaskRun 不超过 YAML 预算,相同发布意图不产生第二次构建,所有构建 Pod 带资源合同,并且 NC01 Node、CoreDNS、Sub2API、Public Edge 和公开 API 在构建期间保持健康。 - L2:正常 HWLAB source PR merge 后不产生 PipelineRun。受控 `plan` 显示 env reuse、镜像构建数量、精确构建与 rollout 范围且没有非预期扩大;随后使用手动 `trigger` 向 PaC 发送一次 webhook,并由 PaC 创建一次 PipelineRun。观察到同一时刻构建 TaskRun 不超过 YAML 预算,相同发布意图不产生第二次构建,所有构建 Pod 带资源合同,并且 NC01 Node、CoreDNS、Sub2API、Public Edge 和公开 API 在构建期间保持健康。
验收失败时保留计划、手动触发链和运行证据,并修复 owning YAML、planner 或 renderer。重启 k3s、删除 Pod/PipelineRun、手工 Argo sync、运行时 patch、mirror flush 或裸补跑均不能作为最终通过证据。 验收失败时保留计划、手动触发链和运行证据,并修复 owning YAML、planner 或 renderer。重启 k3s、删除 Pod/PipelineRun、手工 Argo sync、运行时 patch、mirror flush 或裸补跑均不能作为最终通过证据。
@@ -71,6 +71,8 @@ YAML运维负责 HWLAB/UniDesk 自有平台配置的真相源、解析、渲染
| 有界默认输出 | CLI 在未显式请求机器输出或完整输出时返回的短摘要、表格和 drill-down 命令集合。 | | 有界默认输出 | CLI 在未显式请求机器输出或完整输出时返回的短摘要、表格和 drill-down 命令集合。 |
| host proxy client | 新节点 k3s 安装前由 `trans` 分发的 0 依赖出网代理客户端,用于给 Docker、k3s、containerd、包管理器、Git 和 git-mirror 提供初始出网。 | | host proxy client | 新节点 k3s 安装前由 `trans` 分发的 0 依赖出网代理客户端,用于给 Docker、k3s、containerd、包管理器、Git 和 git-mirror 提供初始出网。 |
| 配置组合 | 通过 baseline、configRef、继承、覆盖和变量渲染生成有效计划的配置方式;组合结果是执行视图,不是第二份可编辑真相源。 | | 配置组合 | 通过 baseline、configRef、继承、覆盖和变量渲染生成有效计划的配置方式;组合结果是执行视图,不是第二份可编辑真相源。 |
| 稳定工作负载选择器 | 由 namespace 和 Kubernetes labels 组成、跨 Pod 重建保持不变的工作负载身份;Pod UID 只能是运行面解析结果。 |
| 精确 cgroup 回收 | 受控 CLI 将稳定工作负载选择器解析为当前 Running Pod cgroup,并在执行前重新解析身份后请求有界 `memory.reclaim`。 |
## 4. 系统边界和接口 ## 4. 系统边界和接口
@@ -103,6 +105,7 @@ YAML运维负责 HWLAB/UniDesk 自有平台配置的真相源、解析、渲染
| PJ2026-01060312 | 配置组合 | 本规格 6.10 | baseline、configRef、继承、覆盖、变量渲染和重复块治理 | 配置真相、目标解析、公共原语 | 多节点 YAML 复用、长期维护 | | PJ2026-01060312 | 配置组合 | 本规格 6.10 | baseline、configRef、继承、覆盖、变量渲染和重复块治理 | 配置真相、目标解析、公共原语 | 多节点 YAML 复用、长期维护 |
| PJ2026-01060313 | K3s本地Registry | 本规格 6.11 | node-local registry 的 k3s workload/PVC、零种子迁移、镜像预加载和 Docker 依赖分类 | Host引导代理、目标解析、发布流水 | 纯净 k3s bring-up、HWLAB/AgentRun 构建 | | PJ2026-01060313 | K3s本地Registry | 本规格 6.11 | node-local registry 的 k3s workload/PVC、零种子迁移、镜像预加载和 Docker 依赖分类 | Host引导代理、目标解析、发布流水 | 纯净 k3s bring-up、HWLAB/AgentRun 构建 |
| PJ2026-01060314 | Hyper-V Ubuntu虚拟机 | [PJ2026-01060314 Hyper-V Ubuntu虚拟机](PJ2026-01060314-hyperv-ubuntu-vm.md) | Windows Hyper-V 宿主上的 Ubuntu VM、网络、初始化、常驻和 Provider 接入 | 配置真相、目标解析、Host引导代理 | Docker、k3s、provider-gateway、trans | | PJ2026-01060314 | Hyper-V Ubuntu虚拟机 | [PJ2026-01060314 Hyper-V Ubuntu虚拟机](PJ2026-01060314-hyperv-ubuntu-vm.md) | Windows Hyper-V 宿主上的 Ubuntu VM、网络、初始化、常驻和 Provider 接入 | 配置真相、目标解析、Host引导代理 | Docker、k3s、provider-gateway、trans |
| PJ2026-01060315 | 精确工作负载回收 | 本规格 6.12 | 稳定工作负载选择器、当前 Pod cgroup 解析和有界内存回收 | 配置真相、目标解析、公共原语 | 平台 GC、核心运行面保护 |
## 6. 原子需求 ## 6. 原子需求
@@ -239,3 +242,15 @@ YAML运维应允许新节点在没有旧 registry 数据和没有长期 host Doc
当迁移目标是证明从零部署能力时,受控 CLI 应先停止旧 host Docker registry,并在状态中明确 `seededFromOldRegistry=false``zeroSeeded=true`、PVC bound、workload ready 和 endpoint ready。迁移后所需的 BuildKit、runtime/base、tools image 和 service build artifact 必须通过 YAML-first `runtime-image preload`、tools image build/status 或等价 k3s 构建入口补齐,不能通过旧 registry 拷贝伪装成从零成功。 当迁移目标是证明从零部署能力时,受控 CLI 应先停止旧 host Docker registry,并在状态中明确 `seededFromOldRegistry=false``zeroSeeded=true`、PVC bound、workload ready 和 endpoint ready。迁移后所需的 BuildKit、runtime/base、tools image 和 service build artifact 必须通过 YAML-first `runtime-image preload`、tools image build/status 或等价 k3s 构建入口补齐,不能通过旧 registry 拷贝伪装成从零成功。
YAML运维状态输出应区分三类依赖:必须保留的 host bootstrap 面(provider-gateway/trans、0 依赖 host proxy、k3s installer/systemd)、待迁移的 host Docker/one-shot build 面,以及已经进入 k3s 的长期服务。后续减少 Docker 依赖时,应优先把长期服务、registry、git-mirror cache、web-probe/哨兵和构建执行迁入 k3shost proxy client 不属于迁移目标。 YAML运维状态输出应区分三类依赖:必须保留的 host bootstrap 面(provider-gateway/trans、0 依赖 host proxy、k3s installer/systemd)、待迁移的 host Docker/one-shot build 面,以及已经进入 k3s 的长期服务。后续减少 Docker 依赖时,应优先把长期服务、registry、git-mirror cache、web-probe/哨兵和构建执行迁入 k3shost proxy client 不属于迁移目标。
### 6.12 OPS-YAML-REQ-012 精确工作负载内存回收
| 编号 | 短名 | 主责模块 | 关联模块 |
| --- | --- | --- | --- |
| 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 路径或从运行面反解出的可调数值。
受控 plan 只选择 Running Pod,并披露 selector、解析出的 namespace、Pod、UID、cgroup、当前内存、inactive anonymous memory 和保护原因。零匹配、超过最大匹配数、API 不可读、cgroup 缺失、symlink、内存下限不足或 swap 保留线不足时必须保护,禁止回退到父 cgroup、按名称猜测或扩大 selector。
执行前必须使用同一 YAML selector 重新解析当前 Running Pod,并逐项核对 Pod UID、cgroup 路径和全部回收参数。身份变化、目标已满足或候选不再合格时跳过且结构化披露;只有身份闭合的当前 cgroup 才能写入有界 `memory.reclaim`。执行不得停止、重启、驱逐或迁移 workload,也不得把内核请求上限宣称为实际收益;终态继续分别披露 cgroup 下降、宿主 `MemAvailable` 变化、目标缺口和 swap 排除状态。
@@ -59,6 +59,7 @@ describe("AgentRun YAML-owned managed repository reconciler", () => {
expect(documents[2].spec.template.spec.automountServiceAccountToken).toBe(false); expect(documents[2].spec.template.spec.automountServiceAccountToken).toBe(false);
expect(documents[2].spec.template.spec.hostNetwork).toBe(spec.gitMirror.repositoryReconciler.hostNetwork); expect(documents[2].spec.template.spec.hostNetwork).toBe(spec.gitMirror.repositoryReconciler.hostNetwork);
expect(documents[2].spec.template.spec.dnsPolicy).toBe(spec.gitMirror.repositoryReconciler.dnsPolicy); expect(documents[2].spec.template.spec.dnsPolicy).toBe(spec.gitMirror.repositoryReconciler.dnsPolicy);
expect(documents[2].spec.template.spec.containers[0].command.slice(0, 2)).toEqual(["tini", "--"]);
expect(documents[2].spec.template.spec.containers[0].readinessProbe.exec.command).toContain("--health"); expect(documents[2].spec.template.spec.containers[0].readinessProbe.exec.command).toContain("--health");
const authVolume = documents[2].spec.template.spec.volumes.find((item: any) => item.name === "git-auth"); const authVolume = documents[2].spec.template.spec.volumes.find((item: any) => item.name === "git-auth");
expect(authVolume.projected.sources).toHaveLength(2); expect(authVolume.projected.sources).toHaveLength(2);
@@ -235,6 +235,8 @@ function managedRepositoryObjects(spec: AgentRunLaneSpec): Record<string, unknow
image: spec.gitMirror.toolsImage, image: spec.gitMirror.toolsImage,
imagePullPolicy: reconciler.imagePullPolicy, imagePullPolicy: reconciler.imagePullPolicy,
command: [ command: [
"tini",
"--",
"node", "node",
`${controllerMountPath}/controller.mjs`, `${controllerMountPath}/controller.mjs`,
"--config", "--config",
@@ -123,6 +123,13 @@ 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); 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", () => {
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);
});
test("memory-pressure plan includes only YAML allowlisted cgroup reclaim stages with swap reserve", () => { test("memory-pressure plan includes only YAML allowlisted cgroup reclaim stages with swap reserve", () => {
const result = runPythonFixture(String.raw` const result = runPythonFixture(String.raw`
MEMORY_CONFIG = { MEMORY_CONFIG = {
@@ -239,7 +246,7 @@ test("default memory-pressure plan projects an actionable YAML-bounded preview b
assert.equal(projected.runEligibility.allowed, true); assert.equal(projected.runEligibility.allowed, true);
assert.equal(projected.runEligibility.allCandidatesEvaluated, true); assert.equal(projected.runEligibility.allCandidatesEvaluated, true);
assert.equal(projected.candidatePreview.limit, remoteTarget.memoryPressure.planPreviewLimit); assert.equal(projected.candidatePreview.limit, remoteTarget.memoryPressure.planPreviewLimit);
assert.equal(projected.candidatePreview.candidates.length, 6); assert.equal(projected.candidatePreview.candidates.length, Math.min(candidates.length, remoteTarget.memoryPressure.planPreviewLimit));
assert.equal(projected.candidatePreview.candidates[2].identityClosure.identitiesComplete, true); assert.equal(projected.candidatePreview.candidates[2].identityClosure.identitiesComplete, true);
assert.equal(projected.candidatePreview.candidates[2].processIdentities, undefined); assert.equal(projected.candidatePreview.candidates[2].processIdentities, undefined);
assert.equal(projected.policy.runCommand, "bun scripts/cli.ts gc remote NC01 run --confirm --memory-pressure-only"); assert.equal(projected.policy.runCommand, "bun scripts/cli.ts gc remote NC01 run --confirm --memory-pressure-only");
+146 -6
View File
@@ -359,15 +359,40 @@ def configured_cgroup_reclaim_targets():
return [] return []
target_id = item.get("id") target_id = item.get("id")
path = item.get("path") path = item.get("path")
workload_selector = item.get("workloadSelector")
reclaim_bytes = item.get("reclaimBytes") reclaim_bytes = item.get("reclaimBytes")
swappiness = item.get("swappiness") swappiness = item.get("swappiness")
minimum_inactive_anon = item.get("minimumInactiveAnonBytes", 0) 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: 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 _MEMORY_RECLAIM_CONFIG_ERROR = "yaml-cgroup-reclaim-target-id-invalid:%s" % index
return [] return []
if (path is None) == (workload_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: 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 _MEMORY_RECLAIM_CONFIG_ERROR = "yaml-cgroup-reclaim-target-path-invalid:%s" % target_id
return [] return []
if workload_selector is not None:
if not isinstance(workload_selector, dict):
_MEMORY_RECLAIM_CONFIG_ERROR = "yaml-cgroup-reclaim-workload-selector-invalid:%s" % target_id
return []
namespace = workload_selector.get("namespace")
match_labels = workload_selector.get("matchLabels")
max_matched_pods = workload_selector.get("maxMatchedPods")
if not isinstance(namespace, str) or not re.match(r"^[a-z0-9]([-a-z0-9]*[a-z0-9])?$", namespace):
_MEMORY_RECLAIM_CONFIG_ERROR = "yaml-cgroup-reclaim-workload-namespace-invalid:%s" % target_id
return []
if not isinstance(match_labels, dict) or not match_labels or any(
not isinstance(key, str) or not re.match(r"^[A-Za-z0-9./_-]{1,253}$", key)
or not isinstance(value, str) or not re.match(r"^[A-Za-z0-9._-]{1,63}$", value)
for key, value in match_labels.items()
):
_MEMORY_RECLAIM_CONFIG_ERROR = "yaml-cgroup-reclaim-workload-labels-invalid:%s" % target_id
return []
if isinstance(max_matched_pods, bool) or not isinstance(max_matched_pods, int) or max_matched_pods < 1 or max_matched_pods > 20:
_MEMORY_RECLAIM_CONFIG_ERROR = "yaml-cgroup-reclaim-workload-max-pods-invalid:%s" % target_id
return []
if isinstance(reclaim_bytes, bool) or not isinstance(reclaim_bytes, int) or reclaim_bytes <= 0 or reclaim_bytes > 8 * 1024 * 1024 * 1024: if isinstance(reclaim_bytes, bool) or not isinstance(reclaim_bytes, int) or reclaim_bytes <= 0 or reclaim_bytes > 8 * 1024 * 1024 * 1024:
_MEMORY_RECLAIM_CONFIG_ERROR = "yaml-cgroup-reclaim-bytes-invalid:%s" % target_id _MEMORY_RECLAIM_CONFIG_ERROR = "yaml-cgroup-reclaim-bytes-invalid:%s" % target_id
return [] return []
@@ -388,6 +413,7 @@ def configured_cgroup_reclaim_targets():
validated.append({ validated.append({
"id": target_id, "id": target_id,
"path": path, "path": path,
"workloadSelector": workload_selector,
"reclaimBytes": reclaim_bytes, "reclaimBytes": reclaim_bytes,
"swappiness": swappiness, "swappiness": swappiness,
"minimumInactiveAnonBytes": minimum_inactive_anon, "minimumInactiveAnonBytes": minimum_inactive_anon,
@@ -397,6 +423,92 @@ def configured_cgroup_reclaim_targets():
}) })
return sorted(validated, key=lambda item: item.get("priority")) return sorted(validated, key=lambda item: item.get("priority"))
_CGROUP_RECLAIM_RUNTIME_INVENTORY = None
def cgroup_reclaim_runtime_inventory():
global _CGROUP_RECLAIM_RUNTIME_INVENTORY
if _CGROUP_RECLAIM_RUNTIME_INVENTORY is not None:
return _CGROUP_RECLAIM_RUNTIME_INVENTORY
attribution = MEMORY_CONFIG.get("attribution") or {}
timeout = safe_int(attribution.get("commandTimeoutSeconds"))
runtime = command(["k3s", "crictl", "pods", "-o", "json"], timeout)
if runtime.get("exitCode") != 0:
_CGROUP_RECLAIM_RUNTIME_INVENTORY = {
"ok": False,
"reason": "workload-runtime-inventory-failed",
"command": bounded(runtime),
"pods": [],
}
return _CGROUP_RECLAIM_RUNTIME_INVENTORY
try:
payload = json.loads(runtime.get("stdout") or "{}")
except (TypeError, ValueError, json.JSONDecodeError):
_CGROUP_RECLAIM_RUNTIME_INVENTORY = {"ok": False, "reason": "workload-runtime-inventory-invalid", "pods": []}
return _CGROUP_RECLAIM_RUNTIME_INVENTORY
cgroup_root = attribution.get("cgroupRoot")
pod_paths = {}
scan_limit = safe_int(attribution.get("cgroupScanLimit"))
scanned = 0
for current, directories, files in os.walk(os.path.join(cgroup_root, "kubepods.slice"), 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:
_CGROUP_RECLAIM_RUNTIME_INVENTORY = {"ok": False, "reason": "workload-cgroup-scan-truncated", "pods": []}
return _CGROUP_RECLAIM_RUNTIME_INVENTORY
match = re.search(r"pod([0-9a-f_]{36})\.slice$", os.path.basename(current))
if match is not None and "memory.current" in files:
pod_paths[match.group(1).replace("_", "-")] = current
directories[:] = []
pods = []
for pod in payload.get("items") or []:
metadata = pod.get("metadata") if isinstance(pod, dict) else None
uid = metadata.get("uid") if isinstance(metadata, dict) else None
name = metadata.get("name") if isinstance(metadata, dict) else None
namespace = metadata.get("namespace") if isinstance(metadata, dict) else None
labels = pod.get("labels") if isinstance(pod, dict) else None
if pod.get("state") != "SANDBOX_READY" or not all(isinstance(value, str) and value for value in [uid, name, namespace]):
continue
pods.append({
"namespace": namespace,
"name": name,
"uid": uid,
"labels": labels if isinstance(labels, dict) else {},
"path": pod_paths.get(uid),
})
_CGROUP_RECLAIM_RUNTIME_INVENTORY = {"ok": True, "pods": pods}
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}]}
selector = item.get("workloadSelector") or {}
labels = selector.get("matchLabels") or {}
label_selector = ",".join("%s=%s" % pair for pair in sorted(labels.items()))
inventory = cgroup_reclaim_runtime_inventory()
if inventory.get("ok") is not True:
return {"ok": False, "reason": inventory.get("reason"), "command": inventory.get("command"), "matches": []}
pods = [pod for pod in inventory.get("pods") or [] if
pod.get("namespace") == selector.get("namespace")
and all((pod.get("labels") or {}).get(key) == value for key, value in labels.items())
]
if len(pods) == 0:
return {"ok": False, "reason": "workload-selector-no-running-pods", "matches": []}
if len(pods) > safe_int(selector.get("maxMatchedPods")):
return {"ok": False, "reason": "workload-selector-max-pods-exceeded", "matchedPodCount": len(pods), "matches": []}
matches = []
for pod in pods:
uid = pod.get("uid")
name = pod.get("name")
resolved_path = pod.get("path")
if not isinstance(uid, str) or not isinstance(name, str) or not isinstance(resolved_path, str):
return {"ok": False, "reason": "workload-pod-cgroup-unavailable", "matches": []}
matches.append({
"path": resolved_path,
"pod": {"namespace": selector.get("namespace"), "name": name, "uid": uid, "labelSelector": label_selector},
})
return {"ok": True, "matches": matches}
def collect_cgroup_reclaim_candidates(): def collect_cgroup_reclaim_candidates():
global _MEMORY_RECLAIM_CONFIG_ERROR, _CGROUP_RECLAIM_DIAGNOSTICS global _MEMORY_RECLAIM_CONFIG_ERROR, _CGROUP_RECLAIM_DIAGNOSTICS
targets = configured_cgroup_reclaim_targets() targets = configured_cgroup_reclaim_targets()
@@ -412,7 +524,18 @@ def collect_cgroup_reclaim_candidates():
swap_free = safe_int((host.get("swap") or {}).get("freeBytes")) swap_free = safe_int((host.get("swap") or {}).get("freeBytes"))
candidates = [] candidates = []
for item in targets: for item in targets:
path = item.get("path") resolution = resolve_cgroup_reclaim_target(item)
if resolution.get("ok") is not True:
diagnostics["protected"].append({
"id": item.get("id"),
"workloadSelector": item.get("workloadSelector"),
"reason": resolution.get("reason"),
"matchedPodCount": resolution.get("matchedPodCount"),
})
continue
for resolved in resolution.get("matches") or []:
path = resolved.get("path")
pod = resolved.get("pod")
reclaim_path = os.path.join(path, "memory.reclaim") reclaim_path = os.path.join(path, "memory.reclaim")
protected_reason = None protected_reason = None
if os.path.realpath(path) != path or not os.path.isdir(path) or os.path.islink(path): if os.path.realpath(path) != path or not os.path.isdir(path) or os.path.islink(path):
@@ -432,20 +555,28 @@ def collect_cgroup_reclaim_candidates():
diagnostics["protected"].append({ diagnostics["protected"].append({
"id": item.get("id"), "id": item.get("id"),
"path": path, "path": path,
"workloadSelector": item.get("workloadSelector"),
"resolvedPod": pod,
"reason": protected_reason, "reason": protected_reason,
"memoryCurrentBytes": current, "memoryCurrentBytes": current,
"inactiveAnonBytes": inactive_anon, "inactiveAnonBytes": inactive_anon,
}) })
continue continue
reclaim_bytes = safe_int(item.get("reclaimBytes")) reclaim_bytes = safe_int(item.get("reclaimBytes"))
identity_suffix = (pod or {}).get("uid")
candidate_id = "cgroup-memory-reclaim:%s" % item.get("id")
if identity_suffix:
candidate_id += ":%s" % identity_suffix
candidates.append({ candidates.append({
"id": "cgroup-memory-reclaim:%s" % item.get("id"), "id": candidate_id,
"configId": item.get("id"), "configId": item.get("id"),
"kind": "cgroup-memory-reclaim", "kind": "cgroup-memory-reclaim",
"risk": "medium", "risk": "medium",
"description": "Request bounded cgroup v2 memory reclaim from an exact YAML allowlist path", "description": "Request bounded cgroup v2 memory reclaim from a YAML-resolved workload",
"reason": "host-memavailable-below-yaml-target", "reason": "host-memavailable-below-yaml-target",
"path": path, "path": path,
"workloadSelector": item.get("workloadSelector"),
"resolvedPod": pod,
"priority": safe_int(item.get("priority")), "priority": safe_int(item.get("priority")),
"reclaimBytes": reclaim_bytes, "reclaimBytes": reclaim_bytes,
"swappiness": safe_int(item.get("swappiness")), "swappiness": safe_int(item.get("swappiness")),
@@ -462,7 +593,7 @@ def collect_cgroup_reclaim_candidates():
"expectedMemoryRss": fmt_bytes(reclaim_bytes), "expectedMemoryRss": fmt_bytes(reclaim_bytes),
"estimatedDiskBytes": 0, "estimatedDiskBytes": 0,
"estimatedReclaimBytes": 0, "estimatedReclaimBytes": 0,
"action": {"op": "cgroup-v2-memory-reclaim", "allowlist": "yaml-exact-cgroup-path"}, "action": {"op": "cgroup-v2-memory-reclaim", "allowlist": "yaml-workload-selector-or-exact-path"},
}) })
diagnostics["eligibleCount"] = len(candidates) diagnostics["eligibleCount"] = len(candidates)
return candidates return candidates
@@ -679,14 +810,23 @@ def extend_with_allowlisted_session_members(identities, processes, sid, browser_
return added return added
def execute_cgroup_reclaim_candidate(candidate): def execute_cgroup_reclaim_candidate(candidate):
global _CGROUP_RECLAIM_RUNTIME_INVENTORY
config_id = candidate.get("configId") config_id = candidate.get("configId")
configured = next((item for item in configured_cgroup_reclaim_targets() if item.get("id") == config_id), None) configured = next((item for item in configured_cgroup_reclaim_targets() if item.get("id") == config_id), None)
if configured is None: if configured is None:
raise RuntimeError("cgroup reclaim candidate is no longer present in YAML allowlist") raise RuntimeError("cgroup reclaim candidate is no longer present in YAML allowlist")
for key in ["path", "reclaimBytes", "swappiness", "minimumInactiveAnonBytes", "targetMemAvailableBytes", "minimumSwapFreeBytesAfterReclaim"]: for key in ["workloadSelector", "reclaimBytes", "swappiness", "minimumInactiveAnonBytes", "targetMemAvailableBytes", "minimumSwapFreeBytesAfterReclaim"]:
if configured.get(key) != candidate.get(key): if configured.get(key) != candidate.get(key):
raise RuntimeError("cgroup reclaim candidate no longer matches YAML: %s" % key) raise RuntimeError("cgroup reclaim candidate no longer matches YAML: %s" % key)
path = configured.get("path") _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"))
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)
if current_match is None:
raise RuntimeError("cgroup reclaim candidate identity changed before execution")
reclaim_path = os.path.join(path, "memory.reclaim") reclaim_path = os.path.join(path, "memory.reclaim")
if os.path.realpath(path) != path or not os.path.isdir(path) or os.path.islink(path): if os.path.realpath(path) != path or not os.path.isdir(path) or os.path.islink(path):
raise RuntimeError("cgroup reclaim path is unavailable or redirected") raise RuntimeError("cgroup reclaim path is unavailable or redirected")
+32 -4
View File
@@ -411,6 +411,23 @@ function positiveInteger(value: unknown): number | null {
return typeof value === "number" && Number.isInteger(value) && value > 0 ? value : null; return typeof value === "number" && Number.isInteger(value) && value > 0 ? value : null;
} }
function compactWorkloadSelector(value: unknown): string | undefined {
const selector = recordOrEmpty(value);
const namespace = typeof selector.namespace === "string" ? selector.namespace : null;
const labels = recordOrEmpty(selector.matchLabels);
const pairs = Object.entries(labels)
.filter((entry): entry is [string, string] => typeof entry[1] === "string")
.sort(([left], [right]) => left.localeCompare(right))
.map(([key, label]) => `${key}=${label}`);
return namespace !== null && pairs.length > 0 ? `${namespace}:${pairs.join(",")}` : undefined;
}
function compactResolvedPod(value: unknown): Record<string, unknown> | undefined {
const pod = recordOrEmpty(value);
if (typeof pod.namespace !== "string" || typeof pod.name !== "string" || typeof pod.uid !== "string") return undefined;
return { name: `${pod.namespace}/${pod.name}`, uid: pod.uid };
}
function compactMemoryPressureCandidate(value: unknown): Record<string, unknown> { function compactMemoryPressureCandidate(value: unknown): Record<string, unknown> {
const candidate = recordOrEmpty(value); const candidate = recordOrEmpty(value);
const action = recordOrEmpty(candidate.action); const action = recordOrEmpty(candidate.action);
@@ -426,14 +443,14 @@ function compactMemoryPressureCandidate(value: unknown): Record<string, unknown>
...base, ...base,
configId: candidate.configId, configId: candidate.configId,
path: candidate.path, path: candidate.path,
workloadSelector: compactWorkloadSelector(candidate.workloadSelector),
resolvedPod: compactResolvedPod(candidate.resolvedPod),
reclaimBytes: candidate.reclaimBytes, reclaimBytes: candidate.reclaimBytes,
requestedMemoryReclaimBytes: candidate.requestedMemoryReclaimBytes, requestedMemoryReclaimBytes: candidate.requestedMemoryReclaimBytes,
requestedMemoryReclaim: candidate.requestedMemoryReclaim, requestedMemoryReclaim: candidate.requestedMemoryReclaim,
estimateKind: candidate.estimateKind, estimateKind: candidate.estimateKind,
swappiness: candidate.swappiness, swappiness: candidate.swappiness,
minimumInactiveAnonBytes: candidate.minimumInactiveAnonBytes, minimumInactiveAnonBytes: candidate.minimumInactiveAnonBytes,
targetMemAvailableBytes: candidate.targetMemAvailableBytes,
minimumSwapFreeBytesAfterReclaim: candidate.minimumSwapFreeBytesAfterReclaim,
memoryCurrentBytes: candidate.memoryCurrentBytes, memoryCurrentBytes: candidate.memoryCurrentBytes,
memorySwapCurrentBytes: candidate.memorySwapCurrentBytes, memorySwapCurrentBytes: candidate.memorySwapCurrentBytes,
}; };
@@ -478,7 +495,10 @@ function memoryPressureCandidateAllowed(value: unknown): boolean {
&& swappiness !== null && swappiness >= 0 && swappiness <= 200 && swappiness !== null && swappiness >= 0 && swappiness <= 200
&& (swappiness === 0 || positiveInteger(candidate.minimumInactiveAnonBytes) !== null) && (swappiness === 0 || positiveInteger(candidate.minimumInactiveAnonBytes) !== null)
&& action.op === "cgroup-v2-memory-reclaim" && action.op === "cgroup-v2-memory-reclaim"
&& action.allowlist === "yaml-exact-cgroup-path"; && (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"));
} }
if (candidate.kind !== "browser-orphan-process-tree" && candidate.kind !== "stale-web-observer-process-tree") return false; if (candidate.kind !== "browser-orphan-process-tree" && candidate.kind !== "stale-web-observer-process-tree") return false;
const compact = compactMemoryPressureCandidate(candidate); const compact = compactMemoryPressureCandidate(candidate);
@@ -526,7 +546,15 @@ function compactMemoryPressureCandidateScan(value: unknown): Record<string, unkn
protectedCount: cgroupProtected.length, protectedCount: cgroupProtected.length,
protected: cgroupProtected.map((item) => { protected: cgroupProtected.map((item) => {
const protectedItem = recordOrEmpty(item); const protectedItem = recordOrEmpty(item);
return { id: protectedItem.id, reason: protectedItem.reason, memoryCurrentBytes: protectedItem.memoryCurrentBytes }; return {
id: protectedItem.id,
reason: protectedItem.reason,
matchedPodCount: protectedItem.matchedPodCount,
memoryCurrentBytes: protectedItem.memoryCurrentBytes,
inactiveAnonBytes: protectedItem.inactiveAnonBytes,
workloadSelector: compactWorkloadSelector(protectedItem.workloadSelector),
resolvedPod: compactResolvedPod(protectedItem.resolvedPod),
};
}), }),
}, },
policySource: scan.policySource, policySource: scan.policySource,
+1 -1
View File
@@ -2423,7 +2423,7 @@ function gitMirrorDeployment(name: string, namespace: string, labels: Record<str
containers: [{ containers: [{
name: "git-mirror", name: "git-mirror",
image: target.tekton.toolsImage.output, image: target.tekton.toolsImage.output,
command: ["node", "/etc/git-mirror/server.js"], command: ["tini", "--", "node", "/etc/git-mirror/server.js"],
env: [ env: [
{ name: "PORT", value: String(containerPort) }, { name: "PORT", value: String(containerPort) },
{ name: "GIT_PROJECT_ROOT", value: "/cache" }, { name: "GIT_PROJECT_ROOT", value: "/cache" },
@@ -3,7 +3,7 @@ FROM ${TODO_NOTE_BASE_IMAGE}
WORKDIR /app/src/components/microservices/todo-note WORKDIR /app/src/components/microservices/todo-note
RUN apt-get update \ RUN apt-get update \
&& apt-get install -y --no-install-recommends git openssh-client ca-certificates \ && apt-get install -y --no-install-recommends git openssh-client ca-certificates tini \
&& apt-get clean \ && apt-get clean \
&& rm -rf /var/lib/apt/lists/* && rm -rf /var/lib/apt/lists/*
COPY src/components/microservices/todo-note/package.json src/components/microservices/todo-note/bun.lock ./ COPY src/components/microservices/todo-note/package.json src/components/microservices/todo-note/bun.lock ./
@@ -13,4 +13,5 @@ COPY src/components/shared /app/src/components/shared
COPY src/components/microservices/todo-note/src ./src COPY src/components/microservices/todo-note/src ./src
EXPOSE 4288 EXPOSE 4288
ENTRYPOINT ["tini", "--"]
CMD ["bun", "run", "src/index.ts"] CMD ["bun", "run", "src/index.ts"]