在前面的文章中,我们学习了 Codex CLI 的工作树并行()—— 通过 `git worktree` 创建多个独立工作目录,每个目录运行一个独立的 Codex 实例,实现真正的并行开发。

多进程并发控制 —— 进程数限制、CPU/内存监控、优雅降级

简介

在前面的文章中,我们学习了 Codex CLI 的工作树并行()—— 通过 git worktree 创建多个独立工作目录,每个目录运行一个独立的 Codex 实例,实现真正的并行开发。

但并行开发带来了一个不可避免的问题:

资源是有限的,你不能无限地启动 Codex 实例。

如果你在一台 8 核 16GB 的服务器上同时启动 20 个 Codex 实例,结果会怎样?

  • CPU 使用率飙到 100%,每个进程都抢不到时间片
  • 内存耗尽,系统开始使用 swap,性能暴跌
  • 部分进程被 OOM Killer 杀死,任务失败且没有通知
  • 网络请求排队,API 调用超时

并发控制的本质,是在"吞吐量"和"稳定性"之间找到平衡点。

本文将系统地介绍如何在多 Codex 实例场景下实施并发控制:进程数限制、CPU/内存监控、优雅降级策略,以及完整的自动化管理脚本。

一、为什么需要并发控制

1.1 无限制的并发 = 灾难

想象一个场景:你有 50 个 PR 需要 AI Review。

bash
# ❌ 错误做法:一次性启动 50 个 Codex 实例
for pr in $(gh pr list --json number -q '.[].number'); do
  codex exec --full-auto "Review PR #$pr" &
done
wait

这个脚本会在几秒钟内创建 50 个后台进程。每个 Codex 实例:

  • 需要加载项目上下文(消耗 CPU)
  • 发送 API 请求(占用网络带宽)
  • 处理大文件内容(消耗内存)
  • 可能执行测试(额外的 CPU/内存开销)
text
┌───────────────────────────────────────────────────┐
│          50Codex 实例同时运行的结果              │
├───────────────────┬───────────────────────────────┤
│ 指标              │ 结果                          │
├───────────────────┼───────────────────────────────┤
│ CPU 使用率        │ 100%(系统级争抢)             │
│ 内存使用          │ 超出物理内存,触发 OOM         │
│ API 请求排队      │ 超过速率限制,大量 429 错误    │
│ 网络 I/O          │ 带宽占满,SSH 都卡顿           │
│ 磁盘 I/O          │ 大量临时文件写入,IOPS 打满    │
│ 最终结果          │ 45/50 失败,5/50 勉强完成      │
└───────────────────┴───────────────────────────────┘

1.2 并发控制的三个维度

text
┌──────────────────────────────────────────────┐
│              并发控制三维度                    │
├──────────────────────────────────────────────┤
│                                              │
│  1. 进程数限制(Concurrency Limit)           │
│     ├── 固定上限                              │
│     ├── 动态调整                              │
│     └── 队列管理                              │
│                                              │
│  2. 系统资源监控(Resource Monitoring)        │
│     ├── CPU 使用率                           │
│     ├── 内存使用率                           │
│     ├── 磁盘 I/O                             │
│     └── 网络带宽                             │
│                                              │
│  3. 优雅降级(Graceful Degradation)          │
│     ├── 自动暂停                              │
│     ├── 任务优先级调整                        │
│     └── 失败重试                              │
│                                              │
└──────────────────────────────────────────────┘

二、进程数限制

2.1 基于固定上限的并发控制

最简单也是最有效的方法:设置一个固定的最大并发数。

bash
#!/bin/bash
# concurrent-codex-fixed.sh —— 固定并发数控制

set -euo pipefail

# 配置参数
MAX_CONCURRENT=${CODEX_MAX_CONCURRENT:-4}  # 最大并发数
JOB_QUEUE=()                               # 任务队列
RUNNING_PIDS=()                            # 运行中的进程 PID

# 日志函数
log() {
  echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1"
}

# 获取当前运行的 Codex 进程数
get_running_count() {
  local count=0
  for pid in "${RUNNING_PIDS[@]}"; do
    if kill -0 "$pid" 2>/dev/null; then
      ((count++))
    fi
  done
  echo "$count"
}

# 清理已完成的进程 PID
cleanup_pids() {
  local new_pids=()
  for pid in "${RUNNING_PIDS[@]}"; do
    if kill -0 "$pid" 2>/dev/null; then
      new_pids+=("$pid")
    fi
  done
  RUNNING_PIDS=("${new_pids[@]+"${new_pids[@]}"}")
}

