千问3.5-27B生产环境部署:日志分级+端口监听+服务健康检查

1. 引言:从“能用”到“好用”的部署进阶

如果你已经成功部署了千问3.5-27B模型,体验了它的中文对话和图片理解能力,那么恭喜你,你已经迈出了第一步。但你可能也发现了,在浏览器里聊聊天、调调接口,这只是“能用”的层面。一旦想把模型真正用在生产环境,比如给团队内部使用、集成到自己的应用里,或者需要长时间稳定运行,就会遇到一堆新问题:

  • 服务突然挂了,怎么快速知道?
  • 日志文件越来越大,想查个错误信息得翻半天。
  • 端口被占用了,服务起不来,一脸懵。
  • 想看看服务的CPU、内存占用,不知道怎么下手。

这些问题不解决,模型再好用,也只是一个“玩具”,没法成为可靠的“生产力工具”。

这篇文章,就是帮你把千问3.5-27B从“能用”升级到“好用”的实战指南。我们不谈复杂的模型原理,只聚焦于生产环境部署必须掌握的三个核心运维技能日志分级管理端口监听配置服务健康检查。我会手把手带你,基于现有的镜像环境,把这些能力加进去,让你的模型服务变得稳定、可观测、易维护。

读完本文,你将能搭建一个带有完善监控和自愈能力的千问3.5-27B服务环境。

2. 环境准备与现状分析

在开始改造之前,我们先摸清家底,看看当前镜像提供了什么,以及我们需要在什么基础上进行增强。

2.1 当前部署架构速览

根据提供的使用手册,当前的部署已经相当成熟,为我们打下了很好的基础:

  • 模型与框架:使用的是 Qwen/Qwen3.5-27B 模型,通过 transformers + accelerate 库加载,运行在 conda 环境 qwen3527 中。
  • 服务化:使用 FastAPI 提供了 Web 界面(端口7860)和 RESTful API(/generate, /generate_with_image)。
  • 进程管理:通过 supervisor 托管服务进程,服务名为 qwen3527。这已经解决了“进程崩溃后自动重启”的基础问题。
  • 日志:目前日志输出到 /root/workspace/qwen3527.log/root/workspace/qwen3527.err.log。但所有信息都混在一起,不利于排查。

2.2 我们需要增强什么?

当前的架构像一辆能跑的车,但缺少仪表盘和故障报警灯。我们的目标是给它装上:

  1. 结构化日志系统:把“发动机转速”、“油耗”、“故障码”(对应INFO、WARNING、ERROR日志)分门别类地记录和展示。
  2. 端口健康监听:确保“车门”(服务端口)是正常打开的,并且没有被其他东西堵住。
  3. 全面的健康检查:不仅能看“车门”,还要能检查“发动机”(模型推理进程)、“油箱”(GPU内存)是否都工作正常。

接下来,我们分三步,逐一实现这些能力。

3. 第一步:实现日志分级与管理

把所有日志都往一个文件里写,就像把所有的衣服都塞进一个衣柜,找起来会非常痛苦。日志分级就是给日志贴上标签(INFO, DEBUG, ERROR等),并分到不同的“抽屉”里。

3.1 修改服务启动脚本,集成结构化日志

首先,我们需要修改模型的启动方式,使其支持Python标准的 logging 模块,并将日志输出到文件的同时,也按级别区分。

找到你的服务启动脚本。根据手册,服务目录在 /opt/qwen3527-27b,我们假设主启动文件是 app.py。我们需要修改或创建一个新的启动文件,例如 app_with_logging.py

# /opt/qwen3527-27b/app_with_logging.py
import sys
import os
import logging
from datetime import datetime
from pathlib import Path

# 1. 创建独立的日志目录
LOG_DIR = Path("/root/workspace/logs/qwen3527")
LOG_DIR.mkdir(parents=True, exist_ok=True)

# 2. 生成按日期分割的日志文件名
current_date = datetime.now().strftime("%Y-%m-%d")
log_file_info = LOG_DIR / f"qwen3527_info_{current_date}.log"
log_file_error = LOG_DIR / f"qwen3527_error_{current_date}.log"
log_file_debug = LOG_DIR / f"qwen3527_debug_{current_date}.log"

# 3. 配置根日志记录器
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger("QwenService")

# 清除可能已有的处理器
logger.handlers.clear()

