部署场景#

为了将您的应用程序变成用户友好的服务,您需要部署您的工作。本节探讨了部署的各个方面。

独立 Bokeh 服务器#

您可以让 Bokeh 服务器在网络上运行,以便用户直接与您的应用程序交互。对于本地网络部署来说,这可能是一个简单的解决方案,前提是运行服务器的硬件功能能够满足您的应用程序需求和预期的用户数量。

但是,如果您有身份验证、扩展或正常运行时间的要求,则需要考虑更复杂的部署配置。

将 Bokeh 服务器集成到其他 Web 服务中#

Bokeh 服务器经常用于创建嵌入在更大父应用程序中的绘图和仪表板。例如,在金融领域,这可能涉及将基于 Bokeh 的趋势线(绘制一段时间内的账户余额)集成到基于 Web 的交易平台中。在供应链环境中,可以将 Bokeh 视图集成到现有的库存管理系统中,以交互方式监控商店的物品供应。

为了满足此用例,bokeh.embed 模块提供了 server_document()server_session() 方法。有关其用法的详细讨论以及示例,请参阅 Bokeh 应用程序。简而言之,这些方法返回 HTML 脚本标签的文本,该标签从 Bokeh 服务器加载视图,并将视图添加到放置脚本标签的任何页面的 DOM 中。

如果要与之集成的父服务不是基于 Python 的,您仍然可以通过 server_document / server_session 方法与 Bokeh 集成。但是,您需要通过调用一个小型、长期存在的 Python 脚本(通过任何标准的 IPC 形式返回这些方法的文本内容)来实现这一点。

要允许您的父应用程序显示嵌入的 Bokeh 视图,您必须将父应用程序配置为允许对 Bokeh 服务器实例进行跨源请求。这可以通过将 Bokeh 服务器的公共主机名和端口号添加到父服务的 script-srcconnect-src 指令中来实现,方法是使用 HTTP(S) 和 WS(S) 协议。配置 CSP 的确切过程将取决于父服务构建所使用的工具包或框架,因此请参考该软件的具体文档。作为最后的手段,也可以通过反向代理覆盖 CSP 标头。

警告

在通过 HTTPS 运行父服务的同时,不要通过 HTTP 运行 Bokeh 服务器,反之亦然。Bokeh 的加载程序代码通过 window.location.protocol 在客户端确定加载资源的协议,因此,如果父服务的协议与 Bokeh 服务器实例的协议不匹配,则加载程序脚本对 Bokeh 服务器的请求将失败。

如果您的父应用程序是基于 Python 的,并且您不介意将 Bokeh 服务器应用程序紧密集成到父应用程序的代码库中,Bokeh 还支持通过父应用程序启动的线程运行其底层的 Tornado Web 服务器。实际示例链接在 Bokeh 服务器 API 下。此方法仍然需要使用 server_documentserver_session,但也可以简化 CSP 配置以及 Bokeh 服务器应用程序的部署。

SSH 隧道#

要在访问受限的主机上运行 Bokeh 服务器的独立实例,请使用 SSH “隧道”连接到服务器。

在最简单的场景中,用户从另一个位置(例如没有中间机器的笔记本电脑)访问 Bokeh 服务器。

照常在 **远程主机** 上运行服务器。

bokeh serve

接下来,在 **本地机器** 上发出以下命令以建立到远程主机的 SSH 隧道

ssh -NfL localhost:5006:localhost:5006 [email protected]

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

稍微复杂一点的场景涉及服务器和本地机器之间的网关。在这种情况下,必须从服务器到网关建立一个反向隧道,另一个隧道连接网关和本地机器。

在将运行 Bokeh 服务器的 **远程主机** 上发出以下命令

nohup bokeh server &
ssh -NfR 5006:localhost:5006 [email protected]

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

要设置本地机器和网关之间的隧道,请在 **本地机器** 上运行以下命令

ssh -NfL localhost:5006:localhost:5006 [email protected]

同样,将 user 替换为网关上的用户名,将 gateway.host 替换为网关的主机名或 IP 地址。

您现在应该能够通过导航到 localhost:5006 从本地机器访问 Bokeh 服务器。您甚至可以设置从在本地机器上运行的 Jupyter Notebook 建立的客户端连接。

注意

我们打算扩展本节,为其他工具和配置提供更多指导。如果您有其他 Web 部署场景的经验并希望在此处贡献您的知识,请通过 https://discourse.bokeh.org 与我们联系。

