重构为基于 NATS 的 source adapter、scheduler 与 executor 微服务 #19

Open
opened 2026-09-17 08:20:45 +00:00 by panxiao81 · 6 comments
Owner

背景

当前 PoC 已验证 webhook → NATS → Pod/microVM 的完整生命周期,但实现仍带有明显的纵向耦合:

  • controller.py 同时负责 Gitea webhook 协议、任务筛选和内部消息发布
  • Pod 与 microVM worker 分别实现了相似的 JetStream 消费、容量、ACK、heartbeat 和重试逻辑
  • executor 配置和内部模型仍直接包含 Gitea runner 概念
  • 新增 GitHub Actions 支持时,如果继续沿当前结构扩展,会复制一套控制面逻辑

NATS 已经提供了正确的解耦边界。项目应重构为可独立演进的 source adapter、scheduler 和 executor 微服务,让 Gitea 与 GitHub 控制协议共享执行后端。

关联:

  • #7:兼容 Gitea Runner 协议的动态调度器
  • #18:GitHub Actions Runner Scale Set

目标架构

Gitea webhook / RunnerService ─► gitea-adapter ─┐
                                                ├─► NATS ─► scheduler ─► NATS ─► pod-executor
GitHub Runner Scale Set ───────► github-adapter ┘                         └─► microvm-executor

source adapter ◄────────────── lifecycle/result events ◄──────────────────────────────┘

source adapter

  • 维护 Gitea RunnerService 或 GitHub scale-set/message-session 协议状态
  • 将外部任务转换为版本化的内部 job intent
  • 持有外部系统 credential、lease、JIT config 等敏感状态
  • 根据内部 lifecycle event 向对应 CI 系统汇报状态与结果

scheduler

  • 只理解内部协议、labels、provider 能力、容量和调度策略
  • 不依赖 Gitea/GitHub API
  • 支持在资源增加后调整并发上限,不把当前单 VM 限制编码为全局锁

executor

  • 只负责创建、观察和销毁一次性 Pod/microVM
  • 通过公共 worker runtime 获得 JetStream ACK、heartbeat、重试、取消和幂等行为
  • 通过一次性 capability endpoint 取得执行当前任务所需的短期材料;敏感凭据不进入普通 NATS 消息

内部协议

至少定义并版本化:

  • JobIntent:source、外部任务引用、repository、job name、labels、约束
  • ExecutionAssignment:内部 execution ID、provider、资源规格、短期 capability reference
  • LifecycleEvent:accepted、provisioning、ready、running、completed、failed、cancelled、timed_out
  • 幂等键、correlation ID、创建时间、deadline 和 schema version

外部系统的 registration token、JIT config、task lease 或长期 credential 不直接放入 JetStream payload。

JetStream 边界

  • demand stream:adapter → scheduler,按任务幂等,保留期有限
  • assignment stream:scheduler → provider executor,按 provider 分 subject
  • lifecycle stream:executor/scheduler → adapter 与观测消费者
  • 明确 ACK 时点、ack_wait、max_deliver、DLQ/advisory 与过期清理策略
  • NATS 不是 Gitea/GitHub 的任务事实来源;adapter 重启后必须能与上游重新协调

增量迁移顺序

  1. 从现有模型中抽取独立、版本化的内部消息契约与契约测试
  2. 抽取公共 JetStream worker runtime,消除 Pod/VM 的消费与 heartbeat 重复实现
  3. 将 Pod/microVM 创建、观察、销毁整理为统一 provider interface
  4. 把现有 webhook controller 改造成 gitea-webhook-adapter,保持线上行为不变
  5. 引入独立 scheduler,并迁移 label、容量与 assignment 逻辑
  6. 通过 lifecycle event 完成状态闭环、取消、超时和重启恢复
  7. 在稳定边界上分别实现 #7 和 支持 GitHub Actions self-hosted runner scale set (#18)
  8. 双轨验证后再删除 bootstrap webhook 与临时 runner 注册路径

验收标准

  • adapter、scheduler、Pod executor、microVM executor 可独立启动和部署
  • 内部消息 schema 有版本、校验、幂等键和契约测试
  • 两种 executor 共用 NATS 消费、heartbeat、重试和取消 runtime
  • scheduler 不导入任何 Gitea/GitHub 客户端代码
  • executor 不持有 source 控制面长期 credential
  • 现有 Gitea Pod 与 microVM smoke test 在迁移期间保持通过
  • 支持 scheduler/adapter/executor 分别重启后的恢复和遗留资源清理
  • README 与架构文档记录服务边界、stream/subject、ACK 语义和安全边界

非目标

  • 不在一次变更中重写全部可运行代码
  • 不为了“微服务化”拆分无状态的小函数;部署边界以协议所有权、凭据边界和独立伸缩需求为依据
  • 不让 NATS payload 成为外部 runner 协议对象的无约束透传
