资讯动态

远程执行代理架构设计与实践:基于消息通道的轻量级Agent实现

发布时间:2026/10/24 9:14:20 来源:尧图企业网站定制
1. 这个项目到底解决什么问题简单说这个项目的名字叫reaRemote Execution Agent远程执行代理。如果只用一句话描述它是一个运行在服务器上、通过消息通道接收指令并执行具体动作的轻量级守护进程。我第一次接触这类需求是帮一个朋友处理一个让人头疼的场景他手上有若干台没有公网 IP 的机器散布在不同网络环境里平时想远程执行一条命令、拉个日志、传个文件都得先想办法连进去要么靠内网穿透要么找台跳板机要么人在现场。更麻烦的是这些机器上跑的任务大多是定时的、脚本化的一旦要临时调整、补跑数据、查看状态就非常被动。那时候市面上其实已经有不少成熟的方案比如各类任务调度系统、CI Runner、Agent 类的管理工具但要么太重型要么依赖外部服务要么部署起来牵一发动全身。我需要的是一个足够简单、足够隐私、部署起来能在一台普通机器上几分钟跑起来的东西它不依赖某个中心化平台不需要复杂的服务网格就是老老实实驻留在机器上听指令、干活、把结果送回来仅此而已。于是就有了 rea 这个项目。它的核心定位是一个常驻后台的执行代理类似“机器人管家”你发消息给它它去干活。通信层独立于具体的业务逻辑便于接入不同的消息通道。执行任务时具备基本的安全隔离和校验机制避免误操作。配置足够灵活能够适配“边缘节点执行命令”“定时任务补跑”“远程日志采集”等常见场景。这个项目适合谁适合那些有若干台服务器、但不想引入整套复杂运维系统的人适合需要快速在远端机器上执行命令的开发者也适合想学习“异步任务系统 进程管理 消息协议设计”这三个方向的人。它不是一个商业级全家桶而是一个思路清晰、可以拿来改、拿来用的骨架。2. 整体设计思路与方案选型2.1 先定边界它不该做什么很多同类项目失败不是因为做得太少而是因为做得太多。我在设计 rea 时就给自己划了几条红线不做任务编排不搞 DAG 依赖那是调度系统的事。不做可视化控制台那是管理平台的事。不内置复杂的插件机制至少第一版不做保持核心路径清晰。不把自己绑死在某种特定协议上比如只支持 SSH 或只支持 MQTT那样会限制使用场景。这个思路其实有点像做微服务时的“服务拆分”先把执行器本身做干净命令来了能接得住、能跑、能反馈结果就好。至于“消息从哪来”“结果送到哪去”通过可配置的通道解决。2.2 为什么选择“代理 消息通道”模式以前我尝试过另一种做法直接在服务器上开一个 HTTP 服务外部系统通过 REST API 提交命令。这种方式对“可控环境”来说没问题但一旦机器位于 NAT 后面、没有公网入口外部请求根本到达不了它。rea 换了一个思路让代理主动向外连接。它像一个“外呼客服”而不是“等待来电”的座机。代理启动后主动连接到一个它可以访问到的消息通道然后保持长连接一直等着听指令。这样无论目标机器在多么刁钻的网络环境里只要它能访问到通道就能收发消息。这种模式的最大收益是不再需要为目标机器单独开放入站端口。安全上也相对好做些出站连接通常比入站开放更容易被防火墙接受。2.3 消息通道的选型取舍我在实现中抽象了一个 Channel 接口目前支持了两种通道基于 Redis 的 Stream / Pub-Sub。基于 WebSocket 的服务端。为什么要保留两种因为它们的适用场景差别挺大通道类型优势劣势适用场景Redis Stream消息可以持久化代理掉线后重连还能补收消费者组天然支持负载均衡依赖 Redis 服务部署时多一个组件任务可靠性要求高、机器数量多的场景WebSocket部署简单任何能跑 WebSocket 的中间件都可以调试直观代理断开期间消息容易丢除非服务端另外做持久化测试环境、单机少量代理场景实际使用中我在生产环境推荐 Redis Stream。尤其是有一次某台机器凌晨断网重启网络后代理重连把断线期间的 3 个任务全部补跑完成那一次让我确定 Redis Stream 是更稳妥的默认选择。2.4 语言与依赖的选择我用 Go 写了这个项目理由很直接编译出来就是单个二进制文件扔到服务器上就能跑没有运行时依赖交叉编译也方便。对于“远程执行代理”这种偏底层、偏系统管理的工具Go 的进程管理能力和并发模型都比较顺手。依赖方面刻意保持克制核心只有Redis 客户端库用于 channel 对接。gorilla/websocket用于 WebSocket 通道。一个轻量级 YAML 解析库用于配置文件。没有引重量级的框架这让整个项目非常简单任何人拿到源码都能很快看清楚调用关系。3. 核心模块拆解与实现要点3.1 总体模块划分整个 rea 项目的代码组织大致分为四个模块agent代理核心负责生命周期管理、配置加载、任务执行循环。channel消息通道实现目前有 Redis 和 WebSocket 两种。executor执行器负责真正去跑命令、捕获输出、处理超时。protocol消息协议定义包括任务请求、任务结果、心跳包的数据结构。这个划分方式很朴素但保持了单向依赖agent 依赖 channel 和 executorexecutor 不依赖 channelprotocol 被各处引用但不反向依赖具体的实现。3.2 任务执行循环的设计代理启动后的主循环可以简化成这样从 Channel 拉取一条消息。解析消息校验格式和任务参数合法性。启动一个带超时控制的子进程执行命令。等待子进程结束或者超时强制杀掉。把退出码、标准输出、标准错误打包成结果消息发回 Channel。回到第一步。这里有几个细节值得展开讲讲。超时控制每个任务都可以在消息里携带一个timeout字段默认我设成了 60 秒。对于跑批任务可能太短对于正常命令足够。超时后不能只是简单通知调用方“任务超时了”还要把子进程连同它的子子进程一起杀掉否则很容易留下孤儿进程这可能就是那种“明明超时了但 CPU 还在持续飙高”的问题来源。我在实现里用了进程组的方式启动命令时单独创建进程组超时后将整组进程 kill。这个操作非常关键。并发限制代理不能无限制并发执行任务否则一台小机器可能直接被任务打爆。我提供两个参数max_concurrent和queue_size。超出并发上限的任务会先放在内存队列里等待队列也满就直接拒绝。这两个参数就是最简单的“背压”机制没有它再稳的代理也扛不住突发流量。3.3 消息协议的设计消息协议是整个系统的“通用语言”所以设计时我非常仔细。每个消息是一个 JSON 对象包含以下核心字段{ id: task-uuid-xxx, type: exec, payload: { command: bash /opt/scripts/backup.sh, args: [], timeout: 30, cwd: /tmp }, timestamp: 1700000000 }结果消息则形如{ id: task-uuid-xxx, type: result, payload: { exit_code: 0, stdout: backup done, stderr: , duration_ms: 1234, killed: false }, timestamp: 1700000005 }为什么把command和args分开这样可以在执行端做更精确的参数处理避免直接拼接字符串带来的转义和注入问题。虽然这个项目定位是“可信网络内的执行工具”安全习惯还是要养成的。此外协议里预留了type字段未来可以扩展file_upload、script_exec等不同任务类型而不用改动通信层的代码。3.4 心跳与状态上报代理存活状态怎么感知我做了心跳机制代理每隔heartbeat_interval秒向 Channel 发送一条心跳消息内容包括主机名、当前并发数、最近任务数、负载情况。调用方可以根据心跳来做简单的健康判断。有一个细节心跳消息的 Channel 要和使用频道分开或者至少用不同的消息类型标记。否则调度端在处理“任务结果”和“心跳”时会混在一起逻辑容易乱。3.5 安全与认证机制这个项目不是部署在绝对信任的网络里所以认证必然要做。我用的是 Token 认证代理启动时配置一个 token所有消息任务请求、结果、心跳都必须在 header 或消息体内携带该 tokenChannel 收到消息后会先校验。实现上我在 Redis Stream 场景中把 token 放在消息字段里在 WebSocket 场景中放在连接建立时的 Query 参数里。两种方式都不完美但对于这个定位够用。更进一步的改进是增加请求签名比如用 HMAC 对消息体做摘要防止消息被中间人篡改。这个作为一个扩展点留在了 TODO 清单里。4. 实操过程与关键配置详解4.1 构建与安装rea 使用 Go 编写构建过程比较直接git clone 项目地址 rea cd rea go mod tidy make buildmake build会在bin/目录下生成rea-agent和rea-cli两个二进制文件。前者是代理本体后者是本地调试时手动发送消息用的命令行工具。如果你想交叉编译到 Linux amd64 平台CGO_ENABLED0 GOOSlinux GOARCHamd64 make build静态编译后二进制文件扔到目标机器上就能跑连libc都不用依赖这一点在有些精简系统镜像上特别省心。4.2 服务端与代理端配置代理的配置文件采用 YAML 格式一个最小配置如下agent: id: node-sz-01 max_concurrent: 4 queue_size: 16 channel: type: redis redis: addr: 10.0.0.5:6379 password: db: 0 read_key: rea:task write_key: rea:result heartbeat_key: rea:heartbeat token: your-auth-token-here executor: default_timeout: 60 max_timeout: 3600 allowed_prefixes: - /usr/bin - /bin - /opt/scripts allow_shell: true这里逐个解释一下关键配置项的意图agent.id每台机器的唯一标识。调度端可以根据 ID 将任务发到特定机器。如果不设置默认用 hostname。max_concurrent和queue_size前面说过控制并发和排队行为实际部署时建议先设一个小值观察机器负载。read_key和write_key代理读取任务的频道和写入结果的频道。任务的 key 可以多台代理共用通过消费者组做负载均衡也可以每台代理用自己的 key。allowed_prefixes白名单机制这是我最看重的安全设计之一。只有以这些路径开头的命令才会被允许执行其余直接拒绝。比如“调用/usr/bin/python3执行某个脚本”可以而“直接用bash -c跑一段任意命令”则需要单独配置允许。allow_shell是否允许bash -c这类 shell 调用。默认我建议关闭但有些场景确实需要于是做成了可配置项。4.3 启动代理代理可以作为前台进程运行也可以配合 systemd 托管./rea-agent -config /etc/rea/agent.yaml如果一切正常日志会显示类似“agent started, listening via redis channel”的信息然后开始发送心跳。生产环境建议用 systemd 托管并开启自动重启。一个简单的 service 单元文件这样写[Unit] Descriptionrea agent Afternetwork-online.target [Service] ExecStart/usr/local/bin/rea-agent -config /etc/rea/agent.yaml Restartalways RestartSec5 Userrea [Install] WantedBymulti-user.target这里有个注意点不要用 root 用户跑代理最好单独创建一个低权限用户并且该用户只对需要执行的脚本目录有读和执行权限。毕竟代理的能力本质上等同于“能在机器上执行命令”最小化权限总是对的。4.4 调度端发送任务我写的rea-cli简单封装了消息发送逻辑。一个典型的用法rea-cli -channel redis -addr 10.0.0.5:6379 -token xxx \ -target node-sz-01 -command /opt/scripts/daily_backup.sh \ -timeout 120发送后rea-cli可以进入“等待模式”阻塞监听rea:result频道直到拿到与任务 ID 对应的结果消息才退出。这在脚本中特别有用if rea-cli ... -wait -timeout 180; then echo task executed ok else echo task failed fi4.5 带参数的任务提交如果你不想修改调度端代码可以通过-args传递额外参数rea-cli ... -command /usr/bin/python3 -args /opt/scripts/process_data.py --mode full --date 2025-06-01代理端会在校验前缀后将命令和参数分隔执行。这里再次强调参数传递绝不使用字符串拼接是解析成列表后调用系统调用以避免特殊字符干扰。5. 常见问题与排查技巧实录5.1 任务执行了但结果永远收不到这个现象我第一次遇到时排查了很久。任务在远端机器上执行了从日志看也发送了结果消息但调度端就是收不到。后来发现问题的根因在 Redis Stream 的消费者组偏移量上如果代理程序异常退出且没有正确处理 ACK消息会一直处于待确认状态而调度端如果用的是同一个消费者组不会重复读取已经被其他消费者“读取但未确认”的消息。解决方式有两步。第一代理端在成功执行任务后必须显式 ACK第二调度端查询结果时不能依赖同一个消费者组而应该用独立的消费者读取结果频道或者使用XREADGROUP时设置不同的消费者名称。5.2 任务花了一分钟但客户端等了几秒就超时有一次我发现某个任务执行需要两三分钟但调度端总是提前报超时。查了之后发现调度端设置的超时和代理端执行任务用的timeout是两码事。代理端的timeout控制子进程的执行时间超时后杀掉进程并返回 killed 标记。调度端的-timeout控制的是“等待代理返回结果”的等待时长。如果代理还在跑任务调度端已经不耐烦了就会显示超时。两者需要配合设置并且调度端的等待时间要留有余量建议在代理端超时时间基础上再加 N 秒网络传输余量。5.3 命令被杀掉了但进程还在跑这是进程组没设置正确导致的经典问题。如果你在执行命令时只杀掉了直接子进程而命令内部又fork了其他子进程那些子进程会变成孤儿继续跑。我的做法是在启动命令前设置Setpgid把子进程放入独立的进程组然后通过kill(-pgid)方式杀掉整个进程组。如果你在使用其他语言实现类似的逻辑也一定要确认对应的“进程组”支持。5.4 代理重启后 Redis Stream 里的历史任务被重放这个属于“恢复机制”带来的副作用代理重启后将未 ACK 的任务重新读出来执行。如果这些任务本身是幂等的比如“导出当前状态”“拉取最新日志”没问题但如果是“发送邮件”“扣减库存”这类非幂等操作就会出乱子。我的建议是rea 适合执行幂等性好的任务。如果的确存在非幂等任务需要在消息协议里增加deduplicate_id字段代理端维护一个最近已处理任务 ID 的缓存重复消息直接丢弃并返回“重复任务已忽略”。5.5 并发稍微调大机器负载立刻飙升这不是 rea 的问题而是任务本身太吃资源。我见过有人把max_concurrent调到 16然后同时跑了 16 个需要大量内存的 Python 脚本机器直接卡死。从操作层面建议代理只负责执行资源限制交给系统层面的手段。例如使用cgroup限制 CPU 和内存或者在上游调度时就限制同一个目标机器的任务并发数。max_concurrent只是一个粗粒度的并发信号量解决不了资源超卖。5.6 常见问题速查表问题现象可能原因快速处理办法代理无法连接 Redis网络不通、密码错误用redis-cli ping测试连通性检查 YAML 中的addr和password命令总是被拒绝白名单配置过严检查allowed_prefixes确认命令所在的完整路径是否匹配心跳正常但任务的 key 没消费消费者组或 Stream Key 不一致核对read_key是否与你发送任务的 Key 完全一致执行结果乱码字符集问题在命令前加export LANGC.UTF-8或者在执行环境统一设置字符集任务明明执行成功但退出码为 1脚本自身逻辑问题在远端手动执行同样命令对比输出和退出码5.7 日志与追踪的小技巧调试 rea 相关的消息问题时我强烈建议把消息 ID 打出来对账。分别在调度端发送时、代理端收到时、代理端执行完发送结果时、调度端收到结果时输出一次 ID这样链路中任何一环断了都能快速定位。如果觉得日志不够用还有一个笨但有效的方法直接用redis-cli手动往任务频道塞一条消息redis-cli XADD rea:task * id test-001 type exec payload {command:/bin/echo hello,timeout:5}然后观察代理日志。这比反复猜测要快得多。6. 部署场景与扩展方向6.1 几个典型场景复盘在我实际使用过程中有几个场景特别能体现 rea 的价值。第一个是边缘节点日志收集。我在若干台分散的机器上部署 rea调度端定时发送“打包指定目录日志并上传到统一存储”的任务代理执行完就把结果汇报回来。整个过程不需要登录到每台机器也不用担心个别机器没有公网 IP。第二个是定时任务补跑。某个数据处理链路偶尔会因为上游数据未就绪而失败传统做法是人工登录补跑。用 rea 之后我写了个小工具扫描失败记录自动向对应机器发送补跑指令。即使是凌晨也能自动把该补的跑掉。第三个是临时诊断。用户报了个诡异的问题需要在几台机器上同时抓网络状态、进程列表、系统负载。以前要挨个登录执行命令现在一条指令发出去几台机器的结果在几秒钟内全部返回。6.2 值得扩展的几个方向rea 目前像个“毛坯房”好用但还缺不少装修。如果你打算拿来二次开发我建议优先考虑这几个方向文件分发能力除了执行命令把它扩展成可以推送脚本和配置文件的小型分发工具。加密传输层对正在飞的消息做加密特别是经过不可信网络时。任务密钥管理接入 KMS 或者 Vault解决“脚本中数据库密码怎么传”的问题。批量管理接口提供一个简单的 HTTP 管理端口便于与已有运维系统集成。7. 写在最后的经验总结rea 这个项目从头到尾是我按“自己能用的顺手的工具”标准做的没有刻意追求功能全面。正因如此它保持了足够的简单出了问题我能在一两个小时内定位并解决。我个人实际使用中的体会是做远程执行工具反而不是执行能力最重要而是三件事——消息的可靠性、超时与生命周期控制、安全边界约束。可靠性做不到再快的执行都没有意义生命周期控制做不到杀掉一个任务都能留下尾巴安全边界做不到你就等于在每台机器上开了一个后门。如果你也想做一个类似的工具我的建议是不要一上来就堆功能先从最核心的一条路径跑通——消息从 A 到代理代理执行命令结果带回来。这条链路走通之后再慢慢加认证、加密、文件分发、任务编排。基础牢了上层盖多高都稳。最后一个小技巧调试这类代理程序时人为制造故障的效果远好于反复看日志。试试在任务跑到一半时把代理 kill 掉再重启看看消息是否会丢失或重放。这种“混沌实验”式的测试能暴露出许多纸上谈兵时根本想不到的问题。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