为何Gunicorn+Flask+Bokeh应用开启4个worker后无法运行?
多用户Bokeh应用在Gunicorn多Worker下的连接超时问题分析
问题背景
我正在使用gunicorn、flask和bokeh搭建多用户Bokeh应用,部署在带静态IP的AWS Debian服务器上。参考示例代码修改后,执行命令gunicorn3 -w 1 --bind 0.0.0.0:8000 wsgi:app可正常运行,但将worker数改为4,并在flaskapp.py中把绑定端口设为0后,浏览器控制台出现以下错误:
:34163/bkapp/autoload.js?bokeh-autoload-element=4755&bokeh-app-path=/bkapp&bokeh-absolute-url=http://35.XXX.XXX.153:34163/bkapp:1 Failed to load resource: net::ERR_CONNECTION_TIMED_OUT
相关代码
flaskapp.py
import asyncio from threading import Thread from flask import Flask, render_template from tornado.httpserver import HTTPServer from tornado.ioloop import IOLoop from bokeh.application import Application from bokeh.application.handlers import FunctionHandler from bokeh.embed import server_document from bokeh.layouts import column from bokeh.models import ColumnDataSource, Slider from bokeh.plotting import figure from bokeh.sampledata.sea_surface_temperature import sea_surface_temperature from bokeh.server.server import BaseServer from bokeh.server.tornado import BokehTornado from bokeh.server.util import bind_sockets from bokeh.themes import Theme app = Flask(__name__) def bkapp(doc): df = sea_surface_temperature.copy() source = ColumnDataSource(data=df) plot = figure(x_axis_type='datetime', y_range=(0, 25), y_axis_label='Temperature (Celsius)', title="Sea Surface Temperature at 43.18, -70.43") plot.line('time', 'temperature', source=source) def callback(attr, old, new): if new == 0: data = df else: data = df.rolling(f"{new}D").mean() source.data = ColumnDataSource.from_df(data) slider = Slider(start=0, end=30, value=0, step=1, title="Smoothing by N Days") slider.on_change('value', callback) doc.add_root(column(slider, plot)) doc.theme = Theme(filename="theme.yaml") bkapp = Application(FunctionHandler(bkapp)) sockets, port = bind_sockets("0.0.0.0", 0) @app.route('/', methods=['GET']) def bkapp_page(): script = server_document('http://35.XXX.XXX.153:%d/bkapp' % port) print("app port: %s", port) return render_template("embed.html", script=script, template="Flask") def bk_worker(): asyncio.set_event_loop(asyncio.new_event_loop()) bokeh_tornado = BokehTornado({'/bkapp': bkapp}, extra_websocket_origins=["*"]) bokeh_http = HTTPServer(bokeh_tornado) bokeh_http.add_sockets(sockets) server = BaseServer(IOLoop.current(), bokeh_tornado, bokeh_http) server.start() server.io_loop.start() t = Thread(target=bk_worker) t.daemon = True t.start()
wsgi.py
from flaskapp import app if __name__ == "__main__": app.run()
embed.html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> {{ bokeh_css }} {{ bokeh_js }} <link rel="stylesheet" href="./static/css/styles.css" type="text/css"/> <link rel="shortcut icon" href="./static/favicon/favicon.ico" type="image/x-icon"> </head> <body> <div class="head"> <a href="http://"><img src="./static/images/logo.png" width="93px" height="36px"/></a> </div> <div class="body"> {{ script|safe }} </div> </body> </html>
错误原因分析
- 多Worker重复初始化Bokeh服务:Gunicorn启动4个Worker时,每个Worker都会加载
flaskapp.py,导致每个Worker都执行bind_sockets("0.0.0.0", 0)随机分配端口,并启动独立的Bokeh Tornado线程。但浏览器只会获取第一个Worker生成的端口,后续请求被分配到其他Worker时,对应的Bokeh服务端口不匹配,且未对外暴露,引发连接超时。 - 端口未开放:
bind_sockets用0端口时系统随机分配的端口(如34163)未在AWS安全组或服务器防火墙中开放,浏览器无法访问该端口。 - 多Worker下Bokeh服务冲突:多个Worker各自启动Bokeh服务,会造成资源竞争和端口冲突,前端无法正确对应到每个Worker的Bokeh服务实例。
解决方法
1. 独立部署Bokeh服务(推荐)
将Bokeh服务与Flask分离,单独启动:
- 启动Bokeh服务,指定固定端口并允许Flask的访问来源:
bokeh serve --allow-websocket-origin=35.XXX.XXX.153:8000 --port=5006 /path/to/your/bkapp - 修改
flaskapp.py中server_document的地址为固定的Bokeh服务地址:script = server_document('http://35.XXX.XXX.153:5006/bkapp') - 这种方式下,无论Gunicorn启动多少Worker,前端都指向同一个Bokeh服务,避免端口混乱。
2. 限制Gunicorn为单Worker
若必须在Flask进程内嵌入Bokeh服务,Gunicorn只能使用1个Worker:
gunicorn3 -w 1 --bind 0.0.0.0:8000 wsgi:app
同时可将Bokeh服务端口设为固定值(如5006),并确保该端口在AWS安全组和防火墙中开放:
sockets, port = bind_sockets("0.0.0.0", 5006)
注意:多Worker模式下仍会重复启动Bokeh服务,引发端口冲突,因此不推荐。
3. 使用Nginx反向代理统一入口
配置Nginx将Flask和Bokeh的请求统一代理到同一IP下,避免跨端口问题:
server { listen 80; server_name 35.XXX.XXX.153; location / { proxy_pass http://127.0.0.1:8000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } location /bkapp { proxy_pass http://127.0.0.1:5006; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; } }
前端只需访问http://35.XXX.XXX.153/,Nginx会自动转发Flask和Bokeh的请求。
内容的提问来源于stack exchange,提问作者NikoD
相关产品推荐
相关产品推荐