# 等待有空闲槽位
wait_for_slot() {
  while true; do
    cleanup_pids
    local running=$(get_running_count)
    if [ "$running" -lt "$MAX_CONCURRENT" ]; then
      return 0
    fi
    sleep 2
  done
}

# 执行单个任务
run_task() {
  local task_id=$1
  local worktree_path=$2
  local task_desc=$3

  log "Starting task $task_id: $task_desc"
  log "  Worktree: $worktree_path"

  cd "$worktree_path"

  # 运行 Codex,记录输出
  codex exec --full-auto "$task_desc" \
    --output-format json \
    > "/tmp/codex-task-${task_id}.json" 2>&1

  local exit_code=$?
  log "Task $task_id completed with exit code: $exit_code"
  return $exit_code
}

# 主循环:处理任务队列
process_queue() {
  local total=${#JOB_QUEUE[@]}
  local completed=0

  for task in "${JOB_QUEUE[@]}"; do
    wait_for_slot

    # 解析任务
    IFS='|' read -r task_id worktree_path task_desc <<< "$task"

    # 启动后台进程
    run_task "$task_id" "$worktree_path" "$task_desc" &
    RUNNING_PIDS+=($!)

    log "  → PID ${RUNNING_PIDS[-1]} | Running: $(get_running_count)/$MAX_CONCURRENT"
    ((completed++))
  done

  # 等待所有任务完成
  wait
  log "All $total tasks completed"
}

# 示例:批量 PR Review
setup_pr_review_tasks() {
  local repo_path="/opt/data/my-project"
  local wt_base="/tmp/codex-wt"
  mkdir -p "$wt_base"

  local task_id=0

  for pr_number in $(gh pr list --state open --json number -q '.[].number'); do
    # 为每个 PR 创建独立的 worktree
    local wt_path="${wt_base}/pr-${pr_number}"
    if [ ! -d "$wt_path" ]; then
      (cd "$repo_path" && git worktree add "$wt_path" main)
    fi

    JOB_QUEUE+=("${task_id}|${wt_path}|Review PR #${pr_number}: check for bugs, security issues, and style violations")
    ((task_id++))
  done
}

# 入口
setup_pr_review_tasks
process_queue

2.2 基于 CPU 核心的动态并发数

更好的策略是根据服务器的 CPU 核心数自动设置并发上限:

bash
#!/bin/bash
# concurrent-codex-dynamic.sh —— 基于 CPU 核心数的动态并发

set -euo pipefail

# 获取逻辑 CPU 核心数
CPU_CORES=$(nproc 2>/dev/null || sysctl -n hw.ncpu 2>/dev/null || echo 4)

# 推荐并发策略
# - 每个 Codex 实例大约消耗 1 个 CPU 核心的算力
# - 保留 1-2 个核心给系统和监控
RECOMMENDED_CONCURRENT=$(( CPU_CORES - 2 ))
if [ "$RECOMMENDED_CONCURRENT" -lt 1 ]; then
  RECOMMENDED_CONCURRENT=1
fi
if [ "$RECOMMENDED_CONCURRENT" -gt 8 ]; then
  RECOMMENDED_CONCURRENT=8  # 上限 8,避免 API 速率限制
fi

log "CPU cores: $CPU_CORES"
log "Recommended max concurrent: $RECOMMENDED_CONCURRENT"
log "Actual max concurrent: $MAX_CONCURRENT"

2.3 使用 GNU parallel 简化并发

如果你有 GNU parallel 工具,可以大幅简化并发控制:

bash
#!/bin/bash
# parallel-codex.sh —— 使用 GNU parallel

set -euo pipefail

# 创建任务列表文件
TASK_FILE="/tmp/codex-tasks.txt"
> "$TASK_FILE"

# 生成任务
for pr_number in $(gh pr list --state open --json number -q '.[].number'); do
  echo "Review PR #${pr_number}: check for bugs, security issues, and style violations" >> "$TASK_FILE"
done

# 使用 parallel 控制并发
# --jobs: 最大并发数
# --joblog: 记录执行日志
# --eta: 显示预计完成时间
# --halt: 失败时停止
parallel \
  --jobs 4 \
  --joblog /tmp/codex-parallel.log \
  --eta \
  'cd /opt/data/my-project && codex exec --full-auto "{}"' \
  :::: "$TASK_FILE"

echo "All tasks complete. Check log: /tmp/codex-parallel.log"

2.4 使用 tmux 编排并发会话

结合前文介绍的后台模式,可以用 tmux 管理多个 Codex 会话:

bash
#!/bin/bash
# tmux-concurrent-codex.sh —— tmux 编排多 Codex 并发会话

set -euo pipefail

SESSION_NAME="codex-batch"
MAX_WINDOWS=4

# 创建 tmux 会话
tmux new-session -d -s "$SESSION_NAME"

# 拆分为多个窗口
for i in $(seq 1 $((MAX_WINDOWS - 1))); do
  tmux new-window -t "$SESSION_NAME"
done

# 任务队列
TASKS=(
  "审查 src/auth/ 目录的安全问题"
  "重构 src/services/ 中的重复代码"
  "为 src/api/ 补充单元测试"
  "优化 src/db/ 中的数据库查询"
  "检查 src/config/ 的配置一致性"
  "分析 src/utils/ 的工具函数"
  "更新 src/types/ 的类型定义"
  "修复 src/handlers/ 的错误处理"
)

# 分配任务到窗口
task_idx=0
for window_idx in $(seq 0 $((MAX_WINDOWS - 1))); do
  window_name="codex-$window_idx"

  while [ $task_idx -lt ${#TASKS[@]} ]; do
    # 在当前窗口执行任务
    tmux send-keys -t "${SESSION_NAME}:${window_idx}" \
      "cd /opt/data/my-project-wt-${window_idx} && \
       codex exec --full-auto \"${TASKS[$task_idx]}\" && \
       echo \"Task ${task_idx} done in window ${window_idx}\"" \
      Enter

    ((task_idx++))

    # 如果任务已分配完,跳出
    if [ $task_idx -ge ${#TASKS[@]} ]; then
      break
    fi

    # 给每个任务一个简短的间隔
    sleep 1
  done
done

echo "✅ Started $MAX_WINDOWS concurrent Codex sessions"
echo "📺 View: tmux attach -t $SESSION_NAME"
echo "📋 List: tmux list-windows -t $SESSION_NAME"

三、CPU/内存监控

3.1 实时监控脚本

并发控制不只是"限制数量",还需要"实时观察"系统状态:

bash
#!/bin/bash
# codex-resource-monitor.sh —— Codex 进程资源监控

set -euo pipefail

# 监控阈值
CPU_WARN_THRESHOLD=80      # CPU 警告阈值(%)
CPU_CRITICAL_THRESHOLD=95  # CPU 临界阈值(%)
MEM_WARN_THRESHOLD=80      # 内存警告阈值(%)
MEM_CRITICAL_THRESHOLD=95  # 内存临界阈值(%)

# 获取所有 Codex 进程的资源使用
get_codex_stats() {
  echo "=== Codex Process Resource Usage ==="
  echo ""

  # 查找所有 codex 进程
  local pids=$(pgrep -f "codex exec" 2>/dev/null || true)

  if [ -z "$pids" ]; then
    echo "No Codex processes running."
    return
  fi

  local total_cpu=0
  local total_mem=0
  local count=0

  printf "%-10s %-8s %-8s %-8s %s\n" "PID" "CPU%" "MEM%" "RSS(MB)" "COMMAND"
  printf "%-10s %-8s %-8s %-8s %s\n" "------" "----" "----" "-------" "-------"

  for pid in $pids; do
    if kill -0 "$pid" 2>/dev/null; then
      local stats=$(ps -p "$pid" -o pid=,pcpu=,pmem=,rss=,args= 2>/dev/null || true)
      if [ -n "$stats" ]; then
        local cpu=$(echo "$stats" | awk '{print $2}')
        local mem=$(echo "$stats" | awk '{print $3}')
        local rss_kb=$(echo "$stats" | awk '{print $4}')
        local rss_mb=$((rss_kb / 1024))
        local cmd=$(echo "$stats" | awk '{for(i=5;i<=NF;i++) printf "%s ", $i; print ""}' | head -c 60)

        printf "%-10s %-8s %-8s %-8s %s\n" "$pid" "$cpu" "$mem" "$rss_mb" "$cmd"

        total_cpu=$(echo "$total_cpu + $cpu" | bc 2>/dev/null || echo "$total_cpu")
        total_mem=$(echo "$total_mem + $mem" | bc 2>/dev/null || echo "$total_mem")
        ((count++))
      fi
    fi
  done

  echo ""
  echo "Total: $count processes | CPU: ${total_cpu}% | MEM: ${total_mem}%"
  echo ""

  # 系统总体资源
  local sys_cpu=$(top -bn1 | grep "Cpu(s)" | awk '{print $2}' 2>/dev/null || echo "N/A")
  local sys_mem_total=$(free -m | awk '/Mem:/ {print $2}')
  local sys_mem_used=$(free -m | awk '/Mem:/ {print $3}')
  local sys_mem_pct=$((sys_mem_used * 100 / sys_mem_total))

  echo "System: CPU=${sys_cpu}% | Memory=${sys_mem_used}MB/${sys_mem_total}MB (${sys_mem_pct}%)"
}

# 检查是否需要降级
check_degradation_needed() {
  local sys_mem_pct=$(free | awk '/Mem:/ {printf "%.0f", $3/$2 * 100}')
  local sys_cpu=$(top -bn1 | grep "Cpu(s)" | awk '{print $2 + $4}' 2>/dev/null || echo 0)
  sys_cpu=${sys_cpu%.*}  # 取整数部分

  if [ "$sys_cpu" -ge "$CPU_CRITICAL_THRESHOLD" ] || [ "$sys_mem_pct" -ge "$MEM_CRITICAL_THRESHOLD" ]; then
    echo "CRITICAL"
    return 2
  elif [ "$sys_cpu" -ge "$CPU_WARN_THRESHOLD" ] || [ "$sys_mem_pct" -ge "$MEM_WARN_THRESHOLD" ]; then
    echo "WARNING"
    return 1
  else
    echo "OK"
    return 0
  fi
}

# 持续监控模式
monitor_loop() {
  local interval=${1:-5}  # 默认 5 秒

  echo "Starting resource monitor (interval: ${interval}s)"
  echo "Press Ctrl+C to stop"
  echo ""

  while true; do
    clear
    get_codex_stats
    echo ""

    local status=$(check_degradation_needed)
    case "$status" in
      OK)
        echo "🟢 System status: OK"
        ;;
      WARNING)
        echo "🟡 System status: WARNING - Consider reducing concurrency"
        ;;
      CRITICAL)
        echo "🔴 System status: CRITICAL - Initiate graceful degradation"
        ;;
    esac

    sleep "$interval"
  done
}

# 入口
case "${1:-monitor}" in
  stats)
    get_codex_stats
    ;;
  check)
    check_degradation_needed
    ;;
  monitor)
    monitor_loop "${2:-5}"
    ;;
  *)
    echo "Usage: $0 {stats|check|monitor [interval]}"
    exit 1
    ;;
