Loading... ## 前情提要 最近做了一个活动大屏项目,博主主要负责后端开发。因为活动大屏有实时增加用户的需求,所以需要创建websocket进行双向通信。 后端采用Python Flask开发,运行在linux服务器中,为了提高并发性能,使用gunicorn进行守护并使用`--workers=8`创建了8个worker。使用nginx进行反向代理,使用eventlet作为classworker并打上了补丁: ```python import eventlet eventlet.monkey_patch() ``` 使用Flask-socketio库搭配前端的socket.io实现前后端的信息交换(其实只是后端单向的向前端发送消息,本来可以使用长轮询,但是组长觉得服务器压力太大向想换成websocket(虽然socket.io本身是多种方式进行通讯的,包括长轮询和websocket))。 博主表 ## 问题描述 先说说主要表现吧: 前端会反复报错,控制台提示服务器返回400(`Failed to load resource: the server responded with a status of 400()`),或者提示在完成连接建立前Websocket被关闭`WebSocket is closed before the connection is established `。  偶尔会出现连接成功但是刷新页面后就无法连接,此时打开网络捕获应该会发现大量Get请求,返回200、400  并且sid相同的请求数量大概为3个。 其中,状态码为200的请求是socketio用于获取sid的。400请求返回的值类似`invalid session xxxxx`:  ## 问题排查过程 最开始觉得是nginx反代有问题,因为在本地测试环境跑是没有问题的,只有服务器上部署了nginx。防火墙之类的因为有正常200请求,直接排除。 后来觉得是前端的session处理不对,但是负责前端的哥们的代码和socket.io官网的示例基本一模一样,不会有错,因此也排除。 stackoverflow等等社区论坛上的内容看了一圈,基本都是在Node.js中用socket.io的,没有与flask搭配的,因此也没有参考性。 在仔细看socket.io官方文档的时候发现,文档中提到了出现类似表现[解决连接问题 | Socket.IO](https://socket.io/zh-CN/docs/v4/troubleshooting-connection-issues/#you-didnt-enable-sticky-sessions-in-a-multi-server-setup),但是这是在多个服务器负载均衡的情况下因为没有共享session出现的。考虑到gunicorn配置时使用了多个worker其中提到的是需要配置服务器粘性会话,于是我就去flask-socketio文档中查找了一下,找到了相关的内容:[Deployment — Flask-SocketIO documentation](https://flask-socketio.readthedocs.io/en/latest/deployment.html#using-multiple-workers),即,使用redis等中间件作为共享session的服务即可。 但是配置完成后问题并没有的到解决(崩溃!!!!) 于是我就崩溃的乱翻文档(什么奇怪的解压方式……) 解压方式奇怪不重要,重要的是给我翻到解决方案了! 就在上面链接对应的位置上方一点点[Deployment — Flask-SocketIO documentation](https://flask-socketio.readthedocs.io/en/latest/deployment.html#gunicorn-web-server),关于gunicorn部署的说明这里,小小的写了一个由于gunicorn负载均衡存在问题,应该避免使用多worker(所以示例文档都是1个worker)  找到原因所在,问题就非常好解决了 ## 解决方案 既然是因为gunicorn多worker导致的,就不用多worker了: 使用nginx进行反向代理和负载均衡,gunicorn专心守护进程即可(虽然感觉和直接python跑没有很大区别) 因此: 1. 将gunicron配置修改为只使用一个worker,即`-w 1`(可以有多个threading)并启动多个占有不同端口的进程提供服务 2. 在nginx反向代理配置中使用负载均衡: **http块**下添加: ```nginx upstream nodes{ ip_hash; server 127.0.0.1:45677; server 127.0.0.1:45676; server 127.0.0.1:45675; ## 添加所有你绑定的gunicorn服务端口 } ``` **sever块**下添加反向代理配置(可以根据需要删除一些配置,timeout通常会更长一些): ```nginx location /{ proxy_pass http://nodes; proxy_set_header Host $host; proxy_http_version 1.1; #proxy_cache_bypass $http_upgrade; proxy_ssl_server_name on; # Proxy headers proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection $connection_upgrade; proxy_set_header X-Real-IP $remote_addr; proxy_set_header Forwarded $proxy_add_forwarded; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-Host $host; proxy_set_header X-Forwarded-Port $server_port; # Proxy timeouts proxy_connect_timeout 60s; proxy_send_timeout 60s; proxy_read_timeout 60s; } location /socket.io { proxy_pass http://nodes/socket.io; proxy_buffering off; proxy_set_header Host $host;proxy_http_version 1.1; #proxy_cache_bypass $http_upgrade; proxy_ssl_server_name on; # Proxy headers proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection $connection_upgrade; proxy_set_header X-Real-IP $remote_addr; proxy_set_header Forwarded $proxy_add_forwarded; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-Host $host; proxy_set_header X-Forwarded-Port $server_port; # Proxy timeouts proxy_connect_timeout 60s; proxy_send_timeout 60s; proxy_read_timeout 60s; } ``` 可以自己写个基本自动启动并配置反向代理的端口,类似下图(由于闭源和保密,代码就不贴出来了~,实现不是很难,思路就是使用python脚本创建guniconrn进程,并在nginx配置文件里直接include一个由脚本生成的config来包括所有节点进行负载均衡的管理和以及热重启)  ## 反思 由于gunicorn需要在linux环境下再能正常使用,我的电脑windows环境是没有办法运行的,所以在本地跑的时候都是直接用python的开发环境(甚至还有debug=True)来跑的。最开始没有怀疑gunicorn的原因是,我之前几个python项目都是用gunicron来跑的,不管是flask框架还是tornado框架都是没有任何问题的,下意识的觉得gunicorn的配置不会有问题。主要问题还是测试环境与本地环境不完全相同以及过于依赖过去的经验。 ## 2025/04/21补: 注意注意!单worker多进程的时候也是需要部署redis的,需要在socketIO对象处增加message_queue参数,填写redis地址,同时需要安装redis模块,搭配使用。如果不用redis的话,会出现一个emit出错的问题。 具体来说就是:假如A连到的是进程1,B连接到的是进程2。如果没有redis的话,进程1emit的内容只有A能收到,B是收不到的,只有使用redis的频道功能才能确保A和B都能收到emit。 代码应该形如: ```python socketio = SocketIO( app, cors_allowed_origins='*', message_queue="redis://xxxxxxxx", channel="socketio" ) ``` 存在的一个问题是redis似乎不能设置密码,因为socketIO模块没有提供填写密码的地方……不过这在内网环境下都不算特别大的问题,我就没有管。如果是外网环境使用的就需要注意这里会产生的安全问题。 Last modification:April 21, 2025 © Allow specification reprint Support Appreciate the author AliPayWeChat Like 如果觉得我的文章对你有用,请随意赞赏