如何优雅地直连无公网 IP 的远程 GPU 服务器

前言

故事是这样的。

我在做 AIGC 相关的开发,主力环境是一台没有公网 IP 的 GPU 实例以及一台阿里云灵构服务器 ,主要用来跑 ComfyUI、训练模型。

但使用过程中有几个让人难受的问题:

  • 阿里云官方自带的远程桌面控制延迟巨大,体验感太差
  • 文件传输太过麻烦,阿里云灵构需要借助网盘中转,而魔搭 WebIDE 有文件大小限制

我第一时间想到的解决办法就是内网穿透,不过第三方的穿透工具不太稳定,要么限速要么频繁掉线。刚好手上有一台 99 元的阿里云服务器,我寻思着不用也是浪费,干脆直接拿它来当做跳板机

不过我并没有考虑自建 frp 的方式来实现穿透,因为安装软件比较麻烦,每台目标机器都要安装 frpc 客户端的话太消耗时间,

本来薅来的免费 GPU 算力时长就有限,所以省事高效是第一原则。

于是我选择了 SSH 反向隧道的方式来穿透

我想要的效果是:

  • 本地 VS Code 直接打开远程 GPU 的终端,并实现文件实时编辑和传输
  • 本地浏览器直接访问 127.0.0.1:8188 操作 ComfyUI
  • 服务器和本地电脑重启后自动打通隧道

实现原理

穿透的本质就是借道,借有公网IP服务器的道

网络拓扑如下:

scss 复制代码
┌──────────┐     SSH 隧道      ┌──────────┐     反向隧道      ┌──────────┐
│  本地电脑 │ ───────────────▶ │ 阿里云 ECS │ ◀────────────── │  GPU 实例 │
│          │                   │ (公网 IP) │                   │ (无公网) │
└──────────┘                   └──────────┘                   └──────────┘
    ↑
    └── 浏览器访问 127.0.0.1:8188

关键点只有三个:

  1. GPU 主动连阿里云(因为 GPU 能出网,阿里云有公网)
  2. 建立反向隧道(-R):把 GPU 的端口"伸"到阿里云本机
  3. 本地再连阿里云(-L):把本地端口"引"到阿里云

最终效果:

  • localhost:8188 → 阿里云 → GPU 的 ComfyUI
  • VS Code ssh gpu → 阿里云 → GPU 的终端

具体实现

准备工作:阿里云侧

确保阿里云允许端口转发:

bash 复制代码
# 在阿里云上执行
echo 'net.ipv4.ip_forward=1' >> /etc/sysctl.conf
sysctl -p

这里需要明白的是,由于我们走的是 SSH 22 端口通道,所以无需去阿里云安全组开放 2222 和 8188 等端口,有公网 ip 即可,22 端口默认都是开放的


第一步:GPU 实例配置

执行以下指令,让GPU 实例主动将端口暴露给阿里云服务器(记得将IP替换成你的):

bash 复制代码
# 1. 创建 GPU 专用密钥
ssh-keygen -t ed25519 -f ~/.ssh/gpu_key -N ""

# 2. 把公钥拷贝到阿里云(让 GPU 能免密连阿里云)
ssh-copy-id -i ~/.ssh/gpu_key.pub root@阿里云公网 IP

# 3. 建立反向隧道:把 GPU 的 SSH 和 ComfyUI 映射到阿里云
ssh -N \
  -R 127.0.0.1:2222:127.0.0.1:22 \
  -R 127.0.0.1:8188:127.0.0.1:8188 \
  -i ~/.ssh/gpu_key \
  root@阿里云公网 IP

当然了,如果你不想每次手动挨个复制命令,我还准备了一键安装脚本,以下是在 GPU 上跑的脚本,核心逻辑是:密钥复用 + 反向隧道 +前台运行:

bash 复制代码
#!/bin/bash
# gpu_tunnel_setup.sh
# 用法:sudo ./gpu_tunnel_setup.sh <实例ID> [INDEX]
# 例:sudo ./gpu_tunnel_setup.sh gpu-01
#     sudo ./gpu_tunnel_setup.sh gpu-01 3

set -euo pipefail

# ===== 配置区 =====
ALIYUN_HOST="阿里云服务器IP"
ALIYUN_USER="root"
PERSIST_DIR="./gpu_keys"
KEY_NAME="gpu_key"
SERVICE_NAME="gpu-tunnel"
SUPERVISOR_CONF_DIR="/etc/supervisor/conf.d"
SUPERVISORD_CONF="/etc/supervisor/supervisord.conf"
SOCK_FILE="/var/run/supervisor.sock"
# ==================

SSH_BASE=2222
COMFY_BASE=8188

# ===== 颜色 =====
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
RED='\033[0;31m'
CYAN='\033[0;36m'
NC='\033[0m'

info()  { echo -e "${GREEN}[INFO]${NC} $*"; }
warn()  { echo -e "${YELLOW}[WARN]${NC} $*"; }
err()   { echo -e "${RED}[ERROR]${NC} $*" >&2; }
step()  { echo -e "\n${CYAN}==> $*${NC}"; }

# ===== 参数校验 =====
if [[ -z "${1:-}" ]]; then
    err "用法: $0 <实例ID> [INDEX]"
    echo "  例: $0 gpu-01"
    echo "  例: $0 gpu-01 3"
    exit 1
fi

INSTANCE_ID="$1"

if [[ -n "${2:-}" ]]; then
    INDEX="$2"
else
    INDEX=$(echo "$INSTANCE_ID" | grep -oE '[0-9]+' | head -n1 || true)
    INDEX=${INDEX:-1}