SSL 终止#

您可以将 Bokeh 服务器配置为终止 SSL 连接并直接提供安全的 HTTPS 和 WSS 会话。为此,您需要使用 --ssl-certfile 参数,其值为包含证书以及建立证书真实性所需任意数量的 CA 证书 的单个 PEM 文件的路径。

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

您还可以通过设置环境变量 BOKEH_SSL_CERTFILE 来提供证书文件路径。

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

或者,您可能希望在代理后面运行 Bokeh 服务器,并让代理终止 SSL 连接。有关详细信息,请参阅下一节。

基本反向代理设置#

要向通用互联网提供 Web 应用程序,您可能希望将您的应用程序托管在内部网络上,并通过某些专用的 HTTP 服务器代理连接到它。本节提供了有关如何配置一些常见反向代理的指导。

Nginx#

一个非常常见的 HTTP 和反向代理服务器是 Nginx。这是一个 server 配置节的示例

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,以便将传入的 80 端口上的 127.0.0.1 连接代理到 5100 端口上的 127.0.0.1。要在这种配置下工作,您需要使用一些命令行选项来配置 Bokeh 服务器。特别是,使用 --port 使 Bokeh 服务器监听 5100 端口。

bokeh serve myapp.py --port 5100

上面的基本服务器块没有配置对静态资源(例如 Bokeh JS 和 CSS 文件)的任何特殊处理。这意味着 Bokeh 服务器直接提供这些文件。

虽然这是一个可行的选项,但它要求 Bokeh 服务器执行 Nginx 可以更好地处理的额外工作。要使用 Nginx 提供静态资产,请将以下子块添加到上面的代码中,将静态资产的路径替换为 /path/to/bokeh/server/static

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

确保运行 Nginx 的帐户有权访问 Bokeh 资源。或者,您可以在部署期间将资源复制到全局静态目录。

为了跨进程传递 cookie 和标头,Bokeh 可能会将这些信息包含在 JSON Web 令牌中,并通过 WebSocket 发送。在某些情况下,此令牌可能会变得非常大,导致 Nginx 丢弃请求。您可能需要通过覆盖默认的 Nginx 设置 large_client_header_buffers 来解决此问题。

large_client_header_buffers 4 24k;

Apache#

另一个常见的 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 静态资源目录的位置。但是,也可以(可能更可取)将静态资源复制到您在部署过程中为 Apache 配置的任何标准静态文件位置。

您可能还需要为以上配置启用一些模块

a2enmod proxy
a2enmod proxy_http
a2enmod proxy_wstunnel
apache2ctl restart

根据您的系统,您可能需要使用 sudo 来运行以上命令。

和之前一样,使用以下命令运行 Bokeh 服务器

bokeh serve myapp.py --port 5100

Unix 套接字与代理#

在某些情况下,您可能希望使用 Unix 套接字而不是 Websocket 将代理的 Bokeh 服务器连接到代理。您可以将 Bokeh 服务器绑定到 Unix 套接字,并使用 Nginx 或 Apache 代理到 Unix 域套接字。

注意

在 Windows 上不支持绑定到 Unix 套接字。

bokeh serve --unix-socket /path/to/socket.sock

Nginx 配置可能如下所示

upstream myserver {
    server unix:/path/to/socket.sock;
}

server {
    listen 80 default_server;
    server_name _;

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

    location / {
        proxy_pass http://myserver;
    }

}

请注意,Bokeh 服务器的网络选项(例如 websocket 来源和 SSL 配置)与 Unix 套接字不兼容。代理将负责在前端执行这些限制。

如果有多个用户共享主机,您可以限制套接字的文件权限以限制对代理服务器的访问。

使用 Nginx 和 SSL 进行反向代理#

要在 SSL 终止的 Nginx 代理后面部署 Bokeh 服务器,您需要一些额外的自定义。特别是,您必须使用 --use-xheaders 标记配置 Bokeh 服务器。

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

--use-xheaders 标记会导致 Bokeh 使用 X-Real-IpX-Forwarded-ForX-SchemeX-Forwarded-Proto 标头覆盖所有请求的远程 IP 和 URI 方案/协议(如果可用)。

您还需要自定义 Nginx。特别是,您必须配置 Nginx 以发送 X-Forwarded-Proto 标头并使用 SSL 终止。可选地,您可能希望将所有 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;

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

    ssl on;

    # SSL installation details 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 the deprecated SSLv2 or v3
    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 代理到在 http://127.0.0.1:5100 上内部运行的 Bokeh 服务器。

