主题
14 - 部署与演进:K8s 落地路线与未来规划
一句话:落地靠五阶段自举串起拉起、初始化、就绪、注册与分发,注册从 postStart hook 改为控制面异步重试并由 Saga 补偿,灰度因分片 StatefulSet 与 Argo Rollouts 不兼容而自研按 user_id 分桶;换来的是升级等于换镜像重建 Pod、PVC 保留才能免备份,代价是同批次员工同时承受抖动,而跨集群复制是异步的,主集群整体故障要如实接受 24 小时内的数据损失。
一、K8s 部署架构
1.1 完整部署拓扑
1.2 资源规划
| 组件 | 副本数 | CPU Request | Memory Request | CPU Limit | Memory Limit | 存储 |
|---|---|---|---|---|---|---|
| Higress Gateway | 3 | 500m | 512Mi | 2000m | 2Gi | — |
| ext_authz 服务 | 3 | 250m | 256Mi | 1000m | 1Gi | — |
| 渠道聚合器 | 3 | 250m | 256Mi | 1000m | 1Gi | — |
| 产品控制面 | 3 | 500m | 512Mi | 2000m | 2Gi | — |
| PostgreSQL | 3(主从) | 1000m | 2Gi | 4000m | 8Gi | 100Gi SSD |
| Redis Sentinel | 3 | 500m | 1Gi | 2000m | 4Gi | — |
| Nacos | 3 | 500m | 1Gi | 2000m | 4Gi | 20Gi SSD |
| MinIO | 4(P0 修复) | 1000m | 1Gi | 4000m | 4Gi | 500Gi HDD |
| 员工实例 Pod | N | 500m | 1Gi | 2000m | 4Gi | 10Gi+1Gi+5Gi PVC |
P0 修复:MinIO 从 2 节点扩展到 4 节点。MinIO 纠删码至少需要 4 节点才能实现单节点故障不中断服务。2 节点模式下任一节点故障即写入中断,与灾备方案矛盾。
集群规模估算(1000 员工):
| 资源 | 估算 | 说明 |
|---|---|---|
| 节点数 | 35-40 节点(8C16G) | 每节点 30 实例 + 系统 Pod |
| 总 CPU | 280-320 核 | 含系统开销 |
| 总内存 | 560-640 Gi | 含系统开销 |
| 总存储 | ~16Ti SSD + ~5Ti HDD | PVC + 基础设施 |
二、实例自举流程
2.1 五阶段自举
P0 修复:原方案④注册阶段通过 postStart hook 中的 curl 命令注册 Nacos,如果 Nacos 短暂不可用,hook 失败但 K8s 不会重试,导致实例已就绪但未注册。改为控制面异步注册:Pod 就绪后由控制面主动完成注册,失败可重试。
2.2 各阶段详细说明
① 拉起阶段
控制面调用 K8s API 创建资源:
python
async def launch_phase(user_id: str, instance_id: str):
# 创建 PVC ×3
for vol_type, size, storage_class in [
("data", "10Gi", "qwenpaw-ssd"),
("secrets", "1Gi", "qwenpaw-ssd"),
("backups", "5Gi", "qwenpaw-hdd"),
]:
await k8s_client.create_pvc(
name=f"qwenpaw-{user_id}-{vol_type}",
namespace="qwenpaw-instances",
storage_class=storage_class,
size=size,
labels={"owner_user_id": user_id, "instance_id": instance_id},
)
# 创建 K8s Secret(RT Token + 模型 Key)
await k8s_client.create_secret(
name=f"qwenpaw-{user_id}-secret",
namespace="qwenpaw-instances",
data={
"runtime_token": rt_token,
"model_api_key": model_key,
},
)
# 创建 StatefulSet
await k8s_client.create_stateful_set(
name=f"qwenpaw-{user_id}",
namespace="qwenpaw-instances",
image=IMAGE_REF,
user_id=user_id,
instance_id=instance_id,
)② 初始化阶段(initContainer)
yaml
initContainers:
- name: post-init
image: harbor.example.com/qwenpaw/post-init:v1.2.0
command: ["/bin/sh", "-c"]
args:
- |
set -e
# 1. 写入模型预置配置
mkdir -p /app/working.secret/providers
cat > /app/working.secret/providers/active_model.json << EOF
{"provider_id": "tokenplan", "model": "qwen3.8-flash"}
EOF
# 2. 校验 ACP 执行体
for cmd in opencode qwen; do
if ! command -v $cmd &> /dev/null; then
echo "ERROR: ACP executor '$cmd' not found in image"
exit 1
fi
done
# 3. 写入安全基线
cat > /app/working/security_baseline.json << EOF
{"sandbox": true, "deny_paths": ["/etc", "/root", "/var"]}
EOF
echo "post-init completed successfully"
volumeMounts:
- name: secrets
mountPath: /app/working.secret
env:
- name: QWENPAW_RUNTIME_INTERNAL_TOKEN
valueFrom:
secretKeyRef:
name: qwenpaw-zhangsan-secret
key: runtime_token③ 就绪阶段
K8s readinessProbe 自动检测:
每 10s 调用 GET /api/healthz
→ 200:标记为 Ready,加入 Service Endpoints
→ 503:继续等待,不接收流量
→ 连续 3 次失败:标记为 Not Ready,从 Endpoints 摘除④ 注册阶段(控制面异步注册,P0 修复)
P0 修复:原方案通过 postStart hook 中的 curl 命令注册 Nacos,如果 Nacos 短暂不可用,hook 失败但 K8s 不会重试,导致实例已就绪但未注册。改为控制面异步注册:Pod 就绪后由控制面主动完成注册,失败可重试。
python
async def registration_phase(instance_id: str, shard_id: str, pod_index: int):
"""控制面异步注册(替代 postStart hook)"""
inst = await registry.get(instance_id)
pod_dns = f"qwenpaw-shard-{shard_id}-{pod_index}.qwenpaw-instances.qwenpaw-instances.svc.cluster.local"
# 1. 等待 Pod readinessProbe 200
for attempt in range(30): # 最多等待 150 秒
try:
resp = await http_client.get(f"http://{pod_dns}:8088/api/healthz", timeout=5)
if resp.status_code == 200:
break
except Exception:
pass
await asyncio.sleep(5)
else:
raise TimeoutError(f"Pod {instance_id} not ready after 150s")
# 2. Nacos 注册(带重试)
for attempt in range(3):
try:
await nacos_client.register_instance(
service="qwenpaw",
ip=pod_dns,
port=8088,
metadata={
"user_id": inst.user_id,
"instance_id": instance_id,
"shard_id": shard_id,
"pod_index": pod_index,
"state": "active"
}
)
break
except Exception as e:
if attempt == 2:
audit_log.error(f"Nacos registration failed after 3 attempts: {e}")
await registry.update_observed_state(instance_id, "failed")
raise
await asyncio.sleep(5)
# 3. 更新 registry
await registry.update(instance_id, {
"observed_state": "active",
"pod_dns": pod_dns,
"shard_id": shard_id,
"pod_index": pod_index,
"registered_at": datetime.utcnow()
})Saga 事务保证:注册阶段失败时,控制面自动补偿——停止 Pod(StatefulSet replicas=0),更新 registry 为 failed 状态,避免不一致窗口。
⑤ 分发阶段
控制面在确认实例注册完成后,异步执行 Skill/MCP 分发:
python
async def post_init_distribution(instance_id: str):
"""实例注册完成后的分发流程"""
inst = await registry.get(instance_id)
user = await user_store.get(inst.user_id)
# 1. 分发订阅的 Skill
skills = await subscription_store.get_skills(user.tier)
await distribution_engine.distribute_skills(inst.user_id, skills)
# 2. 分发订阅的 MCP Client
mcps = await subscription_store.get_mcps(user.tier)
await distribution_engine.distribute_mcp_clients(inst.user_id, mcps)
# 3. 预置 Agent 配置
agents = await template_store.get_agents(user.role)
await preset_engine.preset_agents(inst.user_id, agents)
# 4. 更新 registry
await registry.update(instance_id, {"post_init_completed": True})
# 5. 通知员工
await notification_engine.send_welcome(inst.user_id)三、灰度发布
3.1 架构决策:控制面自研灰度引擎替代 Argo Rollouts
P0 修复:原方案使用 Argo Rollouts 管理灰度,但 Argo Rollouts 设计用于管理单个 Deployment 的多 replica 灰度,与本方案的分片 StatefulSet 模型不兼容(Rollout YAML 写
replicas: 1000但实际是 50 个分片 StatefulSet)。改为控制面自研灰度引擎:按 user_id 哈希分桶,逐批调用upgrade_instanceAPI,每批之间自动观察指标。
3.2 灰度引擎实现
python
class CanaryUpgradeEngine:
"""控制面灰度引擎"""
async def start_upgrade(self, from_version: str, to_version: str):
"""启动灰度升级"""
# 1. 获取所有 active 实例
instances = await registry.list_active(image_ref=from_version)
# 2. 按 user_id 哈希分桶
buckets = defaultdict(list)
for inst in instances:
bucket = hash(inst.user_id) % 100
buckets[bucket].append(inst)
# 3. 定义灰度批次
steps = [
{"percent": 5, "observe_hours": 24},
{"percent": 20, "observe_hours": 12},
{"percent": 50, "observe_hours": 6},
{"percent": 100, "observe_hours": 0},
]
for step in steps:
# 4. 选择本批次实例
target_buckets = range(step["percent"])
batch_instances = []
for b in target_buckets:
batch_instances.extend(buckets[b])
# 5. 并行升级本批次(10 并发)
semaphore = asyncio.Semaphore(10)
tasks = [
self._upgrade_instance(inst, to_version, semaphore)
for inst in batch_instances
]
results = await asyncio.gather(*tasks, return_exceptions=True)
# 6. 检查失败率
failed = sum(1 for r in results if isinstance(r, Exception))
if failed / len(batch_instances) > 0.05: # 失败率 > 5%
await self._rollback_batch(batch_instances, from_version)
raise UpgradeError(f"Batch failed: {failed}/{len(batch_instances)}")
# 7. 观察期
if step["observe_hours"] > 0:
await self._observe(step["observe_hours"])
if await self._check_anomaly():
await self._rollback_batch(batch_instances, from_version)
raise UpgradeError("Anomaly detected during observation")
async def _upgrade_instance(self, inst, to_version, semaphore):
"""升级单个实例"""
async with semaphore:
# 对于分片 StatefulSet,升级 = 更新 Pod 镜像
# PVC 保留,数据不丢失
# 常规场景(PVC 保留)无需备份,直接换镜像
await k8s_client.update_pod_image(
shard_id=inst.shard_id,
pod_index=inst.pod_index,
image=f"harbor.example.com/qwenpaw/runtime:{to_version}"
)
# 等待 Pod 就绪
await self._wait_ready(inst, timeout=120)3.3 升级期间员工体验
| 场景 | 员工体验 | 恢复时间 |
|---|---|---|
| 常规升级(PVC 保留) | 访问 → 202 waking → 等待 10-15s → 恢复 | 10-15 秒 |
| 低峰期升级(凌晨) | 员工无感知(实例已暂存,升级后等待唤醒) | 0(无中断) |
| 升级失败回滚 | 访问 → 202 waking → 等待 15-30s → 恢复旧版本 | 15-30 秒 |
升级策略优化:
- 低峰期优先:凌晨 0:00-6:00 自动升级已暂存实例,活跃实例延迟到下次暂存/恢复周期
- 批次内并行:每批 50 实例,10 并发升级,单批耗时约 2-5 分钟
- 跳过常规备份:PVC 保留场景下数据在 PVC 中,换镜像不影响,无需暂存+备份步骤
四、灾备方案
4.1 RPO / RTO 目标
| 指标 | 目标 | 实现方式 |
|---|---|---|
| RPO(恢复点目标) | ≤ 24h | 每日定时备份 + 暂存前强制备份 |
| RTO(恢复时间目标) | ≤ 15min | 备份从对象存储恢复 + Pod 自动重建 |
4.2 灾备架构
4.3 故障切换
| 故障场景 | 切换方式 | 数据损失 | 恢复时间 |
|---|---|---|---|
| 单实例 Pod 崩溃 | K8s 自动重建(PVC 保留) | 无(数据在 PVC) | < 30s |
| 单节点故障 | K8s 调度到其他节点(PVC 跟随) | 无 | < 2min |
| PostgreSQL 主库故障 | Patroni 自动切换从库 | < 1s 事务 | < 30s |
| Redis 主故障 | Sentinel 自动切换从 | < 5min 缓存 | < 30s |
| MinIO 主集群故障 | 切换到灾备集群 | < 24h 备份 | < 15min |
| 整个主集群不可用 | DNS 切换到灾备集群 | < 24h | < 30min |
五、MVP 落地检查清单
5.1 阶段一:基础链路(2 周)
| 检查项 | 完成标准 | 验证方式 |
|---|---|---|
| K8s 集群就绪 | 3 节点以上,StorageClass 配置完成 | kubectl get nodes |
| Higress 网关部署 | 网关 Pod Running,Ingress 规则生效 | curl https://agent.example.com/healthz |
| SSO 对接完成 | OIDC 登录 → 获取 JWT → 网关验签通过 | 浏览器登录流程 |
| ext_authz 服务部署 | 认证服务 Running,RT 注入正常 | 携带 JWT 访问网关 → 实例收到 RT |
| 单实例创建 | StatefulSet + PVC + Secret 创建成功 | kubectl get pods -n qwenpaw-instances |
| Nacos 注册 | 实例 healthz 200 后 Nacos 可查 | Nacos 控制台 |
| 端到端对话 | 员工通过 Web → 网关 → 实例对话成功 | 浏览器对话测试 |
5.2 阶段二:多租户与生命周期(3 周)
| 检查项 | 完成标准 | 验证方式 |
|---|---|---|
| 多实例创建 | 10 个员工实例全部 Running | kubectl get pods |
| 身份路由 | 不同员工路由到不同实例 | 网关日志 |
| 暂存/恢复 | 实例暂存后 Pod 停止,恢复后数据完整 | 手动触发 + 验证 |
| 渠道聚合器 | 飞书适配器部署,消息收发正常 | 飞书 @ 机器人测试 |
| 身份映射 | IM sender → user_id 映射正确 | 绑定表查询 |
| 端到端 IM 对话 | 员工在飞书 → 聚合器 → 网关 → 实例 → 飞书回复 | 飞书对话测试 |
5.3 阶段三:能力扩展(3 周)
| 检查项 | 完成标准 | 验证方式 |
|---|---|---|
| Skill 分发 | 管理员分发 Skill → 实例安装成功 | 实例 skill_pool 检查 |
| MCP 分发 | 管理员配置 MCP → 实例连接成功 | 实例 MCP Client 检查 |
| 审批通知 | HITL 事件 → IM 卡片 → 审批回传 → 实例继续 | 端到端审批测试 |
| 产物交付 | 任务完成 → 产物回传 IM 会话 | 飞书文件消息检查 |
| 配额管控 | 虚 SK 计量正常,超限返回 429 | 压测验证 |
5.4 阶段四:生产加固(2 周)
| 检查项 | 完成标准 | 验证方式 |
|---|---|---|
| 安全分层 | 五层安全模型全部生效 | 安全审计 |
| 全链路审计 | trace_id 贯通,审计日志可查 | 审计平台查询 |
| 密钥轮换 | 定时轮换 + 紧急吊销正常 | 手动触发验证 |
| 监控告警 | Prometheus + Grafana + Loki 全部就绪 | 仪表盘检查 |
| 灾备方案 | 备份/恢复/故障切换演练通过 | 灾备演练 |
| 灰度发布 | Argo Rollouts 配置完成,灰度升级正常 | 模拟升级 |
六、演进路线
6.1 近期(3-6 个月)
| 方向 | 内容 |
|---|---|
| 钉钉/企微适配器 | 补全三大 IM 平台覆盖 |
| 跨实例协作 | 员工 A 向员工 B 分享产物(显式授权) |
| 移动端适配 | 数字分身移动端入口 |
| 多语言 Agent | 支持英语/日语等多语言对话 |
6.2 中期(6-12 个月)
| 方向 | 内容 |
|---|---|
| QwenPaw Hub 集成 | 评估直接采用 hub 作为控制面(路径 B) |
| 智能调度 | 根据负载自动迁移实例到其他节点 |
| 多集群联邦 | 跨地域多 K8s 集群统一管理 |
| 模型路由 | 按任务类型自动选择最优模型 |
6.3 远期(12+ 个月)
| 方向 | 内容 |
|---|---|
| 数字分身协作 | 多个员工的数字分身互相协作完成复杂任务 |
| 知识沉淀 | 数字分身自动总结员工工作模式,形成组织知识 |
| 自主决策 | 低风险任务数字分身自主完成,无需人工审批 |
| 生态市场 | 企业间 Skill/MCP 共享市场 |