fi
[[ "$INDEX" -eq 0 ]] && INDEX=1

SSH_PORT=$(( SSH_BASE + INDEX - 1 ))
COMFY_PORT=$(( COMFY_BASE + INDEX - 1 ))

echo "============================================"
echo " GPU 反向隧道部署"
echo " 实例ID  : $INSTANCE_ID"
echo " INDEX   : $INDEX"
echo " SSH端口 : $SSH_PORT"
echo " ComfyUI : $COMFY_PORT"
echo "============================================"

# ===== 密钥 =====
step "准备 SSH 密钥"
mkdir -p "$PERSIST_DIR"
chmod 700 "$PERSIST_DIR"

KEY_PATH="$PERSIST_DIR/$KEY_NAME"
PUB_PATH="$KEY_PATH.pub"

if [[ -f "$KEY_PATH" && -f "$PUB_PATH" ]]; then
    info "复用已有密钥: $KEY_PATH"
else
    info "生成新密钥..."
    ssh-keygen -t ed25519 -f "$KEY_PATH" -N "" -C "gpu-$INSTANCE_ID"
    chmod 600 "$KEY_PATH"
    chmod 644 "$PUB_PATH"
fi

chmod 600 "$KEY_PATH"
chmod 644 "$PUB_PATH"

# ===== 安装 autossh =====
step "安装 autossh"
if ! command -v autossh &>/dev/null; then
    info "安装 autossh..."
    if command -v apt &>/dev/null; then
        apt update && apt install -y autossh
    elif command -v yum &>/dev/null; then
        yum install -y autossh
    elif command -v dnf &>/dev/null; then
        dnf install -y autossh
    else
        err "无法安装 autossh,请手动安装"
        exit 1
    fi
fi

AUTOSSH_BIN=$(which autossh)
info "autossh: $AUTOSSH_BIN"

# ===== 运行用户 =====
if [[ -n "${SUDO_USER:-}" ]]; then
    RUN_USER="$SUDO_USER"
else
    RUN_USER="$(whoami)"
fi

if [[ "$RUN_USER" == "root" ]]; then
    HOME_DIR="/root"
else
    HOME_DIR="$(eval echo ~$RUN_USER)"
fi

mkdir -p "$HOME_DIR/.ssh"
chmod 700 "$HOME_DIR/.ssh"

info "运行用户: $RUN_USER"
info "HOME目录: $HOME_DIR"

# ===== 信任主机 + 公钥 =====
step "配置免密登录"
info "信任阿里云主机 ($ALIYUN_HOST)..."
ssh-keyscan -H "$ALIYUN_HOST" >> "$HOME_DIR/.ssh/known_hosts" 2>/dev/null || true
sort -u "$HOME_DIR/.ssh/known_hosts" -o "$HOME_DIR/.ssh/known_hosts"

if ssh -o BatchMode=yes -o ConnectTimeout=5 \
    -i "$KEY_PATH" "$ALIYUN_USER@$ALIYUN_HOST" "echo ok" &>/dev/null; then
    info "免密登录已就绪"
else
    info "写入公钥到 $ALIYUN_USER@$ALIYUN_HOST..."
    ssh -o ConnectTimeout=10 -o StrictHostKeyChecking=no \
        "$ALIYUN_USER@$ALIYUN_HOST" \
        "mkdir -p ~/.ssh && chmod 700 ~/.ssh && \
         echo '$(cat "$PUB_PATH")' >> ~/.ssh/authorized_keys && \
         sort -u ~/.ssh/authorized_keys -o ~/.ssh/authorized_keys && \
         chmod 600 ~/.ssh/authorized_keys"
    info "公钥写入完成"
fi

# ===== 端口检测 =====
step "端口检测"
for PORT in $SSH_PORT $COMFY_PORT; do
    if ss -tlnp 2>/dev/null | grep -q ":${PORT} "; then
        warn "端口 $PORT 已被占用"
    fi
done

# ===== 构建 autossh 命令 =====
build_cmd() {
    echo "export AUTOSSH_GATETIME=0 && export AUTOSSH_POLL=30 && \
export HOME=$HOME_DIR && \
$AUTOSSH_BIN -M 0 -N \
  -R 127.0.0.1:${SSH_PORT}:127.0.0.1:22 \
  -R 127.0.0.1:${COMFY_PORT}:127.0.0.1:8188 \
  -o ServerAliveInterval=30 \
  -o ServerAliveCountMax=3 \
  -o ExitOnForwardFailure=yes \
  -o UserKnownHostsFile=$HOME_DIR/.ssh/known_hosts \
  -i ${KEY_PATH} \
  ${ALIYUN_USER}@${ALIYUN_HOST}"
}


# ===== 前台启动 =====
step "前台启动 autossh 隧道"
FOREGROUND_CMD=$(build_cmd)

info "执行命令:"
info "$FOREGROUND_CMD"
echo ""
info "按 Ctrl+C 停止隧道"
echo "============================================"

# 直接前台执行,autossh 输出直接打到终端
eval "$FOREGROUND_CMD"

将脚本上传到 GPU 实例中并执行,启动反向隧道(记得将阿里云IP替换成你的服务器)

如果你不想每次启动服务器都运行一遍脚本, 那我们可以开启 systemd 后台运行, 以下是补充开机自启动功能的版本:

sh 复制代码
#!/bin/bash
# gpu_tunnel_setup.sh
# 全环境兼容 GPU 反向隧道部署 / 停止 / 状态查看
# 用法:
#   部署: sudo ./gpu_tunnel_setup.sh <实例ID> [INDEX]
#   停止: sudo ./gpu_tunnel_setup.sh <实例ID> --stop
#   状态: sudo ./gpu_tunnel_setup.sh <实例ID> --status

