服务器体系结构

本章是对Bokeh服务器内部结构的“深入研究”。它假设您已经熟悉中的Bokeh服务器信息 运行Bokeh服务器 .

如果您是:

  • 试着在Bokeh代码库上工作

  • 编写自己的自定义服务器进程来使用而不是 bokeh serve

自定义服务器进程可以使用Tornado的web框架添加额外的路由(web页面或REST端点)。

如果应用程序开发人员使用 bokeh serve ,它们通常不需要从 bokeh.server 完全。应用程序开发人员只会使用 Server 类,如果它正在执行特定的操作,例如自定义或嵌入式服务器进程。

应用程序、会话和连接

您可以将一个或多个应用程序的一个或多个会话作为一个或多个应用程序的模板。会话与 bokeh.document.Document :每个会话都有一个文档实例。当浏览器连接到服务器时,它将获得一个新的会话;应用程序用它想要的任何绘图、小部件或其他内容填充会话的文档。应用程序还可以设置回调,以便定期运行或在文档更改时运行。

应用程序表示 Application 班级。此类包含 Handler 实例和可选元数据。处理程序可以通过多种方式创建:从JSON文件、从Python函数、从Python文件创建,也许将来还有更多的方法。可选元数据作为JSON blob通过 /metadata 终结点。例如,创建 Application 实例:

Application(metadata=dict(hi="hi", there="there"))

将有 http://server/myapp/metadata 返回 (application/json ):

{
    "data": {
        "hi": "hi",
        "there": "there"
    },
    "url": "/myapp"
}

围绕每个应用程序,服务器创建一个 ApplicationContext . 它的主要作用是为应用程序保存会话集。

会话由类表示 ServerSession .

每个应用程序都有一个路由(称为 app_path 在客户端API中),并且每个会话都有一个ID Document 实例(服务器按路径查找应用程序,然后按ID查找会话)。

每个会话都有0-N个连接,由 ServerConnection 班级。连接是websocket连接。一般来说,会话只要有连接就可以持续,尽管只有在超时之后才会过期(允许页面重新加载等)。

应用程序和应用程序处理程序无法访问 Server ServerSessionApplicationContext 直接地说,它们有一个更为有限的接口,分为两部分, ServerContextSessionContext . ServerContext 在某些方面提供了有限的接口 ApplicationContextServer ,同时 SessionContext 在某些方面提供了有限的接口 ServerSession . 这些接口的具体实现是 BokehServerContextBokehSessionContext .

总结对象图:

  • Server implemented by BokehTornado

  • 有N ApplicationContext

  • 有1个 Application 能够创建新会话

  • 有1个路径用于在URL中标识它

  • 有1个 ServerContext 表示对应用程序代码可见的服务器方面

  • 有N ServerSession

  • 有一个会话ID,它是一个命名会话的字符串

  • 有1个 Document 表示会话状态

  • 有N ServerConnection 表示附加到会话的WebSocket

  • 有1个 SessionContext 表示会话的各个方面对应用程序代码可见

Tornado IOLoop 和异步代码

要在服务器上工作,您需要了解Tornado的 IOLoop 以及 tornado.gen 模块。

Tornado文档将是最好的资源,但以下是一些快速了解的信息:

  • Bokeh服务器是单线程的,所以不要编写“阻塞”代码,这意味着代码在等待IO或执行长时间计算时会耗尽单线程。如果您这样做,您将快速增加应用程序用户看到的延迟。例如,如果每次有人移动滑块时都会阻塞100毫秒,而10个用户同时执行此操作,则用户很容易看到10*100毫秒=1秒的延迟,而只有10个用户。

  • 在Tornado中,非阻塞代码使用返回 Future 班级。你可能看到了 @gen.coroutine 装饰工。此修饰符将修饰方法转换为返回 Future .

  • 当没有代码运行时,Tornado会在其 IOLoop (有时称为“主循环”或“事件循环”),这意味着它正在等待发生的事情。当事情发生时, IOLoop 执行对该事件感兴趣的任何回调。

应用程序和 IOLoop

我们不希望应用程序碰到龙卷风 IOLoop 直接添加回调,因为当会话到期或应用程序重新加载时,我们需要能够删除属于会话或应用程序的所有回调。

若要启用此功能,应用程序应仅使用上的API添加回调 DocumentServerContext . 这些应用程序上的方法允许 add_periodic_callbackadd_timeout_callbackadd_next_tick_callback . 我们拦截这些回调添加,并能够在卸载应用程序或销毁会话时删除它们。

生命周期

如果你看看 Application 类,服务器可以通过两种方式调用它。

  1. 这个 modify_document() 方法执行它所说的:它在会话的 Document 并允许应用程序对其进行修改(可能会添加一些绘图和小部件)。

  2. 一套“钩子” on_server_loaded()on_server_unloaded()on_session_created()on_session_destroyed() .

