运行Bokeh服务器

目的

Bokeh服务器的目的是让Python用户更容易创建交互式web应用程序,这些应用程序可以将前端UI事件与实际运行的Python代码连接起来。

Bokeh的体系结构使得高级“模型对象”(表示绘图、范围、轴、字形等)都是用Python创建的,然后转换为客户端库BokehJS使用的JSON格式。(见 定义关键概念 就其本身而言,这种灵活和解耦的设计提供了优势。例如,很容易让其他语言(R、Scala、Lua,…)在浏览器中驱动完全相同的Bokeh图和可视化。

但是,如果可以使Python和浏览器中的“模型对象”彼此保持同步,则会立即打开更多更强大的可能性:

  • 使用Python的全部功能通过计算或查询响应浏览器中生成的UI和工具事件

  • 自动将服务器端更新推送到UI(即浏览器中的小部件或绘图)

  • 使用定期、超时和异步回调来驱动流式更新

这种在Python和浏览器之间同步的功能是Bokeh服务器的主要目的。


下面的简单示例 demo.bokeh.org ,说明了这一点。

当控件被操纵时,它们的新值在Bokeh服务器中自动同步。将触发回调,同时更新服务器中绘图的数据。这些更改将自动同步回浏览器,并更新打印。

用例场景

既然我们知道了Bokeh服务器的用途,以及它的功能,那么当您可能想要使用Bokeh服务器时,有必要考虑一些不同的场景。

本地或个人使用

您可能希望使用Bokeh服务器的一种方法是在探索性数据分析期间,可能在Jupyter笔记本中使用。或者,您可能希望创建一个可以在本地运行的小应用程序,也可以发送给同事在本地运行。Bokeh服务器在这种情况下非常有用,并且易于使用。以下两种方法均可有效使用:

对于能够最直接地过渡到可部署应用程序的最灵活的方法,建议遵循中的技术 构建Bokeh应用程序 .

创建可部署应用程序

您可能希望使用Bokeh服务器的另一种方法是发布交互式数据可视化和应用程序,以供更广泛的用户查看和使用(可能在internet上,也可能在公司内部网络上)。Bokeh服务器也非常适合这种用法,您首先需要参考以下章节:

共享发布

上述两种情况都涉及到 单一创建者 在服务器上制作应用程序,要么供本地使用,要么供更大的用户使用。另一种情况是 几个创作者 所有人都想将不同的应用程序发布到同一个服务器上。 This is not a good use-case for a single Bokeh server. 因为有可能创建执行任意Python代码的应用程序,进程隔离和安全问题使得这种共享租赁成为禁止。

为了支持这种多创建者、多应用程序的环境,一种方法是建立基础设施,使其能够根据需要运行任意数量的Bokeh服务器,无论是在每个应用程序上,还是至少在每个用户的基础上。我们有可能创建一个公共服务,以便在将来实现这种使用,而且第三方当然也有可能构建自己的私有基础设施,但这超出了本用户指南的范围。

另一种可能性是拥有一个单一的集中创建的应用程序(可能由一个组织创建),它可以访问由许多不同的人发布的数据或其他工件(可能具有访问控制)。这种情况 is Bokeh服务器是可能的,但通常需要将Bokeh服务器与其他web应用程序框架集成。

构建Bokeh应用程序

到目前为止,使用Bokeh服务器创建交互式数据可视化的最灵活的方法是创建Bokeh应用程序,并使用 bokeh serve 命令。在这种情况下,Bokeh服务器使用应用程序代码为所有连接的浏览器创建会话和文档:

../../_images/bokeh_serve.svg

Bokeh服务器(左)使用应用程序代码创建Bokeh文档。浏览器(右)的每个新连接都会导致Bokeh服务器为该会话创建一个新文档。

每次建立新连接时,应用程序代码都在Bokeh服务器中执行,以创建新的Bokeh Document 将同步到浏览器。应用程序代码还设置了当控件值等属性发生更改时应该运行的任何回调。

提供应用程序代码有几种不同的方法。

单模块格式

让我们再看一个完整的示例,然后更详细地检查一些特定部分:

# myapp.py

from random import random

from bokeh.layouts import column
from bokeh.models import Button
from bokeh.palettes import RdYlBu3
from bokeh.plotting import figure, curdoc

# create a plot and style its properties
p = figure(x_range=(0, 100), y_range=(0, 100), toolbar_location=None)
p.border_fill_color = 'black'
p.background_fill_color = 'black'
p.outline_line_color = None
p.grid.grid_line_color = None

# add a text renderer to our plot (no data yet)
r = p.text(x=[], y=[], text=[], text_color=[], text_font_size="26px",
           text_baseline="middle", text_align="center")

i = 0

ds = r.data_source

# create a callback that will add a number in a random location
def callback():
    global i

    # BEST PRACTICE --- update .data in one step with a new dict
    new_data = dict()
    new_data['x'] = ds.data['x'] + [random()*70 + 15]
    new_data['y'] = ds.data['y'] + [random()*70 + 15]
    new_data['text_color'] = ds.data['text_color'] + [RdYlBu3[i%3]]
    new_data['text'] = ds.data['text'] + [str(i)]
    ds.data = new_data

    i = i + 1

# add a button widget and configure with the call back
button = Button(label="Press Me")
button.on_click(callback)

# put the button and plot in a layout and add to the document
curdoc().add_root(column(button, p))

请注意,我们没有在代码中的任何地方指定输出或连接方法。它是一个创建和更新对象的简单脚本。的灵活性 bokeh 命令行工具意味着我们可以将输出选项推迟到最后。我们可以,例如,跑步 bokeh json myapp.py 获取应用程序的JSON序列化版本。但在本例中,我们希望在Bokeh服务器上运行应用程序,因此我们执行:

bokeh serve --show myapp.py

这个 --show 选项将使浏览器自动打开一个新的标签页到正在运行的应用程序的地址,在本例中为:

http://localhost:5006/myapp

如果只有一个应用程序,服务器根目录将重定向到它。否则,您可以在服务器根目录下看到所有正在运行的应用程序的索引:

http://localhost:5006/

可以使用 --disable-index 选项,则可以使用 --disable-index-redirect 选择权。

除了从单个python文件创建Bokeh应用程序外,还可以从目录创建应用程序。

目录格式

Bokeh应用程序也可以通过创建文件系统目录并用适当的文件填充来创建。在目录中启动目录应用程序 myapp 执行 bokeh serve 使用目录的名称,例如:

bokeh serve --show myapp

目录至少必须包含 main.py 它为Bokeh服务器构造一个文档以提供服务:

myapp
   |
   +---main.py

Bokeh server知道的完整文件集是:

myapp
   |
   +---__init__.py
   +---app_hooks.py
   +---main.py
   +---request_handler.py
   +---static
   +---theme.yaml
   +---templates
        +---index.html

可选组件包括

  • __init__.py 将此目录标记为包的文件。包装相关进口,例如。 from . import mymodfrom .mymod import func 有可能。

  • A request_handler.py 文件,该文件允许声明一个可选函数,该函数处理HTTP请求并返回要包含在会话令牌中的项的字典,如中所述 请求处理程序挂钩 .

  • A app_hooks.py 允许在应用程序执行的不同阶段触发可选回调的文件,如中所述 生命周期挂钩请求处理程序挂钩 .

  • A static 可用于服务与此应用程序关联的静态资源的子目录。

  • A theme.yaml 声明性定义应用于Bokeh模型类型的默认属性的文件。

  • A templates 子目录 index.html Jinja模板文件。该目录可能包含附加的Jinja模板 index.html 参考。模板的参数应与 FILE 模板。看到了吗 自定义应用程序的Jinja模板 了解更多详细信息。

在执行 main.py ,Bokeh服务器确保 __file__ module属性的工作方式与您预期的一样。因此,可以在目录中随意包含数据文件或自定义用户定义模型。

此外,还将应用程序目录添加到 sys.path 这样就可以方便地导入应用程序目录中的Python模块。但是,如果 __init__.py 存在于目录中,应用程序可用作包,标准包相关导入也可以工作。

例如:

myapp
   |
   +---__init__.py
   |
   +---app_hooks.py
   +---data
   |    +---things.csv
   |
   +---helpers.py
   +---main.py
   |---models
   |    +---custom.js
   |
   +---request_handler.py
   +---static
   |    +---css
   |    |    +---special.css
   |    |
   |    +---images
   |    |    +---foo.png
   |    |    +---bar.png
   |    |
   |    +---js
   |        +---special.js
   |
   |---templates
   |    +---index.html
   |
   +---theme.yaml

在这种情况下,您的代码可能类似于:

from os.path import dirname, join
from .helpers import load_data

load_data(join(dirname(__file__), 'data', 'things.csv'))

以及从中加载自定义模型的JavaScript实现的类似代码 models/custom.js

自定义应用程序的Jinja模板

如上所述 目录格式 ,您可以重写Bokeh服务器使用的默认Jinja模板来生成为用户浏览器提供服务的HTML代码。

这就为使用CSS在客户端浏览器中管理应用程序的布局提供了可能,同时还可以使用BokehJS之外的其他Javascript库。

Jinja Project Documentation 有关Jinja模板如何工作的更多详细信息。

在模板中嵌入图形

在Bokeh应用程序的主线程中,即。 main.py ,模板代码中要引用的任何Bokeh图都需要 name 属性集并添加到当前文档根目录。

from bokeh.plotting import curdoc

# templates can refer to a configured name value
plot = figure(name="bokeh_jinja_figure")

curdoc().add_root(plot)

然后,在对应的Jinja模板代码中,可以通过 roots 模板参数,使用图的 name ,即

{% extends base %}

{% block contents %}
<div>
    {{ embed(roots.bokeh_jinja_figure) }}
</div>
{% endblock %}

定义自定义变量

自定义变量可以通过 curdoc().template_variables 字典就位:

# set a new single key/value
curdoc().template_variables["user_id"] = user_id

# or update multiple at once
curdoc().template_variables.update(first_name="Mary", last_name="Jones")

然后,在对应的Jinja模板代码中,可以直接引用变量:

{% extends base %}

{% block contents %}
<div>
    <p> Hello {{ user_id }}, AKA '{{ last_name }}, {{ first_name }}'! </p>
</div>
{% endblock %}

访问HTTP请求

当为Bokeh应用程序创建会话时,会话上下文可用作 curdoc().session_context . 会话上下文最有用的功能是将Tornado HTTP请求对象作为 session_context.request . 由于使用的不兼容问题 --num-procs ,HTTP请求不直接可用。相反,只有 arguments 属性是完全可用的,并且只有 cookiesheaders 允许 --include-headers--exclude-headers--include-cookies--exclude-cookies 提供。正在尝试访问上的任何其他属性 request 将导致错误。

如中所述,可以访问请求的任何其他属性 请求处理程序挂钩 .

例如,下面的代码将访问请求 arguments 为变量设置值 N (可能控制一个图中的点数):

# request.arguments is a dict that maps argument names to lists of strings,
# e.g, the query string ?N=10 will result in {'N': [b'10']}

args = curdoc().session_context.request.arguments

try:
  N = int(args.get('N')[0])
except:
  N = 200

警告

提供请求对象,以便 arguments 易于检查。调用任何龙卷风方法,例如 finish() 或者直接写信给 request.connection 不受支持,将导致未定义的行为。

请求处理程序挂钩

由于不能保证完整的Tornado HTTP请求在服务于会话的进程上可用,因此可以定义一个自定义处理程序来提供附加信息。

要定义此类钩子,必须在中创建应用程序 目录格式 ,并包含一个名为 request_handler.py 在目录中。在这个文件中,必须包含一个按惯例命名的 process_request 功能:

def process_request(request):
    ''' If present this function is called when the HTTP request arrives. '''
    return {}

处理程序收到了Tornado HTTP请求,可以处理该请求并返回一个字典,该字典将在 curdoc().session_context.token_payload . 在这种情况下,可以提供一些附加的信息 --num-procs 使用。

回调和事件

在进入回调和事件之前,特别是在Bokeh服务器的上下文中,有必要讨论一般回调的不同用例。

浏览器中的JavaScript回调

无论是否涉及Bokeh服务器,都可以使用 CustomJS 以及其他方法。看到了吗 JavaScript回调 更详细的信息和例子。

需要注意的是 当使用CustomJS回调时,不会执行任何Python代码 . 即使将回调作为Python代码提供以转换为JavaScript,也是如此。A CustomJS 回调只在浏览器的JavaScript解释器中执行,因此只能直接与JavaScript数据和函数(例如BokehJS模型)交互。

带有Jupyter交互器的Python回调

如果您在Jupyter笔记本中工作,那么可以使用Jupyter交互器来自动快速创建简单的GUI表单。对GUI中小部件的更新可以触发在Jupyter python内核中执行的python回调函数。调用这些回调通常很有用 push_notebook() 将更新推送到显示的绘图。有关详细信息,请参阅 Jupyter交互器 .

注解

目前可以使用 push_notebook() . 要添加双向通信(例如,让范围或选择更新触发Python回调),请在笔记本中嵌入Bokeh服务器。看到了吗 examples/howto/server_embed/notebook_embed.ipynb

从线程更新

如果应用程序需要执行块计算,可以在单独的线程中执行该工作。但是,必须通过下一个tick回调来计划对文档的更新。回调将在Tornado事件循环的下一次迭代中尽快执行,并将自动获取必要的锁以安全地更新文档状态。

警告

对来自不同线程的文档执行的唯一安全操作是 add_next_tick_callback()remove_next_tick_callback()

必须强调的是,文档更新必须安排在“下一个tick回调”中。任何从另一个线程直接更新文档状态的用法,无论是通过调用其他文档方法还是通过设置Bokeh模型的属性,都有可能导致数据和协议损坏。

保存的本地副本也很重要 curdoc() 以便所有线程都可以访问同一个文档。下面的例子说明了这一点:

from functools import partial
from random import random
from threading import Thread
import time

from bokeh.models import ColumnDataSource
from bokeh.plotting import curdoc, figure

from tornado import gen

# this must only be modified from a Bokeh session callback
source = ColumnDataSource(data=dict(x=[0], y=[0]))

# This is important! Save curdoc() to make sure all threads
# see the same document.
doc = curdoc()

@gen.coroutine
def update(x, y):
    source.stream(dict(x=[x], y=[y]))

def blocking_task():
    while True:
        # do some blocking computation
        time.sleep(0.1)
        x, y = random(), random()

        # but update the document from callback
        doc.add_next_tick_callback(partial(update, x=x, y=y))

p = figure(x_range=[0, 1], y_range=[0,1])
l = p.circle(x='x', y='y', source=source)

doc.add_root(p)

thread = Thread(target=blocking_task)
thread.start()

要查看这个示例的实际操作,请将其保存到python文件中,例如。 testapp.py 然后执行

bokeh serve --show testapp.py

警告

在向文档添加下一个tick回调时,当前没有锁定。建议最多有一个线程向文档添加回调。计划在将来为回调方法添加更多细粒度的锁。

从未锁定的回调更新

通常,Bokeh会话回调递归地锁定文档,直到它们启动的所有未来工作完成为止。但是,您可能希望使用Tornado的回调来驱动阻塞计算 ThreadPoolExecutor 在异步回调中。这可以工作,但需要提供Bokeh without_document_lock() decorator来抑制正常的锁定行为。

与上面的线程示例一样, all actions that update document state must go through a next-tick callback .

下面的示例演示了一个应用程序,该应用程序通过生成在线程池执行器上运行的阻塞函数,从一个未锁定的Bokeh会话回调来驱动阻塞计算,并通过使用next tick回调进行更新。该示例还简单地从一个标准的锁定会话回调以不同的更新速率更新状态。

from functools import partial
import time

from concurrent.futures import ThreadPoolExecutor
from tornado import gen

from bokeh.document import without_document_lock
from bokeh.models import ColumnDataSource
from bokeh.plotting import curdoc, figure

source = ColumnDataSource(data=dict(x=[0], y=[0], color=["blue"]))

i = 0

doc = curdoc()

executor = ThreadPoolExecutor(max_workers=2)

def blocking_task(i):
    time.sleep(1)
    return i

# the unlocked callback uses this locked callback to safely update
@gen.coroutine
def locked_update(i):
    source.stream(dict(x=[source.data['x'][-1]+1], y=[i], color=["blue"]))

# this unlocked callback will not prevent other session callbacks from
# executing while it is in flight
@gen.coroutine
@without_document_lock
def unlocked_task():
    global i
    i += 1
    res = yield executor.submit(blocking_task, i)
    doc.add_next_tick_callback(partial(locked_update, i=res))

@gen.coroutine
def update():
    source.stream(dict(x=[source.data['x'][-1]+1], y=[i], color=["red"]))

p = figure(x_range=[0, 100], y_range=[0,20])
l = p.circle(x='x', y='y', color='color', source=source)

doc.add_periodic_callback(unlocked_task, 1000)
doc.add_periodic_callback(update, 200)
doc.add_root(p)

与前面一样,您可以通过保存到python文件并运行 bokeh serve 关于它。

生命周期挂钩

有时,希望代码在服务器或会话生命周期中的特定时间执行。例如,如果您在Django服务器旁边使用Bokeh服务器,则需要调用 django.setup() 在每个Bokeh服务器启动时,一次正确初始化Django以供Bokeh应用程序代码使用。

Bokeh通过一组 生命周期挂钩 . 要使用这些钩子,必须在中创建应用程序 目录格式 ,并包含一个名为 app_hooks.py 在目录中。在此文件中,您可以包含以下任何或所有常规命名函数:

def on_server_loaded(server_context):
    ''' If present, this function is called when the server first starts. '''
    pass

def on_server_unloaded(server_context):
    ''' If present, this function is called when the server shuts down. '''
    pass

def on_session_created(session_context):
    ''' If present, this function is called when a session is created. '''
    pass

def on_session_destroyed(session_context):
    ''' If present, this function is called when a session is closed. '''
    pass

此外, on_session_destroyed 生命周期钩子也可以直接在 Document 正在服侍。由于在用户关闭会话后进行清理的任务很常见,例如关闭数据库连接,因此这为执行此类操作提供了一个简单的路径,而无需绑定单独的文件。若要声明此类回调,请定义一个函数并将其注册到 Document.on_session_destroyed 方法:

doc = Document()

def cleanup_session(session_context):
    ''' This function is called when a session is closed. '''
    pass

doc.on_session_destroyed(cleanup_session)

除了上面的“生命周期”钩子之外,您还可以定义一个“请求钩子”来访问用户发出的HTTP请求。看到了吗 请求处理程序挂钩 详细信息。

将Bokeh服务器嵌入为库

将Bokeh服务器嵌入一个更大的Tornado应用程序或Jupyter笔记本中,并使用已经存在的Tornado,这会很有用 IOloop . 以下是在这种情况下如何整合Bokeh的基础:

from bokeh.server.server import Server

server = Server(
    bokeh_applications,  # list of Bokeh applications
    io_loop=loop,        # Tornado IOLoop
    **server_kwargs      # port, num_procs, etc.
)

# start timers and services and immediately return
server.start()

也可以创建和控制 IOLoop 直接。这对于创建独立的“普通”Python脚本(为Bokeh应用程序提供服务)或将Bokeh应用程序嵌入到Flask或Django之类的框架中时非常有用,而不必运行单独的Bokeh服务器进程。该技术的一些示例可以在examples目录中找到:

还要注意的是 bokeh serve 具有对应的关键字参数 Server . 例如,设置 --allow-websocket-origin 命令行参数等效于传递 allow_websocket_origin 作为参数。

连接 bokeh.client

还有一个客户机API用于直接与Bokeh服务器交互。客户机API可用于修改Bokeh服务器中现有会话中的Bokeh文档。

../../_images/bokeh_serve_client.svg

通常,web浏览器连接到Bokeh服务器,但是可以使用 bokeh.client 模块。

这可能很有用,例如,对由Flask或Django等其他web框架嵌入的Bokeh应用程序进行特定于用户的定制。下面是一个例子。在这种情况下,“滑块”示例是单独运行的,例如via bokeh serve sliders.py . Flask端点嵌入滑块应用程序,但会更改绘图标题 之前 传递给用户:

from flask import Flask, render_template

from bokeh.client import pull_session
from bokeh.embed import server_session

app = Flask(__name__)

@app.route('/', methods=['GET'])
def bkapp_page():

    with pull_session(url="http://localhost:5006/sliders") as session:

        # update or customize that session
        session.document.roots[0].children[1].title.text = "Special Sliders For A Specific User!"

        # generate a script to load the customized session
        script = server_session(session_id=session.id, url='http://localhost:5006/sliders')

        # use the script in the rendered page
        return render_template("embed.html", script=script, template="Flask")

if __name__ == '__main__':
    app.run(port=8080)

警告

可以使用 bokeh.client 在Bokeh服务器之外“从头开始”构建应用程序,包括通过对 session.loop_until_closed 在外部Python进程中使用 bokeh.client . 这种用法有许多固有的技术缺陷,应该被认为是不受支持的。

部署方案

有了我们正在开发的应用程序,我们可以随时在本地运行它与它交互。为了与其他能够安装所需Python堆栈的人共享它,我们可以共享应用程序并让他们以相同的方式在本地运行它。但是,我们可能还希望以其他人可以将其作为服务访问的方式部署应用程序:

  • 无需安装所有必备组件

  • 不需要源代码

  • 像其他网页一样

本节介绍将Bokeh服务器应用程序部署为供其他人使用的服务时需要考虑的一些事项。

独立Bokeh服务器

首先,可以简单地在网络上运行Bokeh服务器,让用户直接与之交互。根据应用程序代码的计算负担、用户数量、用于运行的机器的功率等,这可能是部署内部网络的一个简单而直接的选择。

然而,在这种情况下,经常需要身份验证、扩展和正常运行时间。在这些情况下,需要更复杂的部署配置。在以下几节中,我们将讨论其中一些考虑因素。

SSH隧道

在不允许直接访问的主机上运行Bokeh服务器的独立实例可能比较方便或必要。在这种情况下,可以使用SSH“隧道”到服务器。

在最简单的场景中,Bokeh服务器将运行在一台主机上,并且将从另一个位置(例如笔记本电脑)访问,而不使用中间机器。

在上像往常一样运行服务器 远程主机

bokeh server

接下来,在 本地计算机 要建立到远程主机的SSH隧道:

ssh -NfL localhost:5006:localhost:5006 user@remote.host

替换 user 你的用户名在远程主机上 remote.host 包含托管Bokeh服务器的系统的主机名/IP地址。系统可能会提示您输入远程系统的登录凭据。建立连接后,您将能够导航到 localhost:5006 就像Bokeh服务器在本地机器上运行一样。

第二种稍微复杂一些的情况发生在服务器和本地机器之间有一个网关时。在这种情况下,必须建立从服务器到网关的反向隧道。此外,来自本地计算机的隧道也将指向网关。

在上发出以下命令 远程主机 Bokeh服务器的运行位置:

nohup bokeh server &
ssh -NfR 5006:localhost:5006 user@gateway.host

替换 user 你的用户名在网关上 gateway.host 网关的主机名/IP地址。可能会提示您输入网关的登录凭据。

现在设置隧道的另一半,从本地机器到网关。上 本地计算机

ssh -NfL localhost:5006:localhost:5006 user@gateway.host

再次更换 user 你的用户名在网关上 gateway.host 网关的主机名/IP地址。现在,您应该能够从本地计算机访问Bokeh服务器,就像Bokeh服务器在本地计算机上运行一样 localhost:5006 在本地计算机上。您甚至可以从本地计算机上运行的Jupyter笔记本设置客户端连接。

注解

我们打算扩展本节,为其他工具和配置提供更多指导。如果您有其他web部署方案的经验,并希望在此提供您的知识,请联系我们https://discussion.bokeh.org

SSL终止

Bokeh服务器可以配置为直接终止SSL连接(即服务于安全的HTTPS和WSS会话)。至少 --ssl-certfile 必须提供参数。该值必须是指向PEM格式的单个文件的路径,该文件包含证书以及建立证书真实性所需的任意数量的CA证书:

bokeh serve --ssl-certfile /path/to/cert.pem

也可以通过设置环境变量来提供证书文件的路径 BOKEH_SSL_CERTFILE .

如果私钥单独存储,则可以通过设置 --ssl-keyfile 命令行参数,或通过设置 BOKEH_SSL_KEYFILE 环境变量。如果私钥需要密码,则应通过设置 BOKEH_SSL_PASSWORD 环境变量。

或者,您可能希望在代理之后运行Bokeh服务器,并让代理终止SSL。下一节将描述这种情况。

基本反向代理设置

如果目标是将web应用程序服务于通用internet,则通常希望将应用程序托管在内部网络上,并通过某个专用的HTTP服务器进行代理连接。本节提供一些常见反向代理背后的基本配置指南。

恩吉克斯

Nginx是一个非常常见的HTTP和反向代理服务器。服务器配置块示例如下所示:

server {
    listen 80 default_server;
    server_name _;

    access_log  /tmp/bokeh.access.log;
    error_log   /tmp/bokeh.error.log debug;

    location / {
        proxy_pass http://127.0.0.1:5100;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_http_version 1.1;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header Host $host:$server_port;
        proxy_buffering off;
    }

}

以上 server 块设置Nginx以代理传入连接到 127.0.0.1 在端口80到 127.0.0.1:5100 内部。要在这种配置中工作,我们需要使用一些命令行选项来配置Bokeh服务器。特别是,我们需要使用 --port 指定Bokeh服务器应在端口5100上侦听自己。

bokeh serve myapp.py --port 5100

请注意,在上面的basic server块中,我们没有为静态资源配置任何特殊处理,例如bokehjs和CSS文件。这意味着这些文件直接由Bokeh服务器本身提供服务。虽然这样做是可行的,但它给Bokeh服务器增加了不必要的额外负载,因为Nginx有一个快速的静态资产处理程序。要利用Nginx为Bokeh的静态资产提供服务,可以在 server 上面的方块,类似于:

location /static {
    alias /path/to/bokeh/server/static;
}

请注意,无论运行Nginx服务器进程的任何用户帐户都可以访问Bokeh资源的文件权限。或者,可以在部署过程中将资源复制到全局静态目录。

为了在进程间传递cookie和消息头,Bokeh可以在JWT令牌中包含此信息,JWT令牌通过Websocket发送。在某些情况下,此令牌可能会变得非常大,nginx可能会放弃请求。因此,您可能需要覆盖nginx的默认值 large_client_header_buffers 设置:

large_client_header_buffers 4 24k;

阿帕奇

另一个常见的HTTP服务器和代理是Apache。下面是在Apache后面运行Bokeh服务器的配置示例:

<VirtualHost *:80>
    ServerName localhost

    CustomLog "/path/to/logs/access_log" combined
    ErrorLog "/path/to/logs/error_log"

    ProxyPreserveHost On
    ProxyPass /myapp/ws ws://127.0.0.1:5100/myapp/ws
    ProxyPassReverse /myapp/ws ws://127.0.0.1:5100/myapp/ws

    ProxyPass /myapp http://127.0.0.1:5100/myapp/
    ProxyPassReverse /myapp http://127.0.0.1:5100/myapp/

    <Directory />
        Require all granted
        Options -Indexes
    </Directory>

    Alias /static /path/to/bokeh/server/static
    <Directory /path/to/bokeh/server/static>
        # directives to effect the static directory
        Options +Indexes
    </Directory>

</VirtualHost>

以上配置别名 /static 到Bokeh静态资源目录的位置。但是,作为部署的一部分,也可以(可能更可取)将Bokeh静态资源复制到为Apache配置的任何标准静态文件位置。

请注意,您可能还需要为上述配置启用一些模块:

a2enmod proxy
a2enmod proxy_http
a2enmod proxy_wstunnel
apache2ctl restart

这些可能需要运行 sudo ,取决于您的系统。

如前所述,您将使用以下命令运行Bokeh服务器:

bokeh serve myapp.py --port 5100

使用Nginx和SSL进行反向代理

如果您想在以SSL终止的Nginx代理后面部署Bokeh服务器,那么需要一些额外的定制。尤其是,Bokeh服务器必须配置为 --use-xheaders 旗帜:

bokeh serve myapp.py --port 5100 --use-xheaders

这个 --use-xheaders 选项使Bokeh覆盖所有请求的远程IP和URI方案/协议 X-Real-IpX-Forwarded-ForX-SchemeX-Forwarded-Proto 标题可用时。

您还必须自定义Nginx。尤其是,您必须配置Nginx来发送 X-Forwarded-Proto 头,以及为SSL终止配置Nginx。或者,您可能希望将所有HTTP流量重定向到HTTPS。此配置的完整详细信息(例如,如何以及在何处安装SSL证书和密钥)将因平台而异,但仅供参考 nginx.conf 如下所示:

# redirect HTTP traffic to HTTPS (optional)
server {
    listen      80;
    server_name foo.com;
    return      301 https://$server_name$request_uri;
}

server {
    listen      443 default_server;
    server_name foo.com;

    # add Strict-Transport-Security to prevent man in the middle attacks
    add_header Strict-Transport-Security "max-age=31536000";

    ssl on;

    # SSL installation details will vary by platform
    ssl_certificate /etc/ssl/certs/my-ssl-bundle.crt;
    ssl_certificate_key /etc/ssl/private/my_ssl.key;

    # enables all versions of TLS, but not SSLv2 or v3 which are deprecated.
    ssl_protocols TLSv1 TLSv1.1 TLSv1.2;

    # disables all weak ciphers
    ssl_ciphers "ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA:ECDHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:DHE-RSA-AES128-SHA256:DHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA:ECDHE-RSA-DES-CBC3-SHA:EDH-RSA-DES-CBC3-SHA:AES256-GCM-SHA384:AES128-GCM-SHA256:AES256-SHA256:AES128-SHA256:AES256-SHA:AES128-SHA:DES-CBC3-SHA:HIGH:!aNULL:!eNULL:!EXPORT:!DES:!MD5:!PSK:!RC4";

    ssl_prefer_server_ciphers on;

    location / {
        proxy_pass http://127.0.0.1:5100;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_http_version 1.1;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header Host $host:$server_port;
        proxy_buffering off;
    }

}

此配置将代理所有传入的HTTPS连接到 foo.com 到内部运行的Bokeh服务器 http://127.0.0.1:5100 .

使用Nginx实现负载平衡

Bokeh服务器的体系结构是专门为可扩展而设计的——大体上,如果您需要更大的容量,只需运行额外的服务器。在这种情况下,通常需要在负载平衡器后面运行所有Bokeh服务器实例,以便在各个服务器之间分布新的连接。

../../_images/bokeh_serve_scale.svg

Bokeh服务器是水平可伸缩的。为了增加容量,可以在负载平衡器后面运行更多的服务器。

Nginx提供了负载平衡功能。我们将介绍一种可能的配置的一些基础知识,但也请参考 Nginx load balancer documentation . 例如,有多种不同的策略可用于选择下一个要连接的服务器。

首先我们需要添加一个 upstream 节到我们的NGinx配置,通常在 server 诗节。本节内容如下:

upstream myapp {
    least_conn;                 # Use Least Connections strategy
    server 127.0.0.1:5100;      # Bokeh Server 0
    server 127.0.0.1:5101;      # Bokeh Server 1
    server 127.0.0.1:5102;      # Bokeh Server 2
    server 127.0.0.1:5103;      # Bokeh Server 3
    server 127.0.0.1:5104;      # Bokeh Server 4
    server 127.0.0.1:5105;      # Bokeh Server 5
}

我们已经给它贴上了标签 upstream 节as myapp . 我们将在下面使用这个名字。此外,我们在节中列出了6个不同的Bokeh服务器实例(每个实例运行在不同的端口上)的内部连接信息。您可以根据需要运行和列出任意数量的Bokeh服务器。

您可以使用类似以下命令运行Bokeh服务器:

serve myapp.py --port 5100
serve myapp.py --port 5101
...

下一步,在 location 对于Bokeh服务器,更改 proxy_pass 参考的值 upstream 我们在上面创作的诗节。在这种情况下,我们使用 proxy_pass http://myapp; 如图所示:

server {

    location / {
        proxy_pass http://myapp;

        # all other settings unchanged
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_http_version 1.1;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header Host $host:$server_port;
        proxy_buffering off;
    }

}

认证

Bokeh服务器本身没有任何用于身份验证或授权的工具。但是,Bokeh服务器可以配置为一个“Auth Provider”,它可以连接到Tornado的底层功能。有关背景信息,请参阅Tornado文档 Authentication and security . 本节的其余部分假设您对该材料有些熟悉。

身份验证模块

Bokeh服务器只能配置为仅在有经过适当身份验证的用户时才允许连接。这是通过提供到模块的路径来实现命令行上的必要功能的:

bokeh serve --auth-module=/path/to/auth.py

或者通过设置 BOKEH_AUTH_MODULE 环境变量。

模块必须包含 one 将返回当前用户(或无)的以下两个函数之一:

def get_user(request_handler):
    pass

async def get_user_async(request_handler):
    pass

这个功能通过龙卷风传递 RequestHandler 并且可以检查cookie或请求头来确定经过身份验证的用户。如果没有有效的经过身份验证的用户,这些函数应该返回None。

此外,模块必须指定重定向未经验证的用户的位置。它必须包含:

  • 模块a属性 login_url 和(可选)a LoginHandler

  • 的函数定义 get_login_url

login_url = "..."

class LoginHandler(RequestHandler):
    pass

def get_login_url(request_handler):
    pass

当一个亲戚 login_url 是一个可选的 LoginHandler 类也可以提供,它将作为路由自动安装在Bokeh服务器上。

这个 get_login_url 函数在登录URL必须根据请求或Cookie等而变化的情况下很有用。不能指定 LoginHandler 什么时候? get_url_function 定义。

与登录选项类似,可选 logout_urlLogoutHandler 值可用于定义注销用户的端点。

如果没有提供身份验证模块,则假定为默认用户,并且访问Bokeh服务器端点不需要身份验证。

警告

将执行auth模块的内容!

安全Cookie

如果你想用龙卷风 set_secure_cookieget_secure_cookie 函数,则必须设置cookie机密。这可以通过 BOKEH_COOKIE_SECRET 环境变量。例如

export BOKEH_COOKIE_SECRET=<cookie secret value>

值应该是一个长的、随机的字节序列

安全性

默认情况下,Bokeh服务器将接受允许的websocket源上的任何传入连接。如果指定了会话ID,并且服务器上已经存在具有该ID的会话,则会建立到该会话的连接。否则,将自动创建并使用新会话。

如果您正在大型组织或更广泛的internet上部署嵌入式Bokeh应用程序,您可能需要限制谁可以启动会话,以及从何处启动会话。Bokeh有限制会话创建的选项。

Websocket来源

当向Bokeh服务器发出HTTP请求时,它会立即返回一个脚本,该脚本将启动websocket连接,所有后续通信都将通过websocket进行。为了减少跨站点误用的风险,bokeh服务器将只从显式允许的源启动websocket连接。原始标头与allowlist不匹配的请求将生成HTTP 403错误响应。

默认情况下,仅 localhost:5006 是允许的。一、 e以下两次调用相同:

bokeh serve --show myapp.py

bokeh serve --show --allow-websocket-origin=localhost:5006 myapp.py

这两种方法都将打开浏览器以访问默认的应用程序URL localhost:5006 ,从那以后 localhost:5006 在允许的websocket origin allowlist中,Bokeh服务器将创建并显示一个新会话。

现在,考虑一下当Bokeh服务器嵌入到另一个web页面中时,使用 server_document()server_session() . 在本例中,Bokeh服务器请求的“Origin”头是嵌入Bokeh内容的页面的URL。例如,如果用户导航到我们的页面 https://acme.com/products ,其中嵌入了Bokeh应用程序,则浏览器报告的原始标头将为 acme.com . 在本例中,我们通常希望将Bokeh服务器限制为 only 来自我们的请求 acme.com 页面,以便其他页面在我们不知情的情况下无法嵌入我们的Bokeh应用程序。

通过设置 --allow-websocket-origin 命令行参数:

bokeh serve --show --allow-websocket-origin=acme:com myapp.py

这将阻止其他网站将我们的Bokeh应用程序嵌入到他们的页面中,因为来自查看这些页面的用户的请求报告的来源与 acme.com ,Bokeh服务器将拒绝它们。

警告

记住,这只会防止 其他网页 从偷偷地嵌入我们的Bokeh应用程序到使用标准web浏览器的用户。一个有决心和知识渊博的攻击者可以伪造源文件头。

如果需要多个允许的源,则 --allow-websocket-origin 可以在命令行上传递。

也可以将Bokeh服务器配置为允许任何和所有连接,而不考虑来源:

bokeh serve --show --allow-websocket-origin='*' myapp.py

不建议在外部测试和实验中使用。

签名的会话ID

默认情况下,Bokeh服务器将自动为来自允许的websocket源的所有新请求创建新会话,即使没有提供会话ID。当在另一个web应用程序(例如Flask、Django)中嵌入Bokeh应用程序时,我们希望确保我们的web应用程序,以及 only 我们的web应用程序能够生成对Bokeh服务器的适当请求。可以将Bokeh服务器配置为仅在提供加密签名的会话ID时创建会话。

为此,您需要首先创建一个密钥来签署会话id,使用 bokeh secret 命令,例如。

export BOKEH_SECRET_KEY=`bokeh secret`

然后在启动BOKEH服务器时设置BOKEH_SIGN_会话(通常还设置允许的websocket源):

BOKEH_SIGN_SESSIONS=yes bokeh serve --allow-websocket-origin=acme.com myapp.py

然后在您的web应用程序中,我们使用 generate_session_id

from bokeh.util.token import generate_session_id

script = server_session(url='http://localhost:5006/bkapp',
                        session_id=generate_session_id())
return render_template("embed.html", script=script, template="Flask")

确保 BOKEH_SECRET_KEY 为Bokeh服务器和web应用程序进程(例如Flask或Django或任何正在使用的工具)设置环境变量(并且相同)。

注解

签名的会话id是有效的访问令牌。与任何令牌系统一样,安全性的前提是对令牌保密。我们还建议在终止SSL的代理之后运行Bokeh服务器,以便将会话ID安全地传输到用户的浏览器。

XSRF曲奇

Bokeh可以使用Tornado的跨站点请求防伪。要启用此功能,请使用 --enable-xsrf-cookies 选项,或设置环境变量 BOKEH_XSRF_COOKIES=yes . 如果启用此设置,则必须正确检测自定义或登录处理程序上的任何PUT、POST或DELETE操作才能正常工作。通常,这意味着添加代码:

{% module xsrf_form_html() %}

所有HTML表单提交模板。有关详细信息,请参阅上的Tornado文档 XSRF Cookies .

扩展服务器

可以使用派生多个服务器进程 num-procs 选项。例如,要分叉3个进程:

bokeh serve --num-procs 3

请注意,分叉操作发生在底层Tornado服务器中,请参见中的注释 Tornado docs .

进一步阅读

你现在熟悉的概念 运行Bokeh服务器 ,您可能有兴趣了解更多关于Bokeh服务器内部的信息 服务器体系结构 .