负载均衡#

Bokeh 服务器在设计上是可扩展的。如果您需要更多容量,只需运行更多服务器即可。在这种情况下,您通常希望在负载均衡器后面运行所有 Bokeh 服务器实例,以便将新连接分布到各个服务器。

../../../_images/bokeh_serve_scale.svg

Bokeh 服务器是水平可扩展的。要增加容量,可以在负载均衡器后面运行更多服务器。#

您可以根据需要运行任意数量的 Bokeh 服务器。以下示例基于在三个不同端口上运行三个 Bokeh 服务器的设置

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

以下部分根据此设置提出了基本配置。有关更详细信息,请参阅 Nginx 负载均衡器文档Apache 代理均衡器模块文档。例如,有不同的策略可用于定义如何将传入连接分布到服务器实例之间。

Nginx#

首先,您需要在 Nginx 配置中添加一个 upstream 代码段。这通常位于 server 代码段之上,如下所示

upstream myapp {
    least_conn;            # Use the least-connected strategy
    server 127.0.0.1:5100;
    server 127.0.0.1:5101;
    server 127.0.0.1:5102;
}

配置的其余部分使用名称 myapp 来引用以上 upstream 代码段,该代码段列出了三个 Bokeh 服务器实例的内部连接信息。

接下来,在 Bokeh 服务器的 location 代码段中,更改 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;
    }

}

Apache#

首先,确保您已启用 proxy_balancerrewrite 模块。

为 http 和 websocket 协议添加均衡器

<Proxy "balancer://myapp_http">
    BalancerMember "http://127.0.0.1:5100/myapp"
    BalancerMember "http://127.0.0.1:5101/myapp"
    BalancerMember "http://127.0.0.1:5102/myapp"
    ProxySet lbmethod=bybusyness
</Proxy>

<Proxy "balancer://myapp_ws">
    BalancerMember "ws://127.0.0.1:5100/myapp"
    BalancerMember "ws://127.0.0.1:5101/myapp"
    BalancerMember "ws://127.0.0.1:5102/myapp"
    ProxySet lbmethod=bybusyness
</Proxy>

bybusyness 负载均衡方法确保将传入连接分配给当时活动连接最少的实例。它应该比 其他可用算法(如 byrequests)产生更好的结果。您可能需要启用 mod_lbmethod_bybusyness

最后,您可以将 websocket 和 http 请求代理到相应的均衡器

RewriteEngine On
RewriteCond %{HTTP:Upgrade} =websocket [NC]
RewriteRule /myapp(.*)    balancer://myapp_ws$1 [P,L]
RewriteCond %{HTTP:Upgrade} !=websocket [NC]
RewriteRule /myapp(.*)    balancer://myapp_http$1 [P,L]

身份验证#

Bokeh 服务器本身没有任何身份验证或授权功能。但是,您可以使用“身份验证提供程序”配置 Bokeh 服务器,该提供程序可挂接到 Tornado 的底层功能。有关背景信息,请参阅 Tornado 文档中的 身份验证和安全性。本节的其余部分假定您对该资料有所了解。

身份验证模块#

您可以配置 Bokeh 服务器,只允许经过身份验证的用户连接。为此,请在命令行上提供实现必要函数的模块的路径。

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

或者,您可以将 BOKEH_AUTH_MODULE 环境变量设置为此路径。