“钩子”被称为“生命周期钩子”,因为它们发生在应用程序和会话的生命周期中的定义点。

以下是生命周期中的步骤:

  1. 当服务器进程启动时,它调用 on_server_loaded() 每次申请。

  2. 当客户端使用以前未使用的会话ID连接时,服务器将创建一个 ServerSession 和电话 on_session_created() 一个空的 Document 然后 modify_document() 初始化 Document . 这个 on_session_created() 也可以初始化 Document 如果它喜欢的话。 on_session_created() 以前发生过 modify_document() .

  3. 当会话没有连接时,它最终将超时,并且 on_session_destroyed() 将被调用。

  4. 如果服务器进程完全关闭,它将调用 on_server_unloaded() 每次申请。这在生产中可能很少见:服务器进程通常会被信号杀死。 on_server_unloaded() 在开发过程中可能更有用,这样就可以在不泄漏资源的情况下重新加载应用程序。

这些钩子可以向 ServerContext . 这些回调可能是异步的(使用Tornado的异步IO功能),并且能够更新所有实时会话文档。

Critical consideration when using ``on_server_loaded()`` :进程全局与群集全局不同。如果你扩展一个Bokeh应用程序,你大概需要为每个CPU核心使用一个单独的进程。群集中的进程甚至可能不在同一台计算机上。服务器进程决不能假定它知道“存在的所有会话”,只知道“此进程中托管的所有会话”

详细信息 ServerSession

session对象处理客户端和服务器之间的大多数交互。

锁定

最棘手的方面 ServerSession 可能正在锁定。一般来说,我们希望一次处理一个回调或一个websocket请求;我们不希望将它们交错,因为如果回调和请求处理程序不得不担心交错,则很难实现它们。

所以 ServerSession 一次做一件事,由 ServerSession._lock ,这是一个龙卷风锁。

如果您熟悉锁和线程,这里的情况在概念上是相同的;但是争用条件只能发生在“屈服点”(当我们返回到 IOLoop )而不是在任何时候,锁是龙卷风锁而不是线程锁。

The rule is: to touch ServerSession.document code must hold ServerSession._lock.

对于通过 Document API,在执行回调之前,我们自动代表回调获取锁,然后释放它。

通过 ServerContext API,只能使用获取对会话文档的引用 SessionContext.with_locked_document() . 它在保持文档锁的情况下执行提供的函数,并将文档传递给该函数。

警告

当函数运行时,锁被保持 即使函数是异步的 ! 如果函数返回 Future ,锁一直保持到 Future 完成。

It is very easy to modify the server code in such a way that you're touching the document without holding the lock. If you do this, things will break in subtle and painful-to-debug ways. When you touch the session document, triple-check that the lock is held.

会话安全性

我们依赖于会话ID的加密随机性和难以猜测。如果攻击者知道某人的会话ID,他们可以窃听或修改会话。如果你正在编写一个嵌入了Bokeh应用的大型web应用程序,这可能会影响你设计大型应用程序的方式。

在服务器上进行黑客攻击时,会话ID在很大程度上是不透明的字符串,在最初验证ID之后,服务器代码中的ID是什么并不重要。

会话超时

为了避免资源耗尽,未使用的会话将根据中的代码超时 application_context.py

Websocket协议

服务器有一个websocket连接,每个客户端都打开(在典型用法中,每个浏览器选项卡)。websocket的主要作用是保持会话的 Document 在客户端和服务器之间同步。

Bokeh代码库中有两个客户机实现:一个是Python ClientSession 另一个是JavaScript ClientSession . 客户机和服务器会话大多是对称的。在双方,我们都收到对方的变更通知 Document ,并发送我方更改的通知。就这样,两个 Document 保持同步。

websocket协议的Python实现可以在 bokeh.server.protocol ,尽管客户端和服务器端都使用它。

Websockets已经为我们实现了“帧”,并且它们保证帧将按照发送的相同顺序到达。帧是字符串或字节数组(或特殊的内部帧类型,如ping)。websocket看起来像两个帧序列,每个方向都有一个序列(“全双工”)。

在websocket框架之上,我们实现了自己的框架 Message 概念。博克 Message 跨越多个websocket帧。它始终包含头帧、元数据帧和内容帧。这三个帧都包含一个JSON字符串。代码允许这三个帧后跟可选的二进制数据帧。原则上,这可以允许,例如,直接从内存缓冲区向websocket发送NumPy数组,而不需要额外的副本。然而,二进制数据帧尚未在Bokeh中使用。

头帧指示消息类型并给消息一个ID。消息ID用于将答复与请求相匹配(回复包含一个字段,表示“我是ID为xyz的请求的答复”)。

元数据框架目前没有任何内容,但可以用于调试数据或将来用于其他目的。

内容框架具有消息的“正文”。