## 背景 当前 PoC 已验证 webhook → NATS → Pod/microVM 的完整生命周期,但实现仍带有明显的纵向耦合: - `controller.py` 同时负责 Gitea webhook 协议、任务筛选和内部消息发布 - Pod 与 microVM worker 分别实现了相似的 JetStream 消费、容量、ACK、heartbeat 和重试逻辑 - executor 配置和内部模型仍直接包含 Gitea runner 概念 - 新增 GitHub Actions 支持时,如果继续沿当前结构扩展,会复制一套控制面逻辑 NATS 已经提供了正确的解耦边界。项目应重构为可独立演进的 source adapter、scheduler 和 executor 微服务,让 Gitea 与 GitHub 控制协议共享执行后端。 关联: - #7:兼容 Gitea Runner 协议的动态调度器 - #18:GitHub Actions Runner Scale Set ## 目标架构 ```text Gitea webhook / RunnerService ─► gitea-adapter ─┐ ├─► NATS ─► scheduler ─► NATS ─► pod-executor GitHub Runner Scale Set ───────► github-adapter ┘ └─► microvm-executor source adapter ◄────────────── lifecycle/result events ◄──────────────────────────────┘ ``` ### source adapter - 维护 Gitea RunnerService 或 GitHub scale-set/message-session 协议状态 - 将外部任务转换为版本化的内部 job intent - 持有外部系统 credential、lease、JIT config 等敏感状态 - 根据内部 lifecycle event 向对应 CI 系统汇报状态与结果 ### scheduler - 只理解内部协议、labels、provider 能力、容量和调度策略 - 不依赖 Gitea/GitHub API - 支持在资源增加后调整并发上限,不把当前单 VM 限制编码为全局锁 ### executor - 只负责创建、观察和销毁一次性 Pod/microVM - 通过公共 worker runtime 获得 JetStream ACK、heartbeat、重试、取消和幂等行为 - 通过一次性 capability endpoint 取得执行当前任务所需的短期材料;敏感凭据不进入普通 NATS 消息 ## 内部协议 至少定义并版本化: - `JobIntent`:source、外部任务引用、repository、job name、labels、约束 - `ExecutionAssignment`:内部 execution ID、provider、资源规格、短期 capability reference - `LifecycleEvent`:accepted、provisioning、ready、running、completed、failed、cancelled、timed_out - 幂等键、correlation ID、创建时间、deadline 和 schema version 外部系统的 registration token、JIT config、task lease 或长期 credential 不直接放入 JetStream payload。 ## JetStream 边界 - demand stream:adapter → scheduler,按任务幂等,保留期有限 - assignment stream:scheduler → provider executor,按 provider 分 subject - lifecycle stream:executor/scheduler → adapter 与观测消费者 - 明确 ACK 时点、`ack_wait`、`max_deliver`、DLQ/advisory 与过期清理策略 - NATS 不是 Gitea/GitHub 的任务事实来源;adapter 重启后必须能与上游重新协调 ## 增量迁移顺序 1. 从现有模型中抽取独立、版本化的内部消息契约与契约测试 2. 抽取公共 JetStream worker runtime,消除 Pod/VM 的消费与 heartbeat 重复实现 3. 将 Pod/microVM 创建、观察、销毁整理为统一 provider interface 4. 把现有 webhook controller 改造成 `gitea-webhook-adapter`,保持线上行为不变 5. 引入独立 scheduler,并迁移 label、容量与 assignment 逻辑 6. 通过 lifecycle event 完成状态闭环、取消、超时和重启恢复 7. 在稳定边界上分别实现 #7 和 #18 8. 双轨验证后再删除 bootstrap webhook 与临时 runner 注册路径 ## 验收标准 - [ ] adapter、scheduler、Pod executor、microVM executor 可独立启动和部署 - [ ] 内部消息 schema 有版本、校验、幂等键和契约测试 - [ ] 两种 executor 共用 NATS 消费、heartbeat、重试和取消 runtime - [ ] scheduler 不导入任何 Gitea/GitHub 客户端代码 - [ ] executor 不持有 source 控制面长期 credential - [ ] 现有 Gitea Pod 与 microVM smoke test 在迁移期间保持通过 - [ ] 支持 scheduler/adapter/executor 分别重启后的恢复和遗留资源清理 - [ ] README 与架构文档记录服务边界、stream/subject、ACK 语义和安全边界 ## 非目标 - 不在一次变更中重写全部可运行代码 - 不为了“微服务化”拆分无状态的小函数;部署边界以协议所有权、凭据边界和独立伸缩需求为依据 - 不让 NATS payload 成为外部 runner 协议对象的无约束透传
Author
Owner

技术栈决策:移除 shell 编排层

microVM 路径不应长期保留 Python 调用多段 shell 的结构。当前脚本已经承担磁盘生命周期、cloud-init、TAP/MAC、bridge/NAT/DHCP、进程监督、超时、日志归档和清理,实际上是一个缺少类型与测试边界的隐式状态机。

重构目标:

  • host 侧 microvm-executor 使用 Go 实现,与 scheduler 和公共 NATS runtime 共享类型与库
  • 将 instance lifecycle 建模为显式状态机,并为创建失败、启动超时、取消、异常退出和进程重启提供幂等清理
  • 使用 Go netlink/nftables 能力管理 TAP、bridge 和 NAT;避免通过 shell 管道解析 ip/iptables 输出
  • Cloud Hypervisor 与 qemu-img 可以继续作为受控子进程调用;“不使用 shell 编排”不等于重写成熟的虚拟化工具
  • NoCloud seed 优先由 Go 库或明确封装的单一工具生成,输入采用结构化模板
  • guest 内的注册、单任务执行、完成标记与关机收敛为一个小型 Go entrypoint;在真正的 Gitea/GitHub task executor 接入后按 source-specific payload 启动对应 runner
  • 删除 microvm-runner-launch、microvm-runner-network 和 gitea-microvm-guest-runner 应作为迁移完成条件,而非继续增加 shell 功能