# 4. 创建不同级别的处理器
# INFO及以上级别 -> info文件
info_handler = logging.FileHandler(log_file_info)
info_handler.setLevel(logging.INFO)
info_formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
info_handler.setFormatter(info_formatter)

# ERROR及以上级别 -> error文件 (同时也会被info_handler记录)
error_handler = logging.FileHandler(log_file_error)
error_handler.setLevel(logging.ERROR)
error_handler.setFormatter(info_formatter) # 使用相同格式

# DEBUG级别 -> debug文件 (按需开启,生产环境可关闭)
debug_handler = logging.FileHandler(log_file_debug)
debug_handler.setLevel(logging.DEBUG)
debug_handler.setFormatter(info_formatter)

# 5. 将处理器添加到logger
logger.addHandler(info_handler)
logger.addHandler(error_handler)
# logger.addHandler(debug_handler) # 默认不开启DEBUG日志以节省空间

# 6. 同时输出到控制台,方便supervisor捕获
console_handler = logging.StreamHandler(sys.stdout)
console_handler.setLevel(logging.INFO)
console_handler.setFormatter(info_formatter)
logger.addHandler(console_handler)

logger.info("="*50)
logger.info("Qwen 3.5-27B 服务启动 - 结构化日志已初始化")
logger.info(f"INFO日志文件: {log_file_info}")
logger.info(f"ERROR日志文件: {log_file_error}")
logger.info("="*50)

# 7. 这里是原有的FastAPI应用启动代码 (假设从原app.py导入)
# 例如:from app import app
# 为了演示,我们模拟一个简单的启动过程
from fastapi import FastAPI
import uvicorn

app = FastAPI(title="Qwen3.5-27B API")

@app.get("/health")
async def health_check():
    logger.info("健康检查端点被调用")
    return {"status": "healthy", "model": "Qwen3.5-27B"}

# 模拟模型加载
logger.info("开始加载 Qwen3.5-27B 模型...")
# 这里替换为实际的模型加载代码
# from transformers import AutoModelForCausalLM, AutoTokenizer
# model = AutoModelForCausalLM.from_pretrained(...)
# tokenizer = AutoTokenizer.from_pretrained(...)
logger.info("模型加载完成。")

if __name__ == "__main__":
    host = "0.0.0.0"
    port = 7860
    logger.info(f"启动Uvicorn服务器,监听 {host}:{port}")
    uvicorn.run(app, host=host, port=port, log_config=None) # 禁用uvicorn默认日志,使用我们的

关键点解释

  • 按日期分割:每天生成新的日志文件,避免单个文件过大。
  • 级别分离INFO日志记录常规运行信息(如服务启动、接口调用),ERROR日志只记录错误和异常,方便快速定位问题。
  • 格式统一:每条日志都包含时间戳、模块名、级别和信息,清晰可读。

3.2 更新Supervisor配置,指向新的启动脚本

接下来,我们需要修改Supervisor的配置,让它运行我们新的、带日志管理的脚本。

Supervisor的配置文件通常位于 /etc/supervisor/conf.d//root/workspace/ 下。根据手册,服务名是 qwen3527,我们找到对应的配置文件(例如 qwen3527.conf)进行修改。

; /etc/supervisor/conf.d/qwen3527.conf (或类似路径)
[program:qwen3527]
command=/root/miniconda3/envs/qwen3527/bin/python /opt/qwen3527-27b/app_with_logging.py
directory=/opt/qwen3527-27b
user=root
autostart=true
autorestart=true
startsecs=10
stopwaitsecs=30
; 重点修改:重定向标准输出和错误到我们的日志系统(或者让Python自己处理)
; 因为我们在Python代码里已经配置了StreamHandler输出到stdout,所以supervisor可以捕获。
; 我们将supervisor的日志仅作为备份和进程状态记录。
stdout_logfile=/root/workspace/supervisor_qwen3527_out.log
stdout_logfile_maxbytes=10MB
stdout_logfile_backups=5
stderr_logfile=/root/workspace/supervisor_qwen3527_err.log
stderr_logfile_maxbytes=10MB
stderr_logfile_backups=5
environment=PYTHONUNBUFFERED="1"

修改完成后,让Supervisor重新加载配置并重启服务:

supervisorctl reread
supervisorctl update
supervisorctl restart qwen3527

现在,检查新的日志目录:

ls -la /root/workspace/logs/qwen3527/
tail -f /root/workspace/logs/qwen3527/qwen3527_info_2024-06-15.log

你应该能看到按日期和级别分类的、格式清晰的日志了。

4. 第二步:强化端口监听与冲突处理

服务启动失败,很多时候是因为端口被占用。我们需要一个机制,在启动前检查端口,并在启动后确认监听成功。

4.1 创建端口检查与守护脚本

我们可以创建一个独立的脚本,作为服务的“守门员”。

#!/bin/bash
# /opt/qwen3527-27b/check_port.sh

PORT=7860
SERVICE_NAME="qwen3527"
LOG_FILE="/root/workspace/logs/qwen3527/port_guard.log"

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

# 检查端口是否被占用
check_port() {
    # 使用ss命令检查7860端口是否已被监听
    if ss -ltnp | grep -q ":${PORT} "; then
        PID=$(ss -ltnp | grep ":${PORT} " | awk '{print $6}' | cut -d= -f2 | cut -d, -f1)
        log "ERROR: 端口 ${PORT} 已被进程 PID ${PID} 占用。"
        echo "端口 ${PORT} 已被占用,PID: ${PID}。请先停止该进程。"
        return 1
    else
        log "INFO: 端口 ${PORT} 空闲。"
        echo "端口 ${PORT} 空闲。"
        return 0
    fi
}

# 等待端口被成功监听(超时30秒)
wait_for_port() {
    local timeout=30
    local count=0
    log "INFO: 等待服务在端口 ${PORT} 上启动..."
    echo "等待服务启动..."
    while [ $count -lt $timeout ]; do
        if ss -ltnp | grep -q ":${PORT} "; then
            PID=$(ss -ltnp | grep ":${PORT} " | awk '{print $6}' | cut -d= -f2 | cut -d, -f1)
            log "INFO: 服务已成功在端口 ${PORT} 上启动,PID: ${PID}。"
            echo "服务启动成功,PID: ${PID}。"
            return 0
        fi
        sleep 1
        ((count++))
    done
    log "ERROR: 等待 ${timeout} 秒后,端口 ${PORT} 仍未监听,服务可能启动失败。"
    echo "服务启动超时,请检查日志。"
    return 1
}

case "$1" in
    "check")
        check_port
        ;;
    "wait")
        wait_for_port
        ;;
    *)
        echo "用法: $0 {check|wait}"
        exit 1
        ;;
esac

给脚本执行权限:chmod +x /opt/qwen3527-27b/check_port.sh

4.2 集成到启动流程中

有两种方式集成:

  1. 在Supervisor启动前手动检查:在重启服务前,先运行 ./check_port.sh check
  2. 在应用启动脚本中检查:修改 app_with_logging.py,在启动Uvicorn前调用端口检查逻辑(可以通过调用上述shell脚本或使用Python的 socket 库实现)。

这里演示第二种,更自动化。在 app_with_logging.py__main__ 部分加入检查:

# 在 app_with_logging.py 的 if __name__ == "__main__": 部分加入
import socket
import sys

def is_port_in_use(port: int) -> bool:
    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
        try:
            s.bind(('0.0.0.0', port))
            return False
        except OSError:
            logger.error(f"端口 {port} 已被占用,无法启动服务。")
            return True

if __name__ == "__main__":
    port = 7860
    if is_port_in_use(port):
        logger.error("服务启动失败,端口被占用。")
        sys.exit(1) # 非正常退出,Supervisor会检测到并尝试重启

    host = "0.0.0.0"
    logger.info(f"启动Uvicorn服务器,监听 {host}:{port}")
    uvicorn.run(app, host=host, port=port, log_config=None)

这样,如果端口被占,服务会直接启动失败并记录错误日志,由Supervisor接管后续的重启策略(如果配置了的话)。更完善的方案是在启动后,调用 wait_for_port 逻辑来确认服务确实成功监听了端口。

5. 第三步:构建多层次服务健康检查

健康检查是运维的“眼睛”。一个 /health 端点是最基础的,但还不够。我们需要一个综合的健康检查脚本,定期运行,并检查多个维度。

5.1 创建综合健康检查脚本

#!/bin/bash
# /opt/qwen3527-27b/health_check.sh