set -euo pipefail

# ===== 配置区 =====
ALIYUN_HOST="阿里云服务器IP"
ALIYUN_USER="root"
PERSIST_DIR="./gpu_keys"
KEY_NAME="gpu_key"
SERVICE_NAME="gpu-tunnel"
# ==================

SSH_BASE=2222
COMFY_BASE=8188

# ===== 颜色 =====
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
RED='\033[0;31m'
CYAN='\033[0;36m'
NC='\033[0m'

info()  { echo -e "${GREEN}[INFO]${NC} $*"; }
warn()  { echo -e "${YELLOW}[WARN]${NC} $*"; }
err()   { echo -e "${RED}[ERROR]${NC} $*" >&2; }
step()  { echo -e "\n${CYAN}==> $*${NC}"; }

# ===== 参数解析 =====
MODE="deploy"

if [[ -z "${1:-}" ]]; then
    err "用法: $0 <实例ID> [INDEX|--stop|--status]"
    exit 1
fi

INSTANCE_ID="$1"

if [[ "${2:-}" == "--stop" ]] || [[ "${1:-}" == "--stop" ]]; then
    MODE="stop"
fi
if [[ "${2:-}" == "--status" ]] || [[ "${1:-}" == "--status" ]]; then
    MODE="status"
fi

# 运行用户
if [[ -n "${SUDO_USER:-}" ]]; then
    RUN_USER="$SUDO_USER"
else
    RUN_USER="$(whoami)"
fi
if [[ "$RUN_USER" == "root" ]]; then
    HOME_DIR="/root"
else
    HOME_DIR="$(eval echo ~$RUN_USER)"
fi

# ============================================================
# 查找同端口的 autossh 进程 PID(兼容不同 pgrep 版本)
# ============================================================
find_autossh_pids() {
    local port="$1"
    # 方法1: pgrep -f(大多数系统)
    local pids
    pids=$(pgrep -f "autossh" 2>/dev/null || true)
    if [[ -n "$pids" ]]; then
        echo "$pids" | while read pid; do
            if [[ -d "/proc/$pid" ]]; then
                cmdline=$(cat /proc/$pid/cmdline 2>/dev/null | tr '\0' ' ' || true)
                if echo "$cmdline" | grep -q "$port"; then
                    echo "$pid"
                fi
            fi
        done
    fi
}

# ============================================================
# --status 模式
# ============================================================
if [[ "$MODE" == "status" ]]; then
    echo "============================================"
    echo " GPU 隧道状态"
    echo "============================================"

    # 进程
    echo ""
    echo "--- 进程 ---"
    if pgrep -af autossh >/dev/null 2>&1; then
        pgrep -af autossh
        echo ""
        echo "--- 隧道端口映射 ---"
        pgrep -af autossh | grep -oP '(?<=127\.0\.0\.1:)\d+' | sort -u | while read PORT; do
            info "远程 ${ALIYUN_HOST} 上监听 127.0.0.1:${PORT}"
        done
    else
        warn "没有运行中的 autossh 进程"
    fi

    # systemd
    echo ""
    echo "--- systemd ---"
    if command -v systemctl &>/dev/null && systemctl list-unit-files 2>/dev/null | grep -q "${SERVICE_NAME}"; then
        systemctl status ${SERVICE_NAME}.service --no-pager 2>/dev/null | head -5 || true
    else
        info "无 systemd 服务"
    fi

    # supervisor
    echo ""
    echo "--- supervisor ---"
    if [[ -S /var/run/supervisor.sock ]]; then
        supervisorctl -c /etc/supervisor/supervisord.conf status 2>/dev/null || true
    else
        info "supervisor 未运行"
    fi

    # crontab
    echo ""
    echo "--- crontab ---"
    crontab -l 2>/dev/null | grep -v '^#' | grep -v '^$' | grep 'autossh' || info "无 crontab 条目"

    # .bashrc
    echo ""
    echo "--- shell 自启动 ---"
    if grep -q "GPU_TUNNEL_AUTO_START" ~/.bashrc 2>/dev/null; then
        info ".bashrc 中有自启动片段"
    else
        info ".bashrc 中无自启动"
    fi

    exit 0
fi

# ============================================================
# --stop 模式
# ============================================================
if [[ "$MODE" == "stop" ]]; then
    echo "============================================"
    echo " 停止 GPU 隧道"
    echo "============================================"

    step "终止 autossh 进程..."
    if pgrep -f 'autossh' >/dev/null 2>&1; then
        pkill -9 -f 'autossh' || true
        sleep 1
        info "✅ autossh 进程已终止"
    else
        info "没有运行中的 autossh 进程"
    fi

    step "停止 systemd 服务..."
    if command -v systemctl &>/dev/null; then
        systemctl stop ${SERVICE_NAME}.service 2>/dev/null || true
        systemctl disable ${SERVICE_NAME}.service 2>/dev/null || true
        rm -f /etc/systemd/system/${SERVICE_NAME}.service 2>/dev/null || true
        systemctl daemon-reload 2>/dev/null || true
        info "✅ systemd 已清理"
    fi

    step "停止 supervisor..."
    if [[ -S /var/run/supervisor.sock ]]; then
        supervisorctl -c /etc/supervisor/supervisord.conf stop ${SERVICE_NAME} 2>/dev/null || true
        supervisorctl -c /etc/supervisor/supervisord.conf remove ${SERVICE_NAME} 2>/dev/null || true
    fi
    if [[ -f "/etc/supervisor/conf.d/${SERVICE_NAME}.conf" ]]; then
        rm -f "/etc/supervisor/conf.d/${SERVICE_NAME}.conf"
        info "✅ supervisor 配置已移除"
    fi
    if pgrep -x supervisord >/dev/null 2>&1; then
        pkill -x supervisord 2>/dev/null || true
        info "✅ supervisord 已停止"
    fi

    step "清理 crontab..."
    TMP_CRON=$(mktemp)
    crontab -l 2>/dev/null | grep -v 'autossh' | grep -v 'gpu-tunnel' > "$TMP_CRON" || true
    crontab "$TMP_CRON" 2>/dev/null || true
    rm -f "$TMP_CRON"
    info "✅ crontab 已清理"

    step "清理 shell 自启动..."
    if grep -q "GPU_TUNNEL_AUTO_START" ~/.bashrc 2>/dev/null; then
        sed -i '/# GPU_TUNNEL_AUTO_START_/,+6d' ~/.bashrc
        info "✅ .bashrc 自启动已移除"
    fi

    echo ""
    echo "============================================"
    echo -e "${GREEN}✅ GPU 隧道已完全停止并清理${NC}"
    echo "============================================"
    exit 0
