聊一个我最近反复折腾的话题:OpenClaw 的生产级部署。如果你只是想在本地把 OpenClaw 跑起来验证功能,那一条 docker run 就够了,顶多再配个环境变量。但一旦要放到正式环境里,事情就完全变味了——服务要扛得住流量波动,节点要经得起故障,权限要防得住误操作,日志还得能追溯出每一次变更来源。这篇文章就是我前后踩了小一个月坑之后沉淀下来的整套方案,包含集群架构拆分、高可用部署步骤、滚动发布、定制化安全加固和故障排查记录,适合已经接触过 OpenClaw、正准备把它放到正式业务里的团队。
我不会讲那些花里胡哨的“最佳实践”概念,只讲我实际用过的、验证过的做法。下面每一节都有可复制的配置、命令和踩坑提醒,你可以直接对照着落地。
1. 高可用部署的整体架构设计
1.1 OpenClaw 的核心工作模型
先理清 OpenClaw 是怎么跑起来的,否则后面所有高可用、安全加固都没有依据。OpenClaw 的运行时大致可以分成两个平面:控制面和数据面。
控制面由一个 controller 进程负责,主要干三件事:接收任务请求、把任务拆分并投递到队列、维护任务状态机。数据面由多个 worker 进程组成,每个 worker 从队列里拉取任务、执行具体的业务逻辑(采集、转码、回调、处理等等),执行完成后再把结果回写到元数据库。
这个模型的关键点在于:controller 是有状态的,任务状态一旦错乱,整个调度链路就废了;worker 是无状态的,任何 worker 挂了,它手里的任务只需要重新入队即可。所以我们做高可用时,思路非常清晰——controller 做一主一备,worker 做横向扩容,中间的消息队列和元数据库必须比 OpenClaw 本身更加稳。
这部分我最初没理解透,以为只要把 controller 多启动几个实例就行,结果多个 controller 同时调度同一个任务,直接把队列搞乱套了。后面翻文档才确认,OpenClaw 的 controller 默认就是单活的,必须靠外部的分布式锁来做选主。
1.2 高可用设计的四条主线
我把高可用方案拆成了四条主线,每条对应一套机制,后面部署时全部要落地:
第一,状态外置。controller 和 worker 进程本身不保存任何持久化数据。任务元数据全部写到 PostgreSQL,队列数据全部放在 Redis Stream 里,选主锁也放在 Redis 里。做到这一点之后,任何一个节点被销毁再重建,都不会影响整体链路。
第二,冗余部署。controller 部署两个节点,由 Redis 分布式锁保证同一时刻只有一个 controller 对外服务,另一个节点热备。worker 部署至少两个节点,因为 worker 无状态,横向加机器就是加能力,不需要额外协调。
第三,健康检查与自动摘流量。Nginx 会定时探测 controller 的 /healthz 接口,如果主 controller 连续三次探测失败,Nginx 直接把流量切到备节点,同时 Redis 锁自动过期,备节点升级为主节点。
第四,平滑发布。发布不能粗暴 kill 进程,OpenClaw 的 worker 在接收 SIGTERM 后需要一点时间把手上正在跑的任务标记为待重试,然后优雅退出。如果直接强杀,轻则任务重复执行,重则消息队列积压告警。
1.3 网络拓扑与端口规划
生产环境的网络分区必须图省事,我见过太多人把 PostgreSQL 和 Redis 的端口直接暴露在公网上,最后被扫库。下面是我实际使用的端口规划表:
| 组件 | 默认端口 | 是否对外 | 说明 |
|---|---|---|---|
| Nginx | 443 | 对外 | 唯一对外的入口,TLS 终止在这里 |
| controller | 8080 | 仅内网 | 被 Nginx 反向代理,不直接暴露 |
| worker | 9001 | 仅内网 | 由 controller 主动连接,不对外 |
| PostgreSQL | 5432 | 仅内网 | 元数据库,SSH 隧道兜底 |
| Redis | 6379 | 仅内网 | 队列与分布式锁 |
| Node Exporter | 9100 | 仅内网 | 运维监控采集 |
这里有个细节:worker 是不需要经网关的。controller 会通过配置文件里的 worker 注册地址直连 worker,相当于控制面到数据面的内部通道。所以我在安全加固时,重点管控的是 controller 和 worker 之间的这个内部网络,而不是把所有流量都堵在 Nginx 一层。
2. 部署环境准备与基础高可用组件搭建
2.1 服务器选型与基础环境
我先说一个经验:不要一上来就买大内存机器。OpenClaw 这类任务调度型应用,瓶颈通常不在单机配置,而在架构冗余。我这边最终用了四台 4 核 8G 的云主机加两台 2 核 4G 的中间件机器,分配如下:
- controller 主备:两台 4C8G,跑 controller + Nginx。
- worker 集群:两台 4C8G,跑 worker,后面流量上来可以再加。
- 中间件隔离:一台 2C4G 跑 PostgreSQL,一台 2C4G 跑 Redis。
为什么把中间件单独拆出来?因为有次我对 controller 做滚动发布时,误把 Redis 容器一起重启了,结果队列全丢,那次故障给我留下的教训是:中间件和应用绝不能混在一台机器上,否则应用发布就是在赌数据库不会挂。
基础环境统一用 Linux 发行版,内核版本尽量新一点。我这边用的是一个长期维护版本,主要看重它对 cgroup、网络命名空间以及 TCP BBR 的支持。装完系统后第一件事不是装 OpenClaw,而是先调内核参数,后面第 5 节会展开讲。
2.2 PostgreSQL 主从流复制与 Redis 哨兵搭建
PostgreSQL 的高可用我选的是流复制加手动切换。为什么不直接用自动 failover 组件?因为自动切换需要额外引入一个仲裁节点,复杂度上去了,收益对我来说不大。业务允许的恢复时间窗口是 5 分钟,手动切换到从库完全够用。
主库配置里要开 wal_level = replica,并创建一个专门用于复制的账号:
# 在主库执行 CREATE ROLE replica WITH REPLICATION LOGIN PASSWORD '要用强密码'; # pg_hba.conf 中增加 host replication replica 内网网段/24 md5从库直接用 pg_basebackup 拉取基础数据,然后配置 standby.signal 文件,流复制就起来了。切换时把从库 promote,然后把 Nginx 里数据库连接串改成新主库地址即可。
Redis 这边我用了 Redis Sentinel,三个 sentinel 节点监控一主一从。这种 3 节点 sentinel 是最常见的最小高可用单元,任何一个 sentinel 挂掉都不会影响故障判断的法定人数。sentinel 配置核心是下面几行,要放到每台 Redis 节点上:
sentinel monitor openclaw-redis 主库IP 6379 2 sentinel down-after-milliseconds openclaw-redis 5000 sentinel failover-timeout openclaw-redis 10000 sentinel parallel-syncs openclaw-redis 1我提一个很容易被忽略的点:OpenClaw 连接 Redis 时,Java 或 Go 的客户端库大多支持 sentinel 模式,你需要在 OpenClaw 的配置里写 sentinel 的地址列表,而不是写 Redis 主库的地址。否则 sentinel 切换主从之后,客户端还抱着旧主库地址不放,会一直报连接拒绝。
2.3 Nginx 负载均衡与健康检查配置
Nginx 作为入口,我的配置思路是:upstream 里放 controller 的主备两个节点,启用主动健康检查,每 2 秒探测一次 /healthz。一旦主节点失败,流量自动落到备节点。
这里有一个关键点:健康检查接口不要返回包含版本号、内部模块状态这类信息。我第一次只图省事,让 /healthz 直接把 controller 的版本和队列状态打出来,结果在一次安全扫描中暴露了内部信息。后面我把健康检查接口改成了只输出固定字符串 UP,任何内部细节都不往外带。
Nginx 反向代理关键配置节选如下:
upstream openclaw_controller { server controller1.internal:8080 max_fails=3 fail_timeout=10s; server controller2.internal:8080 backup; keepalive 32; } server { listen 443 ssl; server_name openclaw.example; ssl_certificate /etc/nginx/ssl/fullchain.pem; ssl_certificate_key /etc/nginx/ssl/privkey.pem; ssl_protocols TLSv1.2 TLSv1.3; location / { proxy_pass http://openclaw_controller; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-Proto $scheme; } location = /healthz { access_log off; proxy_pass http://openclaw_controller; } }这里用了 backup 指令把备节点标成备份节点,正常情况下 Nginx 不会把流量分给它。这不是偷懒,而是避免两个 controller 同时处理写请求导致选主逻辑混乱。健康检查照常对 backup 节点发起,所以备节点会一直保持热就绪的状态。
3. 核心服务部署与滚动发布落地方案
3.1 controller 与 worker 的 systemd 部署
服务编排我最终用了 systemd,而不是上容器编排。为什么?因为 OpenClaw 本身的依赖只有 Redis 和 PostgreSQL,没有复杂的服务网格需求。systemd 配好之后,ssh 到机器上 systemctl status 就能看状态,排查问题的成本比容器编排低非常多。后面如果需要弹性扩缩容,再套一层容器编排也不冲突。
controller 的 systemd unit 文件长这样:
[Unit] Description=OpenClaw Controller After=network-online.target Wants=network-online.target [Service] User=openclaw Group=openclaw PermissionsStartOnly=true ExecStartPre=/usr/bin/mkdir -p /var/log/openclaw ExecStart=/opt/openclaw/bin/openclaw-controller \ --config /etc/openclaw/controller.yaml \ --log-format json Restart=always RestartSec=3 LimitNOFILE=65536 [Install] WantedBy=multi-user.targetworker 的 unit 类似,只是 ExecStart 换成 openclaw-worker,配置里 worker 要指定一个唯一的节点 ID,同时配置 controller 的地址和用于认证的集群 Token。这个 Token 必须是每个 worker 独有的,不能所有 worker 用同一个,否则日志审计时分不清任务到底由哪个节点处理的。
3.2 容器化部署与 systemd 的方案对比
我这里把两种方案做一次对比,方便你按团队情况选择。不是所有场景都用 systemd,也不是所有场景都该上容器:
| 对比维度 | systemd 裸机部署 | 容器编排部署 |
|---|---|---|
| 排查复杂度 | 低,日志直接 journalctl 看 | 中等,要先查容器事件 |
| 扩缩容速度 | 较慢,需要预置机器 | 快,拉起容器即可 |
| 资源隔离 | 依赖 cgroup 手动配置 | 内置但需要调优 |
| 适合场景 | 团队规模小、服务数量少 | 服务多、需要自动伸缩 |
| 我踩过的坑 | 文件句柄上限默认太小 | 容器内时间不同步导致任务超时 |
如果你要上容器编排,有一个很隐蔽的坑:容器默认的日志驱动会吃掉大量磁盘。Task 类应用本来就日志多,我建议在编排层面直接限制单容器的日志文件大小和轮转数量,否则运行 3 天之后 /var/lib 会被撑爆。
3.3 滚动发布与优雅下线操作流程
滚动发布我们踩过一次很大的坑,当时直接 kill worker 进程,导致一批正在执行的任务全部卡在 running 状态,直到超时 30 分钟才被回收。从那以后,我发布流程里强制加了优雅下线步骤。
优雅下线的原理是:worker 收到 SIGTERM 信号后,先把正在执行的任务标记为“提前返回”,再把任务重新投递到 Redis Stream,最后退出进程。OpenClaw 原生支持这个行为,但前提是进程收到信号后必须等待最大执行时间。所以我在发布脚本里用了等待循环,而不是直接 kill -9:
#!/usr/bin/env bash # 对 worker 做优雅下线 for worker in worker1 worker2; do ssh $worker "sudo systemctl stop openclaw-worker" & pid=$! # 最多等 90 秒,给正在执行的任务留出归还时间 timeout 90 tail --pid=$pid -f /dev/null || true done # 所有 worker 停完之后,再启新版本 for worker in worker1 worker2; do ssh $worker "sudo systemctl start openclaw-worker" done这个脚本看起来简单,但里面有个重要细节:worker 要逐个停止,不能同时全停。如果一个任务特别耗时,同时停掉所有 worker 会导致队列积压,而 controller 的背压机制会自动降速接收新任务,用户的调用方会看到超时。
controller 的滚动发布流程更保守一点:先停备节点,把备节点升级成主节点;确认新版本主节点运行正常后,再处理旧主节点。顺序反过来的话,整个调度会停摆数十秒。
4. 定制化安全加固实战
4.1 应用层身份认证与授权
OpenClaw 原生带了最简单的 API Token 认证,但生产环境我强烈建议再加一层网关层的统一身份校验,把用户的身份信息统一透传到后端。否则每个 controller 都自己维护一份密钥,密钥轮换时你会疯掉。
我在 Nginx 后面加了一个独立的认证中间件,负责校验 JWT。用户请求先经过这个中间件,token 合法后会把 user_id 和 role 塞到请求头里,再转发给 controller。这样做的好处是认证逻辑和业务逻辑彻底解耦,OpenClaw 本身不用关心谁在调用它。
角色我划分了三档,权限矩阵如下:
| 角色 | 查看任务 | 创建任务 | 修改任务 | 删除任务 | 管理 worker |
|---|---|---|---|---|---|
| viewer | 允许 | 拒绝 | 拒绝 | 拒绝 | 拒绝 |
| operator | 允许 | 允许 | 允许 | 拒绝 | 拒绝 |
| admin | 允许 | 允许 | 允许 | 允许 | 允许 |
这个矩阵简单清晰,实际使用中已经覆盖了 90% 的场景。最开始我多设计了几档角色,后来发现没人用,反而增加维护成本。
4.2 传输层加密与证书自动轮换
生产环境禁止任何裸 HTTP 流量,这一点没有妥协空间。Nginx 终止 TLS 之后,内部 controller 与 worker 之间的通讯也不能裸跑。OpenClaw 支持在 controller 与 worker 之间配置双向 TLS,也就是 mTLS,我在内部网络里启用了它。
启用 mTLS 之后,controller 连接 worker 时需要同时出示客户端证书,worker 也要校验 controller 证书,双方都验明身份才建立连接。这一步可以有效防止内部网络的端口扫描和伪造节点接入。
证书轮换我配置了自动脚本,到期前 15 天自动续签。第一次配置证书自动轮换时我忽略了一个细节:续签后的证书没有重启 Nginx 进程,导致 Nginx 一直持有旧证书,实际对外服务的还是过期那份。所以轮换脚本的最后一步必须强制 reload 一下相关服务:
certbot renew --quiet --deploy-hook "systemctl reload nginx" systemctl reload nginx4.3 密钥管理与最小权限原则
我见过不少团队把数据库密码直接写在 OpenClaw 的配置文件里,配置文件权限还是 644,任何能登录服务器的用户都能读出来。这个习惯只要出一次事故就够你后悔半辈子。
我的做法是:数据库密码、Redis 密码、集群 Token 全部从 OpenClaw 的配置里剥离,通过 systemd environment file 注入。systemd 的 EnvironmentFile 指向一个权限为 600 的文件,属主是 openclaw 用户,只有它自己能读。这样即使配置仓库泄露,攻击者也拿不到真实密钥。
密钥轮换策略上,我坚持每个环境独立密钥,生产环境的密钥绝不与测试环境共用。集群 Token 每 90 天强制轮换一次,轮换时我先把新 Token 写到所有节点,再重启服务,不能先重启部分节点,否则会出现一部分 worker 拿着新 Token 连不上旧 controller 的问题。
4.4 审计日志与异常行为监控
生产环境出问题不可怕,可怕的是出了问题之后查不到“是谁、在什么时间、做了什么”。我把 OpenClaw 的日志全部切换成了结构化 JSON,每条日志包含 user_id、action、resource、result、client_ip、request_id 这些字段。后面排查问题时,直接按 request_id 串联一整条调用链,效率比看文本日志高一个数量级。
审计日志会记录到独立文件,并且通过网络日志采集直接送进日志平台,不在本地保留太长时间。原因是本地日志很容易在排查问题时被误删,更别提被攻击者清理痕迹。
异常监控方面我重点盯三个指标:任务失败率突增、Redis 队列积压数量、controller 的健康检查失败次数。其中队列积压是我发现过多次问题的源头,单独设置了告警:积压超过 5000 条持续 1 分钟就触发告警。
5. 容量规划、压力测试与性能调优
5.1 容量估算与资源预算方法
生产环境一定不能等资源不够了才去扩容,但也不能一开始就堆一堆用不上的机器。我提供一个简单可用的容量估算方法,拿我们实际业务举例:
假设每天要处理 10 万条任务,大部分集中在工作时段 8 小时,峰值是平均值的 3 倍。那么峰值 QPS 是:
100000 × 3 ÷ (8 × 3600) ≈ 10.4 QPS
这个 QPS 对 controller 来说毫无压力,真正决定需要多少 worker 的是单任务平均耗时。假设单任务平均耗时 200ms,那么单 worker 的单核处理能力大约是 5 QPS,要覆盖 10.4 QPS 的峰值,至少需要 3 个 worker 节点再加上 1 个冗余节点,共 4 个 worker。
这个计算过程你一定要自己跑一遍,因为每个业务的任务耗时差异巨大。我之前用别人给的例行建议直接配 worker 数量,结果发现任务平均耗时是 1.2 秒,直接队列雪崩。
5.2 关键内核参数与中间件调优
TCP 连接数在任务调度场景下很容易成为瓶颈。我统一在服务器上执行了下面的核参调整,这套配置在 4C8G 的机器上实测很稳:
net.ipv4.tcp_max_syn_backlog = 8192 net.core.somaxconn = 65535 net.ipv4.ip_local_port_range = 1024 65535 net.ipv4.tcp_tw_reuse = 1 fs.file-max = 2097152 vm.overcommit_memory = 1其中 vm.overcommit_memory = 1 是为了给 Redis 的 fork 操作留足内存,避免在内存碎片化时 bgsave 直接失败。这个参数不调的话,Redis 偶尔会在低峰期做持久化时崩溃,表面现象是 Redis 连接突然全部断开。
中间件调优方面,PostgreSQL 的 shared_buffers 设置为总内存的 25%,work_mem 设置为 4MB。这两个参数我改过很多次,目前这个配置在 2G 内存的专用机器上表现最佳。Redis 的 maxmemory-policy 要看具体用法,如果 Redis 只做队列和锁,不要用 allkeys-lru,否则任务队列里的消息可能被意外淘汰。
5.3 压测方法与性能基线
压测的核心目标不是把机器打满,而是找到系统的拐点。我用的压测方式是模拟任务提交请求打到 Nginx,逐步加并发,同时观测四个指标:controller 接口 P99 延迟、worker 消费延迟、队列积压数量、错误率。
下面基线是我们历时一个月压出来的,可以参考:
| 指标 | 基准值 | 告警阈值 |
|---|---|---|
| controller 接口 P99 | < 300ms | > 500ms 告警 |
| worker 消费延迟 | < 2s | > 10s 告警 |
| 队列积压数量 | 0 | > 5000 告警 |
| 任务失败率 | < 0.5% | > 2% 告警 |
压测工具用开源压测工具就行,直接压 Nginx 的 443 端口,模拟真实流量路径。我特别说明一点:压测数据不能只看 QPS,还要看队列积压恢复速度。如果压测结束后队列积压需要超过 10 分钟才能消化完,说明 worker 能力不足,要扩容而不是加超时时间。
6. 常见故障排查与运维避坑实录
6.1 高频故障速查表
下面这些故障类型是我在实际运维中真的遇到过的,整理成速查表,遇到问题可以直接对号入座:
| 现象 | 可能原因 | 排查命令 | 解决办法 |
|---|---|---|---|
| controller 频繁重启 | 内存不足或文件句柄耗尽 | dmesg | tail, ulimit -n | 调整 systemd 里 LimitNOFILE |
| 任务一直 pending | worker 未注册 | openclaw-ctl worker list | 检查 worker 的 Token 是否有效 |
| 队列积压只增不减 | worker 消费能力不足 | redis-cli xlen 队列名 | 扩容 worker 节点 |
| 数据库连接数爆了 | controller 连接池未释放 | pg_stat_activity | 调小连接池 max 值 |
| Redis 主从切换后丢任务 | 客户端仍连旧主库 | redis-cli -p 26379 sentinel get-master-addr-by-name | 改用 sentinel 模式连接 |
| 任务重复执行 | 幂等性没做或超时误判 | 查看 task_id 对应日志 | 引入全局去重 ID |
| Nginx 502 | 主 controller 挂了,备份切换失败 | journalctl -u nginx | 检查健康检查接口是否正常 |
这里要特别强调表格里的第二行:worker 的 Token 失效后,worker 会启动成功,但 controller 会拒绝它的注册请求,任务永远处于 pending 状态。表面看起来 worker 没有问题,实际上根本没有被纳入调度。
6.2 三次真实故障的排查过程
第一次故障是数据库主从切换后,部分 controller 连接句柄没有及时回收,导致主库压力翻倍。排查时发现新主库的连接数持续增长,老主库的连接不释放。这个问题的根源是连接池配置里没有开启自动回收,切换到新主库后旧连接还在等待超时。解决方式是把连接空闲超时从 30 分钟缩短到 5 分钟,同时强制在切换命令中重启一遍 controller。
第二次故障是任务重复执行。现象是同一个任务出现了两次结果,对下游系统产生了重复数据。排查后确认是网络抖动导致 worker 执行完成后没有及时回执,controller 判定超时后重新投递。解决办法是引入全局 task_id 去重,worker 在真正执行业务前先查一次执行记录,存在则直接返回旧结果。
第三次故障是健康检查接口泄露了内部信息,被外部扫描工具发现版本号。那次之后我把健康检查返回内容改成了固定字符串 UP,同时把健康检查路径从 /health 改成了一段不可猜的随机路径,避免被无差别扫描命中。
6.3 两个容易忽略的排查小技巧
第一个技巧:遇到队列积压时,不要先去动 controller,先看 worker 的日志。因为 90% 的情况下积压是 worker 消费能力不够,而不是 controller 投递出了问题。我浪费过好几轮邮件往来才意识到这个顺序问题。
第二个技巧:PostgreSQL 主从切换时,最好先检查主从之间的复制延迟。如果延迟太高,切主后会有大量数据丢失,而 OpenClaw 的任务状态机对数据一致性非常敏感,丢数据会导致任务永久卡在某个中间状态。我培养了一个习惯:每次切主前先看 pg_stat_replication,确保 replay_lag 接近 0 再执行 promote。
7. 最后再分享一点经验
这套方案落地的过程中,我最大的体会是:高可用部署和定制化安全加固不是两个独立的阶段,而是从第一天就要绑在一起设计。如果先做高可用、后补安全,你会发现很多安全措施根本加不进去——比如内部节点之间的 mTLS,如果最初没有在 worker 启动时配置证书,后面只能让所有 worker 停机重启,代价极高。
如果你所在团队目前只有两三个人,我依然建议从这套最小方案开始,哪怕暂时用不到这么多冗余。因为 OpenClaw 这类调度型服务的故障模式通常不是突然宕机,而是慢性的资源耗尽、连接泄露、状态不一致。真正省时间的做法是在一开始就堵住这些口子,而不是等告警响了再去救火。