允许迁移期间保留现有脚本作为经过 smoke test 的兼容实现,但新生命周期逻辑不再加入 shell。Python webhook adapter 可以独立保留,直到被 source adapter 替换;不要求整个仓库在同一提交中统一技术栈。

## 技术栈决策:移除 shell 编排层 microVM 路径不应长期保留 Python 调用多段 shell 的结构。当前脚本已经承担磁盘生命周期、cloud-init、TAP/MAC、bridge/NAT/DHCP、进程监督、超时、日志归档和清理,实际上是一个缺少类型与测试边界的隐式状态机。 重构目标: - host 侧 `microvm-executor` 使用 Go 实现,与 scheduler 和公共 NATS runtime 共享类型与库 - 将 instance lifecycle 建模为显式状态机,并为创建失败、启动超时、取消、异常退出和进程重启提供幂等清理 - 使用 Go netlink/nftables 能力管理 TAP、bridge 和 NAT;避免通过 shell 管道解析 `ip`/`iptables` 输出 - Cloud Hypervisor 与 `qemu-img` 可以继续作为受控子进程调用;“不使用 shell 编排”不等于重写成熟的虚拟化工具 - NoCloud seed 优先由 Go 库或明确封装的单一工具生成,输入采用结构化模板 - guest 内的注册、单任务执行、完成标记与关机收敛为一个小型 Go entrypoint;在真正的 Gitea/GitHub task executor 接入后按 source-specific payload 启动对应 runner - 删除 `microvm-runner-launch`、`microvm-runner-network` 和 `gitea-microvm-guest-runner` 应作为迁移完成条件,而非继续增加 shell 功能 允许迁移期间保留现有脚本作为经过 smoke test 的兼容实现,但新生命周期逻辑不再加入 shell。Python webhook adapter 可以独立保留,直到被 source adapter 替换;不要求整个仓库在同一提交中统一技术栈。
Author
Owner

更正:不要自行实现 hypervisor 管理层

上一条评论提出把 TAP、bridge、NAT、磁盘和 VM lifecycle 全部迁入 Go,这个方向不正确:它只是把现有 shell PoC 改写成自制的 hypervisor 管理工具,会扩大项目职责,也会重复实现成熟基础设施已有的资源管理、恢复与清理能力。

现有 shell launcher 的定位应明确为验证 Cloud Hypervisor + LXC/KVM + kind 可行性的 PoC,而不是未来 executor 的实现蓝本。

修正后的原则:

  • scheduler/executor 只依赖一个窄的 provider API:Create、Observe、Cancel/Delete,以及必要的状态查询
  • VM 网络、镜像、磁盘、进程监督和遗留资源回收交给现成 VM 管理平台/provider
  • 优先评估能够通过 API 管理临时 VM、支持 cloud-init、KVM passthrough、快速创建/销毁的现成方案;不要先决定用 Go 重写
  • microVM executor 本身只做 NATS assignment 与外部 provider API 之间的适配,因此可以是很薄的 Go 或 Python 服务
  • 若最终仍直接调用 Cloud Hypervisor,现有脚本只作为有明确支持范围的临时 provider,并限制新增功能;不能继续演化为通用 VM manager
  • 删除 shell 的前提是切换到成熟 provider,而不是逐行翻译成另一种语言

因此撤回“使用 Go netlink/nftables 自行管理 TAP、bridge、NAT”和“在 Go 中实现完整 instance lifecycle”的建议。下一步应先做 provider 选型,再决定 adapter 技术栈。

## 更正:不要自行实现 hypervisor 管理层 上一条评论提出把 TAP、bridge、NAT、磁盘和 VM lifecycle 全部迁入 Go,这个方向不正确:它只是把现有 shell PoC 改写成自制的 hypervisor 管理工具,会扩大项目职责,也会重复实现成熟基础设施已有的资源管理、恢复与清理能力。 现有 shell launcher 的定位应明确为验证 Cloud Hypervisor + LXC/KVM + kind 可行性的 PoC,而不是未来 executor 的实现蓝本。 修正后的原则: - scheduler/executor 只依赖一个窄的 provider API:Create、Observe、Cancel/Delete,以及必要的状态查询 - VM 网络、镜像、磁盘、进程监督和遗留资源回收交给现成 VM 管理平台/provider - 优先评估能够通过 API 管理临时 VM、支持 cloud-init、KVM passthrough、快速创建/销毁的现成方案;不要先决定用 Go 重写 - microVM executor 本身只做 NATS assignment 与外部 provider API 之间的适配,因此可以是很薄的 Go 或 Python 服务 - 若最终仍直接调用 Cloud Hypervisor,现有脚本只作为有明确支持范围的临时 provider,并限制新增功能;不能继续演化为通用 VM manager - 删除 shell 的前提是切换到成熟 provider,而不是逐行翻译成另一种语言 因此撤回“使用 Go netlink/nftables 自行管理 TAP、bridge、NAT”和“在 Go 中实现完整 instance lifecycle”的建议。下一步应先做 provider 选型,再决定 adapter 技术栈。
Author
Owner