esac

3.2 cgroups 资源限制

如果你使用的是 Linux 系统,可以用 cgroups 对 Codex 进程组施加硬限制:

bash
#!/bin/bash
# cgroup-codex-limit.sh —— 使用 cgroups 限制 Codex 资源

set -euo pipefail

CGROUP_NAME="codex-batch"
CPU_QUOTA="200000"    # 2 个 CPU 核心(100000 = 1 core)
MEM_LIMIT="4G"        # 4GB 内存上限

# 创建 cgroup(cgroup v2)
setup_cgroup() {
  sudo mkdir -p /sys/fs/cgroup/"$CGROUP_NAME"

  # 设置 CPU 配额
  echo "$CPU_QUOTA" | sudo tee /sys/fs/cgroup/"$CGROUP_NAME"/cpu.max

  # 设置内存限制
  echo "$MEM_LIMIT" | sudo tee /sys/fs/cgroup/"$CGROUP_NAME"/memory.max

  echo "✅ cgroup '$CGROUP_NAME' created"
  echo "   CPU quota: $((CPU_QUOTA / 100000)) cores"
  echo "   Memory limit: $MEM_LIMIT"
}

# 将进程加入 cgroup
add_to_cgroup() {
  local pid=$1
  sudo echo "$pid" | sudo tee /sys/fs/cgroup/"$CGROUP_NAME"/cgroup.procs
  echo "Added PID $pid to cgroup '$CGROUP_NAME'"
}

