Flask+mod_wsgi部署Apache后SSH隧道启动时请求冻结问题
我之前处理过类似的mod_wsgi部署Flask应用时的阻塞问题,你的情况核心矛盾在于mod_wsgi的多进程/线程模型和SSH隧道启动的阻塞特性不兼容,再加上潜在的端口冲突问题,最终导致服务完全冻结、无日志输出。
为什么Werkzeug正常,Apache+mod_wsgi就挂了?
Werkzeug默认是单进程单线程运行,如果你在模块全局初始化Controler实例,a.start()的阻塞操作只会在启动时执行一次,完成后就能正常处理请求。但Apache+mod_wsgi默认是多进程模式(通常会启动多个子进程处理请求),每个子进程启动时都会重新执行模块级别的代码——也就是重新初始化Controler,而a.start()是阻塞调用,会导致子进程卡在隧道启动步骤,永远无法就绪处理请求。
另外你的代码里还有个隐藏坑:循环创建2个SSH隧道时,都绑定了本地5555端口,第二个隧道会因为端口被占用而启动失败,进一步加剧阻塞问题。
具体修复步骤
1. 改用非阻塞方式启动SSH隧道
sshtunnel库的open_tunnel返回的SSHTunnelForwarder对象提供了start_in_background()方法,会在后台线程启动隧道,不会阻塞进程初始化。同时要给每个隧道分配唯一的本地端口,避免冲突。修改Controler.py的__initConn方法:
import time # 记得导入time模块 class Controler(): ips = 2 # number of machines in the distributed service def __init__(self): self.con = []# ssh conn self.data = []# conn metadata self.__initConn() def __initConn(self): i = 0 config = SSHConfig() config.parse(open('./config')) for j in range(self.ips): i += 1 o = config.lookup('server' + str(i)) # 给每个隧道分配唯一本地端口,避免冲突 local_port = 5555 + j params = dict ( ssh_address=o['ip'], ssh_port=22, ssh_host_key=None, ssh_username=o['user'], ssh_password=o['password'], ssh_private_key=None, remote_bind_address=('127.0.0.1', local_port), ) a = open_tunnel(**params) # 后台启动隧道,不阻塞进程初始化 a.start_in_background() self.con.append(a) # 等待隧道激活,避免后续操作时隧道未就绪 while not a.is_active: time.sleep(0.1)
2. 延迟Controler初始化,避免多进程重复创建
不要在模块全局直接实例化Controler,而是用Flask的before_first_request钩子,确保每个进程只初始化一次隧道。修改init.py:
from flask import Flask, jsonify,make_response,abort,g,request,url_for from flask_httpauth import HTTPBasicAuth from flask_sqlalchemy import SQLAlchemy from flask_bson import accept_bson, bsonify import base64 from controler import Controler # 先声明全局变量,不立即初始化 ctrl = None app = Flask(__name__) # ... 其他配置代码(如数据库、认证配置) ... @app.before_first_request def init_controller(): global ctrl # 每个进程只初始化一次Controler if not ctrl: ctrl = Controler() @app.route('/api/register', methods=['POST']) def new_user(): try: username = request.json.get('username') password = request.json.get('password') except: abort(400) user = users.new_user(username,password) return (jsonify({'username': user.username,'uri':url_for('get_user', username=user.username, _external=True)}), 201) # ... 其他路由代码 ...
3. 调整mod_wsgi配置(可选)
如果你的应用不需要多进程带来的性能优势,可以在Apache的mod_wsgi配置里设置单进程模式,减少复杂度:
WSGIDaemonProcess your_app processes=1 threads=5 python-path=/path/to/your/app WSGIProcessGroup your_app
不过更推荐前面的修复方式,保持多进程的性能优势。
验证修复
部署修改后的代码,重启Apache服务,发起请求测试:
- 检查Apache的错误日志(通常在
/var/log/apache2/error.log),确认没有端口冲突或隧道启动错误 - 测试
/api/register等接口,确认请求不再冻结
内容的提问来源于stack exchange,提问作者Álvaro