fi

# ============================================================
# deploy 模式
# ============================================================

INDEX="${2:-}"
if [[ -z "$INDEX" ]]; then
    INDEX=$(echo "$INSTANCE_ID" | grep -oE '[0-9]+' | head -n1 || true)
    INDEX=${INDEX:-1}
fi
[[ "$INDEX" -eq 0 ]] && INDEX=1

SSH_PORT=$(( SSH_BASE + INDEX - 1 ))
COMFY_PORT=$(( COMFY_BASE + INDEX - 1 ))

echo "============================================"
echo " GPU 反向隧道部署(全环境兼容)"
echo " 实例ID  : $INSTANCE_ID"
echo " INDEX   : $INDEX"
echo " SSH端口 : $SSH_PORT"
echo " ComfyUI : $COMFY_PORT"
echo "============================================"

# ===== 密钥 =====
step "准备 SSH 密钥"
mkdir -p "$PERSIST_DIR"
chmod 700 "$PERSIST_DIR"

KEY_PATH="$PERSIST_DIR/$KEY_NAME"
PUB_PATH="$KEY_PATH.pub"

if [[ -f "$KEY_PATH" && -f "$PUB_PATH" ]]; then
    info "复用已有密钥: $KEY_PATH"
else
    info "生成新密钥..."
    ssh-keygen -t ed25519 -f "$KEY_PATH" -N "" -C "gpu-$INSTANCE_ID"
    chmod 600 "$KEY_PATH"
    chmod 644 "$PUB_PATH"
fi

chmod 600 "$KEY_PATH"
chmod 644 "$PUB_PATH"

# ===== 安装 autossh =====
step "安装 autossh"
if ! command -v autossh &>/dev/null; then
    info "安装 autossh..."
    if command -v apt &>/dev/null; then
        apt update && apt install -y autossh
    elif command -v yum &>/dev/null; then
        yum install -y autossh
    elif command -v dnf &>/dev/null; then
        dnf install -y autossh
    else
        err "无法安装 autossh,请手动安装"
        exit 1
    fi
fi

AUTOSSH_BIN=$(which autossh)
info "autossh: $AUTOSSH_BIN"

mkdir -p "$HOME_DIR/.ssh"
chmod 700 "$HOME_DIR/.ssh"

info "运行用户: $RUN_USER"
info "HOME目录: $HOME_DIR"

# ===== 免密登录 =====
step "配置免密登录"
ssh-keyscan -H "$ALIYUN_HOST" >> "$HOME_DIR/.ssh/known_hosts" 2>/dev/null || true
sort -u "$HOME_DIR/.ssh/known_hosts" -o "$HOME_DIR/.ssh/known_hosts"

if ! ssh -o BatchMode=yes -o ConnectTimeout=5 \
    -i "$KEY_PATH" "$ALIYUN_USER@$ALIYUN_HOST" "echo ok" &>/dev/null; then
    info "写入公钥..."
    ssh -o ConnectTimeout=10 -o StrictHostKeyChecking=no \
        "$ALIYUN_USER@$ALIYUN_HOST" \
        "mkdir -p ~/.ssh && chmod 700 ~/.ssh && \
         echo '$(cat "$PUB_PATH")' >> ~/.ssh/authorized_keys && \
         sort -u ~/.ssh/authorized_keys -o ~/.ssh/authorized_keys && \
         chmod 600 ~/.ssh/authorized_keys"
    info "公钥写入完成"
else
    info "免密登录已就绪"
fi

# ===== 端口检测(说明是远程端口)=====
step "端口检测"
for PORT in $SSH_PORT $COMFY_PORT; do
    info "端口 $PORT 将在远程主机 ${ALIYUN_HOST} 上监听(本地无需监听)"
done

# ===== autossh 命令 =====
build_autossh_cmd() {
    echo "$AUTOSSH_BIN -M 0 -N" \
        "-R 127.0.0.1:${SSH_PORT}:127.0.0.1:22" \
        "-R 127.0.0.1:${COMFY_PORT}:127.0.0.1:8188" \
        "-o ServerAliveInterval=30" \
        "-o ServerAliveCountMax=3" \
        "-o ExitOnForwardFailure=yes" \
        "-o UserKnownHostsFile=$HOME_DIR/.ssh/known_hosts" \
        "-i ${KEY_PATH}" \
        "${ALIYUN_USER}@${ALIYUN_HOST}"
}