该模块必须包含以下两个函数之一,它们返回当前用户(或 None

def get_user(request_handler):
    pass

async def get_user_async(request_handler):
    pass

该模块将该函数传递给 Tornado RequestHandler,该函数可以检查 cookie 或请求标头以确定经过身份验证的用户。如果没有经过身份验证的用户,这些函数应返回 None

此外,该模块必须指定通过包含以下任一内容将未经身份验证的用户重定向到何处

  • 模块属性 login_url 和(可选)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 或其他因素而变化的情况下很有用。在定义 get_url_function 时,您也可以指定 LoginHandler

要定义注销用户的端点,您还可以使用可选的 logout_urlLogoutHandler 参数,类似于登录选项。

如果您不提供身份验证模块,则配置将不需要任何身份验证即可访问 Bokeh 服务器端点。

警告

配置将执行身份验证模块的内容。

安全 Cookie#

如果您想在身份验证模块中使用 Tornado 的 set_secure_cookieget_secure_cookie 函数,则必须设置 cookie 密钥。为此,请使用 BOKEH_COOKIE_SECRET 环境变量。

export BOKEH_COOKIE_SECRET=<cookie secret value>

该值应为一个长长的随机字节序列。

安全性#

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

如果您在大型组织或更广泛的互联网中部署嵌入式 Bokeh 应用程序,您可能希望限制谁可以发起会话以及从哪里发起会话。Bokeh 允许您管理会话创建权限。

WebSocket 来源#

当 Bokeh 服务器收到 HTTP 请求时,它会立即返回一个启动 WebSocket 连接的脚本。所有后续通信都通过 WebSocket 进行。

为了降低跨站点滥用的风险,Bokeh 服务器只会从明确允许的来源启动 WebSocket 连接。具有不在允许列表中的 Origin 标头的请求将生成 HTTP 403 错误响应。

默认情况下,只允许 localhost:5006,这使得以下两次调用相同

bokeh serve --show myapp.py

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

这两者都会在您的默认浏览器中打开默认应用程序 URL localhost:5006,并且由于 localhost:5006 在允许的 WebSocket 来源列表中,因此 Bokeh 服务器会创建并显示一个新会话。

当您使用 server_document()server_session() 将 Bokeh 服务器嵌入到另一个网页中时,对 Bokeh 服务器的请求的 Origin 标头是托管 Bokeh 内容的页面的 URL。

例如,如果用户导航到 https://acme.com/products 上的页面,则浏览器报告的来源标头将为 acme.com。在这种情况下,您通常会限制 Bokeh 服务器只响应源自 acme.com 页面的请求,防止其他页面在您不知情的情况下嵌入您的 Bokeh 应用程序。

您可以通过设置以下--allow-websocket-origin命令行参数来实现。

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

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

警告

请记住,这只会阻止**其他网页**在您不知情的情况下嵌入您的 Bokeh 应用程序。

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

您还可以配置 Bokeh 服务器以允许所有连接,而不管其来源如何。

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

此选项仅适用于测试、实验和本地笔记本使用。

已签名会话 ID#

默认情况下,即使您没有提供会话 ID,Bokeh 服务器也会自动为来自允许的 WebSocket 来源的所有新请求创建新会话。

在将 Bokeh 应用程序嵌入另一个 Web 应用程序(如 Flask 或 Django)时,请确保**只有**您的 Web 应用程序能够生成对 Bokeh 服务器的有效请求,您可以将其配置为仅使用加密签名的会话 ID 创建会话。

首先,使用bokeh secret命令创建一个用于签名会话 ID 的密钥。

export BOKEH_SECRET_KEY=`bokeh secret`

然后在启动 Bokeh 服务器时将BOKEH_SIGN_SESSIONS设置为yes。此时,您通常还需要设置允许的 WebSocket 来源。

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

然后,在您的 Web 应用程序中,使用generate_session_id显式提供已签名的会话 ID。

from bokeh.util.token import generate_session_id

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

确保为 Bokeh 服务器和 Web 应用程序进程(例如 Flask、Django 或您使用的任何其他工具)设置相同的BOKEH_SECRET_KEY环境变量。

注意

已签名的会话 ID 用作访问令牌。与任何令牌系统一样,安全性取决于保持令牌的机密性。您还应该在终止 SSL 连接的代理后面运行 Bokeh 服务器,或配置 Bokeh 服务器以直接终止 SSL。这使您可以安全地将会话 ID 传输到客户端浏览器。

XSRF Cookie#

Bokeh 服务器可以使用 Tornado 的跨站点请求伪造保护。要启用此功能,请使用--enable-xsrf-cookies选项或将环境变量BOKEH_XSRF_COOKIES设置为yes

使用此设置,您必须正确地为自定义和登录处理程序上的所有 PUT、POST 和 DELETE 操作设置工具,才能使其正常工作。通常,这意味着将以下代码添加到所有 HTML 表单提交模板中

{% module xsrf_form_html() %}

有关完整详细信息,请参阅 Tornado 文档中关于XSRF Cookie的部分。

扩展服务器#

您可以使用num-procs选项分叉多个服务器进程。例如,运行以下命令以分叉 3 个进程

bokeh serve --num-procs 3

请注意,分叉操作发生在底层的 Tornado 服务器中。有关更多信息,请参阅Tornado 文档