ResNet18部署教程:多并发请求处理方案
1. 背景与目标
在实际AI服务部署中,单次图像识别只是起点。面对真实业务场景——如智能相册分类、内容审核系统或边缘设备联动——高并发、低延迟的批量请求处理能力才是关键挑战。
本文聚焦于ResNet-18 官方稳定版镜像的工程化落地,基于 PyTorch + TorchVision 构建的通用物体识别服务,进一步实现多用户并发上传、异步推理、资源隔离与性能优化的完整解决方案。我们将从零搭建一个支持 WebUI 交互和后端高并发处理的 Flask 服务,并深入探讨 CPU 环境下的最佳实践。
💡 本教程适用于: - AI 应用开发者 - 模型部署工程师 - 边缘计算/私有化部署项目负责人
2. 系统架构设计
2.1 整体架构概览
我们采用“前后端分离 + 异步任务队列”的轻量级架构,确保 WebUI 响应流畅的同时,后台能高效处理多个推理请求。
[用户浏览器] ↓ (HTTP POST 图片) [Flask Web Server] ←→ [Redis Queue] ↓ [Worker Pool: 多进程推理] ↓ [TorchVision ResNet-18 (CPU)] ↓ [返回 Top-3 分类结果]该架构具备以下优势:
- 解耦请求与执行:前端接收请求后立即响应,避免阻塞
- 横向扩展性强:可通过增加 Worker 数量提升吞吐
- 资源可控:限制最大并发数,防止 CPU 过载
- 容错性好:异常请求不影响整体服务稳定性
2.2 核心组件说明
| 组件 | 作用 |
|---|---|
| Flask | 提供 REST API 和 WebUI 页面渲染 |
| Redis | 作为轻量级消息队列,暂存待处理任务 |
| RQ (Redis Queue) | Python 任务队列库,管理异步任务分发 |
| TorchVision.models.resnet18() | 加载预训练 ResNet-18 模型 |
| Joblib/Pickle | 序列化模型输出结果 |
3. 实践部署步骤
3.1 环境准备
假设你已通过 CSDN 星图平台获取resnet18-cpu-stable镜像并启动容器,接下来进行本地开发调试或二次封装。
# 进入容器环境 docker exec -it <container_id> /bin/bash # 安装必要依赖(若未预装) pip install redis rq flask gunicorn pillow⚠️ 注意:生产环境中建议使用
gunicorn + gevent替代默认 Flask 开发服务器。
3.2 文件结构规划
/resnet18-deploy/ ├── app.py # Flask 主程序 ├── worker.py # RQ 工人进程 ├── model_loader.py # 模型加载与推理逻辑 ├── static/ │ └── style.css ├── templates/ │ └── index.html # WebUI 页面 └── requirements.txt3.3 模型加载与推理封装(model_loader.py)
# model_loader.py import torch import torchvision.transforms as T from PIL import Image import json # 预定义 ImageNet 类别标签(可从官方下载 synset.json) with open("imagenet_classes.json") as f: CLASS_NAMES = json.load(f) # 初始化模型 def load_model(): model = torch.hub.load('pytorch/vision:v0.10.0', 'resnet18', pretrained=True) model.eval() # 切换为推理模式 return model # 推理函数 def predict_image(image_path, model): input_image = Image.open(image_path).convert("RGB") preprocess = T.Compose([ T.Resize(256), T.CenterCrop(224), T.ToTensor(), T.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]), ]) input_tensor = preprocess(input_image) input_batch = input_tensor.unsqueeze(0) # 添加 batch 维度 with torch.no_grad(): output = model(input_batch) probabilities = torch.nn.functional.softmax(output[0], dim=0) top_probs, top_indices = torch.topk(probabilities, 3) results = [] for i in range(3): idx = top_indices[i].item() label = CLASS_NAMES[idx] score = round(top_probs[i].item(), 4) results.append({"label": label, "score": score}) return results✅亮点解析: - 使用
torch.hub.load直接调用 TorchVision 官方模型,保证一致性 - 添加torch.no_grad()减少内存开销 - Normalize 参数严格匹配 ImageNet 训练配置
3.4 后端服务实现(app.py)
# app.py from flask import Flask, request, render_template, jsonify, redirect, url_for import os import uuid from rq import Queue from redis import Redis from werkzeug.utils import secure_filename import joblib from model_loader import load_model, predict_image # 初始化应用 app = Flask(__name__) app.config['UPLOAD_FOLDER'] = './uploads' os.makedirs(app.config['UPLOAD_FOLDER'], exist_ok=True) # 连接 Redis 并创建任务队列 redis_conn = Redis(host='localhost', port=6379) q = Queue('resnet18_tasks', connection=redis_conn) # 全局加载模型(共享于所有 Worker) model = load_model() @app.route('/') def index(): return render_template('index.html') @app.route('/predict', methods=['POST']) def predict(): if 'file' not in request.files: return jsonify({"error": "No file uploaded"}), 400 file = request.files['file'] if file.filename == '': return jsonify({"error": "Empty filename"}), 400 # 保存上传文件 filename = secure_filename(file.filename) filepath = os.path.join(app.config['UPLOAD_FOLDER'], filename) file.save(filepath) # 生成唯一任务ID job_id = str(uuid.uuid4()) # 提交异步任务 job = q.enqueue_call( func=predict_image, args=(filepath, model), result_ttl=300, job_id=job_id ) return redirect(url_for('result', job_id=job_id)) @app.route('/result/<job_id>') def result(job_id): job = q.fetch_job(job_id) if job is None: return jsonify({"error": "Job not found"}), 404 if job.is_finished: return render_template('result.html', result=job.result, img_url=request.args.get('img')) else: return '<p>正在识别中,请稍候...</p><meta http-equiv="refresh" content="2;url=' + url_for('result', job_id=job_id) + '">' if __name__ == '__main__': app.run(host='0.0.0.0', port=5000, threaded=True)🔍关键点说明: - 使用
uuid生成唯一任务 ID,便于追踪 -result_ttl=300设置结果缓存时间(秒),避免 Redis 内存泄漏 - 页面自动刷新机制实现简单轮询
3.5 启动工人进程(worker.py)
# worker.py import os os.environ.setdefault("REDIS_URL", "redis://localhost:6379") import redis from rq import Worker, Queue, Connection listen = ['resnet18_tasks'] redis_url = os.getenv('REDIS_URL') conn = redis.from_url(redis_url) if __name__ == '__main__': with Connection(conn): worker = Worker(list(map(Queue, listen))) worker.work(with_scheduler=True)启动命令:
python worker.py📌 建议:生产环境使用
supervisord或systemd管理 worker 进程。
4. 性能优化与并发控制
4.1 CPU 推理加速技巧
尽管 ResNet-18 本身较轻量,但在多并发下仍需优化:
启用 TorchScript 编译(提升 15-20%)
# 将模型转为 ScriptModule example_input = torch.rand(1, 3, 224, 224) traced_model = torch.jit.trace(model, example_input) traced_model.save("resnet18_traced.pt")后续加载改为:
model = torch.jit.load("resnet18_traced.pt")使用 ONNX Runtime(可选)
将 PyTorch 模型导出为 ONNX 格式,在 CPU 上获得更高推理效率:
torch.onnx.export(model, example_input, "resnet18.onnx")然后使用onnxruntime加载运行。
4.2 并发数控制策略
为防止 CPU 资源耗尽,建议设置最大 worker 数量:
# 启动多个 worker(建议 = CPU 核心数) rqworker resnet18_tasks --worker-class=gevent --jobs=4或使用线程池限制:
from concurrent.futures import ThreadPoolExecutor executor = ThreadPoolExecutor(max_workers=2) # 最大同时处理2个任务4.3 请求限流(防刷机制)
添加基础限流中间件:
from functools import wraps from time import time REQUEST_LOG = {} def rate_limit(max_per_minute=10): def decorator(f): @wraps(f) def wrapped(*args, **kwargs): client_ip = request.remote_addr now = time() if client_ip not in REQUEST_LOG: REQUEST_LOG[client_ip] = [] # 清理超过1分钟的记录 REQUEST_LOG[client_ip] = [t for t in REQUEST_LOG[client_ip] if now - t < 60] if len(REQUEST_LOG[client_ip]) >= max_per_minute: return "Too many requests", 429 REQUEST_LOG[client_ip].append(now) return f(*args, **kwargs) return wrapped return decorator # 使用方式 @app.route('/predict', methods=['POST']) @rate_limit(max_per_minute=5) def predict(): ...5. WebUI 设计与用户体验
5.1 前端页面(templates/index.html)
<!DOCTYPE html> <html> <head> <title>👁️ AI万物识别 - ResNet-18</title> <link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}"> </head> <body> <div class="container"> <h1>📷 AI 万物识别</h1> <p>上传任意图片,系统将自动识别最可能的3个类别</p> <form method="POST" action="/predict" enctype="multipart/form-data"> <input type="file" name="file" accept="image/*" required> <button type="submit">🔍 开始识别</button> </form> </div> </body> </html>5.2 结果展示页(result.html)
<div class="result"> <h2>✅ 识别完成</h2> <ul> {% for item in result %} <li><strong>{{ item.label }}</strong>: {{ '%.2f'|format(item.score * 100) }}%</li> {% endfor %} </ul> <a href="/">← 返回上传</a> </div>✅ 支持中文标签转换(需映射
imagenet_classes.json中文名)
6. 总结
6.1 核心价值回顾
本文围绕ResNet-18 官方稳定版镜像,构建了一套完整的高并发图像分类服务方案,实现了:
- ✅原生 TorchVision 模型集成:杜绝权限问题,稳定性满分
- ✅异步任务队列处理:支持多用户并发上传不卡顿
- ✅毫秒级 CPU 推理响应:40MB 小模型 + TorchScript 加速
- ✅可视化 WebUI 交互:上传 → 分析 → 展示闭环体验
- ✅可扩展架构设计:易于迁移到 Docker/Kubernetes 生产环境
6.2 最佳实践建议
- 控制并发规模:根据 CPU 核心数合理配置 worker 数量(通常 ≤ 核心数)
- 启用模型缓存:避免重复加载,减少内存抖动
- 定期清理临时文件:设置定时任务删除
uploads/目录旧文件 - 监控 Redis 内存使用:防止任务积压导致 OOM
- 考虑 CDN 加速静态资源:提升 WebUI 加载速度
💡获取更多AI镜像
想探索更多AI镜像和应用场景?访问 CSDN星图镜像广场,提供丰富的预置镜像,覆盖大模型推理、图像生成、视频生成、模型微调等多个领域,支持一键部署。