AUTOSSH_CMD=$(build_autossh_cmd)
LOG_FILE="/var/log/gpu-tunnel/${INSTANCE_ID}.log"
mkdir -p /var/log/gpu-tunnel

# ============================================================
# 层级 1:systemd
# ============================================================
try_systemd() {
    if ! command -v systemctl &>/dev/null; then
        return 1
    fi
    if [[ ! -d /run/systemd/system ]]; then
        return 1
    fi
    if [[ "$(ps -p 1 -o comm= 2>/dev/null)" != "systemd" ]]; then
        return 1
    fi

    step "检测到 systemd,部署 service..."

    cat > "/etc/systemd/system/${SERVICE_NAME}.service" <<EOF
[Unit]
Description=GPU Reverse Tunnel ($INSTANCE_ID)
After=network-online.target
Wants=network-online.target
StartLimitIntervalSec=0

[Service]
Type=simple
User=$RUN_USER
Environment=HOME=$HOME_DIR
Environment=AUTOSSH_GATETIME=0
Environment=AUTOSSH_POLL=30
ExecStart=/bin/sh -c '$AUTOSSH_CMD'
Restart=always
RestartSec=10

[Install]
WantedBy=multi-user.target
EOF

    systemctl daemon-reload
    systemctl enable ${SERVICE_NAME}.service
    systemctl restart ${SERVICE_NAME}.service
    sleep 2

    if systemctl is-active --quiet ${SERVICE_NAME}.service; then
        info "✅ systemd 服务已启动并开机自启"
        return 0
    else
        warn "systemd 服务启动失败"
        return 1
    fi
}

