小虾 Worker Agent 架构设计报告

> 项目: 小虾 (xiaoxia.app) — OpenClaw 托管平台

> 仓库: github.com/xiaojay/xx (Go monorepo)

> 日期: 2026-03-19

> 方案: 方案B — Worker Agent 架构

1. 背景与问题

小虾是一个 OpenClaw 托管平台,为每个用户分配独立的 Docker 容器运行 OpenClaw 实例。当前架构包含两个核心服务:

当前痛点

核心问题在于 DockerRuntime 直接通过 client.FromEnv() 连接本机 Docker socket:


// 当前实现 — 绑死在单机
client, err := client.NewClientWithOpts(client.FromEnv)

这意味着:

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半天
RemoteRuntimeHTTP client 实现 ContainerRuntime 接口~200 行 Go
RuntimeFactory根据 Node 创建对应 Runtime~50 行 Go很小
Scheduler选 Node 逻辑~80 行 Go很小
控制面 APIcreateInstance 加调度 + admin nodes CRUD~200 行 Go
前端Admin Nodes 状态页(可 Phase 2 做)可选
**总计****~1000 行****1-1.5 天**

对现有代码的影响

改动极小

风险极低

8. 演进路线

Phase 1:单 Worker(兼容现有)


┌─────────────┐
│  同一台机器   │
│             │
│ Web Hosting │
│      │      │
│      ▼      │
│   Worker    │
│   Docker    │
└─────────────┘

Phase 2:多 Worker


┌─────────────┐     ┌─────────────┐
│ Web Hosting │────▶│  Worker 1   │
│  (控制面)    │────▶│  Worker 2   │
│  + Scheduler│────▶│  Worker 3   │
└─────────────┘     └─────────────┘

Phase 3:高级功能

9. 与方案A(Docker Remote API)的详细对比

维度Docker Remote APIWorker 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