一次DolphinScheduler失败调度的排查

问题现象

DolphinScheduler任务调度执行大面积失败

且无法重跑/

排查要点

  • 伪集群形态部署,各容器运行状态、是否健康运行。

    docker-dolphinscheduler-*-1 这组容器(api / master / worker / alert / postgresql / zookeeper 各自独立容器

  • 容器日志排查失败信息

容器失败日志查看

worker

docker logs docker-dolphinscheduler-worker-1 --since 24h | grep -iE "error|fail|exception"

里面有错误:

/tmp/dolphinscheduler/exec/process/default/146212983125760/158234529507105_2/25887/31016/25887_31016.sh: line 4: ssh: command not found

master

docker logs docker-dolphinscheduler-master-1 --since 24h | grep -iE "datasource|connection|fail"

根因

验证:(安装openssh-client)

docker exec -u 0 docker-dolphinscheduler-worker-1 bash -c 'apt-get update && apt-get install -y openssh-client'

安装完成后alert可以正常工作。

ssh仍然失败,但原因不再是command not found,而是**Host key verification failed.**相关错误,确认此前问题为手工在容器内安装ssh并配置相关认证信息,但由于未将环境依赖修改同步到镜像中,随着容器重建这些修改被丢失。

总结问题因果链

  1. worker 容器镜像本来就不含 openssh-client
  2. 但早某个时候,openssh-client 被人手动装进了运行中的 worker 容器(装在容器的可写层里,不在镜像里)→ 那段时间用 ssh 的任务正常;
  3. 你改 daemon.json 调日志配置 → 改 log-driver / log-opts 对"已存在的容器"不生效,必须重建容器才能应用;
  4. 重建容器 = 从镜像重新拉起 → 可写层被丢弃 → 手动装的 openssh-client 没了;
  5. 于是:脚本里用 ssh 的任务全部 command not found(exit 127),不用 ssh 的任务照常 → “大面积但非全部失败”。

措施

临时方案(兼容性,让历史任务快速正常运行):worker容器添加对target节点的信息,提供公钥给target节点

#!/usr/bin/env bash
#
# ds_ssh_setup.sh — DolphinScheduler worker 容器 → 目标主机 无密 SSH 一键配置
#
# 背景:DS 的 Shell 任务以 `sudo -u default -i` 在 worker 容器内执行,
#       通过 `ssh <target>` 触发目标主机上的脚本。本脚本把“装客户端 / 生成密钥 /
#       写 known_hosts / 分发公钥 / 写别名 / 测试”整合成幂等流程。
#
# 运行位置:Docker 宿主机(即 docker compose 所在机器)。
#
# 用法:
#   ./ds_ssh_setup.sh                                   # 按下方 TARGETS 批量配置
#   ./ds_ssh_setup.sh add --host IP [--port 22] [--user root] [--alias NAME]   # 新增单台主机
#   ./ds_ssh_setup.sh bake                              # 打印“烤进镜像”的 Dockerfile 片段
#
# 安全:脚本只做“新增/追加”,不删除任何文件或容器内容。公钥分发仅需目标机密码一次。
set -euo pipefail

WORKER_CONTAINER="${WORKER_CONTAINER:-docker-dolphinscheduler-worker-1}"
DEFAULT_USER="default"

# 内置目标列表(批量模式用):  HOST:PORT:USER:ALIAS
# 新增一台主机:直接往这里加一行,或改用 `./ds_ssh_setup.sh add ...`
TARGETS=(
  # "192.168.1.10:22:root:node10"
  # "db.example.com:2222:researcher:dbhost"
)

# ---- 容器内执行助手 ----
dexec()     { docker exec -u "$DEFAULT_USER" "$WORKER_CONTAINER" bash -c "$1"; }
dexec_t()   { docker exec -it -u "$DEFAULT_USER" "$WORKER_CONTAINER" bash -c "$1"; }
dexec_root(){ docker exec -u 0 "$WORKER_CONTAINER" bash -c "$1"; }

log(){  printf '\033[36m[ds-ssh]\033[0m %s\n' "$*"; }
warn(){ printf '\033[33m[WARN]\033[0m %s\n' "$*"; }

# 1) 容器内 openssh-client + psmisc
ensure_client(){
  log "检查容器内 SSH 客户端 ..."
  if dexec 'command -v ssh >/dev/null 2>&1'; then
    log "openssh-client 已存在"
  else
    log "安装 openssh-client ..."
    dexec_root 'apt-get update -qq && apt-get install -y openssh-client'
  fi
  if dexec 'command -v pstree >/dev/null 2>&1'; then
    log "psmisc(pstree) 已存在"
  else
    log "安装 psmisc(让 DS 能正常 kill 进程树,避免 137 清理失败)..."
    dexec_root 'apt-get install -y psmisc'
  fi
}

# 2) 生成 default 用户密钥(若缺失),确保属主为 default
ensure_key(){
  log "确保 default 用户密钥 ..."
  dexec 'mkdir -p ~/.ssh && chmod 700 ~/.ssh'
  if dexec 'test -f ~/.ssh/id_ed25519'; then
    log "密钥已存在"
  else
    log "生成 ed25519 密钥(无口令)..."
    dexec 'ssh-keygen -t ed25519 -N "" -f ~/.ssh/id_ed25519'
    # 用数字 ID,避免容器内 default 主组不叫 default 导致 chown 报错
    dexec_root 'chown -R "$(id -u default):$(id -g default)" /home/default/.ssh'
  fi
}

# 3) known_hosts(防 MITM,避免首次连接交互卡住)
add_known_host(){
  local host="$1" port="$2"
  log "把 $host 加入 known_hosts ..."
  dexec "ssh-keyscan -p $port -H $host >> ~/.ssh/known_hosts 2>/dev/null; sort -u ~/.ssh/known_hosts -o ~/.ssh/known_hosts"
}

# 4) 部署公钥到目标主机 authorized_keys
deploy_key(){
  local host="$1" port="$2" user="$3"
  local pub
  pub="$(dexec 'cat ~/.ssh/id_ed25519.pub')"
  if dexec "ssh -o BatchMode=yes -p $port $user@$host 'true' 2>/dev/null"; then
    log "$user@$host 已免密,跳过"
    return 0
  fi
  log "部署公钥到 $user@$host ..."
  # 需要目标机密码:优先用 ssh-copy-id(需交互 TTY);非交互环境则回退到手动命令
  if [ -t 0 ] && dexec 'command -v ssh-copy-id >/dev/null 2>&1'; then
    log "(需要该主机 $user 的密码,仅此一次)"
    dexec_t "ssh-copy-id -p $port -o StrictHostKeyChecking=accept-new $user@$host"
  else
    warn "非交互环境或容器内无 ssh-copy-id,请手动在目标主机 $user@$host 执行:"
    echo "  mkdir -p ~/.ssh && chmod 700 ~/.ssh"
    echo "  echo '$pub' >> ~/.ssh/  authorized_keys && chmod 600 ~/.ssh/authorized_keys"
  fi
}

# 5) 写 SSH config 别名(DS 任务可直接 ssh <alias>)
write_config(){
  local alias="$1" host="$2" port="$3" user="$4"
  local cfg="Host $alias\n  HostName $host\n  Port $port\n  User $user\n  IdentityFile ~/.ssh/id_ed25519\n  StrictHostKeyChecking yes\n"
  dexec "printf '$cfg' >> ~/.ssh/config; chmod 600 ~/.ssh/config"
  log "已写入 SSH 别名:$alias -> $user@$host:$port"
}

# 6) 连通性测试
test_conn(){
  local alias="$1" user="$2" host="$3" port="$4"
  if dexec "ssh -o BatchMode=yes -p $port $user@$host 'echo CONNECTED-\$(whoami)@\$(hostname)' 2>/dev/null"; then
    log "OK $alias 连通成功"
  else
    warn "FAIL $alias 仍无法免密,请检查目标 authorized_keys / 防火墙 / 端口"
  fi
}

setup_one(){
  local host="$1" port="${2:-22}" user="${3:-root}" alias="${4:-$host}"
  add_known_host "$host" "$port"
  deploy_key "$host" "$port" "$user"
  write_config "$alias" "$host" "$port" "$user"
  test_conn "$alias" "$user" "$host" "$port"
}

bake(){
  cat <<'EOF'
# 把以下内容并入 worker 自定义 Dockerfile(基于官方镜像),避免重建容器后丢失:
# ---------------------------------------------------------------
# FROM apache/dolphinscheduler-worker-server:3.4.2
# USER root
# RUN apt-get update \
#  && apt-get install -y --no-install-recommends openssh-client psmisc \
#  && mkdir -p /home/default/.ssh \
#  && ssh-keygen -t ed25519 -N '' -f /home/default/.ssh/id_ed25519 \
#  && chown -R "$(id -u default):$(id -g default)" /home/default/.ssh \
#  && chmod 700 /home/default/.ssh
# USER default
# ---------------------------------------------------------------
# 注意:密钥会打进镜像,属敏感信息;生产环境建议改用 secret / 挂载卷,或在镜像外分发。
EOF
}

main(){
  [ -z "$(command -v docker)" ] && { echo "未找到 docker,请在 Docker 宿主机运行"; exit 1; }
  case "${1:-}" in
    bake)
      bake; exit 0 ;;
  esac

  ensure_client
  ensure_key

  if [ "$#" -ge 1 ] && [ "$1" = "add" ]; then
    shift
    local_host="" local_port="22" local_user="root" local_alias=""
    while [ "$#" -gt 0 ]; do
      case "$1" in
        --host|-h)  local_host="$2";  shift 2 ;;
        --port|-p)  local_port="$2";   shift 2 ;;
        --user|-u)  local_user="$2";   shift 2 ;;
        --alias|-a) local_alias="$2";  shift 2 ;;
        *) echo "未知参数: $1(用法: add --host HOST [--port 22] [--user root] [--alias NAME])"; exit 2 ;;
      esac
    done
    [ -z "$local_host" ] && { echo "用法: add --host HOST [--port 22] [--user root] [--alias NAME]"; exit 2; }
    [[ "$local_port" =~ ^[0-9]+$ ]] || { echo "端口必须是数字: $local_port"; exit 2; }
    setup_one "$local_host" "$local_port" "$local_user" "${local_alias:-$local_host}"
  elif [ "$#" -ge 1 ]; then
    echo "未知参数:$*"; exit 2
  else
    if [ "${#TARGETS[@]}" -eq 0 ]; then
      warn "TARGETS 为空:请用 add 子命令,或编辑脚本里的 TARGETS 数组"
      exit 0
    fi
    for t in "${TARGETS[@]}"; do
      IFS=: read -r h p u a <<< "$t"
      setup_one "$h" "$p" "$u" "$a"
    done
  fi

  echo
  echo "============================================================"
  echo "⚠️ 容器是临时层:openssh-client / psmisc / 密钥 仅存在于运行容器,"
  echo "   docker compose 重建后会丢失。请用上面 bake 子命令烤进镜像,"
  echo "   或把 /home/default/.ssh 挂成卷持久化。"
}