现在消息不多。简要概述:

  • ACK 用于设置连接时的初始握手

  • OK 当请求不需要任何更具体的答复时,是一个通用的答复

  • ERROR 是出错时的一般错误答复

  • SERVER-INFO-REQSERVER-INFO-REPLY 是一个请求-应答对,其中应答包含有关服务器的信息,例如其Bokeh版本

  • PULL-DOC-REQ 请求获取会话的全部内容 Document 作为JSON,以及 PULL-DOC-REPLY 是包含所述JSON的回复。

  • PUSH-DOC 发送会话的全部内容 Document 另一方应该用这些新内容替换它的文档。

  • PATCH-DOC 将对会话文档的更改发送到另一方

通常,当打开一个连接时,一方将拉动或推送整个文档;在最初的拉动或推送之后,使用 PATCH-DOC 信息。

当前协议的一些注意事项

  1. 在当前的协议中,双方同时更改相同内容的冲突不会得到处理(如果发生这种情况,双方可能会失去同步,因为 PATCH-DOC 同时在飞行中)。设计一个检测这种情况的方案很容易,但是当检测到这种情况时该怎么做就不太清楚了,所以现在,我们没有检测到它,什么也不做。在大多数情况下,应用程序应该避免这种情况,因为即使我们能够理解并以某种方式处理它,对于应用程序的双方来说,为相同的价值“争斗”可能是低效的。(如果现实世界的应用程序在这个问题上失败了,我们将不得不弄清楚他们在做什么,并设计出一个解决方案。)

  2. 目前,我们在修补收藏方面并不明智;如果 Model 属性这是一个巨大的字典,只要其中的任何条目发生更改,我们都会发送整个巨型字典。

  3. 目前,我们没有通过二进制websocket帧来优化二进制数据。但是,数据类型的NumPy数组 float32float64 ,以及小于 int32 base64编码在内容框架中,以避免天真的JSON字符串序列化的性能限制。JavaScript缺少本机64位整数支持,因此无法将它们包含在优化中。base64编码应该对所有人都是完全透明的,除了那些看实际有线协议的人。有关详细信息,请参阅 bokah.util.serialization .

HTTP端点

服务器只支持一些HTTP路由;您可以在中找到它们 bokeh.server.urls .

简而言之:

  • /static/ 服务Bokeh的JS和CSS资源

  • /app_path/ 提供显示新会话的页面

  • /app_path/ws 连接是websocket的URL

  • /app_path/autoload.js 提供支持 bokeh.embed.server_document()bokeh.embed.server_session() 功能

Bokeh服务器不打算是通用的web框架。但是,您可以将新端点传递给 Server 使用 extra_patterns 参数和Tornado API。

其他详细信息

事件

通常,每当修改模型属性时,首先验证新值,然后 Document 收到变更通知。就像模特一样 on_change 回调,a也可以 Document . 当 Document 当它的某个模型发生更改时,它将生成相应的事件(通常是 ModelChangedEvent )并触发 on_change 回调,传递给他们这个新事件。会话就是这样一种回调,它将事件转换为一个补丁,可以通过web套接字连接发送。当客户端或服务器会话接收到消息时,它将提取修补程序并将其直接应用于 Document .

为了避免事件在客户端和服务器之间来回反弹(因为每个补丁都会生成新的事件,而这些事件又会被发回),会话通知 Document 它负责生成修补程序和生成的任何后续事件。这样,当 Session 如果通知文档发生更改,则可以检查 event.setter 与自身相同,因此跳过处理事件。

串行化

一般来说,上面所有的概念对于模型和变更事件的编码和解码是不可知的。每个模型及其属性负责将其值转换为类似JSON的格式,该格式可以通过websocket连接发送。这里的一个困难是,一个模型可以引用其他模型,通常以高度互联甚至循环的方式。因此,在转换为类似JSON格式的过程中,一个模型对其他模型的所有引用都将替换为ID引用。此外,模型和属性可以定义特殊的序列化行为。其中一个例子是 ColumnData 属性 ColumnDataSource ,它将NumPy数组转换为base64编码的表示形式,这比以基于字符串的格式发送数字数组要高效得多。这个 ColumnData 属性 serializable_value 方法应用此编码,from_json方法将数据转换回原处。等效地,JS基于 ColumnDataSource 知道如何解释base64编码的数据并将其转换为JavaScript类型的数组及其 attributes_as_json 方法还知道如何对数据进行编码。通过这种方式,模型可以实现优化的序列化格式。

测试

要测试客户机服务器功能,请使用中的实用程序 bokeh.server.tests.utils .

使用 ManagedServerLoop ,您可以在进程中启动服务器实例。分享 server.io_loop 你可以测试服务器的任何方面。查看现有测试中的许多示例。每当您添加新的websocket消息或HTTP端点时,一定要添加测试!