小虾 Worker Agent 架构设计报告
> 项目: 小虾 (xiaoxia.app) — OpenClaw 托管平台
> 仓库: github.com/xiaojay/xx (Go monorepo)
> 日期: 2026-03-19
> 方案: 方案B — Worker Agent 架构
1. 背景与问题
小虾是一个 OpenClaw 托管平台,为每个用户分配独立的 Docker 容器运行 OpenClaw 实例。当前架构包含两个核心服务:
- hosting 服务:用户管理 + Docker 容器管理(一用户一容器)
- llmproxy 服务:LLM API 代理,统一管理上游 API 调用
当前痛点
核心问题在于 DockerRuntime 直接通过 client.FromEnv() 连接本机 Docker socket:
// 当前实现 — 绑死在单机
client, err := client.NewClientWithOpts(client.FromEnv)
这意味着:
- 无法跨机器扩展 — 所有容器必须跑在同一台机器上
- 单点故障 — 宿主机挂了,所有用户服务中断
- 容量天花板 — 受单机 CPU/内存限制,用户数有上限
- 运维困难 — 无法做滚动升级、灰度发布
2. 方案选型:为什么是 Worker Agent
备选方案对比
| 方案 | 思路 | 优点 | 缺点 |
|---|---|---|---|
| **A. Docker Remote API** | 控制面直连远程 Docker daemon | 改动最小 | 安全风险极高,等于给 root 权限 |
| **B. Worker Agent** ✅ | 每个节点跑轻量 agent,暴露精简 HTTP API | 安全、灵活、可扩展 | 需要新增一个小服务 |
| **C. Kubernetes** | 全面容器编排 | 功能完整 | 过度工程,运维复杂度剧增 |
| **D. Nomad/Swarm** | 轻量编排 | 社区方案 | 引入外部依赖,学习成本 |
选择 Worker Agent 的理由
1. 安全隔离 — Docker Remote API 直接暴露太危险。Worker Agent 只暴露精简的容器操作接口,攻击面极小
2. 状态感知 — Worker 可主动上报心跳(CPU/内存/容器健康),控制面实时掌握集群状态
3. 扩展友好 — 未来加休眠/唤醒、资源限制、日志收集等逻辑,Worker 自己处理更干净
4. 极轻量级 — Worker 二进制几百行 Go 代码,编译后一个单文件,部署零依赖
5. 渐进迁移 — Phase 1 可以控制面 + Worker 同机部署,完全兼容现有架构
3. 架构总览
┌─────────────────────────────┐
│ Web Hosting (控制面) │
│ │
│ ┌───────┐ ┌──────────────┐ │
│ │ 前端UI │ │ API Server │ │
│ └───────┘ │ - Auth │ │
│ │ - User Mgmt │ │
│ │ - Scheduler │ │
│ │ - Admin API │ │
│ └──────┬───────┘ │
│ │ │
│ ┌─────────────────┘ │
│ │ PostgreSQL / SQLite │
│ │ (nodes, instances, users) │
│ └───────────────────────────┘│
└──────────────┬────────────────┘
│
HTTP + 共享密钥鉴权
┌──────────────┼──────────────┐
│ │ │
▼ ▼ ▼
┌──────────────┐┌──────────────┐┌──────────────┐
│ Worker 1 ││ Worker 2 ││ Worker 3 │
│ (hetzner-01)││ (hetzner-02)││ (aws-01) │
│ ││ ││ │
│ HTTP API ││ HTTP API ││ HTTP API │
│ ┌──────────┐ ││ ┌──────────┐ ││ ┌──────────┐ │
│ │Docker API│ ││ │Docker API│ ││ │Docker API│ │
│ └──────────┘ ││ └──────────┘ ││ └──────────┘ │
│ [container] ││ [container] ││ [container] │
│ [container] ││ [container] ││ [container] │
│ [container] ││ [container] ││ [container] │
└──────────────┘└──────────────┘└──────────────┘
数据流
用户请求创建实例
│
▼
控制面 API 收到请求
│
▼
Scheduler 选择最优 Node(最少容器数优先)
│
▼
控制面 → HTTP POST worker:9090/containers
│
▼
Worker 本地调 Docker API,创建 + 启动容器
│
▼
Worker 返回容器信息 → 控制面写入 DB
│
▼
Worker 定期心跳上报 → 控制面更新 Node 状态
4. 详细设计
4.1 数据模型 — 新增 Node 概念
nodes 表
type Node struct {
ID int64 `db:"id"`
Name string `db:"name"` // "hetzner-arm-01"
Endpoint string `db:"endpoint"` // "https://10.0.0.2:9090"
Region string `db:"region"` // "fsn1"
Capacity int `db:"capacity"` // 最多跑几个容器,默认 20
Status string `db:"status"` // active / draining / offline
SecretKey string `db:"secret_key"` // 共享密钥(加密存储)
LastPing time.Time `db:"last_ping"` // 最后心跳时间
CreatedAt time.Time `db:"created_at"`
UpdatedAt time.Time `db:"updated_at"`
}
Status 状态机:
┌────────────┐
注册 ──────▶│ active │◀──── 恢复
└──────┬─────┘
│ 维护/迁移
▼
┌────────────┐
│ draining │ ← 不再调度新容器,等待现有容器迁走
└──────┬─────┘
│ 全部迁完 / 心跳超时
▼
┌────────────┐
│ offline │ ← 可安全下线
└────────────┘
instances 表改动
ALTER TABLE instances ADD COLUMN node_id INTEGER REFERENCES nodes(id);
现有单机部署时,node_id 指向一个 "local" node 记录,向后兼容。
4.2 Worker Agent
Worker 是一个独立的 Go 二进制(cmd/worker/main.go),每个节点运行一个实例。
HTTP API 定义
POST /containers → 创建并启动容器
GET /containers/:id → 查询容器状态
POST /containers/:id/restart → 重启容器
DELETE /containers/:id → 停止并删除容器
GET /health → 节点健康信息
请求/响应示例
创建容器:
POST /containers HTTP/1.1
Authorization: Bearer <shared-secret>
Content-Type: application/json
{
"container_id": "user-12345",
"image": "openclaw:latest",
"env": {
"OPENCLAW_TOKEN": "xxx",
"OPENCLAW_MODEL": "claude-sonnet-4-20250514"
},
"memory_limit": "512m",
"cpu_limit": "0.5",
"ports": {"8080": "auto"},
"volumes": ["/data/user-12345:/home/openclaw"]
}
响应:
{
"id": "user-12345",
"docker_id": "abc123def456",
"status": "running",
"ports": {"8080": "32768"},
"created_at": "2026-03-19T10:00:00Z"
}
健康检查:
GET /health HTTP/1.1
Authorization: Bearer <shared-secret>
{
"node": "hetzner-arm-01",
"status": "healthy",
"uptime_seconds": 86400,
"resources": {
"cpu_cores": 4,
"cpu_usage_pct": 35.2,
"memory_total_mb": 8192,
"memory_used_mb": 3456,
"disk_total_gb": 100,
"disk_used_gb": 42
},
"containers": {
"total": 12,
"running": 11,
"stopped": 1
}
}
核心实现
// cmd/worker/main.go
package main
import (
"context"
"net/http"
"time"
"github.com/docker/docker/client"
)
type WorkerAgent struct {
docker *client.Client
secret string
nodeName string
ctrlURL string // 控制面地址,用于心跳上报
}
func (w *WorkerAgent) authMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
token := r.Header.Get("Authorization")
if token != "Bearer "+w.secret {
http.Error(rw, "unauthorized", 401)
return
}
next.ServeHTTP(rw, r)
})
}
// 心跳上报 — 每 30 秒
func (w *WorkerAgent) heartbeatLoop(ctx context.Context) {
ticker := time.NewTicker(30 * time.Second)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return
case <-ticker.C:
w.reportHealth()
}
}
}
安全设计
| 层面 | 措施 |
|---|---|
| 传输加密 | HTTPS(TLS 1.3),内网可用 mTLS |
| 身份验证 | 共享密钥(Bearer Token),每个 Worker 独立密钥 |
| 访问控制 | Worker 只接受来自控制面 IP 的请求(防火墙规则) |
| Docker 隔离 | Worker 进程有 Docker 权限,但只暴露精简 API |
| 审计日志 | 所有容器操作记录到本地日志 + 上报控制面 |
4.3 RemoteRuntime — 适配现有接口
关键设计:RemoteRuntime 实现现有的 ContainerRuntime 接口,控制面代码无需大改。
// 现有接口(不变)
type ContainerRuntime interface {
Create(ctx context.Context, opts CreateOpts) (string, error)
Start(ctx context.Context, id string) error
Stop(ctx context.Context, id string) error
Restart(ctx context.Context, id string) error
Remove(ctx context.Context, id string) error
Status(ctx context.Context, id string) (ContainerStatus, error)
}
// 新增:通过 HTTP 调用 Worker 的实现
type RemoteRuntime struct {
endpoint string // Worker 的地址
secret string // 共享密钥
http *http.Client // 带超时的 HTTP client
nodeID int64 // 对应的 Node ID
}
func NewRemoteRuntime(node Node) *RemoteRuntime {
return &RemoteRuntime{
endpoint: node.Endpoint,
secret: node.SecretKey,
http: &http.Client{
Timeout: 30 * time.Second,
Transport: &http.Transport{
TLSClientConfig: &tls.Config{
// 生产环境配置证书验证
},
},
},
nodeID: node.ID,
}
}
func (r *RemoteRuntime) Create(ctx context.Context, opts CreateOpts) (string, error) {
body, _ := json.Marshal(opts)
req, _ := http.NewRequestWithContext(ctx, "POST",
r.endpoint+"/containers", bytes.NewReader(body))
req.Header.Set("Authorization", "Bearer "+r.secret)
req.Header.Set("Content-Type", "application/json")
resp, err := r.http.Do(req)
if err != nil {
return "", fmt.Errorf("worker unreachable: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return "", fmt.Errorf("worker error: %d", resp.StatusCode)
}
var result CreateResult
json.NewDecoder(resp.Body).Decode(&result)
return result.ID, nil
}
// Stop, Restart, Remove 类似...
RuntimeFactory — 根据 Node 创建对应的 Runtime
type RuntimeFactory struct {
localRuntime ContainerRuntime // 本地 Docker(兼容 Phase 1)
remoteCache map[int64]*RemoteRuntime
}
func (f *RuntimeFactory) GetRuntime(node Node) ContainerRuntime {
if node.Name == "local" {
return f.localRuntime
}
if rt, ok := f.remoteCache[node.ID]; ok {
return rt
}
rt := NewRemoteRuntime(node)
f.remoteCache[node.ID] = rt
return rt
}
4.4 调度器
创建实例时,调度器选择最优 Node。初期策略简单直接:最少容器数优先。
type Scheduler struct {
db *sql.DB
}
func (s *Scheduler) SelectNode(ctx context.Context) (*Node, error) {
query := `
SELECT n.*,
(SELECT COUNT(*) FROM instances
WHERE node_id = n.id AND status = 'running') as running_count
FROM nodes n
WHERE n.status = 'active'
AND n.last_ping > NOW() - INTERVAL '2 minutes'
ORDER BY running_count ASC
LIMIT 1
`
var node Node
err := s.db.QueryRowContext(ctx, query).Scan(&node)
if err != nil {
return nil, fmt.Errorf("no available nodes: %w", err)
}
// 容量检查
if node.RunningCount >= node.Capacity {
return nil, fmt.Errorf("all nodes at capacity")
}
return &node, nil
}
未来可扩展的调度策略:
| 策略 | 描述 | 适用场景 |
|---|---|---|
| 最少容器数 | 选容器最少的 Node | 均匀分布(默认) |
| 资源感知 | 综合 CPU/内存使用率 | 异构节点 |
| 区域亲和 | 优先选用户最近的区域 | 多地域部署 |
| 打包优先 | 尽量填满一个 Node | 节省成本(休眠空闲 Node) |
4.5 Node 管理 API
管理员通过 API 注册和管理 Worker 节点。
POST /api/admin/nodes → 注册新 Node
GET /api/admin/nodes → 列出所有 Nodes + 实时状态
GET /api/admin/nodes/:id → 单个 Node 详情
PATCH /api/admin/nodes/:id → 更新 Node(设 draining 等)
DELETE /api/admin/nodes/:id → 下线 Node
注册 Node 流程
管理员 → POST /api/admin/nodes
{ name, endpoint, region, capacity }
│
▼
控制面 → 生成共享密钥
→ 写入 DB
→ 尝试 GET worker:9090/health 验证连通
│
▼
返回 → { node_id, secret_key } ← 管理员配置到 Worker
节点状态监控
控制面持续检查 Worker 心跳:
// 每分钟检查一次
func (m *NodeMonitor) checkNodes() {
nodes, _ := m.db.GetAllNodes()
for _, node := range nodes {
if time.Since(node.LastPing) > 2*time.Minute {
if node.Status == "active" {
log.Warn("node heartbeat timeout", "node", node.Name)
// 可选:自动标记为 offline
// m.db.UpdateNodeStatus(node.ID, "offline")
}
}
}
}
5. 容器生命周期管理
创建实例(完整流程)
1. 用户点击 "创建实例"
2. API Server 验证用户配额
3. Scheduler.SelectNode() → 选择目标 Node
4. RuntimeFactory.GetRuntime(node) → 获取 RemoteRuntime
5. RemoteRuntime.Create(opts) → HTTP POST worker/containers
6. Worker 本地 docker.ContainerCreate() + docker.ContainerStart()
7. Worker 返回容器信息
8. 控制面写入 instances 表(含 node_id)
9. 返回用户实例信息
停止/删除实例
1. API 收到删除请求
2. 查 instances 表 → 获取 node_id
3. 通过 node_id 找到对应 Node → 获取 RemoteRuntime
4. RemoteRuntime.Remove(containerId) → HTTP DELETE worker/containers/:id
5. Worker 本地 docker.ContainerStop() + docker.ContainerRemove()
6. 更新 instances 表状态
容器健康检查
Worker 定期检查本地所有容器的健康状态,并随心跳一起上报:
func (w *WorkerAgent) checkContainers() []ContainerHealth {
containers, _ := w.docker.ContainerList(ctx, types.ContainerListOptions{All: true})
var health []ContainerHealth
for _, c := range containers {
stats, _ := w.docker.ContainerStats(ctx, c.ID, false)
health = append(health, ContainerHealth{
ID: c.ID,
Status: c.Status,
CPUPct: calculateCPU(stats),
MemoryMB: stats.MemoryStats.Usage / 1024 / 1024,
})
}
return health
}
6. 错误处理与容错
Worker 不可达
func (r *RemoteRuntime) Create(ctx context.Context, opts CreateOpts) (string, error) {
resp, err := r.http.Do(req)
if err != nil {
// Worker 不可达 → 标记 Node 异常,通知管理员
r.markNodeUnhealthy()
return "", ErrNodeUnreachable
}
// ...
}
重试策略
| 操作 | 重试次数 | 退避策略 | 备注 |
|---|---|---|---|
| 创建容器 | 0 | 不重试 | 直接换 Node 重试 |
| 查询状态 | 3 | 指数退避 | 1s → 2s → 4s |
| 停止容器 | 2 | 固定间隔 | 5s |
| 心跳上报 | 不限 | 固定间隔 | 每 30s 自动重试 |
Node 故障转移
Node 心跳超时 (>2min)
│
▼
控制面标记 Node = offline
│
▼
该 Node 上的实例标记为 unknown
│
▼
通知管理员
│
▼
管理员决策:
├── 等待恢复 → Node 恢复后心跳自动更新状态
└── 迁移实例 → 在其他 Node 重建容器
7. 改动量评估
| 模块 | 具体改动 | 新增代码量 | 工作量 |
|---|---|---|---|
| DB Schema | +nodes 表,instances 加 node_id,migration 文件 | ~50 行 SQL | 很小 |
| Worker Agent | `cmd/worker/main.go`,Docker 操作封装 + HTTP API + 心跳 | ~400 行 Go | 半天 |
| RemoteRuntime | HTTP client 实现 ContainerRuntime 接口 | ~200 行 Go | 小 |
| RuntimeFactory | 根据 Node 创建对应 Runtime | ~50 行 Go | 很小 |
| Scheduler | 选 Node 逻辑 | ~80 行 Go | 很小 |
| 控制面 API | createInstance 加调度 + admin nodes CRUD | ~200 行 Go | 小 |
| 前端 | Admin Nodes 状态页(可 Phase 2 做) | — | 可选 |
| **总计** | **~1000 行** | **1-1.5 天** |
对现有代码的影响
改动极小:
ContainerRuntime接口不变- 现有
DockerRuntime不变(Phase 1 继续用) createInstance只需加一步scheduler.SelectNode()和factory.GetRuntime(node)- 数据库加一张表 + 一个外键
风险极低:
- Phase 1 可以控制面 + Worker 同机部署,行为与现有完全一致
- 出问题随时回退到直连 Docker
8. 演进路线
Phase 1:单 Worker(兼容现有)
┌─────────────┐
│ 同一台机器 │
│ │
│ Web Hosting │
│ │ │
│ ▼ │
│ Worker │
│ Docker │
└─────────────┘
- 控制面 + Worker 同机部署
- Node 表只有一条 "local" 记录
RemoteRuntime调用localhost:9090- 目标:验证 Worker Agent 的 HTTP API 和心跳机制
- 工作量:1 天
Phase 2:多 Worker
┌─────────────┐ ┌─────────────┐
│ Web Hosting │────▶│ Worker 1 │
│ (控制面) │────▶│ Worker 2 │
│ + Scheduler│────▶│ Worker 3 │
└─────────────┘ └─────────────┘
- 加入调度器逻辑
- 支持多 Node 注册
- Admin Nodes 管理页面
- 工作量:0.5 天
Phase 3:高级功能
- 容器休眠/唤醒:闲置容器自动暂停,请求时秒级唤醒
- Node 自动伸缩:根据负载自动添加/移除 Node(集成云 API)
- 跨区域调度:用户就近分配 Node
- 实例迁移:Node 维护时自动迁移容器到其他 Node
- 日志聚合:Worker 收集容器日志,统一上报
9. 与方案A(Docker Remote API)的详细对比
| 维度 | Docker Remote API | Worker Agent |
|---|---|---|
| 安全性 | ❌ 暴露 Docker socket = root 权限 | ✅ 精简 API,最小权限 |
| 状态感知 | ❌ 控制面不知道 Node 状态 | ✅ 心跳上报,实时监控 |
| 扩展性 | ⚠️ 能跑但粗糙 | ✅ 天然支持多 Node |
| 运维 | ❌ Docker TLS 证书管理复杂 | ✅ 简单共享密钥 |
| 自定义逻辑 | ❌ 受 Docker API 限制 | ✅ Worker 可加任意逻辑 |
| 代码量 | 少(改 endpoint 即可) | 多一点(~400 行 Worker) |
| 总结 | 快糙猛,安全隐患大 | 稍多工作量,但架构干净 |
10. 部署拓扑建议
小规模(1-50 用户)
1 台控制面 (2C4G) + 1 台 Worker (4C8G)
预估支撑 ~20 个并发容器
中规模(50-200 用户)
1 台控制面 (2C4G) + 3 台 Worker (4C8G each)
预估支撑 ~60 个并发容器
加 DB 备份、监控告警
大规模(200+ 用户)
控制面高可用(2 台 + LB)
N 台 Worker,按需扩缩
对象存储(用户数据)
日志聚合(ELK/Loki)
11. 总结
Worker Agent 架构是小虾从单机走向分布式的最佳路径:
1. 改动小 — 约 1000 行新代码,1-1.5 天工作量
2. 风险低 — Phase 1 完全兼容现有部署,可随时回退
3. 扩展强 — 天然支持多节点、多区域
4. 安全好 — 不暴露 Docker socket,精简 API + 共享密钥
5. 演进清晰 — 三个 Phase,每步都能独立交付价值
核心原则:控制面负责调度决策,数据面负责容器操作。这个分离让两边都可以独立演进,是分布式系统的经典设计模式。
报告作者:托尼 🦾
生成日期:2026-03-19