架构转向:以 OpenSandbox 作为 execution control plane

在调研 #21 后,backend 不应继续沿“自建 Pod worker + microVM worker + 公共 worker runtime”的方向重构。OpenSandbox 已经提供了我们准备实现的大部分 execution lifecycle:版本化 OpenAPI、异步创建、状态查询、TTL、删除重试、重启恢复、runtime provider、资源限制、诊断和 sandbox 内 exec endpoint。

新的职责边界:

Gitea adapter ─┐
                ├─► NATS demand/events ◄─► CI orchestrator ─► OpenSandbox Lifecycle API
GitHub adapter ┘                                      │                    │
                                                      │                    ├─ Docker + Kata
                                                      │                    └─ future provider
                                                      └─ execution mapping / policy / reconciliation

本项目保留

  • Gitea RunnerService 与 GitHub Runner Scale Set/JIT 等 source protocol adapter
  • 将外部 job 转成内部、版本化的 CI demand
  • label、租户、优先级、配额和 provider profile 等 CI 调度策略
  • job 与 sandbox ID 的映射、幂等键、取消传播和最终结果闭环
  • source-specific 短期 credential/capability broker
  • reconciliation:比较上游 job、内部 execution record 与 OpenSandbox sandbox 状态

交给 OpenSandbox

  • sandbox create/get/delete、TTL 与生命周期状态机
  • Docker/Kubernetes runtime backend
  • Kata/gVisor 等 secure runtime 选择
  • CPU、内存、镜像、volume、network policy 与 endpoint
  • runtime/server 重启后的 timer 恢复和删除重试
  • sandbox 内命令/文件/日志通道(是否用于 CI runner 由 PoC 决定)
  • 后续 warm pool、snapshot、pause/resume 等通用 sandbox 能力

删除或降级

  • pod_worker.py 与 worker.py 不再作为长期架构中的独立 backend service
  • microvm-runner-launch、network、guest shell 只保留为现有 PoC,迁移后删除
  • scheduler → provider 不强制再经过一层 JetStream assignment queue;OpenSandbox 是声明式/可查询 API,orchestrator 应采用 create + reconcile 模式
  • NATS 继续用于 source demand、取消和 lifecycle event 解耦,而不是复制 OpenSandbox 的内部生命周期状态

仍需自行解决的 CI 特有问题

OpenSandbox 不理解 Gitea/GitHub job,因此不会替代 CI orchestrator:

  • GitHub JIT config 与 Gitea task capability 的按需交付
  • runner image/entrypoint 及 job 完成信号
  • SPIFFE identity 如何从 repository/job name 绑定到实际 sandbox
  • sandbox 内 Docker/kind 所需的 block-backed /var/lib/docker
  • 上游取消、超时与 sandbox TTL 的双向协调
  • source adapter 重启后如何恢复已领取 job

新的首个设计产物

在继续写 backend 代码前,先定义一个最小 OpenSandboxProvider contract:

  • Ensure(execution):以幂等 metadata 创建或找到 sandbox
  • Get(executionID):映射 OpenSandbox 状态为内部 lifecycle
  • Cancel(executionID):幂等删除 sandbox
  • Reconcile():发现并处理 orphan/missing/expired sandbox

它应只是 OpenSandbox API adapter,不抽象或重写 hypervisor 细节。PoC 通过后再决定直接依赖 OpenSandbox API,还是采用其 lifecycle OpenAPI 作为可替换 provider contract。

