在分布式代理架构中,`SERVER` 是实际执行命令的核心模块。如果不对命令执行过程进行有效管理,**一个长时间运行或资源消耗巨大的命令(如 `find / -name "*.log"` 或死循环脚本)可能导致:**
- 整个 `SERVER` 进程阻塞
- 后续请求无法处理
- 内存溢出、CPU 占满
- 甚至被系统 OOM Killer 终止
---
## ✅ 目标
确保 `SERVER` 在执行用户命令时具备以下能力:
| 要求 | 实现方式 |
|------|----------|
| 🔹 不阻塞主线程 | 使用 Goroutine 异步执行 |
| 🔹 支持超时控制 | Context 超时中断 |
| 🔹 限制资源使用 | CPU/内存/IO 隔离(cgroups/ns) |
| 🔹 流式输出日志 | 实时返回 stdout/stderr |
| 🔹 可安全终止 | 支持优雅 kill 和强制 killall |
| 🔹 防止 fork 炸弹 | 设置进程数限制 |
---
## 🧩 解决方案总览
我们采用 **“异步 + 上下文控制 + 资源隔离 + 流式传输”** 的组合策略。
---
## ✅ 核心代码实现(Go)
```go
// server/command.go
package main
import (
"context"
"fmt"
"io"
"os/exec"
"syscall"
"time"
"unsafe"
"golang.org/x/sys/unix"
"github.com/google/uuid"
)
type CommandRequest struct {
Cmd string `json:"cmd"`
Args []string `json:"args"`
Timeout int `json:"timeout"` // 秒
MaxMem int64 `json:"max_mem"` // 字节
MaxCPU int `json:"max_cpu"` // 百分比(仅提示)
WorkDir string `json:"work_dir"`
Env map[string]string `json:"env"`
}
type CommandResponse struct {
RequestId string `json:"request_id"`
Pid int `json:"pid"`
Status string `json:"status"` // running, success, failed, timeout
ExitCode int `json:"exit_code,omitempty"`
Stdout string `json:"stdout,omitempty"`
Stderr string `json:"stderr,omitempty"`
Error string `json:"error,omitempty"`
Timestamp int64 `json:"timestamp"`
}
// 正在运行的命令池(用于 kill 控制)
var runningCmds sync.Map // request_id -> *exec.Cmd
func ExecuteCommand(req CommandRequest) <-chan CommandResponse {
out := make(chan CommandResponse, 100)
go func() {
defer close(out)
// 1. 创建带超时的 Context
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(req.Timeout)*time.Second)
defer cancel()
// 2. 构建命令
cmd := exec.CommandContext(ctx, req.Cmd, req.Args...)
if req.WorkDir != "" {
cmd.Dir = req.WorkDir
}
if req.Env != nil {
var env []string
for k, v := range req.Env {
env = append(env, fmt.Sprintf("%s=%s", k, v))
}
cmd.Env = env
}
// 3. 捕获输出(支持流式)
stdoutPipe, _ := cmd.StdoutPipe()
stderrPipe, _ := cmd.StderrPipe()
// 4. 开始执行
if err := cmd.Start(); err != nil {
sendError(out, "启动命令失败: "+err.Error())
return
}
requestId := uuid.New().String()
runningCmds.Store(requestId, cmd)
defer runningCmds.Delete(requestId)
// 发送开始事件
out <- CommandResponse{
RequestId: requestId,
Pid: cmd.Process.Pid,
Status: "running",
Timestamp: time.Now().Unix(),
}
// 5. 并发读取 stdout/stderr 流
go func() {
buffer := make([]byte, 4096)
for {
n, err := stdoutPipe.Read(buffer)
if n > 0 {
out <- CommandResponse{
RequestId: requestId,
Status: "stream",
Stdout: string(buffer[:n]),
Timestamp: time.Now().Unix(),
}
}
if err == io.EOF {
break
}
if err != nil {
out <- CommandResponse{
RequestId: requestId,
Status: "error",
Error: "读取 stdout 错误: " + err.Error(),
}
break
}
}
}()
go func() {
buffer := make([]byte, 4096)
for {
n, err := stderrPipe.Read(buffer)
if n > 0 {
out <- CommandResponse{
RequestId: requestId,
Status: "stream",
Stderr: string(buffer[:n]),
Timestamp: time.Now().Unix(),
}
}
if err == io.EOF {
break
}
if err != nil {
out <- CommandResponse{
RequestId: requestId,
Status: "error",
Error: "读取 stderr 错误: " + err.Error(),
}
break
}
}
}()
// 6. 等待命令结束
err := cmd.Wait()
exitCode := 0
if exitError, ok := err.(*exec.ExitError); ok {
if status, ok := exitError.Sys().(syscall.WaitStatus); ok {
exitCode = status.ExitStatus()
}
}
status := "success"
if ctx.Err() == context.DeadlineExceeded {
status = "timeout"
} else if err != nil {
status = "failed"
}
// 最终结果
out <- CommandResponse{
RequestId: requestId,
Status: status,
ExitCode: exitCode,
Timestamp: time.Now().Unix(),
}
}()
return out
}
```
---
## 🔐 如何防止系统级阻塞?—— 四层防护机制
### 1️⃣ 第一层:上下文超时自动中断(Soft Kill)
```go
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
cmd := exec.CommandContext(ctx, "sleep", "100")
_ = cmd.Run() // 30秒后自动中断
```
> ✅ 利用 Go 的 `context` 机制,在超时后发送 `SIGINT` → `SIGKILL`
---
### 2️⃣ 第二层:资源限制(Linux cgroups)
使用 `cgroup v1` 或 `v2` 限制单个命令的资源使用。
#### 示例:通过 shell 封装命令
```bash
#!/bin/sh
# run_limited.sh
CGROUP_NAME="agent-$RANDOM"
MEMORY_LIMIT="100M"
CPU_QUOTA="50000" # 5% of one core
# 创建临时 cgroup
mkdir /sys/fs/cgroup/memory/$CGROUP_NAME
echo $MEMORY_LIMIT > /sys/fs/cgroup/memory/$CGROUP_NAME/memory.limit_in_bytes
mkdir /sys/fs/cgroup/cpu/$CGROUP_NAME
echo $CPU_QUOTA > /sys/fs/cgroup/cpu/$CGROUP_NAME/cpu.cfs_quota_us
# 执行命令并加入 cgroup
echo $$ > /sys/fs/cgroup/memory/$CGROUP_NAME/cgroup.procs
echo $$ > /sys/fs/cgroup/cpu/$CGROUP_NAME/cgroup.procs
exec "$@"
```
然后在 Go 中调用:
```go
cmd := exec.Command("/path/run_limited.sh", "find", "/", "-name", "*.log")
```
> ⚠️ 需要 root 或 CAP_SYS_ADMIN 权限
---
### 3️⃣ 第三层:进程组控制(避免孤儿进程)
防止子进程脱离控制:
```go
cmd.SysProcAttr = &syscall.SysProcAttr{
Setpgid: true, // 设置独立进程组
}
// 终止时 kill 整个进程组
func KillProcessGroup(pid int) {
unix.Kill(-pid, syscall.SIGTERM) // 负号表示 kill pgid
time.Sleep(2 * time.Second)
unix.Kill(-pid, syscall.SIGKILL)
}
```
---
### 4️⃣ 第四层:并发控制(限制并行数量)
防止大量并发命令拖垮系统:
```go
var sem = make(chan struct{}, 10) // 最多同时运行 10 个命令
func ExecuteCommand(req CommandRequest) <-chan CommandResponse {
out := make(chan CommandResponse, 100)
go func() {
sem <- struct{}{} // 获取信号量
defer func() { <-sem }() // 释放
// 执行命令逻辑...
}()
return out
}
```
---
## 📈 流式输出设计(防内存溢出)
不缓存全部输出,而是边读边发:
```go
go func() {
buffer := make([]byte, 4096)
for {
n, err := stdoutPipe.Read(buffer)
if n > 0 {
select {
case out <- streamResponse(buffer[:n], ""):
case <-time.After(5 * time.Second):
// 客户端消费太慢,丢弃或限速
}
}
if err == io.EOF {
break
}
}
}()
```
> ✅ 避免将 GB 级日志加载进内存
---
## 🛑 强制终止 API(Kill by Request ID)
提供外部接口终止正在运行的命令:
```go
// DELETE /api/v1/exec/{request_id}
func HandleKill(w http.ResponseWriter, r *http.Request) {
reqID := mux.Vars(r)["request_id"]
if rawCmd, ok := runningCmds.Load(reqID); ok {
cmd := rawCmd.(*exec.Cmd)
_ = cmd.Process.Kill() // SIGKILL
runningCmds.Delete(reqID)
w.WriteHeader(200)
json.NewEncoder(w).Encode(map[string]string{"status": "killed"})
} else {
http.NotFound(w, r)
}
}
```
---
## ✅ 总结:防阻塞六大原则
| 原则 | 实现方式 |
|------|----------|
| **异步非阻塞** | 每个命令启用独立 goroutine |
| **上下文控制** | `context.WithTimeout` 自动中断 |
| **资源隔离** | cgroups 限制 CPU/Memory |
| **进程管控** | 使用进程组防止泄漏 |
| **流式传输** | 分块返回 stdout/stderr |
| **并发节流** | 信号量控制最大并行数 |
---