From d4e855a35d57b5a545f33e85d0f12fc0aef5a950 Mon Sep 17 00:00:00 2001 From: zhanghao Date: Wed, 28 Oct 2026 16:38:10 +0800 Subject: [PATCH 1/2] Size the route shared dicts to start on 64 KiB-page arm64 hosts nginx carves each shared zone into pages of the kernel page size, and arm64 kernels often use 64 KiB pages. There the per-route cluster_avg dict (16k) is too small for the slab pool, and the openresty that loads the generated route exits with "ngx_slab_alloc() failed: no memory". Measured on Kunpeng 920 with 64 KiB pages: 128k fails, 192k starts. Raise it to 1m, like the other route dicts, and add a test that renders a route and refuses any dict under 256k. Checked against the old template: the test fails on cluster_avg 16k. --- internal/sink/openresty_shm_test.go | 37 +++++++++++++++++++++++++++++ internal/sink/route.tmpl | 4 +++- 2 files changed, 40 insertions(+), 1 deletion(-) create mode 100644 internal/sink/openresty_shm_test.go diff --git a/internal/sink/openresty_shm_test.go b/internal/sink/openresty_shm_test.go new file mode 100644 index 0000000..c2c73d1 --- /dev/null +++ b/internal/sink/openresty_shm_test.go @@ -0,0 +1,37 @@ +package sink + +import ( + "regexp" + "strconv" + "strings" + "testing" +) + +// nginx carves a shared zone into pages of the kernel page size. arm64 hosts often run 64 KiB +// pages, where a zone of 128k or less fails at startup with "ngx_slab_alloc() failed: no memory" +// (measured on Kunpeng 920: 128k fails, 192k starts). A route that renders such a dict takes the +// whole openresty down on those hosts, so none may be under 256k. +func TestRenderRoute_SharedDictsFitLargePages(t *testing.T) { + got, err := RenderRoute(llmData(map[string]string{"tps_limit_tps": "30"})) + if err != nil { + t.Fatalf("render: %v", err) + } + re := regexp.MustCompile(`(?m)^\s*lua_shared_dict\s+(\S+)\s+(\d+)([kKmM]?)\s*;`) + dicts := re.FindAllStringSubmatch(got, -1) + if len(dicts) == 0 { + t.Fatalf("no lua_shared_dict found in the rendered route -- the check would pass on nothing:\n%s", got) + } + for _, d := range dicts { + n, _ := strconv.Atoi(d[2]) + kib := n / 1024 // bytes + switch strings.ToLower(d[3]) { + case "k": + kib = n + case "m": + kib = n * 1024 + } + if kib < 256 { + t.Errorf("lua_shared_dict %s is %s%s: under 256k it fails to start on 64 KiB-page hosts", d[1], d[2], d[3]) + } + } +} diff --git a/internal/sink/route.tmpl b/internal/sink/route.tmpl index d2c62e2..fcfe490 100644 --- a/internal/sink/route.tmpl +++ b/internal/sink/route.tmpl @@ -3,8 +3,10 @@ 按 // 派生 socket 分发;不监听 per-model TCP 端口。 Route(短名,用于 dict/register/socket 名)、Extra(任意调优项)、Peers(发现结果)。 */ -}} # {{.Route}} 路由(autoconfig 模板生成,勿手改;改模板或 ModelRoute) +# Every dict is at least 1m: on arm64 hosts with 64 KiB pages a zone of 128k or less fails at +# startup with "ngx_slab_alloc() failed: no memory" (192k is the smallest that starts there). lua_shared_dict active_conns_{{.Route}} 4m; -lua_shared_dict cluster_avg_{{.Route}} 16k; +lua_shared_dict cluster_avg_{{.Route}} 1m; lua_shared_dict lc_locks_{{.Route}} 1m; lua_shared_dict bad_peers_{{.Route}} 1m; lua_shared_dict bodylog_ctl_{{.Route}} 1m; From b01313f6235d30ecf6292d003899a63bcf7588b4 Mon Sep 17 00:00:00 2001 From: zhanghao Date: Wed, 28 Oct 2026 16:54:29 +0800 Subject: [PATCH 2/2] Release 0.4.3 Moves the chart (version, appVersion), the kustomize image tag, the sample and the e2e scripts' default image tags to 0.4.3, the release with the route shared dicts sized for 64 KiB-page hosts. --- config/manager/kustomization.yaml | 2 +- config/samples/modelroute-qwen.yaml | 4 ++-- deploy/helm/autoconfig/Chart.yaml | 4 ++-- test/e2e/crd_e2e.sh | 4 ++-- test/e2e/ha_failover_check.sh | 2 +- test/e2e/helm_e2e.sh | 2 +- test/e2e/prod_real_model_e2e.sh | 2 +- test/e2e/real_helm_e2e.sh | 4 ++-- test/e2e/real_kimi_e2e.sh | 4 ++-- test/e2e/test_dynamic.sh | 2 +- test/e2e/verify_crossns_port.sh | 4 ++-- test/e2e/verify_status_error.sh | 4 ++-- 12 files changed, 19 insertions(+), 19 deletions(-) diff --git a/config/manager/kustomization.yaml b/config/manager/kustomization.yaml index b87dd66..631b39f 100644 --- a/config/manager/kustomization.yaml +++ b/config/manager/kustomization.yaml @@ -2,4 +2,4 @@ resources: - manager.yaml images: - name: 4pdosc/autoconfig - newTag: "0.4.2" + newTag: "0.4.3" diff --git a/config/samples/modelroute-qwen.yaml b/config/samples/modelroute-qwen.yaml index 00b6970..f17911a 100644 --- a/config/samples/modelroute-qwen.yaml +++ b/config/samples/modelroute-qwen.yaml @@ -3,8 +3,8 @@ # 前置:cart-qwen 需先用 cache_aware_router helm chart 装(一个模型一个 cart): # helm -n llm-route install cart-qwen \ # --set fullnameOverride=cart-qwen \ -# --set reload.image=4pdosc/autoconfig-reload:0.4.2 \ -# --set ha.image=4pdosc/autoconfig-hagate:0.4.2 +# --set reload.image=4pdosc/autoconfig-reload:0.4.3 \ +# --set ha.image=4pdosc/autoconfig-hagate:0.4.3 # (cart 起始 initContainer 等 cart-qwen-config 有 workers → 本 ModelRoute apply 后 autoconfig 写入即就绪) # autoconfig 据此写 openresty-conf(session_route_qwen.conf)+ cart-qwen-config(workers)。 # 所有 service / outputConfigMap 均为 ns/name 全限定 → controller 全集群 watch,本 ModelRoute 放哪个 ns 都行 diff --git a/deploy/helm/autoconfig/Chart.yaml b/deploy/helm/autoconfig/Chart.yaml index 4bea316..ab25d42 100644 --- a/deploy/helm/autoconfig/Chart.yaml +++ b/deploy/helm/autoconfig/Chart.yaml @@ -2,7 +2,7 @@ apiVersion: v2 name: autoconfig description: k8s 后端发现 → 自动同步 openresty peers / cache-aware-router workers(ModelRoute controller) type: application -version: 0.4.2 # chart 版本 -appVersion: "0.4.2" # 镜像版本(image.tag 缺省用它) +version: 0.4.3 # chart 版本 +appVersion: "0.4.3" # 镜像版本(image.tag 缺省用它) keywords: [routing, openresty, cache-aware-router, service-discovery, operator] home: https://github.com/modelsphere/autoconfig diff --git a/test/e2e/crd_e2e.sh b/test/e2e/crd_e2e.sh index 76b2afe..6e44292 100644 --- a/test/e2e/crd_e2e.sh +++ b/test/e2e/crd_e2e.sh @@ -4,13 +4,13 @@ # → 验 cart-config workers + openresty CART优先/后端兜底 peers + status → scale 跟随 → 删除清理。 # # 依赖:autoconfig helm chart(默认 $HERE/charts/autoconfig,含 CRD+RBAC+controller)。 -# 用法:NS=ac-e2e IMG=4pdosc/autoconfig:0.4.2 bash crd_e2e.sh [--keep] +# 用法:NS=ac-e2e IMG=4pdosc/autoconfig:0.4.3 bash crd_e2e.sh [--keep] set -uo pipefail HERE="$(cd "$(dirname "$0")" && pwd)" NS=${NS:-ac-e2e} CTRL_NS=${CTRL_NS:-autoconfig} AC_CHART=${AC_CHART:-$HERE/charts/autoconfig} -IMG=${IMG:-4pdosc/autoconfig:0.4.2} +IMG=${IMG:-4pdosc/autoconfig:0.4.3} MOCK=${MOCK:-python:3.12-alpine} KEEP=0; [ "${1:-}" = "--keep" ] && KEEP=1 FAIL=0 diff --git a/test/e2e/ha_failover_check.sh b/test/e2e/ha_failover_check.sh index e53c2ce..d7dca59 100644 --- a/test/e2e/ha_failover_check.sh +++ b/test/e2e/ha_failover_check.sh @@ -7,7 +7,7 @@ NS=${NS:-kimi} CHARTS=${CHARTS:-$HERE/charts} OR_IMG=${OR_IMG:-4pdosc/llm-openresty:0.1.20} CART_IMG=${CART_IMG:-4pdosc/cache_aware_router:0.6.5} -ACR=${ACR:-4pdosc/autoconfig-reload:0.4.2} +ACR=${ACR:-4pdosc/autoconfig-reload:0.4.3} AUTH_KEY=${AUTH_KEY:?需设置:openresty 入口鉴权 key(Bearer)} FAIL=0; say(){ echo -e "\n=== $* ==="; }; ok(){ echo " PASS: $*"; }; bad(){ echo " FAIL: $*"; FAIL=1; } diff --git a/test/e2e/helm_e2e.sh b/test/e2e/helm_e2e.sh index e95394d..15bcf43 100644 --- a/test/e2e/helm_e2e.sh +++ b/test/e2e/helm_e2e.sh @@ -7,7 +7,7 @@ HERE="$(cd "$(dirname "$0")" && pwd)" CHART=${CHART:-$HERE/autoconfig} NS=${NS:-autoconfig} TNS=${TNS:-ac-helm-e2e} -IMG_TAG=${IMG_TAG:-0.4.2} +IMG_TAG=${IMG_TAG:-0.4.3} MOCK=${MOCK:-python:3.12-alpine} KEEP=0; [ "${1:-}" = "--keep" ] && KEEP=1 FAIL=0 diff --git a/test/e2e/prod_real_model_e2e.sh b/test/e2e/prod_real_model_e2e.sh index 4d2149a..160d351 100644 --- a/test/e2e/prod_real_model_e2e.sh +++ b/test/e2e/prod_real_model_e2e.sh @@ -3,7 +3,7 @@ # 后端 = 真 opt-125m vLLM(复用 e2e-test/vllm-mock-vllm-svc,跨 ns discovery)。真 /v1/completions 推理。 set -uo pipefail NS=${NS:-llm-route}; CTRL_NS=autoconfig; CHARTS=~/prod-e2e/charts -H=4pdosc; TAG=${IMG_TAG:-0.4.2} +H=4pdosc; TAG=${IMG_TAG:-0.4.3} AC=$H/autoconfig; ACR=$H/autoconfig-reload:$TAG; AH=$H/autoconfig-hagate:$TAG OR_IMG=$H/llm-openresty:0.1.20; CART_IMG=$H/cache_aware_router:0.6.5; MON_IMG=${MON_IMG:?set MON_IMG to an llm-monitor image} BACKEND_SVC=e2e-test/vllm-mock-vllm-svc; BPORT=8000; MODEL=facebook/opt-125m diff --git a/test/e2e/real_helm_e2e.sh b/test/e2e/real_helm_e2e.sh index 547bdba..db741e0 100644 --- a/test/e2e/real_helm_e2e.sh +++ b/test/e2e/real_helm_e2e.sh @@ -5,12 +5,12 @@ # Run it where kubectl and helm reach the cluster. Needs charts/{autoconfig,openresty,cache_aware_router,monitor} # next to it (the autoconfig chart carries the CRD, RBAC and controller). # openresty / monitor / cache_aware_router chart 各在其仓(llm-openresty / llm-monitor / cache_aware_router)的 k8s/helm/ —— 跑前拷进 charts/{openresty,monitor,cache_aware_router}。 -# IMG_TAG=0.4.2 bash real_helm_e2e.sh [--keep] +# IMG_TAG=0.4.3 bash real_helm_e2e.sh [--keep] set -uo pipefail HERE="$(cd "$(dirname "$0")" && pwd)" NS=${NS:-ac-helm} CTRL_NS=${CTRL_NS:-autoconfig} -TAG=${IMG_TAG:-0.4.2} +TAG=${IMG_TAG:-0.4.3} CHARTS=${CHARTS:-$HERE/charts} AC=4pdosc/autoconfig ACR=4pdosc/autoconfig-reload:$TAG diff --git a/test/e2e/real_kimi_e2e.sh b/test/e2e/real_kimi_e2e.sh index cb99d65..fd119aa 100644 --- a/test/e2e/real_kimi_e2e.sh +++ b/test/e2e/real_kimi_e2e.sh @@ -5,12 +5,12 @@ # 真发一条 /v1/chat/completions 经 openresty→CART→kimi 拿真实回答。 # 依赖同目录:charts/{autoconfig,openresty,cache_aware_router,monitor}(autoconfig chart 含 CRD+RBAC+controller)。 # openresty / monitor / cache_aware_router chart 各在其仓(llm-openresty / llm-monitor / cache_aware_router)的 k8s/helm/ —— 跑前拷进 charts/{openresty,monitor,cache_aware_router}。 -# IMG_TAG=0.4.2 bash real_kimi_e2e.sh [--keep] +# IMG_TAG=0.4.3 bash real_kimi_e2e.sh [--keep] set -uo pipefail HERE="$(cd "$(dirname "$0")" && pwd)" NS=${NS:-kimi} # 与 kimi LWS 同 ns(discovery 同 ns) CTRL_NS=${CTRL_NS:-autoconfig} -TAG=${IMG_TAG:-0.4.2} +TAG=${IMG_TAG:-0.4.3} CHARTS=${CHARTS:-$HERE/charts} KIMI_SVC=${KIMI_SVC:-kimi-k26-leader} MODEL=${MODEL:-kimi-k2.6} diff --git a/test/e2e/test_dynamic.sh b/test/e2e/test_dynamic.sh index 8b03e4d..496226b 100644 --- a/test/e2e/test_dynamic.sh +++ b/test/e2e/test_dynamic.sh @@ -3,7 +3,7 @@ # 后端用可扩缩 mock(autoconfig 发现行为不需真推理);真推理/GPU 数据在 resilience/opt 用真 opt-125m。 set -uo pipefail NS=${NS:-llm-route}; CTRL_NS=autoconfig; CHARTS=~/prod-e2e/charts -H=4pdosc; TAG=${IMG_TAG:-0.4.2} +H=4pdosc; TAG=${IMG_TAG:-0.4.3} AC=$H/autoconfig; ACR=$H/autoconfig-reload:$TAG; AH=$H/autoconfig-hagate:$TAG OR_IMG=$H/llm-openresty:0.1.20; CART_IMG=$H/cache_aware_router:0.6.5; MON_IMG=${MON_IMG:?set MON_IMG to an llm-monitor image}; MOCK=python:3.12-alpine KEEP=${KEEP:-0}; FAIL=0 diff --git a/test/e2e/verify_crossns_port.sh b/test/e2e/verify_crossns_port.sh index 21acff8..248774d 100644 --- a/test/e2e/verify_crossns_port.sh +++ b/test/e2e/verify_crossns_port.sh @@ -2,10 +2,10 @@ # 验证两个新特性(0.3.8 端口自动推导 + 0.3.22 service ns/name 跨 ns 发现): # 在【别的 ns(xns)】建 ModelRoute,discovery.service=kimi/kimi-k26-leader(跨 ns)、【不写 port】, # 验证仍发现 kimi leader、端口自动 = 8050。前提:kimi LWS 在跑、autoconfig controller 已升到目标 tag。 -# 依赖:autoconfig helm chart($HERE/charts/autoconfig)。用法:IMG_TAG=0.4.2 bash verify_crossns_port.sh +# 依赖:autoconfig helm chart($HERE/charts/autoconfig)。用法:IMG_TAG=0.4.3 bash verify_crossns_port.sh set -uo pipefail HERE="$(cd "$(dirname "$0")" && pwd)" -TAG=${IMG_TAG:-0.4.2} +TAG=${IMG_TAG:-0.4.3} CTRL_NS=${CTRL_NS:-autoconfig} AC=4pdosc/autoconfig AC_CHART=${AC_CHART:-$HERE/charts/autoconfig} diff --git a/test/e2e/verify_status_error.sh b/test/e2e/verify_status_error.sh index 03fd796..f5114b5 100644 --- a/test/e2e/verify_status_error.sh +++ b/test/e2e/verify_status_error.sh @@ -1,10 +1,10 @@ #!/usr/bin/env bash # 验证:发现失败(多端口 Service 未显式配 port)时,原因写进 ModelRoute status(DiscoverError), # kubectl describe/get 看得到——而非只进 controller 日志。前提:controller 已升到目标 tag。 -# 依赖:autoconfig helm chart($HERE/charts/autoconfig)。用法:IMG_TAG=0.4.2 bash verify_status_error.sh +# 依赖:autoconfig helm chart($HERE/charts/autoconfig)。用法:IMG_TAG=0.4.3 bash verify_status_error.sh set -uo pipefail HERE="$(cd "$(dirname "$0")" && pwd)" -TAG=${IMG_TAG:-0.4.2} +TAG=${IMG_TAG:-0.4.3} CTRL_NS=${CTRL_NS:-autoconfig} AC=4pdosc/autoconfig AC_CHART=${AC_CHART:-$HERE/charts/autoconfig}