You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

为何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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.10 09:05:36