feat: 完成 issue #8 ⑥ K8s/Helm 部署底座 + 昇腾适配层
This commit is contained in:
@@ -0,0 +1,51 @@
|
||||
# iAOP-Core · 推理后端抽象层(Inference Backend)
|
||||
|
||||
对应 PRD 5.6「⑥ 部署底座」与 EPIC #8:
|
||||
NVIDIA GPU(5090,Triton/ONNX)与华为昇腾 NPU(ACL/CANN)实现**同一**
|
||||
`InferenceBackend` 接口,业务代码仅依赖接口、不感知硬件;
|
||||
**切换后端 = 改适配层配置**(`backend: gpu|npu`),业务代码零改动。
|
||||
|
||||
## 模块结构
|
||||
|
||||
```
|
||||
core/inference-backend/
|
||||
├── __init__.py 包入口(导出接口/请求/结果/工厂)
|
||||
├── base.py InferenceBackend 统一抽象接口(load_model/infer/health/unload)
|
||||
├── gpu_backend.py NVIDIA 5090 后端(Triton/ONNX,OpenAI 兼容)
|
||||
├── npu_backend.py 华为昇腾 NPU 后端(ACL/CANN,MindIE OpenAI 兼容)
|
||||
├── factory.py 可插拔工厂(backend: gpu|npu -> 实现类)
|
||||
├── config/
|
||||
│ └── backends.template.yaml 模板后端适配层配置资产(ti-cl4 示例)
|
||||
└── tests/
|
||||
├── _bootstrap.py 测试引导(目录含连字符,挂载包名 inference_backend)
|
||||
└── test_inference_backend.py 接口/工厂/行为单元测试
|
||||
```
|
||||
|
||||
## 统一接口(PRD 5.6:loadModel / infer / health / unload)
|
||||
|
||||
业务代码只依赖 `InferenceBackend` 接口,不 import 任何具体后端:
|
||||
|
||||
```python
|
||||
from inference_backend.base import InferRequest
|
||||
from inference_backend.factory import build_backend, load_backend_config
|
||||
|
||||
backend = build_backend(load_backend_config("config/backends.template.yaml"))
|
||||
|
||||
backend.load_model()
|
||||
health = backend.health() # 健康巡检
|
||||
result = backend.infer(InferRequest(prompt="请解释炉温报警")) # result.text
|
||||
backend.unload()
|
||||
```
|
||||
|
||||
## 切换后端(仅改配置)
|
||||
|
||||
`config/backends.template.yaml` 中 `backend: gpu` 改为 `backend: npu` 即切换到
|
||||
昇腾适配层;同一份业务代码、同一调用面,硬件差异被适配层隔离。
|
||||
新增硬件:在 `factory.py` 的 `BACKEND_REGISTRY` 注册实现类即可。
|
||||
|
||||
## 运行测试
|
||||
|
||||
```bash
|
||||
cd core/inference-backend/tests
|
||||
python -m unittest discover -s . -p "test_*.py"
|
||||
```
|
||||
@@ -0,0 +1,18 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""iAOP-Core · 推理后端抽象层(Inference Backend)。
|
||||
|
||||
对应 PRD 5.6「⑥ 部署底座」与 EPIC #8:
|
||||
NVIDIA GPU(5090,Triton/ONNX)与华为昇腾 NPU(ACL/CANN)实现同一
|
||||
`InferenceBackend` 接口,业务代码仅依赖接口、不感知硬件;
|
||||
切换后端 = 改适配层配置(`backend: gpu|npu`),业务代码零改动。
|
||||
"""
|
||||
from inference_backend.base import InferRequest, InferResult, InferenceBackend
|
||||
from inference_backend.factory import build_backend, load_backend_config
|
||||
|
||||
__all__ = [
|
||||
"InferenceBackend",
|
||||
"InferRequest",
|
||||
"InferResult",
|
||||
"build_backend",
|
||||
"load_backend_config",
|
||||
]
|
||||
@@ -0,0 +1,103 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""推理后端统一抽象接口(PRD 5.6「⑥ 部署底座」)。
|
||||
|
||||
设计约束(对齐产品设计文档 5.6):
|
||||
- 统一接口 `load_model / infer / health / unload`,与底层硬件无关;
|
||||
- NVIDIA 5090 实现(Triton/ONNX)与华为昇腾实现(ACL/CANN)均实现本接口;
|
||||
- 业务代码只 import 本模块,不 import 任何具体后端;
|
||||
- 切换后端 = 修改适配层配置(见 config/backends.template.yaml),业务代码零改动。
|
||||
"""
|
||||
from abc import ABC, abstractmethod
|
||||
from dataclasses import dataclass, field
|
||||
import json
|
||||
import time
|
||||
import urllib.request
|
||||
|
||||
|
||||
@dataclass
|
||||
class InferRequest:
|
||||
"""统一的推理请求载荷(业务侧构造,与硬件无关)。"""
|
||||
|
||||
prompt: str
|
||||
max_tokens: int = 1024
|
||||
temperature: float = 0.1
|
||||
extra: dict = field(default_factory=dict)
|
||||
|
||||
def to_payload(self) -> dict:
|
||||
"""转换为后端服务 OpenAI 兼容的请求体。"""
|
||||
return {
|
||||
"model": self.extra.get("model") if self.extra.get("model") else None,
|
||||
"prompt": self.prompt,
|
||||
"max_tokens": self.max_tokens,
|
||||
"temperature": self.temperature,
|
||||
}
|
||||
|
||||
|
||||
@dataclass
|
||||
class InferResult:
|
||||
"""统一的推理结果(业务侧消费,不感知硬件)。"""
|
||||
|
||||
text: str
|
||||
backend: str
|
||||
latency_ms: float = 0.0
|
||||
meta: dict = field(default_factory=dict)
|
||||
|
||||
|
||||
class InferenceBackend(ABC):
|
||||
"""推理后端统一接口(对应 PRD 5.6:loadModel / infer / health / unload)。"""
|
||||
|
||||
#: 后端标识:gpu(NVIDIA 5090)| npu(华为昇腾)
|
||||
backend_name = "base"
|
||||
|
||||
def __init__(self, endpoint: str = "", model: str = "iaop-default",
|
||||
timeout_seconds: float = 10.0):
|
||||
self.endpoint = (endpoint or "").rstrip("/")
|
||||
self.model = model
|
||||
self.timeout = float(timeout_seconds)
|
||||
|
||||
# ---- 统一接口(业务代码仅依赖以下四个方法) ----
|
||||
|
||||
@abstractmethod
|
||||
def load_model(self, model_name: str | None = None) -> dict:
|
||||
"""加载/热载模型(对应 PRD 的 loadModel)。返回加载状态。"""
|
||||
|
||||
@abstractmethod
|
||||
def infer(self, request: InferRequest) -> InferResult:
|
||||
"""执行一次推理(对应 PRD 的 infer)。返回统一结果对象。"""
|
||||
|
||||
@abstractmethod
|
||||
def health(self) -> dict:
|
||||
"""健康巡检(对应 PRD 的 health)。返回后端状态与版本信息。"""
|
||||
|
||||
@abstractmethod
|
||||
def unload(self) -> dict:
|
||||
"""卸载模型/释放显存(对应 PRD 的 unload)。返回卸载结果。"""
|
||||
|
||||
# ---- 内部工具:HTTP JSON 调用(各后端共享) ----
|
||||
|
||||
def _post_json(self, path: str, payload: dict) -> dict:
|
||||
"""向后端服务发起 JSON POST 请求,返回解析后的响应体。"""
|
||||
url = self.endpoint + path
|
||||
data = json.dumps(payload).encode("utf-8")
|
||||
req = urllib.request.Request(
|
||||
url, data=data, headers={"Content-Type": "application/json"}
|
||||
)
|
||||
started = time.monotonic()
|
||||
with urllib.request.urlopen(req, timeout=self.timeout) as resp:
|
||||
raw = resp.read().decode("utf-8")
|
||||
meta = {"http_ms": round((time.monotonic() - started) * 1000, 2)}
|
||||
body = json.loads(raw) if raw else {}
|
||||
if isinstance(body, dict):
|
||||
body.setdefault("_http_ms", meta["http_ms"])
|
||||
return body
|
||||
|
||||
def _healthz(self) -> dict:
|
||||
"""通用 /health 探针封装。"""
|
||||
if not self.endpoint:
|
||||
return {
|
||||
"status": "ok",
|
||||
"backend": self.backend_name,
|
||||
"model": self.model,
|
||||
"reason": "dry-run(未配置 endpoint,适配层就绪)",
|
||||
}
|
||||
return self._post_json("/health", {"model": self.model})
|
||||
@@ -0,0 +1,41 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# 模板「推理后端适配层」配置资产示例:ti-cl4(氯化车间/海绵钛,Template-Ti 一期)。
|
||||
#
|
||||
# 说明:
|
||||
# - 这是「推理后端选择」配置点(PRD 5.6 ⑥ 部署底座):
|
||||
# 切换后端(NVIDIA 5090 GPU ↔ 华为昇腾 NPU)**只改本文件 backend 字段**,
|
||||
# 业务代码零改动(统一走 InferenceBackend 接口);
|
||||
# - backend: gpu —— NVIDIA 5090(Triton/ONNX,vLLM/Triton OpenAI 兼容接口)
|
||||
# npu —— 华为昇腾(ACL/CANN,MindIE/onnxruntime-ascend)
|
||||
# - endpoint 为空时进入 dry-run 模式(适配层就绪,便于离线验证/联调);
|
||||
# - 该配置由 Helm 渲染注入(deploy/k8s/helm/iaop/values.yaml -> ConfigMap)。
|
||||
template: ti-cl4
|
||||
version: 1.0.0
|
||||
|
||||
# ---- 推理后端选择(切换后端只改这一处) ----
|
||||
backend: gpu # gpu | npu
|
||||
|
||||
inference:
|
||||
# 与 Helm values.inference 保持同构,便于一键部署时直接覆盖
|
||||
model: iaop-ti-cl4-v1
|
||||
endpoint: http://iaop-inference.iaop.svc.cluster.local:8000/v1
|
||||
timeout_seconds: 60
|
||||
runtime: vllm # gpu: vllm|triton;npu: mindie|onnx-ascend
|
||||
device: nvidia-5090 # gpu: nvidia-5090;npu: ascend-310p|ascend-910b
|
||||
cann_version: "" # npu: CANN 工具链版本(如 8.0),gpu 忽略
|
||||
max_tokens: 1024
|
||||
temperature: 0.1
|
||||
|
||||
# ---- 资源配额(Helm 部署时以此为默认值) ----
|
||||
resources:
|
||||
requests:
|
||||
cpu: "2"
|
||||
memory: 8Gi
|
||||
limits:
|
||||
cpu: "8"
|
||||
memory: 32Gi
|
||||
|
||||
# ---- 灰度发布策略(ArgoCD 滚动发布参数,PRD 5.6 配置点) ----
|
||||
rollingUpdate:
|
||||
maxUnavailable: 0
|
||||
maxSurge: 1
|
||||
@@ -0,0 +1,64 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""推理后端工厂:按配置选择后端实现(可插拔适配层)。
|
||||
|
||||
PRD 5.6「配置点:资源配额、推理后端选择、灰度发布策略」——
|
||||
`backend: gpu|npu` 即推理后端选择;业务代码只调用 `build_backend()`
|
||||
返回的接口对象,不感知具体硬件。
|
||||
"""
|
||||
import os
|
||||
|
||||
from inference_backend.base import InferenceBackend
|
||||
from inference_backend.gpu_backend import NvidiaGpuBackend
|
||||
from inference_backend.npu_backend import AscendNpuBackend
|
||||
|
||||
#: 可插拔后端注册表:配置名 -> 实现类。新增硬件只需注册新类。
|
||||
BACKEND_REGISTRY = {
|
||||
"gpu": NvidiaGpuBackend, # NVIDIA 5090(Triton/ONNX)
|
||||
"npu": AscendNpuBackend, # 华为昇腾(ACL/CANN)
|
||||
}
|
||||
|
||||
|
||||
def build_backend(config: dict) -> InferenceBackend:
|
||||
"""依据配置构建推理后端实例(切换后端仅改配置,业务代码零改动)。
|
||||
|
||||
Args:
|
||||
config: 后端配置字典(见 config/backends.template.yaml),
|
||||
至少包含 ``backend`` 键(gpu | npu)。
|
||||
|
||||
Returns:
|
||||
实现了 :class:`InferenceBackend` 接口的后端实例。
|
||||
|
||||
Raises:
|
||||
ValueError: 配置缺失或指定了未注册的后端。
|
||||
"""
|
||||
if not isinstance(config, dict) or not config.get("backend"):
|
||||
raise ValueError("推理后端配置缺失:需要 backend: gpu|npu")
|
||||
name = str(config["backend"]).lower()
|
||||
if name not in BACKEND_REGISTRY:
|
||||
raise ValueError(
|
||||
f"未注册的推理后端: {name!r},可用: {sorted(BACKEND_REGISTRY)}"
|
||||
)
|
||||
cls = BACKEND_REGISTRY[name]
|
||||
inf = config.get("inference", {}) or {}
|
||||
return cls(
|
||||
endpoint=inf.get("endpoint", ""),
|
||||
model=inf.get("model", "iaop-default"),
|
||||
timeout_seconds=float(inf.get("timeout_seconds", 10)),
|
||||
runtime=inf.get("runtime", ""),
|
||||
device=inf.get("device", ""),
|
||||
cann_version=inf.get("cann_version", ""),
|
||||
)
|
||||
|
||||
|
||||
def load_backend_config(path: str) -> dict:
|
||||
"""从 YAML 配置资产加载后端配置(模板可覆盖资产)。"""
|
||||
import yaml
|
||||
|
||||
with open(path, "r", encoding="utf-8") as fh:
|
||||
return yaml.safe_load(fh) or {}
|
||||
|
||||
|
||||
def default_config_path() -> str:
|
||||
"""返回本模块模板配置资产的默认路径。"""
|
||||
return os.path.join(os.path.dirname(os.path.abspath(__file__)),
|
||||
"config", "backends.template.yaml")
|
||||
@@ -0,0 +1,74 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""NVIDIA GPU 推理后端实现(5090,Triton/ONNX)。
|
||||
|
||||
对应 PRD 5.6:5090 实现(Triton/ONNX)——通过 OpenAI 兼容接口
|
||||
(vLLM / Triton Inference Server)对外提供推理,业务侧统一走
|
||||
`InferenceBackend` 接口,不感知硬件。
|
||||
"""
|
||||
from inference_backend.base import InferenceBackend, InferRequest, InferResult
|
||||
|
||||
|
||||
class NvidiaGpuBackend(InferenceBackend):
|
||||
"""NVIDIA GPU 后端:面向 5090 服务器(Triton/ONNX,OpenAI 兼容)。"""
|
||||
|
||||
backend_name = "gpu"
|
||||
|
||||
def __init__(self, endpoint: str = "", model: str = "iaop-default",
|
||||
timeout_seconds: float = 10.0, runtime: str = "vllm",
|
||||
device: str = "nvidia-5090", **kwargs):
|
||||
super().__init__(endpoint, model, timeout_seconds)
|
||||
self.runtime = runtime # vllm | triton
|
||||
self.device = device # 硬件型号(默认 5090)
|
||||
self._loaded = False
|
||||
|
||||
def load_model(self, model_name: str | None = None) -> dict:
|
||||
"""加载模型到 GPU。无 endpoint 时仅维护本地状态(适配层就绪)。"""
|
||||
model_name = model_name or self.model
|
||||
if not self.endpoint:
|
||||
self._loaded = True
|
||||
return {"status": "ok", "backend": self.backend_name,
|
||||
"model": model_name, "device": self.device,
|
||||
"reason": "dry-run(未配置 endpoint)"}
|
||||
body = self._post_json("/v1/models/load",
|
||||
{"model": model_name, "device": self.device})
|
||||
self._loaded = body.get("status") in ("ok", "loaded", "ready")
|
||||
return body
|
||||
|
||||
def infer(self, request: InferRequest) -> InferResult:
|
||||
"""OpenAI 兼容生成(/v1/chat/completions)。"""
|
||||
started = __import__("time").monotonic()
|
||||
payload = request.to_payload()
|
||||
payload["model"] = payload["model"] or self.model
|
||||
body = self._post_json("/v1/chat/completions", payload)
|
||||
latency_ms = round((__import__("time").monotonic() - started) * 1000, 2)
|
||||
try:
|
||||
text = body["choices"][0]["message"]["content"]
|
||||
except (KeyError, IndexError, TypeError):
|
||||
text = str(body)
|
||||
return InferResult(
|
||||
text=text,
|
||||
backend=self.backend_name,
|
||||
latency_ms=latency_ms,
|
||||
meta={"runtime": self.runtime, "device": self.device,
|
||||
"model": self.model, "raw": body},
|
||||
)
|
||||
|
||||
def health(self) -> dict:
|
||||
"""健康巡检:探测 /health,返回后端/设备/运行时信息。"""
|
||||
base = self._healthz()
|
||||
base.update({
|
||||
"backend": self.backend_name,
|
||||
"runtime": self.runtime,
|
||||
"device": self.device,
|
||||
"model": self.model,
|
||||
"loaded": self._loaded,
|
||||
})
|
||||
return base
|
||||
|
||||
def unload(self) -> dict:
|
||||
"""卸载模型、释放显存。"""
|
||||
self._loaded = False
|
||||
if not self.endpoint:
|
||||
return {"status": "ok", "backend": self.backend_name,
|
||||
"reason": "dry-run(未配置 endpoint)"}
|
||||
return self._post_json("/v1/models/unload", {"model": self.model})
|
||||
@@ -0,0 +1,81 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""华为昇腾 NPU 推理后端实现(ACL/CANN)。
|
||||
|
||||
对应 PRD 5.6:昇腾实现(ACL/CANN)——通过昇腾推理服务
|
||||
(MindIE / onnxruntime-ascend)的 OpenAI 兼容接口对外提供推理,
|
||||
与 NVIDIA GPU 后端实现同一 `InferenceBackend` 接口:
|
||||
**切换后端仅改适配层配置,业务代码零改动**。
|
||||
"""
|
||||
from inference_backend.base import InferenceBackend, InferRequest, InferResult
|
||||
|
||||
|
||||
class AscendNpuBackend(InferenceBackend):
|
||||
"""华为昇腾 NPU 后端:面向昇腾 310P/910B(CANN/MindIE)。"""
|
||||
|
||||
backend_name = "npu"
|
||||
|
||||
def __init__(self, endpoint: str = "", model: str = "iaop-default",
|
||||
timeout_seconds: float = 10.0, runtime: str = "mindie",
|
||||
device: str = "ascend-910b", cann_version: str = "8.0",
|
||||
**kwargs):
|
||||
super().__init__(endpoint, model, timeout_seconds)
|
||||
self.runtime = runtime # mindie | onnx-ascend
|
||||
self.device = device # ascend-310p | ascend-910b
|
||||
self.cann_version = cann_version # CANN 工具链版本
|
||||
self._loaded = False
|
||||
|
||||
def load_model(self, model_name: str | None = None) -> dict:
|
||||
"""加载模型到 NPU(ACL aclmdlLoadFromFile 语义)。"""
|
||||
model_name = model_name or self.model
|
||||
if not self.endpoint:
|
||||
self._loaded = True
|
||||
return {"status": "ok", "backend": self.backend_name,
|
||||
"model": model_name, "device": self.device,
|
||||
"reason": "dry-run(未配置 endpoint)"}
|
||||
body = self._post_json("/acl/models/load",
|
||||
{"model": model_name, "device": self.device,
|
||||
"cann_version": self.cann_version})
|
||||
self._loaded = body.get("status") in ("ok", "loaded", "ready")
|
||||
return body
|
||||
|
||||
def infer(self, request: InferRequest) -> InferResult:
|
||||
"""昇腾推理(MindIE OpenAI 兼容 /v1/chat/completions)。"""
|
||||
import time
|
||||
started = time.monotonic()
|
||||
payload = request.to_payload()
|
||||
payload["model"] = payload["model"] or self.model
|
||||
body = self._post_json("/v1/chat/completions", payload)
|
||||
latency_ms = round((time.monotonic() - started) * 1000, 2)
|
||||
try:
|
||||
text = body["choices"][0]["message"]["content"]
|
||||
except (KeyError, IndexError, TypeError):
|
||||
text = str(body)
|
||||
return InferResult(
|
||||
text=text,
|
||||
backend=self.backend_name,
|
||||
latency_ms=latency_ms,
|
||||
meta={"runtime": self.runtime, "device": self.device,
|
||||
"cann_version": self.cann_version, "model": self.model,
|
||||
"raw": body},
|
||||
)
|
||||
|
||||
def health(self) -> dict:
|
||||
"""健康巡检:探测 /health,返回后端/设备/CANN 版本信息。"""
|
||||
base = self._healthz()
|
||||
base.update({
|
||||
"backend": self.backend_name,
|
||||
"runtime": self.runtime,
|
||||
"device": self.device,
|
||||
"cann_version": self.cann_version,
|
||||
"model": self.model,
|
||||
"loaded": self._loaded,
|
||||
})
|
||||
return base
|
||||
|
||||
def unload(self) -> dict:
|
||||
"""卸载模型、释放 NPU 资源(ACL aclmdlUnload 语义)。"""
|
||||
self._loaded = False
|
||||
if not self.endpoint:
|
||||
return {"status": "ok", "backend": self.backend_name,
|
||||
"reason": "dry-run(未配置 endpoint)"}
|
||||
return self._post_json("/acl/models/unload", {"model": self.model})
|
||||
@@ -0,0 +1,16 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""测试引导:把 `core/inference-backend` 以包名 `inference_backend` 挂载到 sys.modules。
|
||||
|
||||
目录名 `inference-backend` 含连字符,无法直接以包名 import;挂载后模块内相对导入
|
||||
(`from inference_backend.base import ...`)在 unittest 发现机制下可正常解析。
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
import types
|
||||
|
||||
BACKEND_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
sys.path.insert(0, BACKEND_DIR)
|
||||
if "inference_backend" not in sys.modules:
|
||||
pkg = types.ModuleType("inference_backend")
|
||||
pkg.__path__ = [BACKEND_DIR]
|
||||
sys.modules["inference_backend"] = pkg
|
||||
@@ -0,0 +1,127 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""推理后端抽象层单元测试(EPIC #8 ⑥ 部署底座 / PRD 5.6)。
|
||||
|
||||
覆盖:
|
||||
1. 模板配置资产可解析、含 backend 选择键;
|
||||
2. 工厂按配置构建 GPU / NPU 后端(可插拔);
|
||||
3. 统一接口 load_model / infer / health / unload 行为;
|
||||
4. 切换后端仅改配置:同一业务调用面(接口方法)在两种后端下等价;
|
||||
5. 非法配置报错。
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
import unittest
|
||||
from unittest import mock
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
import _bootstrap # noqa: F401
|
||||
|
||||
from inference_backend.base import InferRequest, InferenceBackend # noqa: E402
|
||||
from inference_backend.factory import ( # noqa: E402
|
||||
BACKEND_REGISTRY,
|
||||
build_backend,
|
||||
default_config_path,
|
||||
load_backend_config,
|
||||
)
|
||||
|
||||
CONFIG_PATH = default_config_path()
|
||||
|
||||
|
||||
class TestTemplateConfig(unittest.TestCase):
|
||||
"""模板配置资产:可解析且包含后端选择配置点。"""
|
||||
|
||||
def test_template_parses_and_has_backend_key(self):
|
||||
cfg = load_backend_config(CONFIG_PATH)
|
||||
self.assertIn("backend", cfg)
|
||||
self.assertIn(cfg["backend"].lower(), ("gpu", "npu"))
|
||||
self.assertIn("inference", cfg)
|
||||
|
||||
def test_registry_covers_gpu_and_npu(self):
|
||||
self.assertEqual(sorted(BACKEND_REGISTRY), ["gpu", "npu"])
|
||||
|
||||
|
||||
class TestFactory(unittest.TestCase):
|
||||
"""工厂:按配置选择后端实现(可插拔适配层)。"""
|
||||
|
||||
def test_build_gpu_from_template(self):
|
||||
cfg = load_backend_config(CONFIG_PATH)
|
||||
backend = build_backend(cfg)
|
||||
self.assertIsInstance(backend, InferenceBackend)
|
||||
self.assertEqual(backend.backend_name, cfg["backend"].lower())
|
||||
self.assertEqual(backend.model, cfg["inference"]["model"])
|
||||
|
||||
def test_build_npu_by_config_switch(self):
|
||||
cfg = load_backend_config(CONFIG_PATH)
|
||||
cfg["backend"] = "npu"
|
||||
backend = build_backend(cfg)
|
||||
self.assertEqual(backend.backend_name, "npu")
|
||||
# 业务调用面不变(接口等价,不感知硬件)
|
||||
self.assertTrue(callable(backend.load_model))
|
||||
self.assertTrue(callable(backend.infer))
|
||||
self.assertTrue(callable(backend.health))
|
||||
self.assertTrue(callable(backend.unload))
|
||||
|
||||
def test_build_rejects_unknown_backend(self):
|
||||
with self.assertRaises(ValueError):
|
||||
build_backend({"backend": "tpu"})
|
||||
|
||||
def test_build_rejects_missing_backend(self):
|
||||
with self.assertRaises(ValueError):
|
||||
build_backend({"inference": {"model": "x"}})
|
||||
|
||||
|
||||
class TestBackendBehavior(unittest.TestCase):
|
||||
"""统一接口行为(dry-run:未配置 endpoint 时适配层就绪)。"""
|
||||
|
||||
def setUp(self):
|
||||
cfg = load_backend_config(CONFIG_PATH)
|
||||
cfg["inference"]["endpoint"] = "" # dry-run
|
||||
self.gpu = build_backend(cfg)
|
||||
cfg["backend"] = "npu"
|
||||
self.npu = build_backend(cfg)
|
||||
|
||||
def test_dry_run_load_health_unload(self):
|
||||
for backend in (self.gpu, self.npu):
|
||||
loaded = backend.load_model()
|
||||
self.assertEqual(loaded["status"], "ok")
|
||||
health = backend.health()
|
||||
self.assertEqual(health["status"], "ok")
|
||||
self.assertEqual(health["backend"], backend.backend_name)
|
||||
self.assertTrue(health["loaded"])
|
||||
unloaded = backend.unload()
|
||||
self.assertEqual(unloaded["status"], "ok")
|
||||
|
||||
def test_infer_extracts_text_from_openai_response(self):
|
||||
fake_body = {
|
||||
"choices": [{"message": {"content": "炉温偏高,建议降低加料比"}}],
|
||||
}
|
||||
for backend in (self.gpu, self.npu):
|
||||
with mock.patch.object(backend, "_post_json",
|
||||
return_value=fake_body) as post:
|
||||
result = backend.infer(InferRequest(prompt="请解释炉温报警"))
|
||||
post.assert_called_once_with(
|
||||
"/v1/chat/completions",
|
||||
mock.ANY,
|
||||
)
|
||||
self.assertEqual(result.text, "炉温偏高,建议降低加料比")
|
||||
self.assertEqual(result.backend, backend.backend_name)
|
||||
# mock 调用耗时可能为 0,仅断言字段类型与量级
|
||||
self.assertIsInstance(result.latency_ms, float)
|
||||
self.assertGreaterEqual(result.latency_ms, 0.0)
|
||||
|
||||
def test_infer_payload_carries_model(self):
|
||||
with mock.patch.object(self.gpu, "_post_json",
|
||||
return_value={"choices": []}) as post:
|
||||
self.gpu.infer(InferRequest(prompt="hi", max_tokens=64))
|
||||
_, payload = post.call_args[0]
|
||||
self.assertEqual(payload["model"], self.gpu.model)
|
||||
self.assertEqual(payload["max_tokens"], 64)
|
||||
|
||||
def test_health_dry_run_notes_reason(self):
|
||||
health = self.gpu.health()
|
||||
self.assertIn("reason", health)
|
||||
self.assertIn("dry-run", health["reason"])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,16 @@
|
||||
apiVersion: v2
|
||||
name: iaop
|
||||
description: |
|
||||
iAOP(云美工业AI优化平台)一键 Helm 部署底座(PRD 5.6「⑥ 部署底座」,
|
||||
EPIC #8):内核(采集/数据总线/LLM 网关/RAG)+ 行业模板一键部署;
|
||||
推理后端(NVIDIA 5090 GPU / 华为昇腾 NPU)可插拔,切换后端仅改 values。
|
||||
type: application
|
||||
version: 1.0.0
|
||||
appVersion: "v1.0.0"
|
||||
keywords:
|
||||
- iAOP
|
||||
- industrial-ai
|
||||
- k8s
|
||||
- helm
|
||||
- inference
|
||||
- ascend
|
||||
@@ -0,0 +1,65 @@
|
||||
# iAOP Helm Chart(一键部署底座)
|
||||
|
||||
对应 PRD 5.6「⑥ 部署底座」与 EPIC #8:
|
||||
K8s/Helm/ArgoCD 部署底座;GPU/NPU 推理后端可插拔(5090 / 华为昇腾)。
|
||||
**验收:一套 Chart 部署内核 + 模板;切换后端仅改 values,不动业务代码。**
|
||||
|
||||
## 目录结构
|
||||
|
||||
```
|
||||
deploy/k8s/helm/iaop/
|
||||
├── Chart.yaml Chart 元信息(name/version/appVersion)
|
||||
├── values.yaml 全部配置点(镜像/后端/副本/资源/存储/灰度/域名)
|
||||
├── templates/
|
||||
│ ├── _helpers.tpl 标签/名称辅助模板
|
||||
│ ├── backend-configmap.yaml 推理后端适配层配置(按 backend 分支渲染)
|
||||
│ ├── deployment.yaml 推理服务 Deployment(nodeSelector 按后端分流)
|
||||
│ ├── service.yaml ClusterIP 服务(:8000/v1)
|
||||
│ ├── ingress.yaml 可选域名入口
|
||||
│ ├── pvc.yaml 模型权重/日志持久化
|
||||
│ └── NOTES.txt 部署后提示
|
||||
└── README.md
|
||||
```
|
||||
|
||||
## 一键部署
|
||||
|
||||
```bash
|
||||
helm repo add iaop http://git.xayunmei.com/iaop/charts # 或本地路径
|
||||
helm install iaop deploy/k8s/helm/iaop -n iaop --create-namespace
|
||||
```
|
||||
|
||||
## 切换推理后端(仅改 values,业务代码零改动)
|
||||
|
||||
```bash
|
||||
# 默认 gpu(NVIDIA 5090,vLLM/Triton):
|
||||
helm install iaop deploy/k8s/helm/iaop -n iaop \
|
||||
--set inference.backend=gpu \
|
||||
--set inference.device=nvidia-5090
|
||||
|
||||
# 切换为华为昇腾 NPU(ACL/CANN/MindIE):
|
||||
helm upgrade iaop deploy/k8s/helm/iaop -n iaop \
|
||||
--set inference.backend=npu \
|
||||
--set inference.device=ascend-910b \
|
||||
--set inference.cannVersion=8.0
|
||||
```
|
||||
|
||||
切换行为(由模板自动处理):
|
||||
- `backend-configmap.yaml`:渲染对应后端适配层配置(npu 追加 `cann_version`);
|
||||
- `deployment.yaml`:nodeSelector 自动切到 `ascend.com/npu=true`(GPU 为 `nvidia.com/gpu=true`),
|
||||
并在 Pod 标签标注 `iaop.ai/inference-backend`;
|
||||
- 后端实现差异全部收敛在 `core/inference-backend/` 适配层,业务代码零改动。
|
||||
|
||||
## 其他配置点(PRD 5.6)
|
||||
|
||||
| 配置点 | values 路径 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| 资源配额 | `resources.requests/limits` | CPU/内存配额 |
|
||||
| 推理后端选择 | `inference.backend` | `gpu` / `npu` |
|
||||
| 灰度发布策略 | `rollingUpdate` | `maxUnavailable: 0, maxSurge: 1`(滚动) |
|
||||
| 存储 | `storage.*` | 模型权重持久卷 |
|
||||
| 域名入口 | `ingress.*` | 可选 Ingress |
|
||||
|
||||
## 说明
|
||||
|
||||
- Chart 未引入任何外部依赖(dependencies 为空),`helm template` 可离线渲染;
|
||||
- 推理服务镜像对应 `core/inference-backend/` 适配层容器化(本文档发布配套镜像时更新 `image.tag`)。
|
||||
@@ -0,0 +1,75 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Helm Chart 结构 sanity 检查(无 helm CLI 环境下的离线基本验证)。
|
||||
|
||||
检查项:
|
||||
1. Chart.yaml / values.yaml 均为合法 YAML;
|
||||
2. templates/ 下所有模板文件存在且非空;
|
||||
3. 模板中引用的 `.Values.xxx` 键均能在 values.yaml 中找到(防拼写错误);
|
||||
4. 每个模板文件的 Go template 标记 `{{` / `}}` 数量配平。
|
||||
|
||||
用法:python _sanity_check.py
|
||||
"""
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
TEMPLATES = os.path.join(HERE, "templates")
|
||||
|
||||
import yaml # noqa: E402
|
||||
|
||||
|
||||
def load(path):
|
||||
with open(path, "r", encoding="utf-8") as fh:
|
||||
return yaml.safe_load(fh)
|
||||
|
||||
|
||||
def values_paths(text):
|
||||
"""提取模板文本中的 .Values.<a>.<b> 路径集合。"""
|
||||
return set(re.findall(r"\.Values\.([A-Za-z0-9_]+(?:\.[A-Za-z0-9_]+)*)", text))
|
||||
|
||||
|
||||
def resolve(values, dotted):
|
||||
node = values
|
||||
for key in dotted.split("."):
|
||||
if not isinstance(node, dict) or key not in node:
|
||||
return False
|
||||
node = node[key]
|
||||
return True
|
||||
|
||||
|
||||
def main():
|
||||
failures = []
|
||||
|
||||
chart = load(os.path.join(HERE, "Chart.yaml"))
|
||||
assert chart.get("apiVersion") == "v2", "Chart.yaml 应为 Helm v2 chart"
|
||||
assert chart.get("name") == "iaop", "Chart.name 应为 iaop"
|
||||
|
||||
values = load(os.path.join(HERE, "values.yaml"))
|
||||
assert values.get("inference", {}).get("backend") in ("gpu", "npu"), \
|
||||
"values.inference.backend 应为 gpu|npu"
|
||||
|
||||
for name in sorted(os.listdir(TEMPLATES)):
|
||||
path = os.path.join(TEMPLATES, name)
|
||||
if not os.path.isfile(path):
|
||||
continue
|
||||
text = open(path, "r", encoding="utf-8").read()
|
||||
if not text.strip():
|
||||
failures.append(f"{name}: 模板文件为空")
|
||||
continue
|
||||
if text.count("{{") != text.count("}}"):
|
||||
failures.append(f"{name}: Go template 标记 {{/}} 数量不配平")
|
||||
for dotted in values_paths(text):
|
||||
if not resolve(values, dotted):
|
||||
failures.append(f"{name}: 引用了 values.yaml 中不存在的键 .Values.{dotted}")
|
||||
|
||||
if failures:
|
||||
print("FAIL")
|
||||
for f in failures:
|
||||
print(" -", f)
|
||||
sys.exit(1)
|
||||
print(f"OK: Chart/values 合法,templates/{len(os.listdir(TEMPLATES))} 个模板校验通过")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,13 @@
|
||||
Thank you for installing {{ .Chart.Name }} {{ .Chart.Version }} (backend: {{ .Values.inference.backend }}).
|
||||
|
||||
iAOP 部署底座(PRD 5.6 / EPIC #8)已就绪,Release 名:{{ .Release.Name }}。
|
||||
|
||||
快速使用:
|
||||
1. 查看部署状态:
|
||||
kubectl get deploy,svc -l app.kubernetes.io/instance={{ .Release.Name }}
|
||||
2. 健康巡检:
|
||||
curl http://{{ include "iaop.fullname" . }}:{{ .Values.service.port }}/health
|
||||
3. 切换推理后端(NVIDIA GPU ↔ 华为昇腾 NPU),仅改 values 后升级:
|
||||
helm upgrade {{ .Release.Name }} . --set inference.backend=npu
|
||||
4. 如需域名入口:
|
||||
helm upgrade {{ .Release.Name }} . --set ingress.enabled=true --set ingress.host=iaop.example.com
|
||||
@@ -0,0 +1,43 @@
|
||||
{{/*
|
||||
iAOP Helm Chart 辅助模板(_helpers.tpl)
|
||||
*/}}
|
||||
|
||||
{{/*
|
||||
展开 Chart 全名(release 名 + chart 名,截断到 63 字符)。
|
||||
*/}}
|
||||
{{- define "iaop.fullname" -}}
|
||||
{{- printf "%s-%s" .Release.Name .Chart.Name | trunc 63 | trimSuffix "-" -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
Chart 名称标签。
|
||||
*/}}
|
||||
{{- define "iaop.name" -}}
|
||||
{{- .Chart.Name | trunc 63 | trimSuffix "-" -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
通用标签。
|
||||
*/}}
|
||||
{{- define "iaop.labels" -}}
|
||||
helm.sh/chart: {{ printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" }}
|
||||
app.kubernetes.io/name: {{ include "iaop.name" . }}
|
||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
|
||||
app.kubernetes.io/managed-by: {{ .Release.Service }}
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
选择器标签。
|
||||
*/}}
|
||||
{{- define "iaop.selectorLabels" -}}
|
||||
app.kubernetes.io/name: {{ include "iaop.name" . }}
|
||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
推理后端镜像完整名(repository:tag)。
|
||||
*/}}
|
||||
{{- define "iaop.image" -}}
|
||||
{{- printf "%s:%s" .Values.image.repository .Values.image.tag -}}
|
||||
{{- end -}}
|
||||
@@ -0,0 +1,36 @@
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: {{ include "iaop.fullname" . }}-backend
|
||||
labels:
|
||||
{{- include "iaop.labels" . | nindent 4 }}
|
||||
data:
|
||||
# 推理后端适配层配置(PRD 5.6:切换后端仅改 values.inference.backend)。
|
||||
# 该配置与 core/inference-backend/config/backends.template.yaml 同构,
|
||||
# 由推理服务容器在启动时读取。
|
||||
backends.yaml: |
|
||||
template: ti-cl4
|
||||
version: 1.0.0
|
||||
backend: {{ .Values.inference.backend }}
|
||||
inference:
|
||||
model: {{ .Values.inference.model | quote }}
|
||||
endpoint: http://{{ include "iaop.fullname" . }}:{{ .Values.service.port }}/v1
|
||||
timeout_seconds: {{ .Values.inference.timeoutSeconds }}
|
||||
runtime: {{ .Values.inference.runtime | quote }}
|
||||
device: {{ .Values.inference.device | quote }}
|
||||
max_tokens: {{ .Values.inference.maxTokens }}
|
||||
temperature: {{ .Values.inference.temperature }}
|
||||
{{- if eq .Values.inference.backend "npu" }}
|
||||
# 昇腾适配层专用字段(CANN 工具链版本;gpu 后端忽略)
|
||||
cann_version: {{ .Values.inference.cannVersion | default "" | quote }}
|
||||
{{- end }}
|
||||
resources:
|
||||
requests:
|
||||
cpu: {{ .Values.resources.requests.cpu | quote }}
|
||||
memory: {{ .Values.resources.requests.memory | quote }}
|
||||
limits:
|
||||
cpu: {{ .Values.resources.limits.cpu | quote }}
|
||||
memory: {{ .Values.resources.limits.memory | quote }}
|
||||
rollingUpdate:
|
||||
maxUnavailable: {{ .Values.rollingUpdate.maxUnavailable }}
|
||||
maxSurge: {{ .Values.rollingUpdate.maxSurge }}
|
||||
@@ -0,0 +1,72 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: {{ include "iaop.fullname" . }}
|
||||
labels:
|
||||
{{- include "iaop.labels" . | nindent 4 }}
|
||||
spec:
|
||||
replicas: {{ .Values.replicaCount }}
|
||||
strategy:
|
||||
type: RollingUpdate
|
||||
rollingUpdate:
|
||||
maxUnavailable: {{ .Values.rollingUpdate.maxUnavailable }}
|
||||
maxSurge: {{ .Values.rollingUpdate.maxSurge }}
|
||||
selector:
|
||||
matchLabels:
|
||||
{{- include "iaop.selectorLabels" . | nindent 6 }}
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
{{- include "iaop.selectorLabels" . | nindent 8 }}
|
||||
iaop.ai/inference-backend: {{ .Values.inference.backend }}
|
||||
spec:
|
||||
{{- if eq .Values.inference.backend "gpu" }}
|
||||
# NVIDIA GPU(5090)节点选择:推理节点打标 nvidia.com/gpu=true
|
||||
nodeSelector:
|
||||
nvidia.com/gpu: "true"
|
||||
{{- else }}
|
||||
# 华为昇腾 NPU 节点选择:推理节点打标 ascend.com/npu=true
|
||||
nodeSelector:
|
||||
ascend.com/npu: "true"
|
||||
{{- end }}
|
||||
containers:
|
||||
- name: inference
|
||||
image: "{{ include "iaop.image" . }}"
|
||||
imagePullPolicy: {{ .Values.image.pullPolicy }}
|
||||
ports:
|
||||
- name: http
|
||||
containerPort: {{ .Values.service.port }}
|
||||
env:
|
||||
- name: IAOP_BACKEND_CONFIG
|
||||
value: /etc/iaop/backends.yaml
|
||||
volumeMounts:
|
||||
- name: backend-config
|
||||
mountPath: /etc/iaop
|
||||
readOnly: true
|
||||
{{- if .Values.storage.enabled }}
|
||||
- name: model-store
|
||||
mountPath: /models
|
||||
{{- end }}
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /health
|
||||
port: http
|
||||
initialDelaySeconds: {{ .Values.livenessProbe.initialDelaySeconds }}
|
||||
periodSeconds: {{ .Values.livenessProbe.periodSeconds }}
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /health
|
||||
port: http
|
||||
initialDelaySeconds: {{ .Values.readinessProbe.initialDelaySeconds }}
|
||||
periodSeconds: {{ .Values.readinessProbe.periodSeconds }}
|
||||
resources:
|
||||
{{- toYaml .Values.resources | nindent 12 }}
|
||||
volumes:
|
||||
- name: backend-config
|
||||
configMap:
|
||||
name: {{ include "iaop.fullname" . }}-backend
|
||||
{{- if .Values.storage.enabled }}
|
||||
- name: model-store
|
||||
persistentVolumeClaim:
|
||||
claimName: {{ include "iaop.fullname" . }}-models
|
||||
{{- end }}
|
||||
@@ -0,0 +1,23 @@
|
||||
{{- if .Values.ingress.enabled }}
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: {{ include "iaop.fullname" . }}
|
||||
labels:
|
||||
{{- include "iaop.labels" . | nindent 4 }}
|
||||
spec:
|
||||
{{- if .Values.ingress.className }}
|
||||
ingressClassName: {{ .Values.ingress.className }}
|
||||
{{- end }}
|
||||
rules:
|
||||
- host: {{ .Values.ingress.host | quote }}
|
||||
http:
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: {{ include "iaop.fullname" . }}
|
||||
port:
|
||||
name: http
|
||||
{{- end }}
|
||||
@@ -0,0 +1,17 @@
|
||||
{{- if .Values.storage.enabled }}
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: {{ include "iaop.fullname" . }}-models
|
||||
labels:
|
||||
{{- include "iaop.labels" . | nindent 4 }}
|
||||
spec:
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
{{- if .Values.storage.className }}
|
||||
storageClassName: {{ .Values.storage.className }}
|
||||
{{- end }}
|
||||
resources:
|
||||
requests:
|
||||
storage: {{ .Values.storage.size }}
|
||||
{{- end }}
|
||||
@@ -0,0 +1,15 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: {{ include "iaop.fullname" . }}
|
||||
labels:
|
||||
{{- include "iaop.labels" . | nindent 4 }}
|
||||
spec:
|
||||
type: {{ .Values.service.type }}
|
||||
ports:
|
||||
- port: {{ .Values.service.port }}
|
||||
targetPort: http
|
||||
protocol: TCP
|
||||
name: http
|
||||
selector:
|
||||
{{- include "iaop.selectorLabels" . | nindent 4 }}
|
||||
@@ -0,0 +1,63 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# iAOP Helm Chart 默认 values(PRD 5.6「⑥ 部署底座」配置点)。
|
||||
#
|
||||
# 切换推理后端(NVIDIA 5090 GPU ↔ 华为昇腾 NPU)只需改:
|
||||
# inference.backend: gpu | npu
|
||||
# 其余部署(内核+模板、资源配额、灰度、存储、域名)无需改动。
|
||||
|
||||
# ---- 镜像 ----
|
||||
image:
|
||||
repository: iaop/inference
|
||||
tag: v1.0.0
|
||||
pullPolicy: IfNotPresent
|
||||
|
||||
# ---- 推理后端选择(PRD 5.6 配置点:切换后端仅改此处) ----
|
||||
inference:
|
||||
backend: gpu # gpu | npu
|
||||
model: iaop-ti-cl4-v1
|
||||
runtime: vllm # gpu: vllm|triton;npu: mindie|onnx-ascend
|
||||
device: nvidia-5090 # gpu: nvidia-5090;npu: ascend-310p|ascend-910b
|
||||
cannVersion: "" # npu: CANN 工具链版本(如 "8.0"),gpu 忽略
|
||||
maxTokens: 1024
|
||||
temperature: 0.1
|
||||
timeoutSeconds: 60
|
||||
|
||||
# ---- 副本与滚动(灰度发布策略,PRD 5.6 配置点) ----
|
||||
replicaCount: 2
|
||||
rollingUpdate:
|
||||
maxUnavailable: 0
|
||||
maxSurge: 1
|
||||
|
||||
# ---- 服务 ----
|
||||
service:
|
||||
type: ClusterIP
|
||||
port: 8000
|
||||
|
||||
# ---- 域名入口 ----
|
||||
ingress:
|
||||
enabled: false
|
||||
host: iaop.example.com
|
||||
className: ""
|
||||
|
||||
# ---- 资源配额(PRD 5.6 配置点) ----
|
||||
resources:
|
||||
requests:
|
||||
cpu: "2"
|
||||
memory: 8Gi
|
||||
limits:
|
||||
cpu: "8"
|
||||
memory: 32Gi
|
||||
|
||||
# ---- 存储(模型权重/日志持久化) ----
|
||||
storage:
|
||||
enabled: true
|
||||
className: "" # 空 = 使用集群默认 StorageClass
|
||||
size: 100Gi
|
||||
|
||||
# ---- 探针 ----
|
||||
livenessProbe:
|
||||
initialDelaySeconds: 30
|
||||
periodSeconds: 10
|
||||
readinessProbe:
|
||||
initialDelaySeconds: 10
|
||||
periodSeconds: 5
|
||||
Reference in New Issue
Block a user