SERVICE_NAME="qwen3527"
PORT=7860
HEALTH_URL="http://127.0.0.1:${PORT}/health"
LOG_FILE="/root/workspace/logs/qwen3527/health_check.log"
STATUS_FILE="/root/workspace/logs/qwen3527/status.json"

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

# 初始化状态
overall_status="healthy"
message="所有检查通过"
details=()

# 1. 检查Supervisor进程状态
log "开始健康检查..."
if supervisorctl status $SERVICE_NAME | grep -q "RUNNING"; then
    details+=("进程状态: RUNNING")
else
    overall_status="unhealthy"
    message="Supervisor报告服务未运行"
    details+=("进程状态: STOPPED 或 ERROR")
    log "ERROR: 服务进程状态异常。"
fi

# 2. 检查端口监听
if ss -ltnp | grep -q ":${PORT} "; then
    details+=("端口监听: 正常")
else
    overall_status="unhealthy"
    message="服务端口未监听"
    details+=("端口监听: 失败")
    log "ERROR: 端口 ${PORT} 未监听。"
fi

# 3. 检查HTTP健康端点
http_code=$(curl -s -o /dev/null -w "%{http_code}" -m 5 $HEALTH_URL)
if [ "$http_code" = "200" ]; then
    details+=("HTTP端点: 正常 (${http_code})")
else
    overall_status="unhealthy"
    message="健康端点响应异常"
    details+=("HTTP端点: 异常 (${http_code})")
    log "ERROR: 健康端点返回 HTTP ${http_code}。"
fi

# 4. 检查GPU内存使用(可选,需要nvidia-smi)
if command -v nvidia-smi &> /dev/null; then
    gpu_util=$(nvidia-smi --query-gpu=utilization.gpu --format=csv,noheader,nounits | head -n 1)
    gpu_mem=$(nvidia-smi --query-gpu=memory.used --format=csv,noheader,nounits | head -n 1)
    details+=("GPU利用率: ${gpu_util}%")
    details+=("GPU显存: ${gpu_mem} MiB")
    # 可以设置阈值告警
    if [ "${gpu_util%.*}" -gt 95 ]; then
        log "WARNING: GPU利用率过高: ${gpu_util}%"
    fi
fi

# 5. 检查系统负载(可选)
load_avg=$(cat /proc/loadavg | awk '{print $1}')
details+=("系统负载(1min): ${load_avg}")

# 生成状态报告
report=$(cat <<EOF
{
  "status": "${overall_status}",
  "message": "${message}",
  "timestamp": "$(date -Iseconds)",
  "details": $(printf '%s\n' "${details[@]}" | jq -R -s -c 'split("\n") | map(select(. != ""))')
}
EOF
)

# 输出到状态文件
echo $report | jq . > $STATUS_FILE
log "健康检查完成,状态: ${overall_status}"

# 如果状态不健康,可以触发告警(例如发送邮件、Webhook)
if [ "$overall_status" != "healthy" ]; then
    log "触发告警: $message"
    # 这里可以添加告警逻辑,例如:
    # curl -X POST -H 'Content-Type: application/json' -d "$report" YOUR_ALERT_WEBHOOK_URL
fi

# 打印最终状态
echo $report | jq .

给脚本执行权限chmod +x /opt/qwen3527-27b/health_check.sh 安装jq(用于JSON处理):apt-get update && apt-get install -y jqyum install -y jq

这个脚本做了以下几件事:

  1. 检查进程:通过Supervisor确认服务进程是否在运行。
  2. 检查端口:确认服务是否成功绑定了端口。
  3. 检查应用:通过调用 /health API,确认应用内部逻辑是否正常。
  4. 检查资源:(可选)监控GPU和系统负载,预防资源瓶颈。
  5. 输出报告:将检查结果生成一个结构化的JSON文件 (status.json)。
  6. 触发告警:当状态异常时,可以扩展脚本发送告警通知。

5.2 配置定时健康检查

我们可以使用Linux自带的 cron 服务,让这个健康检查脚本每分钟运行一次。

编辑crontab:crontab -e 添加一行:

* * * * * /bin/bash /opt/qwen3527-27b/health_check.sh >> /root/workspace/logs/qwen3527/cron.log 2>&1

现在,每分钟都会执行一次健康检查。你可以随时查看最新的状态:

cat /root/workspace/logs/qwen3527/status.json | jq .

5.3 增强健康检查API

