fix(orchestrator): spawn 子进程 PYTHONPATH + entrypoint 回根治孤儿
CI/CD / test (push) Successful in 8s
CI/CD / nas-deploy (push) Failing after 33s
CI/CD / nas-verify (push) Has been skipped

ProcessPoolExecutor(spawn)子进程不继承主进程 sys.path(insert 是内存修改),
import sanguo_orchestrator 失败->BrokenProcessPool->API 回测静默失败,POST 秒回
task_id 但轮询永远 404。spawn 继承 env 不继承 sys.path,故:
1. Dockerfile ENV PYTHONPATH=/app:/app/vnpy_v4.4.0(镜像层,bind mount 覆盖不了)
2. entrypoint.sh 回仓库根(git mv 自 docker/):promote --exclude /docker 致旧版
   entrypoint 在 NAS 成 git 外孤儿(340行,被 bind mount 覆盖镜像层);回根后全量
   rsync 自动推可复现(对齐 docs/deployment/nas-deploy-plan.md 预设修复)+export 兜底
3. 三 Dockerfile COPY 路径同步

反馈: 策略 session(根因诊断+探针验证)
This commit is contained in:
2026-08-01 21:33:20 +08:00
parent ca0ec016f9
commit 6d786fbf41
4 changed files with 9 additions and 3 deletions
+343
View File
@@ -0,0 +1,343 @@
#!/bin/bash
# Sanguo VeighNa Docker 容器启动脚本
set -e
# ============================================
# 颜色定义
# ============================================
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# ============================================
# 日志函数
# ============================================
log_info() {
echo -e "${GREEN}[INFO]${NC} $1"
}
log_warn() {
echo -e "${YELLOW}[WARN]${NC} $1"
}
log_error() {
echo -e "${RED}[ERROR]${NC} $1"
}
log_step() {
echo -e "${BLUE}[STEP]${NC} $1"
}
# ============================================
# 打印启动信息
# ============================================
print_banner() {
cat << "EOF"
╔════════════════════════════════════════════════════════════╗
║ ║
║ _____ ____ ____ _ _____ _ ___ _ ║
| __ \ / __ \ / __ \ | ||_ _|__ _| | |_ _|___| |
| |__) | | | | | | |_| | |/ _` | |_____|| | _ \ |
| ___/| | | | | | |_ | | (_| | |_____| | || |||
|_| | |__| | \___/\__| |\_____|_| |___||_||_|
\____/ ║
║ ║
║ Docker Web 版本 v1.0.0 ║
║ 基于 VeighNa 4.4.0 构建 ║
║ ║
╚════════════════════════════════════════════════════════════╝
EOF
}
# ============================================
# 检查环境变量
# ============================================
check_env() {
log_step "检查环境变量..."
# 设置默认值
export TZ="${TZ:-Asia/Shanghai}"
export PYTHONUNBUFFERED="${PYTHONUNBUFFERED:-1}"
export PYTHONDONTWRITEBYTECODE="${PYTHONDONTWRITEBYTECODE:-1}"
# Python 路径(关键): ProcessPoolExecutor(spawn)子进程不继承主进程 sys.path(内存修改),
# 但继承环境变量。设 PYTHONPATH 让 spawn 子进程 import sanguo_*/vnpy 不 ModuleNotFoundError→BrokenProcessPool。
# 镜像 Dockerfile ENV 同设此值(优先),此行为旧镜像/无 ENV 时兜底。
export PYTHONPATH="${PYTHONPATH:-/app:/app/vnpy_v4.4.0}"
# VeighNa 配置
export VNPY_LOG_LEVEL="${VNPY_LOG_LEVEL:-INFO}"
export VNPY_LOG_FILE="${VNPY_LOG_FILE:-/app/logs/vnpy.log}"
# 数据库配置
export DB_TYPE="${DB_TYPE:-sqlite}"
export DB_PATH="${DB_PATH:-/app/data/vnpy.db}"
# Web 服务配置
export WEB_HOST="${WEB_HOST:-0.0.0.0}"
export WEB_PORT="${WEB_PORT:-8000}"
export WEB_WORKERS="${WEB_WORKERS:-2}"
# 安全配置
export JWT_SECRET_KEY="${JWT_SECRET_KEY:-change-this-to-a-random-secret-key}"
export SESSION_TIMEOUT="${SESSION_TIMEOUT:-3600}"
# Redis 配置
export REDIS_HOST="${REDIS_HOST:-redis}"
export REDIS_PORT="${REDIS_PORT:-6379}"
# 开发选项
export SKIP_REDIS_CHECK="${SKIP_REDIS_CHECK:-0}"
export USE_POSTGRES="${USE_POSTGRES:-0}"
export DEBUG="${DEBUG:-0}"
# 打印关键配置
log_info "时区: $TZ"
log_info "日志级别: $VNPY_LOG_LEVEL"
log_info "数据库类型: $DB_TYPE"
log_info "Web 端口: $WEB_PORT"
log_info "Workers: $WEB_WORKERS"
log_info "环境变量检查完成"
}
# ============================================
# 创建数据目录
# ============================================
create_directories() {
log_step "创建数据目录..."
# 创建主要目录
mkdir -p /app/data
mkdir -p /app/logs
mkdir -p /app/temp
mkdir -p /app/config
# 创建日志子目录
mkdir -p /app/logs/web
mkdir -p /app/logs/vnpy
# 创建数据子目录
mkdir -p /app/data/database
mkdir -p /app/data/storage
# 设置权限
chmod -R 755 /app/data
chmod -R 755 /app/logs
chmod -R 755 /app/temp
log_info "数据目录创建完成"
}
# ============================================
# 初始化数据库
# ============================================
init_database() {
log_step "初始化数据库..."
if [ "$DB_TYPE" = "sqlite" ]; then
if [ ! -f /app/data/vnpy.db ]; then
log_info "创建 SQLite 数据库目录..."
mkdir -p /app/data/database
# 数据库初始化将在 Web 服务启动时完成
log_info "SQLite 数据库准备完成"
else
log_info "SQLite 数据库已存在"
fi
elif [ "$DB_TYPE" = "postgres" ] && [ "$USE_POSTGRES" = "1" ]; then
log_info "使用 PostgreSQL 数据库"
# PostgreSQL 配置由环境变量提供
else
log_warn "未知的数据库类型: $DB_TYPE,使用默认 SQLite"
export DB_TYPE=sqlite
fi
}
# ============================================
# 生成 SSL 证书(自签名,用于测试)
# ============================================
generate_ssl_cert() {
if [ ! -f /etc/nginx/ssl/cert.pem ]; then
log_warn "SSL 证书不存在,生成自签名证书..."
mkdir -p /etc/nginx/ssl
openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
-keyout /etc/nginx/ssl/key.pem \
-out /etc/nginx/ssl/cert.pem \
-subj "/C=CN/ST=Beijing/L=Beijing/O=Sanguo/CN=localhost" \
2>/dev/null
log_warn "自签名证书已生成,请勿用于生产环境"
fi
}
# ============================================
# 等待依赖服务
# ============================================
wait_for_services() {
log_step "等待依赖服务启动..."
# 等待 Redis
if [ "$SKIP_REDIS_CHECK" != "1" ]; then
log_info "等待 Redis 服务..."
REDIS_MAX_WAIT=30
REDIS_WAIT_COUNT=0
while ! nc -z ${REDIS_HOST:-redis} ${REDIS_PORT:-6379} 2>/dev/null; do
REDIS_WAIT_COUNT=$((REDIS_WAIT_COUNT + 1))
if [ $REDIS_WAIT_COUNT -ge $REDIS_MAX_WAIT ]; then
log_error "Redis 连接超时"
log_info "设置 SKIP_REDIS_CHECK=1 可跳过此检查(开发环境)"
exit 1
fi
sleep 1
done
log_info "Redis 服务已就绪 (${REDIS_HOST}:${REDIS_PORT})"
else
log_warn "跳过 Redis 检查"
fi
# 等待 PostgreSQL(如果启用)
if [ "$USE_POSTGRES" = "1" ]; then
log_info "等待 PostgreSQL 服务..."
POSTGRES_MAX_WAIT=30
POSTGRES_WAIT_COUNT=0
while ! nc -z ${POSTGRES_HOST:-postgres} ${POSTGRES_PORT:-5432} 2>/dev/null; do
POSTGRES_WAIT_COUNT=$((POSTGRES_WAIT_COUNT + 1))
if [ $POSTGRES_WAIT_COUNT -ge $POSTGRES_MAX_WAIT ]; then
log_error "PostgreSQL 连接超时"
exit 1
fi
sleep 1
done
log_info "PostgreSQL 服务已就绪"
fi
}
# ============================================
# 健康检查
# ============================================
health_check() {
log_step "执行健康检查..."
# 检查 Python 环境
if ! python --version &>/dev/null; then
log_error "Python 环境检查失败"
exit 1
fi
log_info "Python 版本: $(python --version)"
# 检查关键模块
python -c "import sys; print(f'Python 路径: {sys.executable}')" || {
log_error "Python 系统检查失败"
exit 1
}
# 检查数据目录
if [ ! -d "/app/data" ]; then
log_error "数据目录不存在: /app/data"
exit 1
fi
log_info "健康检查完成"
}
# ============================================
# 启动 Web 服务
# ============================================
start_web_service() {
log_step "启动 Web 服务..."
# 切换到工作目录
cd /app
# 确定 uvicorn 参数
UVICORN_ARGS="--host ${WEB_HOST} --port ${WEB_PORT}"
# Worker 配置
# sanguo_api 的 orchestrator 任务状态在内存(_pending/pool._tasks),
# 必须单 worker:多 worker 下"提交"与"查询状态/结果"可能落到不同 worker
# 而查不到。Phase 3b 起强制单 workerWEB_WORKERS 仅兼容保留)。
log_info "使用单 Worker 模式(sanguo_api 状态化 orchestrator"
# 日志级别
UVICORN_ARGS="$UVICORN_ARGS --log-level ${VNPY_LOG_LEVEL:-info}"
# 访问日志
UVICORN_ARGS="$UVICORN_ARGS --access-log"
# 调试模式(仅开发环境)
if [ "$DEBUG" = "1" ]; then
log_warn "启用调试模式(不应在生产环境使用)"
UVICORN_ARGS="$UVICORN_ARGS --reload"
fi
# 启动服务(Phase 3b 起:研究/回测 API sanguo_api,工厂模式启动)
log_info "启动命令: uvicorn sanguo_api.main:create_app --factory $UVICORN_ARGS"
exec uvicorn sanguo_api.main:create_app --factory $UVICORN_ARGS
}
# ============================================
# 显示访问信息
# ============================================
show_access_info() {
echo ""
log_info "=========================================="
log_info "服务启动中..."
log_info "=========================================="
echo ""
log_info "访问地址:"
echo ""
echo " HTTP: http://localhost:${WEB_PORT:-8000}"
echo " WebSocket: ws://localhost:${WS_PORT:-8080}"
echo ""
log_info "默认登录:"
echo ""
echo " 用户名: admin"
echo " 密码: admin123"
echo ""
log_warn "请在首次登录后修改默认密码!"
echo ""
log_info "=========================================="
echo ""
}
# ============================================
# 主函数
# ============================================
main() {
print_banner
echo ""
check_env
create_directories
init_database
generate_ssl_cert
wait_for_services
health_check
show_access_info
log_info "所有检查完成,启动服务..."
echo ""
start_web_service
}
# ============================================
# 信号处理
# ============================================
trap 'log_info "收到退出信号,优雅关闭..."; exit 0' SIGTERM SIGINT
# ============================================
# 执行主函数
# ============================================
main "$@"