main "$@"

使用方法

新增单台主机

bash ds_ssh_setup.sh add --host 10.0.0.5 --user user --port 22 --alias node5

批量修改

编辑脚本顶部的 TARGETS 数组,每行格式 HOST:PORT:USER:ALIAS,然后直接运行:

bash ds_ssh_setup.sh

长期优化

  • 在目标机器上部署新worker(注册zookeeper)执行任务而非通过ssh调度

附录说明

dolphinscheduler伪集群模式组件明细

组件进程/服务伪集群实例数默认端口关键职责依赖该组件挂掉的故障影响
ApiServerapi-server1(可多开做负载)12345提供 UI/外部 REST API;工作流 CRUD、调度管理、权限、登录元数据库无法在页面操作/提交,但已在跑的任务不受影响
MasterServermaster-server≥1(伪集群常 1,可多)5678DAG 切分、任务派发、容错重试、补数;通过 ZK 选主,仅 leader 干活ZK + DB新任务无法调度;运行中任务由 Worker 续跑,但 failover 无人接管
WorkerServerworker-server≥1(伪集群常 1,可多分组)1234接收并执行具体任务(Shell/SQL/Spark…),回报状态与日志ZK + DB该 Worker 上的任务中断;Master 经 ZK 感知后 failover 到其他 Worker
AlertServeralert-server150053按告警规则发通知(邮件/钉钉/Webhook)DB告警发不出去,但调度与任务执行不受影响
ZooKeeperzk 单节点12181服务注册发现、心跳、Master 选主、任务队列全集群失联:Master/Worker 无法协调,调度瘫痪
元数据库 (Postgres/MySQL)单库15432存所有定义/实例/历史/用户全集群瘫痪:各组件起不来或任务全失败(即你遇到的根因)
StandaloneServer(对照)单进程112345把上述全部揉进一个进程内嵌一停全停,不用于生产
  1. UI → ApiServer:用户提交/触发工作流 → ApiServer 把实例写入元数据库
  2. Master 经 ZK 感知调度事件 → 从 DB 读实例、把 DAG 拆任务、按依赖 RPC 派给 Worker
  3. Worker 执行任务、写日志、把状态回报 Master 并落 DB
  4. 失败 → AlertServer 读 DB 告警规则发通知;Worker 挂 → MasterZK 感知后 failover。

配置文件

dolphinscheduler 3.2.2

application-master.yaml

master:
  exec-threads: 50                    # 流程实例并发降一半
  pre-exec-threads: 5
  dispatch-task-number: 1             # 每次只派 1 个任务,降低瞬时压力
  server-load-protection:
    enabled: true
    max-system-cpu-usage-percentage-thresholds: 0.85   # 临时放宽到 85%
    max-system-memory-usage-percentage-thresholds: 0.85
CoolCats
CoolCats
理学学士

我的研究兴趣是时空数据分析、知识图谱、自然语言处理与服务端开发