RLark 深度解析:跨集群具身智能云原生平台的全栈架构

具身智能有个硬约束:训练在云上 GPU 集群跑,部署在边缘的机械臂/相机/传感器上——两者物理上分布在不同的网络、不同的集群、甚至不同的安全域。现有平台要么只管云训练(K8s/Volcano),要么只管边缘设备(ROS),中间那条"云到边、跨集群、带鉴权"的路没人铺。

RLark(github.com/RLinf/RLark,2026/08 开源,Apache 2.0)就是来铺这条路的——一个跨集群具身智能云原生平台:用 kcp 做轻量控制面,统一管理云 GPU 集群和边缘设备,通过 Domain/Node/Job/Task/Workflow 一套 CRD 把"训练→部署"全链路声明式化,再用 TUN+gVisor+SSH 隧道让跨集群 Pod 直接通信,最后用 Device Plugin 把 ROS 机器人和相机接进 K8s 调度。

本文基于源码深挖,把 RLark 拆成四层:CRD 模型、kcp 控制面、跨集群 Pod 网络、具身运行时。

关联阅读:具身智能训练栈与数据管线见 从 LLM Infra 到具身智能 Infra;VLA 模型格式见 为什么 VLA 训练选 FSDP。RLark 是这些模型/数据之上的"基础设施编排层"。


一、全景:四层架构一张图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
┌──────────────────────────────────────────────────────────────────┐
│ 控制面(kcp 轻量 K8s API) │
│ Server(HTTPS+SSH+反代+CA) Gateway(REST) ControllerManager │
│ CRD: Domain / Node / Job / Task / Workflow / DomainPeer / Pod │
└────────────┬───────────────────────────────────┬─────────────────┘
│ remotedialer WebSocket 反向隧道 │ SSH 隧道
▼ ▼
┌────────────────────────────┐ ┌──────────────────────────┐
│ 数据面 Agent(每集群) │ │ 跨集群 Pod 网络 │
│ Cluster Agent(pull/push) │◄────────┤ TUN + gVisor netstack │
│ Node Agent(网络隧道) │ SSH │ + SSH 隧道(无 NAT 穿透) │
└────────────┬───────────────┘ relay └──────────────────────────┘

┌──────────────────────────────────────────────────────────────────┐
│ 具身运行时(边缘节点) │
│ Device Plugin(advertise rlinf.io/device) │
│ ROS/ROS2 Controller Camera Controller devinit(macvlan) │
│ gRPC: robot.proto / camera.proto / device.proto │
└──────────────────────────────────────────────────────────────────┘

四个二进制构成控制面+数据面:server(中枢)、gateway(REST 入口)、controller-manager(CRD 调谐)、agent(数据面,分 Cluster Agent / Node Agent);外加 network-sidecar(跨集群网络)、embodied-runtime(边缘设备)。前端 React+Vite,数据库 PostgreSQL+Bun ORM。


二、CRD 模型:七个对象表达"训练→部署"

RLark 的 API group 是 rlinf.io/v1alpha1(注意:Go 包目录叫 rlark.io,但 API group 是 rlinf.io),七个 CRD:

CRD 作用 scope 关键字段
Domain 一个虚拟 L3 网络域(跨集群) cluster spec.cidr + status.ipAllocations[]
DomainPeer 域内某集群的 Pod 集合 + SSH 证书 namespaced spec.pods[] + spec.cert/key(SSH 证书)
Node 域内一个计算节点(GPU 服务器/边缘设备) namespaced 支持 unschedulable(cordon)
Job 用户声明的训练作业 cluster spec.tasks[](JobTaskTemplate)+ spec.domain
Task 叶子工作单元(映射到本地 Pod) namespaced(rlark-{clusterID}) spec.kubernetes/docker/raw + role(Actor/Rollout/Env)
Workflow Job 的 DAG cluster spec.jobTemplates[].dependencies[]
Pod 数据面 Pod 的镜像(不是用户创建) namespaced status.phase/node/ip

2.1 Job → Task:声明式 DAG 拆分

Job 是用户面 (cluster-scoped),它声明一组 JobTaskTemplate(每个带 Head bool 标记 Ray head/worker、内联 TaskSpec)。Job controller 在 buildTask(job/build.go:49-73)里对每个 template 生成一个 Task,名字 {job}-{task},namespace 从 template 的 NodeSelector 解析出目标 Node 所在的 rlark-{clusterID} 命名空间,继承 spec.domainspec.sshPublicKey,盖上 Ray 注解(ray-role/ray-total-nodes/ray-node-rank-start)。