# ============================================================
# 层级 2:supervisor(修复版:timeout + 快速失败)
# ============================================================
try_supervisor() {
    if ! command -v supervisord &>/dev/null; then
        warn "supervisor 未安装,跳过"
        return 1
    fi

    step "尝试 supervisor 部署..."

    mkdir -p /etc/supervisor/conf.d
    mkdir -p /var/run /var/log/supervisor

    if [[ ! -f /etc/supervisor/supervisord.conf ]]; then
        cat > /etc/supervisor/supervisord.conf <<EOF
[supervisord]
logfile=/var/log/supervisor/supervisord.log
pidfile=/var/run/supervisord.pid
childlogdir=/var/log/supervisor
nodaemon=false

[unix_http_server]
file=/var/run/supervisor.sock
chmod=0700

[supervisorctl]
serverurl=unix:///var/run/supervisor.sock

[rpcinterface:supervisor]
supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface

[include]
files = /etc/supervisor/conf.d/*.conf
EOF
    fi

    cat > "/etc/supervisor/conf.d/${SERVICE_NAME}.conf" <<EOF
[program:${SERVICE_NAME}]
command=$AUTOSSH_CMD
directory=$HOME_DIR
user=$RUN_USER
environment=HOME="$HOME_DIR",AUTOSSH_GATETIME="0",AUTOSSH_POLL="30"
autostart=true
autorestart=true
startsecs=5
startretries=999
redirect_stderr=true
stdout_logfile=$LOG_FILE
stdout_logfile_maxbytes=10MB
stdout_logfile_backups=5
EOF

    info "程序配置已写入"

    # 清理旧残留
    rm -f /var/run/supervisor.sock /var/run/supervisord.pid

    # timeout 限制启动,5秒起不来直接放弃
    if command -v timeout &>/dev/null; then
        timeout 5 supervisord -c /etc/supervisor/supervisord.conf >/dev/null 2>&1 &
    else
        supervisord -c /etc/supervisor/supervisord.conf >/dev/null 2>&1 &
    fi
    local SUPERVISORD_PID=$!
    sleep 2

    if [[ -S /var/run/supervisor.sock ]]; then
        # 先 stop 再 start,避免 "already started" 问题
        supervisorctl -c /etc/supervisor/supervisord.conf stop ${SERVICE_NAME} 2>/dev/null || true
        supervisorctl -c /etc/supervisor/supervisord.conf reread 2>/dev/null || true
        supervisorctl -c /etc/supervisor/supervisord.conf update 2>/dev/null || true
        supervisorctl -c /etc/supervisor/supervisord.conf start ${SERVICE_NAME} 2>/dev/null || true
        sleep 1
        info "✅ supervisor 部署完成"
        return 0
    else
        kill -9 $SUPERVISORD_PID 2>/dev/null || true
        rm -f /var/run/supervisor.sock /var/run/supervisord.pid
        warn "supervisord 无法创建 socket(容器环境不兼容),跳过"
        return 1
    fi
}

# ============================================================
# 层级 3:crontab 守护
# ============================================================
try_crontab() {
    step "部署 crontab 守护..."

    if ! command -v cron &>/dev/null && ! command -v crond &>/dev/null; then
        info "安装 cron..."
        if command -v apt &>/dev/null; then
            apt update && apt install -y cron
        elif command -v yum &>/dev/null; then
            yum install -y cronie || yum install -y cron || true
        fi
    fi

    TMP_CRON=$(mktemp)
    crontab -u "$RUN_USER" -l 2>/dev/null | grep -v "autossh.*${SSH_PORT}" | grep -v "gpu-tunnel" > "$TMP_CRON" || true

    echo "* * * * * pgrep -f 'autossh' | while read pid; do if cat /proc/\$pid/cmdline 2>/dev/null | tr '\\0' ' ' | grep -q '${SSH_PORT}'; then exit 0; fi; done && (export AUTOSSH_GATETIME=0 AUTOSSH_POLL=30 HOME=$HOME_DIR; $AUTOSSH_CMD >> $LOG_FILE 2>&1 &)" >> "$TMP_CRON"
    echo "@reboot (export AUTOSSH_GATETIME=0 AUTOSSH_POLL=30 HOME=$HOME_DIR; $AUTOSSH_CMD >> $LOG_FILE 2>&1 &)" >> "$TMP_CRON"

    crontab -u "$RUN_USER" "$TMP_CRON" 2>/dev/null || true
    rm -f "$TMP_CRON"

    if command -v service &>/dev/null; then
        service cron start 2>/dev/null || service crond start 2>/dev/null || true
    fi

    info "✅ crontab 守护已部署(每分钟检查 + @reboot 自启)"
    return 0
}

# ============================================================
# 层级 4:shell 自启动兜底
# ============================================================
setup_shell_autostart() {
    step "配置 shell 自启动兜底..."

    local SHELL_RC=""
    if [[ "$RUN_USER" == "root" ]]; then
        SHELL_RC="/root/.bashrc"
    else
        SHELL_RC="$HOME_DIR/.bashrc"
        [[ -f "$SHELL_RC" ]] || SHELL_RC="$HOME_DIR/.profile"
        [[ -f "$SHELL_RC" ]] || touch "$SHELL_RC"
    fi

    local SNIPPET_MARKER="# GPU_TUNNEL_AUTO_START_${INSTANCE_ID}"
    if ! grep -q "$SNIPPET_MARKER" "$SHELL_RC" 2>/dev/null; then
        cat >> "$SHELL_RC" <<EOF

$SNIPPET_MARKER
if ! pgrep -f "autossh" | while read pid; do cat /proc/\$pid/cmdline 2>/dev/null | tr '\\0' ' ' | grep -q '${SSH_PORT}' && echo found; done | grep -q found; then
    export AUTOSSH_GATETIME=0
    export AUTOSSH_POLL=30
    export HOME=$HOME_DIR
    $AUTOSSH_CMD >> $LOG_FILE 2>&1 &
fi
EOF
        info "✅ shell 自启动已写入 $SHELL_RC"
    else
        info "shell 自启动已存在,跳过"
    fi
}

# ============================================================
# 主流程
# ============================================================
DEPLOYED=false

step "检测环境并部署..."

# ① systemd
if try_systemd; then
    DEPLOYED=true
    info "托管方式: systemd"
else
    warn "systemd 不可用,尝试下一层..."
fi

# ② supervisor
if [[ "$DEPLOYED" == "false" ]]; then
    if try_supervisor; then
        DEPLOYED=true
        info "托管方式: supervisor"
    else
        warn "supervisor 不可用,尝试下一层..."
    fi
fi

# ③ crontab
if [[ "$DEPLOYED" == "false" ]]; then
    if try_crontab; then
        DEPLOYED=true
        info "托管方式: crontab"
    fi
fi

# ④ shell 兜底
setup_shell_autostart

# ⑤ 清理同端口旧进程,然后启动
step "确保没有同端口的旧进程..."
OLD_PIDS=$(find_autossh_pids "$SSH_PORT")
if [[ -n "$OLD_PIDS" ]]; then
    echo "$OLD_PIDS" | while read pid; do
        if [[ -n "$pid" ]]; then
            kill -9 "$pid" 2>/dev/null || true
            info "已终止旧进程 PID=$pid"
        fi
    done
    sleep 1
fi

if ! pgrep -f "autossh" | while read pid; do
    if [[ -d "/proc/$pid" ]]; then
        cat /proc/$pid/cmdline 2>/dev/null | tr '\0' ' ' | grep -q "$SSH_PORT" && echo "found"
    fi
done | grep -q "found"; then
    step "立即启动隧道..."
    export AUTOSSH_GATETIME=0 AUTOSSH_POLL=30 HOME=$HOME_DIR
    nohup $AUTOSSH_CMD >> "$LOG_FILE" 2>&1 &
    sleep 2
    if pgrep -f "autossh" | while read pid; do
        cat /proc/$pid/cmdline 2>/dev/null | tr '\0' ' ' | grep -q "$SSH_PORT" && echo "found"
    done | grep -q "found"; then
        info "✅ 隧道进程已启动"
    else
        warn "⚠️  进程启动可能失败,检查日志: $LOG_FILE"
        tail -10 "$LOG_FILE" 2>/dev/null || true
    fi
else
    info "隧道进程已在运行"
fi

# ===== 完成 =====
echo ""
echo "============================================"
if [[ "$DEPLOYED" == "true" ]]; then
    echo -e "${GREEN}✅ GPU 隧道部署完成!${NC}"
else
    echo -e "${YELLOW}⚠️  部署完成(仅 shell 兜底模式)${NC}"
fi
echo ""
echo "📌 连接信息:"
echo "   SSH    : ssh -p $SSH_PORT $ALIYUN_USER@$ALIYUN_HOST"
echo "   ComfyUI: http://$ALIYUN_HOST:$COMFY_PORT"
echo ""
echo "📌 管理:"
echo "   查看状态: $0 $INSTANCE_ID --status"
echo "   停止服务: $0 $INSTANCE_ID --stop"
echo "   查看日志: tail -f $LOG_FILE"
echo "============================================"

两个版本, 根据自身需求任选其一即可


第二步:本地电脑的配置

在本地电脑终端执行以下命令

bash 复制代码
# 1. 创建 SSH 密钥
ssh-keygen -t ed25519 -f ~/.ssh/comfy_key -N ""

# 2. 把公钥拷贝到阿里云
ssh-copy-id -i ~/.ssh/comfy_key.pub root@阿里云公网 ip

# 3. 打通隧道:把本机 8188 引到阿里云 → GPU
ssh -N \
  -L 8188:127.0.0.1:8188 \
  -i ~/.ssh/comfy_key \
  root@阿里云公网 IP

此时我们在浏览器上直接输入http://127.0.0.1:8188即可访问 GPU 实例中的 ComfyUI 了

但是我还要实现终端访问,所以我们需要另起一个终端,并输入以下指令:

bash 复制代码
ssh -J root@阿里云公网 IP:22 -p 2222 ubuntu@127.0.0.1

或者

在用户目录下的 config 文件中添加如下配置:

yaml 复制代码
# ===== 阿里云跳板机 =====
Host aliyun
    HostName 阿里云公网 IP
    User root
    IdentityFile ~/.ssh/comfy_key
    Port 22
    StrictHostKeyChecking accept-new

# ===== GPU(经阿里云跳转)=====
Host gpu
    HostName 127.0.0.1
    User ubuntu
    Port 2222
    IdentityFile ~/.ssh/comfy_key
    ProxyJump aliyun
    ServerAliveInterval 30
    ServerAliveCountMax 3

然后在终端中运行 ssh gpu,一样的效果,不过为了方便 VS Code 连接,建议采用配置的形式

这里需要注意的是, GPU 部分的 User 字段该填 root 还是 ubuntu ,取决于你的 GPU 服务器的设置,可以运行以下指令查看是否为 root:

bash 复制代码
cat /etc/passwd | grep -E '/bin/bash|/bin/zsh'

同样的,本地电脑也有一键配置脚本,全自动化的同时不破坏原有的 SSH 配置,如下:

bash 复制代码
#!/bin/bash
# local_tunnel_setup.sh
# 兼容 Linux / macOS

set -e

ALIYUN_HOST="阿里云服务器IP"
ALIYUN_USER="root"
TUNNEL_PORT="8188"
KEY_DIR="$HOME/.ssh"
KEY_NAME="comfy_key"
BLOCK_NAME="aliyun-tunnel"

mkdir -p "$KEY_DIR/config.d"
chmod 700 "$KEY_DIR"
touch "$KEY_DIR/config"
chmod 600 "$KEY_DIR/config"

KEY_PATH="$KEY_DIR/$KEY_NAME"
PUB_PATH="$KEY_PATH.pub"

# 密钥复用
if [[ -f "$KEY_PATH" && -f "$PUB_PATH" ]]; then
    echo "✅ 复用本地密钥"
else
    ssh-keygen -t ed25519 -f "$KEY_PATH" -N "" -C "local@$(hostname)"
fi

# 安全合并 SSH 配置(不覆盖!)
INCLUDE_LINE="Include $KEY_DIR/config.d/*.conf"
if ! grep -qxF "$INCLUDE_LINE" "$KEY_DIR/config"; then
    if [ -s "$KEY_DIR/config" ]; then
        sed -i '' "1i\\
$INCLUDE_LINE
" "$KEY_DIR/config"
    else
        echo "$INCLUDE_LINE" > "$KEY_DIR/config"
    fi
fi


# 写独立配置片段
cat > "$KEY_DIR/config.d/${BLOCK_NAME}.conf" <<EOF
# Managed by local_tunnel.sh
Host aliyun
    HostName $ALIYUN_HOST
    User $ALIYUN_USER
    Port 22
    IdentityFile $KEY_PATH
    StrictHostKeyChecking accept-new

Host gpu
    HostName 127.0.0.1
    Port 2222
    User root
    ProxyJump aliyun
    IdentityFile $KEY_PATH
    IdentitiesOnly yes
EOF

# 注册公钥
ssh-copy-id -i "$PUB_PATH" "$ALIYUN_USER@$ALIYUN_HOST" || true

# 跨平台自启
case "$(uname)" in
Linux)
    cat > /etc/systemd/system/local-comfy-tunnel.service <<EOF
[Unit]
Description=Local SSH Tunnel to GPU
After=network-online.target

[Service]
Type=simple
User=$USER
ExecStart=/usr/bin/ssh -N \\
  -L ${TUNNEL_PORT}:127.0.0.1:${TUNNEL_PORT} \\
  -o ServerAliveInterval=30 \\
  -o ServerAliveCountMax=3 \\
  -i ${KEY_PATH} \\
  ${ALIYUN_USER}@${ALIYUN_HOST}
Restart=always

[Install]
WantedBy=multi-user.target
EOF
    systemctl daemon-reload
    systemctl enable local-comfy-tunnel.service
    systemctl restart local-comfy-tunnel.service
    ;;
Darwin)
    PLIST="$HOME/Library/LaunchAgents/com.user.local-comfy-tunnel.plist"
    cat > "$PLIST" <<EOF
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
 "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>Label</key>
    <string>com.user.local-comfy-tunnel</string>
    <key>ProgramArguments</key>
    <array>
        <string>$(which autossh)</string>
        <string>-M</string><string>0</string>
        <string>-N</string>
        <string>-L</string>
        <string>${TUNNEL_PORT}:127.0.0.1:${TUNNEL_PORT}</string>
        <string>-i</string>
        <string>${KEY_PATH}</string>
        <string>${ALIYUN_USER}@${ALIYUN_HOST}</string>
    </array>
    <key>RunAtLoad</key><true/>
    <key>KeepAlive</key><true/>
</dict>
</plist>
EOF
    launchctl load "$PLIST"
    ;;
esac

echo "✅ 本地隧道已启动,访问 http://127.0.0.1:$TUNNEL_PORT"

该脚本兼容 Mac 和 Linux 平台

直接拷贝到本地电脑运行即可,(记得将阿里云IP替换成你的服务器IP)


第三步:将本地电脑公钥拷贝至GPU服务器

隧道打通之后, 阿里云服务器作为跳板机只负责数据传输, 不负责身份的验证

要想实现本地和GPU服务器ssh访问, 必须将本地的comfy_key公钥拷贝至GPU服务器的./ssh/authorized_keys文件中

在 GPU 终端里执行以下命令:

bash 复制代码
# 1. 创建目录(没有就建,有了也不报错)
mkdir -p ~/.ssh
chmod 700 ~/.ssh

# 2. 创建 authorized_keys(没有就新建,有了就打开编辑)
# 把本地 ~/.ssh/comfy_key.pub 的输出粘贴进去
vim ~/.ssh/authorized_keys

# 3. 保存退出后,改权限
chmod 600 ~/.ssh/authorized_keys

# 4. 确认内容
cat ~/.ssh/authorized_keys

或者直接从阿里云中复制公钥:

bash 复制代码
# 1. 创建目录和文件(没有就建)
mkdir -p ~/.ssh
chmod 700 ~/.ssh

# 2. 从阿里云拉取 authorized_keys 并追加到本地
ssh root@阿里云IP "cat ~/.ssh/authorized_keys" >> ~/.ssh/authorized_keys

# 3. 去重(防止重复执行导致多行重复)
sort -u ~/.ssh/authorized_keys -o ~/.ssh/authorized_keys

# 4. 改权限
chmod 600 ~/.ssh/authorized_keys

# 5. 确认内容
cat ~/.ssh/authorized_keys

第四步:开始访问

到这里,我想大家应该都知道怎么用了,这里之所以推荐大家使用VS Code的Remote-SSH插件 来连接,因为它既可以访问终端,又能访问文件,省去了手敲命令,一举两得,而且用起来也比较方便:

在VS Code 里:

  • F1 → Remote-SSH: Connect to Host
  • 选择 gpu

会直接进到 GPU 的终端

它和阿里云是独立的互不影响,你想访问阿里云终端,还是按照原来的 ssh -p 22 root@阿里云公网 IP就行


多 GPU 怎么扩展?

假设我有 3 台 GPU,做法是:

GPU SSH 端口 ComfyUI 端口
gpu-01 2222 8188
gpu-02 2223 8189
gpu-03 2224 8190

本地 ~/.ssh/config:

bash 复制代码
Host gpu-01
    HostName 127.0.0.1
    Port 2222
    ProxyJump aliyun-relay

Host gpu-02
    HostName 127.0.0.1
    Port 2223
    ProxyJump aliyun-relay

VS Code 可以同时连多台 GPU,互不干扰。

问题汇总

  1. ssh gpu登录GPU提示需要输入root@127.0.0.1 password

    问题出在公钥上, 检查本地comfy_key.pub公钥和GPU中authorized_keys中存储的公钥是否一致

    另外有一点需要注意的是, 不要使用老版ssh-rsa格式密钥, 目前很多新系统, 推荐使用新的ssh-ed25519格式密钥

  2. 关于密钥问题

    本地连接阿里云和连接GPU服务器可以使用不同的密钥, 比如:

    yaml 复制代码
    # Managed by local_tunnel.sh
    # 连接阿里云使用aliyun密钥对, 连接gpu使用comfy_key密钥对
    Host aliyun
        HostName 阿里云服务器IP
        User root
        Port 22
        IdentityFile /Users/xxx/.ssh/aliyun
        StrictHostKeyChecking accept-new
    
    Host gpu
        HostName 127.0.0.1
        Port 2222
        User root
        ProxyJump aliyun
        IdentityFile /Users/xxx/.ssh/comfy_key
        IdentitiesOnly yes

    隧道建立是先连接阿里云服务器, 成功后再连接GPU服务器, 阿里云只负责数据中转, 而不会拿着comfy_key密钥去替我们连接GPU, 这点需要明白,

    所以, 他们不一定非得使用相同密钥对, 之前我们为了方便管理和使用, 共用了相同的comfy_key密钥

音乐+技术公众号《乱码三千》,欢迎关注~

本文为作者原创 转载时请注明出处 谢谢

乱码三千 -- 点滴积累 ,欢迎来到乱码三千技术博客站

相关推荐
机器之心1 小时前
突发:Claude自主发现未知生物系统,或能编辑基因
前端·人工智能·后端
犀利豆1 小时前
AI 老板五分钟挂出招聘启事,四个月后开除了第一个人
人工智能·llm·aigc
墨林陌1 小时前
AI 热点日报(2026-09-24):奥尔特曼安理会呼吁建全球AI标准,DeepSeek披露智能体沙箱平台DSec
人工智能
9i编程1 小时前
9. 把 DDD 开源脚手架化为自己的:联调照出的问题与事件重构——租户权限、按钮样式与全局 DomainEvent 反哺 SKILL
人工智能·openai·ai编程
桃西西呀1 小时前
从手写数字到ImageNet冠军:CNN架构20年,为什么越深越难训?
人工智能·计算机视觉·llm
Solara1 小时前
1 条 audit=0 的幽灵记录:我说不清是我删的,还是平台吞的
人工智能·程序员·ai编程
yunwei371 小时前
使用 eBPF 跟踪 Nginx 请求
linux·后端·性能优化
yunwei371 小时前
使用 eBPF 跟踪 MySQL 查询
linux·后端·性能优化
诚小纯1 小时前
同一份稿子在两代模型上差 6.44 倍:语音模型价格对齐的工程拆法
人工智能·github