## 架构转向:以 OpenSandbox 作为 execution control plane 在调研 #21 后,backend 不应继续沿“自建 Pod worker + microVM worker + 公共 worker runtime”的方向重构。OpenSandbox 已经提供了我们准备实现的大部分 execution lifecycle:版本化 OpenAPI、异步创建、状态查询、TTL、删除重试、重启恢复、runtime provider、资源限制、诊断和 sandbox 内 exec endpoint。 新的职责边界: ```text Gitea adapter ─┐ ├─► NATS demand/events ◄─► CI orchestrator ─► OpenSandbox Lifecycle API GitHub adapter ┘ │ │ │ ├─ Docker + Kata │ └─ future provider └─ execution mapping / policy / reconciliation ``` ### 本项目保留 - Gitea RunnerService 与 GitHub Runner Scale Set/JIT 等 source protocol adapter - 将外部 job 转成内部、版本化的 CI demand - label、租户、优先级、配额和 provider profile 等 CI 调度策略 - job 与 sandbox ID 的映射、幂等键、取消传播和最终结果闭环 - source-specific 短期 credential/capability broker - reconciliation:比较上游 job、内部 execution record 与 OpenSandbox sandbox 状态 ### 交给 OpenSandbox - sandbox create/get/delete、TTL 与生命周期状态机 - Docker/Kubernetes runtime backend - Kata/gVisor 等 secure runtime 选择 - CPU、内存、镜像、volume、network policy 与 endpoint - runtime/server 重启后的 timer 恢复和删除重试 - sandbox 内命令/文件/日志通道(是否用于 CI runner 由 PoC 决定) - 后续 warm pool、snapshot、pause/resume 等通用 sandbox 能力 ### 删除或降级 - `pod_worker.py` 与 `worker.py` 不再作为长期架构中的独立 backend service - `microvm-runner-launch`、network、guest shell 只保留为现有 PoC,迁移后删除 - scheduler → provider 不强制再经过一层 JetStream assignment queue;OpenSandbox 是声明式/可查询 API,orchestrator 应采用 create + reconcile 模式 - NATS 继续用于 source demand、取消和 lifecycle event 解耦,而不是复制 OpenSandbox 的内部生命周期状态 ### 仍需自行解决的 CI 特有问题 OpenSandbox 不理解 Gitea/GitHub job,因此不会替代 CI orchestrator: - GitHub JIT config 与 Gitea task capability 的按需交付 - runner image/entrypoint 及 job 完成信号 - SPIFFE identity 如何从 repository/job name 绑定到实际 sandbox - sandbox 内 Docker/kind 所需的 block-backed `/var/lib/docker` - 上游取消、超时与 sandbox TTL 的双向协调 - source adapter 重启后如何恢复已领取 job ### 新的首个设计产物 在继续写 backend 代码前,先定义一个最小 `OpenSandboxProvider` contract: - `Ensure(execution)`:以幂等 metadata 创建或找到 sandbox - `Get(executionID)`:映射 OpenSandbox 状态为内部 lifecycle - `Cancel(executionID)`:幂等删除 sandbox - `Reconcile()`:发现并处理 orphan/missing/expired sandbox 它应只是 OpenSandbox API adapter,不抽象或重写 hypervisor 细节。PoC 通过后再决定直接依赖 OpenSandbox API,还是采用其 lifecycle OpenAPI 作为可替换 provider contract。
Author
Owner

首期部署拓扑

OpenSandbox Docker backend 应与 Docker/containerd、Kata runtime 和 /dev/kvm 共置,不能把远程 Docker socket 当作 provider API,也不应为了运行它将主 k3s 节点改造成特权 sandbox host。

建议首期部署:

main k3s
└─ ci-controller
   ├─ Gitea/GitHub protocol
   ├─ job ↔ sandbox reconciliation
   └─ HTTPS ─────────────────────────────┐
                                         ▼
pve2: existing privileged runner LXC
├─ OpenSandbox server (systemd)
├─ Docker/containerd
├─ Kata runtime
├─ /dev/kvm
└─ ephemeral Kata sandboxes

为什么放在 pve2 runner LXC

  • /dev/kvm、privileged LXC、cgroup 与 AppArmor 路径已经通过 Cloud Hypervisor PoC 验证
  • OpenSandbox API 与实际 compute host 同故障域;host 不可用时 API 即不可用,避免返回虚假的可调度状态
  • 不暴露 Docker/containerd socket到网络
  • 不污染主 k3s 节点,也不需要第二 Kubernetes 集群
  • 后续可以把该 LXC 视为 sandbox-node-1,而不是某个 CI runner 本身

服务部署

  • OpenSandbox server 直接作为 LXC 内 systemd 服务运行,连接本机 Docker socket
  • Docker 配置 Kata OCI runtime;具体 VMM/存储方案由 #20/#21 PoC 决定
  • API 只监听 runner/management 内网,使用 TLS;首期使用独立 API key
  • API key 由 OpenBao 管理,通过 ExternalSecret 注入 main k3s 的 controller,不进入 job sandbox
  • 不为内部单节点 API 增加 Kubernetes LoadBalancer;配置内部 DNS 指向 LXC 固定地址

控制面位置

ci-controller 留在主 k3s:它需要稳定维护 Gitea/GitHub 会话、持有 source credential,并在 compute LXC 重启期间继续做退避和 reconciliation。OpenSandbox LXC 只持有 sandbox provider 所需凭据,不持有完整 Gitea/GitHub 控制面凭据。

扩展路径

首期只注册一个 OpenSandbox endpoint。未来增加节点时,controller 维护 provider endpoint 列表与每节点容量/健康状态;每台 compute node 运行独立 OpenSandbox server。不要在 OpenSandbox 单 Docker backend 之上提前实现集群一致性。资源足够并决定采用 Kubernetes backend 后,再由 OpenSandbox 自身接管多节点调度。

故障语义

  • OpenSandbox API 不可达:不领取新 job,已有 job 等待上游超时或恢复后 reconcile
  • LXC 重启:OpenSandbox 恢复 TTL/timer,并由 controller 对照上游 active jobs 清理 orphan
  • main k3s/controller 重启:扫描上游 active jobs 与 OpenSandbox metadata 恢复映射
  • pve2 故障:sandbox 与 API 同时失效,controller 向上游失败/重试,不尝试假定 sandbox 仍存活