# 清理 cgroup
cleanup_cgroup() {
  sudo rmdir /sys/fs/cgroup/"$CGROUP_NAME" 2>/dev/null || true
  echo "✅ cgroup '$CGROUP_NAME' removed"
}

# 在 cgroup 中运行 Codex
run_in_cgroup() {
  local task=$1

  # 使用 systemd-run 更方便(推荐)
  sudo systemd-run \
    --slice="$CGROUP_NAME" \
    --property="CPUQuota=$((CPU_QUOTA / 1000))%" \
    --property="MemoryMax=$MEM_LIMIT" \
    --unit="codex-task-$$" \
    codex exec --full-auto "$task"
}

# 入口
case "${1:-}" in
  setup)
    setup_cgroup
    ;;
  run)
    run_in_cgroup "$2"
    ;;
  cleanup)
    cleanup_cgroup
    ;;
  *)
    echo "Usage: $0 {setup|run <task>|cleanup}"
    ;;
esac

3.3 结合 prometheus 的长期监控

对于生产环境,建议接入 Prometheus + Grafana 做长期监控:

yaml
# prometheus-codex.yml —— Prometheus 自定义指标采集
scrape_configs:
  - job_name: 'codex-concurrency'
    static_configs:
      - targets: ['localhost:9090']
    metrics_path: '/metrics'