我们之前只在 app_with_logging.py 里写了一个简单的 /health 端点。现在可以把它增强,让它也返回更丰富的内部状态。

# 在 app_with_logging.py 的 FastAPI app 中添加
import psutil
import torch

@app.get("/health")
async def health_check():
    """增强的健康检查端点"""
    status = {"status": "healthy"}
    details = {}

    # 1. 基础状态
    details["service"] = "Qwen3.5-27B API"
    details["timestamp"] = datetime.now().isoformat()

    # 2. 检查模型是否加载(这里需要根据实际代码调整)
    # 假设有一个全局变量 `model` 和 `tokenizer`
    try:
        # 这是一个示例检查,实际可能是检查model是否不为None
        # if model is not None and tokenizer is not None:
        #     details["model_loaded"] = True
        # else:
        #     details["model_loaded"] = False
        #     status["status"] = "degraded"
        details["model_loaded"] = True # 假设正常
    except Exception as e:
        details["model_loaded"] = False
        details["model_error"] = str(e)
        status["status"] = "unhealthy"

    # 3. 检查GPU(如果可用)
    if torch.cuda.is_available():
        details["gpu_available"] = True
        details["gpu_device_count"] = torch.cuda.device_count()
        try:
            gpu_mem_alloc = torch.cuda.memory_allocated(0) / 1024**3 # 转成GB
            gpu_mem_cached = torch.cuda.memory_reserved(0) / 1024**3
            details["gpu_memory_allocated_gb"] = round(gpu_mem_alloc, 2)
            details["gpu_memory_cached_gb"] = round(gpu_mem_cached, 2)
        except Exception as e:
            details["gpu_memory_error"] = str(e)
    else:
        details["gpu_available"] = False

    # 4. 检查进程内存
    process = psutil.Process()
    details["process_memory_rss_gb"] = round(process.memory_info().rss / 1024**3, 2)
    details["process_cpu_percent"] = process.cpu_percent(interval=0.1)

    status["details"] = details
    logger.info(f"健康检查被调用,状态: {status['status']}")
    return status

现在,访问 https://你的域名:7860/health,你会得到一个包含模型状态、GPU内存、进程内存等详细信息的JSON响应。外部监控系统(如Prometheus, Zabbix)可以轻松地抓取这个端点进行监控。

6. 总结:打造可靠的生产级服务

通过以上三步,我们为千问3.5-27B模型服务搭建了一个初步的、面向生产环境的运维框架:

  1. 日志分级管理:我们告别了混乱的单一日志文件,拥有了按日期和级别(INFO, ERROR)清晰归档的日志系统。排查问题时,你可以快速定位到错误日志文件,而不是在海量信息中挣扎。
  2. 端口监听保障:通过启动前的端口冲突检查和应用内的双重确认,服务因端口问题而启动失败的概率大大降低。即使失败,也会有明确的错误日志告诉我们原因。
  3. 多层次健康检查
    • 内部自检:增强的 /health API 提供了服务深度的健康状态。
    • 外部巡检:独立的 health_check.sh 脚本作为外部看门狗,定时从进程、端口、HTTP、资源等多个维度检查服务,并生成状态报告和触发告警。

把这些组合起来,你的模型服务就具备了基本的“可观测性”和“自愈能力”。当服务出现异常时,你能通过日志和健康状态报告快速定位问题;甚至可以通过配置Supervisor的自动重启和健康检查脚本的告警,在无人值守时也能得到通知并尝试恢复。

当然,这只是一个起点。在生产环境中,你还可以进一步考虑:

  • 将日志接入 ELK (Elasticsearch, Logstash, Kibana) 或 Loki 等日志聚合系统。
  • 使用 PrometheusGrafana 来收集和可视化 /health 端点以及系统的各项指标。
  • 将健康检查脚本的告警集成到钉钉、企业微信、Slack或PagerDuty等告警平台。

希望这篇指南能帮助你,让强大的千问3.5-27B模型,在一个稳定、可靠的环境中,为你和你的团队持续创造价值。


获取更多AI镜像

想探索更多AI镜像和应用场景?访问 CSDN星图镜像广场,提供丰富的预置镜像,覆盖大模型推理、图像生成、视频生成、模型微调等多个领域,支持一键部署。

Logo

欢迎加入DeepSeek 技术社区。在这里,你可以找到志同道合的朋友,共同探索AI技术的奥秘。

更多推荐