## 首期部署拓扑 OpenSandbox Docker backend 应与 Docker/containerd、Kata runtime 和 `/dev/kvm` 共置,不能把远程 Docker socket 当作 provider API,也不应为了运行它将主 k3s 节点改造成特权 sandbox host。 建议首期部署: ```text main k3s └─ ci-controller ├─ Gitea/GitHub protocol ├─ job ↔ sandbox reconciliation └─ HTTPS ─────────────────────────────┐ ▼ pve2: existing privileged runner LXC ├─ OpenSandbox server (systemd) ├─ Docker/containerd ├─ Kata runtime ├─ /dev/kvm └─ ephemeral Kata sandboxes ``` ### 为什么放在 pve2 runner LXC - `/dev/kvm`、privileged LXC、cgroup 与 AppArmor 路径已经通过 Cloud Hypervisor PoC 验证 - OpenSandbox API 与实际 compute host 同故障域;host 不可用时 API 即不可用,避免返回虚假的可调度状态 - 不暴露 Docker/containerd socket到网络 - 不污染主 k3s 节点,也不需要第二 Kubernetes 集群 - 后续可以把该 LXC 视为 `sandbox-node-1`,而不是某个 CI runner 本身 ### 服务部署 - OpenSandbox server 直接作为 LXC 内 systemd 服务运行,连接本机 Docker socket - Docker 配置 Kata OCI runtime;具体 VMM/存储方案由 #20/#21 PoC 决定 - API 只监听 runner/management 内网,使用 TLS;首期使用独立 API key - API key 由 OpenBao 管理,通过 ExternalSecret 注入 main k3s 的 controller,不进入 job sandbox - 不为内部单节点 API 增加 Kubernetes LoadBalancer;配置内部 DNS 指向 LXC 固定地址 ### 控制面位置 `ci-controller` 留在主 k3s:它需要稳定维护 Gitea/GitHub 会话、持有 source credential,并在 compute LXC 重启期间继续做退避和 reconciliation。OpenSandbox LXC 只持有 sandbox provider 所需凭据,不持有完整 Gitea/GitHub 控制面凭据。 ### 扩展路径 首期只注册一个 OpenSandbox endpoint。未来增加节点时,controller 维护 provider endpoint 列表与每节点容量/健康状态;每台 compute node 运行独立 OpenSandbox server。不要在 OpenSandbox 单 Docker backend 之上提前实现集群一致性。资源足够并决定采用 Kubernetes backend 后,再由 OpenSandbox 自身接管多节点调度。 ### 故障语义 - OpenSandbox API 不可达:不领取新 job,已有 job 等待上游超时或恢复后 reconcile - LXC 重启:OpenSandbox 恢复 TTL/timer,并由 controller 对照上游 active jobs 清理 orphan - main k3s/controller 重启:扫描上游 active jobs 与 OpenSandbox metadata 恢复映射 - pve2 故障:sandbox 与 API 同时失效,controller 向上游失败/重试,不尝试假定 sandbox 仍存活
Author
Owner

更正部署拓扑:使用 OpenSandbox Kubernetes 集群 backend

上一条“每台 LXC 一个 OpenSandbox Docker endpoint”的设计没有覆盖现有 pod profile,也放弃了 OpenSandbox Kubernetes backend 已有的多节点调度、Pool 与 BatchSandbox 生命周期,应撤回。

正确方向是将 pve1/pve2 的 sandbox LXC 加入现有 k3s 集群作为专用 worker,而不是运行独立 Docker backend:

existing k3s control plane
├─ ci-controller
├─ OpenSandbox BatchSandbox operator(单套)
├─ OpenSandbox API: pod profile  ─► runc RuntimeClass ─► 普通 worker
└─ OpenSandbox API: vm profile   ─► Kata RuntimeClass ─► sandbox-pve1 / sandbox-pve2

pve1/sandbox-node-1 LXC: k3s agent + containerd + Kata + /dev/kvm
pve2/sandbox-node-2 LXC: k3s agent + containerd + Kata + /dev/kvm

为什么需要两个 API profile

OpenSandbox 当前 [secure_runtime] 是 server 实例级配置,官方文档明确说明启用后该 server 创建的 ALL sandboxes 都使用指定 runtime;batchsandbox_template_file 也是单个 server 的全局模板。因此同一 API 实例目前不能按每次请求在 runc 与 Kata 间切换。

首期部署两个无状态 OpenSandbox server Deployment,指向同一 Kubernetes backend/operator:

  • opensandbox-pod:不配置 secure runtime,模板用 node affinity/资源策略调度普通 Pod
  • opensandbox-vm:配置 secure_runtime.type = kata 与 k8s_runtime_class = kata-*,模板带 Kata worker selector/toleration

CI controller 根据 runs-on: [self-hosted, pod|vm] 选择对应 API service。两者仍属于同一 OpenSandbox/Kubernetes 集群控制面,不是每节点部署一套 provider。

节点设计

  • pve1/pve2 各运行一个轻量 privileged LXC,加入现有 k3s 为 agent node
  • 节点设置例如 sandbox.ddupan.top/kata=true label 与 sandbox.ddupan.top/dedicated=ci:NoSchedule taint
  • 只在这两个节点安装 Kata runtime handler,并透传 /dev/kvm
  • pve3 不加入该 node pool,现有 workload 不受影响
  • 普通 pod job 可继续使用现有 runc node pool;vm job 只进入 Kata node pool
  • 每节点容量先通过 allocatable/resource request 限制为一个 Kata sandbox,将来增加资源无需修改全局锁

