fix(orchestrator): spawn 子进程 PYTHONPATH + entrypoint 回根治孤儿
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:
+343
@@ -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 起强制单 worker(WEB_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 "$@"
|
||||
Reference in New Issue
Block a user