跳到正文

14 - 部署与演进:K8s 落地路线与未来规划 ​

一句话:落地靠五阶段自举串起拉起、初始化、就绪、注册与分发,注册从 postStart hook 改为控制面异步重试并由 Saga 补偿,灰度因分片 StatefulSet 与 Argo Rollouts 不兼容而自研按 user_id 分桶;换来的是升级等于换镜像重建 Pod、PVC 保留才能免备份,代价是同批次员工同时承受抖动,而跨集群复制是异步的,主集群整体故障要如实接受 24 小时内的数据损失。

一、K8s 部署架构 ​

1.1 完整部署拓扑 ​

1.2 资源规划 ​

组件副本数CPU RequestMemory RequestCPU LimitMemory Limit存储
Higress Gateway3500m512Mi2000m2Gi—
ext_authz 服务3250m256Mi1000m1Gi—
渠道聚合器3250m256Mi1000m1Gi—
产品控制面3500m512Mi2000m2Gi—
PostgreSQL3(主从)1000m2Gi4000m8Gi100Gi SSD
Redis Sentinel3500m1Gi2000m4Gi—
Nacos3500m1Gi2000m4Gi20Gi SSD
MinIO4(P0 修复)1000m1Gi4000m4Gi500Gi HDD
员工实例 PodN500m1Gi2000m4Gi10Gi+1Gi+5Gi PVC

P0 修复:MinIO 从 2 节点扩展到 4 节点。MinIO 纠删码至少需要 4 节点才能实现单节点故障不中断服务。2 节点模式下任一节点故障即写入中断,与灾备方案矛盾。

集群规模估算(1000 员工):

资源估算说明
节点数35-40 节点(8C16G)每节点 30 实例 + 系统 Pod
总 CPU280-320 核含系统开销
总内存560-640 Gi含系统开销
总存储~16Ti SSD + ~5Ti HDDPVC + 基础设施

二、实例自举流程 ​

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_instance API,每批之间自动观察指标。

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 个员工实例全部 Runningkubectl 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 共享市场

把 AI 工程讲到既懂也会用