收益

  • OpenSandbox 真正以集群方式运行并负责跨 pve1/pve2 调度
  • pod 与 microVM 共用同一 Lifecycle API 语义和 BatchSandbox operator
  • 无第二 Kubernetes 集群、无每节点 API endpoint、无远程 Docker socket
  • Kubernetes scheduler、taint/affinity、资源 request 和 OpenSandbox Pool 共同承担容量管理

仍需 PoC 验证:pve1/pve2 LXC 作为 k3s worker 时 Kata runtime、CNI、block-backed /var/lib/docker 与 kind 是否正常。

## 更正部署拓扑:使用 OpenSandbox Kubernetes 集群 backend 上一条“每台 LXC 一个 OpenSandbox Docker endpoint”的设计没有覆盖现有 `pod` profile,也放弃了 OpenSandbox Kubernetes backend 已有的多节点调度、Pool 与 BatchSandbox 生命周期,应撤回。 正确方向是将 pve1/pve2 的 sandbox LXC 加入**现有 k3s 集群**作为专用 worker,而不是运行独立 Docker backend: ```text existing k3s control plane ├─ ci-controller ├─ OpenSandbox BatchSandbox operator(单套) ├─ OpenSandbox API: pod profile ─► runc RuntimeClass ─► 普通 worker └─ OpenSandbox API: vm profile ─► Kata RuntimeClass ─► sandbox-pve1 / sandbox-pve2 pve1/sandbox-node-1 LXC: k3s agent + containerd + Kata + /dev/kvm pve2/sandbox-node-2 LXC: k3s agent + containerd + Kata + /dev/kvm ``` ### 为什么需要两个 API profile OpenSandbox 当前 `[secure_runtime]` 是 server 实例级配置,官方文档明确说明启用后该 server 创建的 **ALL sandboxes** 都使用指定 runtime;`batchsandbox_template_file` 也是单个 server 的全局模板。因此同一 API 实例目前不能按每次请求在 runc 与 Kata 间切换。 首期部署两个无状态 OpenSandbox server Deployment,指向同一 Kubernetes backend/operator: - `opensandbox-pod`:不配置 secure runtime,模板用 node affinity/资源策略调度普通 Pod - `opensandbox-vm`:配置 `secure_runtime.type = kata` 与 `k8s_runtime_class = kata-*`,模板带 Kata worker selector/toleration CI controller 根据 `runs-on: [self-hosted, pod|vm]` 选择对应 API service。两者仍属于同一 OpenSandbox/Kubernetes 集群控制面,不是每节点部署一套 provider。 ### 节点设计 - pve1/pve2 各运行一个轻量 privileged LXC,加入现有 k3s 为 agent node - 节点设置例如 `sandbox.ddupan.top/kata=true` label 与 `sandbox.ddupan.top/dedicated=ci:NoSchedule` taint - 只在这两个节点安装 Kata runtime handler,并透传 `/dev/kvm` - pve3 不加入该 node pool,现有 workload 不受影响 - 普通 `pod` job 可继续使用现有 runc node pool;`vm` job 只进入 Kata node pool - 每节点容量先通过 allocatable/resource request 限制为一个 Kata sandbox,将来增加资源无需修改全局锁 ### 收益 - OpenSandbox 真正以集群方式运行并负责跨 pve1/pve2 调度 - pod 与 microVM 共用同一 Lifecycle API 语义和 BatchSandbox operator - 无第二 Kubernetes 集群、无每节点 API endpoint、无远程 Docker socket - Kubernetes scheduler、taint/affinity、资源 request 和 OpenSandbox Pool 共同承担容量管理 仍需 PoC 验证:pve1/pve2 LXC 作为 k3s worker 时 Kata runtime、CNI、block-backed `/var/lib/docker` 与 kind 是否正常。
Author
Owner

再次修正:独立 OpenSandbox k3s 集群 + 外部 SQL datastore

把 OpenSandbox/Kata/BatchSandbox 加入现有 homelab 集群虽然可行,但会把 CI sandbox 的 CRD、operator、RuntimeClass、特权节点、网络与故障域带入现有工作负载集群。这里应优先选择隔离复杂度,而不是追求少一个集群名字。

K3s 的三节点建议主要来自 embedded etcd quorum;它原生支持 PostgreSQL/MySQL/MariaDB 外部 datastore。官方 HA external DB 架构只要求两个或更多 server node,不要求三个 etcd member。因此建议拓扑改为:

main homelab k3s
└─ ci-controller(Gitea/GitHub 协议与 reconcile)
        │ OpenSandbox API
        ▼
dedicated sandbox k3s
├─ pve1 / sandbox-node-1 LXC
│  ├─ k3s server+agent
│  ├─ runc + Kata + /dev/kvm
│  └─ sandbox workload
├─ pve2 / sandbox-node-2 LXC
│  ├─ k3s server+agent
│  ├─ runc + Kata + /dev/kvm
│  └─ sandbox workload
├─ OpenSandbox operator
├─ OpenSandbox API: pod profile
└─ OpenSandbox API: vm/Kata profile
        │
        └─ external PostgreSQL datastore(K3s state)