# 自定义 exporter 采集的指标:
# codex_running_processes    - 当前运行的 Codex 进程数
# codex_cpu_usage_percent    - Codex 进程组 CPU 使用率
# codex_memory_usage_bytes   - Codex 进程组内存使用
# codex_task_duration_seconds - 任务执行时长
# codex_task_failures_total  - 任务失败次数
# codex_api_rate_limit_remaining - API 剩余配额

四、优雅降级

4.1 降级策略设计

优雅降级的核心思想:当系统资源不足时,主动降低负载,而不是被动崩溃。

text
┌─────────────────────────────────────────────────────────┐
│                    优雅降级策略矩阵                       │
├─────────────┬──────────────┬──────────────┬─────────────┤
│ 级别        │ 触发条件      │ 动作         │ 恢复条件    │
├─────────────┼──────────────┼──────────────┼─────────────┤
│ GREEN       │ CPU<70%      │ 正常并发      │ -          │
│             │ MEM<70%      │ (4 实例)      │            │
├─────────────┼──────────────┼──────────────┼─────────────┤
│ YELLOW      │ CPU 70-85%   │ 减少到 2 实例 │ CPU<60%    │
│             │ MEM 70-85%   │ 暂停新任务    │ MEM<60%    │
│             │              │ 10 秒冷却     │ 持续 30 秒  │
├─────────────┼──────────────┼──────────────┼─────────────┤
│ ORANGE      │ CPU 85-95%   │ 减少到 1 实例 │ CPU<70%    │
│             │ MEM 85-95%   │ 等待完成中    │ MEM<70%    │
│             │              │ 不启动新的    │ 持续 60 秒  │
├─────────────┼──────────────┼──────────────┼─────────────┤
│ RED         │ CPU>95%      │ 全部暂停      │ CPU<80%    │
│             │ MEM>95%      │ 发送告警      │ MEM<80%    │
│             │              │ 保留现场      │ 持续 120 秒 │
└─────────────┴──────────────┴──────────────┴─────────────┘

4.2 完整的降级控制器

bash
#!/bin/bash
# codex-degradation-controller.sh —— 优雅降级控制器

set -euo pipefail

# 状态文件
STATE_FILE="/tmp/codex-degradation-state.json"
LOCK_FILE="/tmp/codex-degradation.lock"

# 降级级别
LEVEL_GREEN=0
LEVEL_YELLOW=1
LEVEL_ORANGE=2
LEVEL_RED=3

# 最大并发数对应每个级别
declare -A MAX_CONCURRENT_BY_LEVEL
MAX_CONCURRENT_BY_LEVEL[$LEVEL_GREEN]=4
MAX_CONCURRENT_BY_LEVEL[$LEVEL_YELLOW]=2
MAX_CONCURRENT_BY_LEVEL[$LEVEL_ORANGE]=1
MAX_CONCURRENT_BY_LEVEL[$LEVEL_RED]=0

# 获取当前系统状态
get_system_metrics() {
  local cpu=$(top -bn1 | grep "Cpu(s)" | awk '{print $2 + $4}' 2>/dev/null || echo 0)
  cpu=${cpu%.*}
  local mem_pct=$(free | awk '/Mem:/ {printf "%.0f", $3/$2 * 100}')
  local load_1m=$(cat /proc/loadavg | awk '{print $1}')

  echo "{\"cpu\":$cpu,\"mem_pct\":$mem_pct,\"load_1m\":$load_1m}"
}

# 计算需要的降级级别
calculate_level() {
  local cpu=$1
  local mem_pct=$2
  local load_1m=$3
  local cpu_cores=$(nproc)

  # CPU 判断
  if [ "$cpu" -ge 95 ] || [ "$mem_pct" -ge 95 ]; then
    echo $LEVEL_RED
    return
  elif [ "$cpu" -ge 85 ] || [ "$mem_pct" -ge 85 ]; then
    echo $LEVEL_ORANGE
    return
  elif [ "$cpu" -ge 70 ] || [ "$mem_pct" -ge 70 ]; then
    echo $LEVEL_YELLOW
    return
  else
    echo $LEVEL_GREEN
    return
  fi
}