Task 是叶子(namespaced,每个数据面集群一个 rlark-{clusterID} namespace),支持三种 runtime:KubernetesTaskSpec(Deployment/DaemonSet/StatefulSet/CloneSet + pvcStorageMap)、DockerTaskSpecRawTaskSpec——这覆盖了"K8s 集群、Docker 节点、裸机"三种数据面形态。Task 的 RoleActor/Rollout/Env,直接对应 RL 训练的角色分工。

2.2 Workflow:Job 之上的 DAG

WorkflowSpec.JobTemplates[] 每个 WorkflowJobTemplateDependencies []string——这就是 DAG 边。Workflow controller 的 dag.go:16-60 用入度表建图,dispatchReady 返回入度为 0 且未派发的 template,逐层创建子 Job({wf}-{jt},带 SetControllerReference + rlinf.io/workflow 标签)。状态机:Pending → Running → Succeeded/Failed

2.3 Job/Task 状态机

Job controller(job/statemachine.go:22-53):

1
2
3
4
5
"" → Pending(init)
Pending → Running(tasks-running)
Running → Succeeded(all-tasks-succeeded) / Failed(any-task-failed)
Pending/Running/Failed → Stopped(job-stopped)
Stopped → Pending(job-resumed)

evaluateJobEvent 把子 Task 的 phase 聚合成事件(EventAllTasksDone/EventAnyTaskFailed/EventTasksRunning),驱动状态流转。Stopped=true 会把所有子 Task 的 Replicas 强制为 0——声明式停机。

2.4 Domain + DomainPeer:跨集群网络的契约

Domain(cluster-scoped)定义一个虚拟 L3 网络(spec.cidr),Domain controller 从 CIDR 里给每个 Pod 分配虚拟 IP(IPPool),按集群 namespace 分组,在每个集群写一个 DomainPeer(namespaced)。DomainPeer 的 spec.pods[] 记录该集群内属于此 domain 的所有 Pod(GlobalNamespace/Namespace/Name/UID/Node/IP 虚拟 IP/LocalIP 真实 Pod IP),外加 spec.cert/key——一份该 domain 的 SSH 证书,用于跨集群 Pod 通信时的隧道鉴权。这套 CRD 就是后面跨集群网络的路由依据。


三、控制面:kcp 上的 Server/Gateway/ControllerManager

控制面跑在 kcp(轻量 K8s API server)上,三个组件:

3.1 Server:中枢(HTTPS + SSH + 反代 + CA)