控制面

  • pve1/pve2 两个 LXC 都运行 k3s server,并使用同一个外部 PostgreSQL datastore;server 本身同时可调度 workload
  • 使用固定 registration/API 地址(VIP、L4 LB 或至少稳定 DNS)访问两个 server
  • 不启用 embedded etcd;无需第三个 PVE 节点凑 quorum
  • 外部 DB 只保存 Kubernetes control-plane state,不承载 CI job queue
  • K3s server token 与 datastore credential 必须备份;外部 DB 的备份由数据库侧负责

如果外部数据库首期尚未准备好,也可以先用单 server + embedded SQLite 完成 PoC,但 SQLite 不能支持多个 server,不能把它误当最终双节点形态。

workload profiles

  • pod:runc RuntimeClass,可在 pve1/pve2 两个 LXC 上运行
  • vm:Kata RuntimeClass,同样跨两个节点调度并透传 /dev/kvm
  • 两个 OpenSandbox API profile 共用一套 BatchSandbox operator,因为 secure_runtime 当前为 server 实例级配置
  • 通过 Kubernetes requests/limits 和每节点 allocatable 控制并发;无需应用级全局锁

隔离收益

  • 现有 homelab 集群完全不安装 OpenSandbox CRD、Kata、特权 runtime 或 CI CNI 策略
  • sandbox 集群可独立升级、重装和做破坏性 runtime 实验
  • CI workload 与长期 workload 分离故障域和安全边界
  • pve3 不参与,不影响现有 workload
  • 未来增加 sandbox compute 只需加入 agent 或 server node

这会撤回上一条“加入现有 k3s”的建议。保留 main k3s 中的 ci-controller 只是为了让 source 会话独立于 sandbox cluster 故障;若后续认为这点也不值得,controller 也可部署到独立 sandbox cluster,但需要接受集群不可用时无法维护 Gitea/GitHub 会话。

## 再次修正:独立 OpenSandbox k3s 集群 + 外部 SQL datastore 把 OpenSandbox/Kata/BatchSandbox 加入现有 homelab 集群虽然可行,但会把 CI sandbox 的 CRD、operator、RuntimeClass、特权节点、网络与故障域带入现有工作负载集群。这里应优先选择隔离复杂度,而不是追求少一个集群名字。 K3s 的三节点建议主要来自 embedded etcd quorum;它原生支持 PostgreSQL/MySQL/MariaDB 外部 datastore。官方 HA external DB 架构只要求两个或更多 server node,不要求三个 etcd member。因此建议拓扑改为: ```text main homelab k3s └─ ci-controller(Gitea/GitHub 协议与 reconcile) │ OpenSandbox API ▼ dedicated sandbox k3s ├─ pve1 / sandbox-node-1 LXC │ ├─ k3s server+agent │ ├─ runc + Kata + /dev/kvm │ └─ sandbox workload ├─ pve2 / sandbox-node-2 LXC │ ├─ k3s server+agent │ ├─ runc + Kata + /dev/kvm │ └─ sandbox workload ├─ OpenSandbox operator ├─ OpenSandbox API: pod profile └─ OpenSandbox API: vm/Kata profile │ └─ external PostgreSQL datastore(K3s state) ``` ### 控制面 - pve1/pve2 两个 LXC 都运行 `k3s server`,并使用同一个外部 PostgreSQL datastore;server 本身同时可调度 workload - 使用固定 registration/API 地址(VIP、L4 LB 或至少稳定 DNS)访问两个 server - 不启用 embedded etcd;无需第三个 PVE 节点凑 quorum - 外部 DB 只保存 Kubernetes control-plane state,不承载 CI job queue - K3s server token 与 datastore credential 必须备份;外部 DB 的备份由数据库侧负责 如果外部数据库首期尚未准备好,也可以先用单 server + embedded SQLite 完成 PoC,但 SQLite 不能支持多个 server,不能把它误当最终双节点形态。 ### workload profiles - `pod`:runc RuntimeClass,可在 pve1/pve2 两个 LXC 上运行 - `vm`:Kata RuntimeClass,同样跨两个节点调度并透传 `/dev/kvm` - 两个 OpenSandbox API profile 共用一套 BatchSandbox operator,因为 `secure_runtime` 当前为 server 实例级配置 - 通过 Kubernetes requests/limits 和每节点 allocatable 控制并发;无需应用级全局锁 ### 隔离收益 - 现有 homelab 集群完全不安装 OpenSandbox CRD、Kata、特权 runtime 或 CI CNI 策略 - sandbox 集群可独立升级、重装和做破坏性 runtime 实验 - CI workload 与长期 workload 分离故障域和安全边界 - pve3 不参与,不影响现有 workload - 未来增加 sandbox compute 只需加入 agent 或 server node 这会撤回上一条“加入现有 k3s”的建议。保留 main k3s 中的 ci-controller 只是为了让 source 会话独立于 sandbox cluster 故障;若后续认为这点也不值得,controller 也可部署到独立 sandbox cluster,但需要接受集群不可用时无法维护 Gitea/GitHub 会话。
Sign in to join this conversation.
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: panxiao81/gitea-dynamic-runner#19