# 执行降级动作
apply_degradation() {
  local new_level=$1
  local old_level=$2
  local max_concurrent=${MAX_CONCURRENT_BY_LEVEL[$new_level]}

  echo "[$(date '+%Y-%m-%d %H:%M:%S')] Degradation: $old_level$new_level (max concurrent: $max_concurrent)"

  # 更新状态文件
  local metrics=$(get_system_metrics)
  echo "{\"level\":$new_level,\"max_concurrent\":$max_concurrent,\"metrics\":$metrics,\"timestamp\":\"$(date -Iseconds)\"}" \
    > "$STATE_FILE"

  case $new_level in
    $LEVEL_RED)
      # 红色:停止所有新任务
      touch "$LOCK_FILE"
      echo "🔴 CRITICAL: All new Codex tasks paused. Send alert."
      # 发送告警(集成钉钉、企业微信、Slack 等)
      send_alert "CRITICAL" "Codex degradation to RED level"
      ;;
    $LEVEL_ORANGE)
      # 橙色:只允许 1 个并发
      echo "🟠 WARNING: Reducing to 1 concurrent instance"
      ;;
    $LEVEL_YELLOW)
      # 黄色:减少到 2 个并发
      echo "🟡 NOTICE: Reducing to 2 concurrent instances"
      ;;
    $LEVEL_GREEN)
      # 绿色:恢复正常
      rm -f "$LOCK_FILE"
      echo "🟢 OK: Full concurrency restored"
      ;;
  esac
}

# 发送告警(示例:curl 调用 Webhook)
send_alert() {
  local level=$1
  local message=$2

  # 企业微信 Webhook 示例
  # curl -s "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=YOUR_KEY" \
  #   -H "Content-Type: application/json" \
  #   -d "{\"msgtype\":\"text\",\"text\":{\"content\":\"[${level}] Codex: ${message}\"}}"

  # 钉钉 Webhook 示例
  # curl -s "https://oapi.dingtalk.com/robot/send?access_token=YOUR_TOKEN" \
  #   -H "Content-Type: application/json" \
  #   -d "{\"msgtype\":\"text\",\"text\":{\"content\":\"[${level}] Codex: ${message}\"}}"

  echo "Alert sent: [$level] $message"
}

# 降级监控循环(守护进程模式)
degradation_daemon() {
  local check_interval=${1:-5}  # 默认 5 秒检查一次
  local recovery_delay=${2:-30} # 恢复延迟 30 秒

  echo "Starting degradation controller daemon"
  echo "Check interval: ${check_interval}s | Recovery delay: ${recovery_delay}s"

  local current_level=$LEVEL_GREEN
  local consecutive_ok=0

  while true; do
    local metrics=$(get_system_metrics)
    local cpu=$(echo "$metrics" | jq -r '.cpu')
    local mem_pct=$(echo "$metrics" | jq -r '.mem_pct')
    local load_1m=$(echo "$metrics" | jq -r '.load_1m')

    local needed_level=$(calculate_level "$cpu" "$mem_pct" "$load_1m")

    # 防止抖动:需要连续 OK 才恢复
    if [ "$needed_level" -eq "$LEVEL_GREEN" ] && [ "$current_level" -gt "$LEVEL_GREEN" ]; then
      ((consecutive_ok++))
      if [ "$consecutive_ok" -ge "$((recovery_delay / check_interval))" ]; then
        apply_degradation $LEVEL_GREEN $current_level
        current_level=$LEVEL_GREEN
        consecutive_ok=0
      fi
    else
      consecutive_ok=0
      if [ "$needed_level" -ne "$current_level" ]; then
        apply_degradation "$needed_level" "$current_level"
        current_level=$needed_level
      fi
    fi

    sleep "$check_interval"
  done
}

# 入口
case "${1:-daemon}" in
  daemon)
    degradation_daemon "${2:-5}" "${3:-30}"
    ;;
  check)
    get_system_metrics
    ;;
  status)
    if [ -f "$STATE_FILE" ]; then
      jq . "$STATE_FILE"
    else
      echo "No degradation state found. System running normally."
    fi
    ;;
  *)
    echo "Usage: $0 {daemon [check_interval] [recovery_delay]|check|status}"
    ;;
esac

4.3 任务优先级队列

降级时,我们还需要对任务进行优先级排序:

bash
#!/bin/bash
# priority-queue-codex.sh —— 优先级任务队列

set -euo pipefail