Server(apps/rlark/pkg/server/)是整个平台的中枢,一个进程跑五件事(server.go:90-116):

  1. HTTPS API(8443):mTLS(tls.RequireAndVerifyClientCert),handleCertCheck 读客户端证书的自定义 OID 扩展,查吊销列表(DB + 内存缓存)。路由包括 /api/connect(agent websocket 隧道)、/api/proxy/:target/*(反代到 agent)、/api/podproxy/api/taskproxy/api/terminal(WebSocket 终端)、/api/sign(签发证书)、/api/revoke/api/kubernetes/*(impersonated kube API)。
  2. SSH server(2222):charmbracelet/ssh+wish,host key 用 TLS CA 的 key。sshPublicKeyAuth 信任 CA 公钥,校验吊销,UserKeyFallback 兜底查 DB 的 SSHUserKeyStorehandleSSHChannel 实现 direct-tcpip 端口转发——这是跨集群 Pod 网络的关键一跳(后面讲)。
  3. 反代到 agent:核心是 reverseproxy/dialer_factory.go(包装 rancher/remotedialer)。Agent 通过 websocket 反向连到 Server(/api/connect,带 X-Remote-Dialer-Client-Key=agentID),Server 注册该 agent 的 RBAC(建 rlark-{id} namespace、rlark-agent SA、CRUD Role)。Gateway/SSH 要访问 agent 时,调 GetDial(ctx, "default", target, ...)getAgentDialer(agentID) → 复用那条反向隧道。约定 agent 本地 HTTP 在 0.0.0.0:1
  4. CA + 证书签发:sign.go 按 role 签不同证书:
    • admin → X.509,kube impersonation 全权
    • agent → X.509,agent-id + namespace=rlark-{id} + kube impersonation system:serviceaccount:rlark-{id}:rlark-agent + remote-dialer-client-id
    • peer → X.509,server-to-server 发现
    • domainSSH 证书,domain-id(跨集群转发身份)
    • ssh-guest → SSH 证书,用户 SSH 到 Pod
  5. Lease 锁初始化:initrlark-server-init-lock Lease,一次性建 CA、签 admin 证书、迁移 DB。

3.2 Gateway:REST 入口

Gateway(:8080)是给前端/CLI 用的 REST API。路由(router.go):/api/v1/rlinf.io/v1alpha1/{nodes,workflows,jobs,tasks,pods,domains} 全 CRUD(Jobs/Workflows/Domains 是 cluster-scoped),Jobs 的 GET /:name/logs(扇出到 Task→Pod CR,经 Server 反代拉 Pod 日志)、Tasks 的 GET /:name/tensorboard/*(反代到 Pod :6006)、Pods 的 GET /:name/terminal(WebSocket 终端)。还有 /api/v1/certificates/agent(签 agent 证书)、/api/v1/ssh-user-keys/api/v1/clusters、addon catalog。

读路径DB 优先、kube 兜底(handler.go:91-113):配了 PostgreSQL 就走 Bun ORM 的 ResourceStore(JSONB 查询),否则走 kcp typed client。

3.3 ControllerManager:五个调谐器

controller_manager.go:61-113 起五个 reconciler(Job/Task/Workflow/Node/Domain),配了 DB 再加四个 *-sync(把 CR 持久化到 PG)。共享 driver(base.go:22-55):Get → IsTerminal → ReconcileStateMachine → Status().Update。Job controller For(Job).Owns(Task)(子 Task 事件触发 Job 调谐),Workflow For(Workflow).Owns(Job),Domain watch Domain+Pod CR 算 IP、写 DomainPeer。


四、跨集群 Pod 网络:TUN+gVisor+SSH,无 NAT 穿透

这是 RLark 最有技术含量的部分。核心命题:让集群 A 的 Pod 能直接用虚拟 IP 访问集群 B 的 Pod,且两边都在 NAT 后、无入站端口

4.1 为什么不用 WireGuard/IPsec

  • TUN 而非内核 VPN 隧道:sidecar 跑在 Pod 容器里(只需 CAP_NET_ADMIN),TUN+用户态 netstack 不需要内核模块、不需要每节点 wg 接口。per-Pod/per-Domain 注入,DomainPeer CR 动态改写不碰节点路由表。
  • gVisor netstack 做"连接级路由"而非"逐包封装":netstack 终止虚拟 TCP 握手,每个 flow 发一条 target line(tcp://10.2.0.5:80),路由变成每连接一次的控制面决策(发往哪个集群/节点),而非逐包 IP 路由+IPsec SA 选择。WireGuard/IPsec 是包级隧道,要全 mesh 路由、封装每个包,且对应用不可见——而 RLark 的 flow 级 agent-node 寻址正需要连接级可见。
  • SSH+WebSocket 反向隧道彻底避免 NAT 穿透:WireGuard/IPsec 至少要一端可达(或 UDP 打洞/STUN/TURN),对 NAT 后的边缘机器和云 GPU(无入站防火墙洞)不可行。RLark 让每个 agent 主动拨出到控制面(SSH/WebSocket),跨集群 Pod 流量经控制面 hub 中继。代价是多一跳,换零入站端口需求——对边缘场景划算。
  • 复用已有 PKI:domain SSH 证书(DomainPeer.Spec.Cert/Key,role domain)在 GetDial 里做 checkHostInDomain 域级鉴权。WireGuard/IPsec 只给传输加密,无应用级授权;RLark 在 SSH 层白拿 mTLS+域策略。

4.2 数据路径:Pod A → Pod B 全程

设集群 A 的 Pod A(虚拟 IP 10.2.0.4)访问集群 B 的 Pod B(10.2.0.5:80):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
Pod A 进程: TCP 10.2.0.4:40000 → 10.2.0.5:80
│ 内核路由进 gnet0 TUN

① TUN → gVisor netstack(tun.go handleRead → netstack.go ep.InjectInbound)
│ netstack 终止虚拟 TCP 握手

② gVisor 发 target line "tcp://10.2.0.5:80" 到 NodeServer unix socket

③ NodeServer A 路由(nodeserver/server.go):
解析 host → PID → Pod → DomainPeer,发现跨集群
target = "10.2.0.5.<nodeB>.<agentID>.agent-node:5700"

④ SSHDialer(ssh_dialer.go)用 DomainPeer 的 SSH 证书,拨控制面 SSH server
开 direct-tcpip channel 到 "agent-node" 目标

⑤ 控制面 SSH server(ssh_server.go handleSSHChannel):
GetDial("ssh", addr) → getTargetFromDomainHost 拆出 agentID/nodeB/localIP
checkHostInDomain 校验目标 IP 在该 domain 的 DomainPeer 里
getAgentDialer(agentID, nodeB) → remotedialer 拨 "10.2.0.5:5700"
复用 agent B 的反向 WebSocket 隧道到 node B
PipeConnections 桥接 SSH channel ↔ 隧道

⑥ 目的 agent 的 netDialer(tunnel.go):net.Dial("10.2.0.5:5700") → Pod B sidecar Proxy

⑦ Proxy B 本地交付(proxy.go):读 target line,net.Dial("10.2.0.5:80") → 内核本地路由 → Pod B 真服务

应用在 A/B 两端看到的是扁平 IP 网络、无 NAT,字节实际经控制面 SSH+WebSocket 中继。同集群流量可绕过控制面直连目标 Pod proxy(network.go:212-219);跨集群直连是 TODO(network.go:222-224),当前都走 relay。

4.3 DomainPeer:路由契约 + SSH 证书

1
2
3
4
5
6
7
8
9
10
type DomainPeerSpec struct {
PrefixLen int // domain CIDR 前缀
Pods []DomainPodInfo // 该集群内属于此 domain 的所有 Pod
Cert string // domain 的 SSH 证书(跨集群鉴权)
Key string // 对应私钥
}
type DomainPodInfo struct {
GlobalNamespace, Namespace, Name, UID, Node, IP, LocalIP string
// IP=虚拟IP, LocalIP=真实 Pod IP
}

Domain controller watch Domain+Pod CR,从 IPPool 分虚拟 IP,按集群 namespace 分组写 DomainPeer,签 domain SSH 证书(role domain)。NodeServer 路由时查 DomainPeer 找目标 Pod 的 LocalIP+Node+agentID,控制面 checkHostInDomain 校验目标在该域内——CRD 即网络路由表 + 鉴权策略


五、具身运行时:把 ROS 机器人和相机接进 K8s

apps/embodied-runtime/ 解决"边缘节点上的机器人/相机怎么被 K8s 调度和控制"。核心是 Kubernetes Device Plugin + gRPC 服务。

5.1 Device Plugin:advertise rlinf.io/device

pkg/deviceplugin/plugin.go 实现 K8s Device Plugin,ListAndWatch(:424-453)上报 rlinf.io/device 资源(每个配置了的机器人/相机算一个 device)。Allocate(:464-474 + :496-570)给请求该资源的 Pod 注入:

  • /var/run/rlark(controller socket)和 /opt/rlinf/bin(rosctr/camctr)只读挂载;
  • RLINF_EMBODIED_{ROS,ROS2,CAMERA}_{ENABLED,SOCKET_PATH} 环境变量;
  • 若只有一个机器人缓存,直接注入 ROS_MASTER_URI(ROS1)或 ROS_DOMAIN_ID(ROS2),让 Pod 里的 ROS 工具零配置连上机器人。

5.2 ROS/ROS2 Controller + Camera Controller

  • pkg/roscontroller/ + pkg/ros2controller/:管理 ROS/ROS2 机器人。一个"机器人"是一个可 roslaunch/ros2 launch 的 launch 文件 + ROS master/domain。gRPC RobotController(robot.proto)暴露 StartRobot/StopRobot/GetRobotStatus/SwitchMode/ResetRobot/ListRobots/ListModes + 包查询(ListPackages/GetLaunchFileArgs)。StartRobotlaunchModeroslaunch serl_franka_controllers impedance.launch robot_ip:=172.16.0.2(robot_ops.go:314-333)。
  • pkg/cameracontroller/:CameraController(camera.proto)暴露 ListCameras/OpenCamera/CaptureFrame/CaptureFrames/WatchFrames(server-streaming)。controller.go 后台 capture loop 拉帧供观测数据采集。

ROS1 和 ROS2 共用同一套 RobotController 契约(差异在 ros_master_uri vs ros_domain_id),上层 SDK 无感。

5.3 devinit + MutatingWebhook:macvlan 注入

边缘机器人常在物理子网(如 172.16.0.0/24),Pod 要 L2 访问就得挂 macvlan。pkg/mutatingwebhook/ 给请求 rlinf.io/device 的 Pod 自动注入 rlark-devinit init container(webhook.go:240-252),它跑 devinit setup,经 devinit.sockDeviceServer.Setup(device_server.go:155-209)。SetupSO_PEERCRED 拿调用方 PID,在该 Pod 的 netns 里创建 macvlan 接口(pkg/netmac/,vishvananda/netlink 纯 Go 实现,无 ip/nsenter shell-out),把 Pod 接到机器人物理子网。

5.4 SDK:Python/Go 客户端

  • Python(sdks/embodied-runtime-python/):RobotClient(robot.py:133)+ CameraClient(camera.py:36),socket 路径从注入的 RLINF_EMBODIED_*_SOCKET_PATH 环境变量解析(_transport.py),也支持 host:port TCP。
  • Go(sdks/embodied-runtime-go/):DialRobot/DialRobot2/DialCamera(transport.go:79-97),env 感知。

5.5 云训练 → 边缘机器人控制流

  1. 训练/评测 Pod 请求 rlinf.io/device: 1,K8s 调度到有该资源的边缘节点;
  2. Allocate 注入 socket 挂载 + ROS endpoint 环境变量;
  3. (若配 macvlan)webhook 注入 devinit,把 Pod 接到机器人子网;
  4. Pod 内的 workload/agent 经 Unix socket 调 rosctr/SDK → StartRobotroslaunch → 机械臂动;CaptureFrame → 拉观测帧;
  5. 云控制面调度 Pod 到边缘节点(可带 nodeSelector 匹配特定机器人),经 gRPC over socket 控制机器人;HTTP gateway 还把 /v1/robots/{id}/proxy/* 反代到机器人自带 web UI。

核心抽象:机器人/相机被 Device Plugin advertise 成 K8s extended resource,Pod request 它就被调度到对应节点并拿到控制 socket——硬件设备变成了 K8s 可调度、可声明式消费的一等公民


六、端到端:一个具身 RL 训练作业怎么跑

把四层串起来,一个"云训练 + 边缘 rollout"的 RL 作业:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
1. 用户提交 Workflow(DAG: train-job → eval-job)


2. Workflow controller 按 DAG 派发 Job,Job controller 拆成 Task
(Actor/Rollout/Env 角色,带 domain + NodeSelector)


3. Task 落到数据面 agent 的 namespace(rlark-{clusterID})
Cluster Agent pull:把 Task 翻成本地 K8s Pod(Deployment/StatefulSet)

├─ 云端 Task:跑 Ray head/worker + RL 训练(如 FSDP VLA)
└─ 边缘 Task:request rlinf.io/device → 调度到机械臂节点
│ Allocate 注入 ROS socket + (macvlan)

4. 边缘 Pod 内 agent 经 gRPC 控制机器人 rollout,采集轨迹
│ 轨迹数据经跨集群 Pod 网络(虚拟 IP)回传云端数据湖

5. 云端训练 Task 拿新数据继续训,ckpt 更新,新策略下发
DomainPeer/Domain controller 动态维护 Pod 虚拟 IP 路由

整个过程:训练在云 GPU、rollout 在边缘机器人、数据经虚拟网络回流——三件事在一个 kcp 控制面下声明式管理,跨集群 Pod 直连不需要 NAT 穿透,机器人作为 K8s 资源被调度。这就是 RLark 的核心价值。


七、读后感:几个设计亮点

  1. kcp 做控制面:不绑死某个 K8s 集群,跨集群统一管 CRD,数据面可以是 K8s/Docker/裸机三种形态——这比"一个大盘子 K8s 集群"灵活得多,尤其边缘设备跑不了完整 K8s。
  2. DomainPeer = 路由表 + 鉴权策略:跨集群网络的路由信息和 SSH 证书都塞进 CRD,声明式管理,Domain controller 动态维护——网络拓扑随 Pod 增减自动更新,不碰节点路由表。
  3. gVisor netstack 做连接级路由:用用户态 TCP 栈把"逐包路由"变成"每连接一次控制面决策",这是相对 WireGuard 的关键差异——应用可见的 flow 级寻址,才能做 agent-node 这种基于 CRD 的路由。
  4. SSH+反向隧道避 NAT:边缘机器人和云 GPU 都在 NAT 后,双方都拨出到控制面 hub,跨集群流量中继——用一跳延迟换零入站端口,边缘场景的正确取舍。
  5. Device Plugin 把硬件变成 K8s 资源:机器人/相机 advertise 成 rlinf.io/device,Pod request 就被调度并注入控制 socket——具身智能的"硬件即 K8s 资源"抽象,让云端训练框架能像消费 GPU 一样消费机械臂。
  6. 双层证书(X.509 + SSH):X.509 管 mTLS+kube impersonation,SSH 证书管跨集群转发和用户到 Pod 的访问,role 分(admin/agent/peer/domain/ssh-guest)——一套 CA 出两种证书,覆盖所有鉴权场景。

它跟之前写的具身 Infra 系列正好互补:《从 LLM Infra 到具身智能 Infra》讲训练栈+数据管线+仿真集群,本篇讲这些之上的跨集群编排与硬件接入层——把云训练、边缘 rollout、跨集群通信、机器人调度串成一个声明式平台。这是具身智能走向生产化的一块关键基础设施。


参考