# 优先级定义(数字越小优先级越高)
# 1 - CRITICAL: 安全漏洞修复、生产事故
# 2 - HIGH: 线上 Bug 修复
# 3 - MEDIUM: 功能开发、PR Review
# 4 - LOW: 文档更新、代码格式化
# 5 - BACKGROUND: 批量重构、技术债清理

PRIORITY_QUEUE=()

add_task() {
  local priority=$1
  local description=$2
  local worktree=$3

  # 按优先级插入(简单冒泡排序)
  PRIORITY_QUEUE+=("${priority}|${description}|${worktree}")

  # 重新排序
  IFS=$'\n' PRIORITY_QUEUE=($(sort -t'|' -k1 -n <<< "${PRIORITY_QUEUE[*]}"))
  unset IFS
}

process_priority_queue() {
  local max_concurrent=$1

  while [ ${#PRIORITY_QUEUE[@]} -gt 0 ]; do
    # 取最高优先级的任务
    local task="${PRIORITY_QUEUE[0]}"
    PRIORITY_QUEUE=("${PRIORITY_QUEUE[@]:1}")

    IFS='|' read -r priority description worktree <<< "$task"

    echo "Processing [Priority $priority]: $description"
    echo "  Worktree: $worktree"

    cd "$worktree"
    codex exec --full-auto "$description"

    echo "  ✅ Complete"
    echo ""
  done
}

# 示例
add_task 3 "Review PR #42: 新增用户注册 API" "/tmp/codex-wt-pr42"
add_task 1 "修复生产环境 SQL 注入漏洞" "/tmp/codex-wt-hotfix"
add_task 5 "批量更新所有文件的 License Header" "/tmp/codex-wt-license"
add_task 2 "修复登录超时问题" "/tmp/codex-wt-login-fix"
add_task 4 "更新 README 文档" "/tmp/codex-wt-docs"

echo "Queue order (by priority):"
for task in "${PRIORITY_QUEUE[@]}"; do
  IFS='|' read -r priority description _ <<< "$task"
  echo "  [$priority] $description"
done
echo ""

process_priority_queue 2

五、实战:完整的并发管理系统

将上述所有模块整合为一个完整的管理系统:

bash
#!/bin/bash
# codex-concurrency-manager.sh —— 完整的并发管理系统

set -euo pipefail

# ===== 配置 =====
MAX_CONCURRENT=${CODEX_MAX_CONCURRENT:-4}
CHECK_INTERVAL=5
CPU_WARN=75
CPU_CRIT=90
MEM_WARN=75
MEM_CRIT=90
RECOVERY_SECONDS=60
WORKTREE_BASE="/tmp/codex-wt"
LOG_DIR="/tmp/codex-logs"
STATE_DIR="/tmp/codex-state"

mkdir -p "$LOG_DIR" "$STATE_DIR" "$WORKTREE_BASE"

# ===== 日志 =====
log() { echo "[$(date '+%H:%M:%S')] [$1] $2"; }

# ===== 资源检查 =====
get_cpu_usage() {
  top -bn1 | grep "Cpu(s)" | awk '{print $2 + $4}' 2>/dev/null | cut -d. -f1
}

get_mem_usage() {
  free | awk '/Mem:/ {printf "%.0f", $3/$2 * 100}'
}

get_running_count() {
  pgrep -f "codex exec" 2>/dev/null | wc -l
}

# ===== 降级判断 =====
check_status() {
  local cpu=$(get_cpu_usage)
  local mem=$(get_mem_usage)
  local running=$(get_running_count)

  if [ "$cpu" -ge "$CPU_CRIT" ] || [ "$mem" -ge "$MEM_CRIT" ]; then
    echo "RED|0"
  elif [ "$cpu" -ge "$CPU_WARN" ] || [ "$mem" -ge "$MEM_WARN" ]; then
    local reduce=$((MAX_CONCURRENT / 2))
    [ "$reduce" -lt 1 ] && reduce=1
    echo "ORANGE|$reduce"
  elif [ "$running" -ge "$MAX_CONCURRENT" ]; then
    echo "YELLOW|$MAX_CONCURRENT"
  else
    echo "GREEN|$MAX_CONCURRENT"
  fi
}

# ===== 任务调度 =====
declare -a TASK_QUEUE=()
declare -a RUNNING_PIDS=()

add_to_queue() {
  TASK_QUEUE+=("$1")
  log "QUEUE" "Added task: $1"
}

can_start_task() {
  local status=$(check_status)
  local level=$(echo "$status" | cut -d'|' -f1)
  local allowed=$(echo "$status" | cut -d'|' -f2)
  local running=$(get_running_count)

  [ "$level" = "RED" ] && return 1
  [ "$running" -ge "$allowed" ] && return 1
  return 0
}

start_task() {
  local task=$1
  local task_id=$(date +%s%N | cut -c10-16)
  local log_file="${LOG_DIR}/task-${task_id}.log"

  (
    echo "[$(date '+%Y-%m-%d %H:%M:%S')] Starting: $task" > "$log_file"
    cd /opt/data/my-project 2>/dev/null || cd "$HOME"
    codex exec --full-auto "$task" >> "$log_file" 2>&1
    local rc=$?
    echo "[$(date '+%Y-%m-%d %H:%M:%S')] Finished (exit: $rc): $task" >> "$log_file"
    echo "$rc" > "${STATE_DIR}/task-${task_id}.exit"
  ) &

  RUNNING_PIDS+=($!)
  log "START" "Task $task_id started (PID: ${RUNNING_PIDS[-1]})"
}

cleanup_finished() {
  local new_pids=()
  for pid in "${RUNNING_PIDS[@]}"; do
    if kill -0 "$pid" 2>/dev/null; then
      new_pids+=("$pid")
    else
      wait "$pid" 2>/dev/null
      log "DONE" "PID $pid finished"
    fi
  done
  RUNNING_PIDS=("${new_pids[@]+"${new_pids[@]}"}")
}

# ===== 调度循环 =====
scheduler_loop() {
  log "SCHEDULER" "Starting scheduler loop"

  while true; do
    cleanup_finished

    # 检查队列
    if [ ${#TASK_QUEUE[@]} -gt 0 ]; then
      if can_start_task; then
        local task="${TASK_QUEUE[0]}"
        TASK_QUEUE=("${TASK_QUEUE[@]:1}")
        start_task "$task"
      else
        log "SCHEDULER" "Queue: ${#TASK_QUEUE[@]} pending, waiting for slot..."
      fi
    fi

    local status=$(check_status)
    local level=$(echo "$status" | cut -d'|' -f1)
    local allowed=$(echo "$status" | cut -d'|' -f2)
    local running=$(get_running_count)

    log "STATUS" "Level=$level | Allowed=$allowed | Running=$running | Queue=${#TASK_QUEUE[@]}"

    # 如果队列空且无运行中任务,退出
    if [ ${#TASK_QUEUE[@]} -eq 0 ] && [ ${#RUNNING_PIDS[@]} -eq 0 ]; then
      log "SCHEDULER" "All tasks complete"
      break
    fi

    sleep "$CHECK_INTERVAL"
  done

  # 等待所有剩余任务
  wait
  log "SCHEDULER" "Scheduler loop exited"
}

# ===== 入口 =====
case "${1:-help}" in
  add)
    add_to_queue "$2"
    ;;
  run)
    scheduler_loop
    ;;
  status)
    echo "Running: $(get_running_count)"
    echo "CPU: $(get_cpu_usage)%"
    echo "MEM: $(get_mem_usage)%"
    echo "Queue: ${#TASK_QUEUE[@]} tasks"
    ;;
  help|*)
    echo "Usage: $0 {add <task>|run|status|help}"
    ;;
esac

总结

本文系统地介绍了 Codex CLI 多进程并发控制的完整方案:

  1. 进程数限制:固定上限、动态基于 CPU 核心数、GNU parallel、tmux 编排四种方式,从简单到复杂,适配不同场景
  2. CPU/内存监控:实时监控脚本、cgroups 硬限制、Prometheus 长期监控三级方案,确保你能随时掌握系统状态
  3. 优雅降级:四级降级策略(GREEN/YELLOW/ORANGE/RED)配合自动恢复机制,防止系统崩溃
  4. 优先级队列:在资源紧张时优先处理关键任务
  5. 完整管理系统:将上述模块整合为统一的调度器

核心原则:

  • 限制永远比崩溃好:宁可慢一点,也不要让系统失控
  • 监控必须先行:没有监控的并发控制是盲目的
  • 降级要优雅:自动恢复 + 告警通知,让系统有自我修复能力
  • 优先级是保险:关键任务永远优先获得资源

📌 下篇预告

成本控制 —— Token 预算管理、--yolo vs 沙箱模式的成本对比、模型选择策略(o3-mini vs o3 vs o4-mini 的性价比分析)、每日/每周/每月的成本监控和告警。

当你的团队每天运行数十个 Codex 任务时,成本控制就不再是一个可选项,而是必须面对的工程问题。下一篇我们将深入探讨如何用最小的预算,获得最大的产出。