





















本文试图尝试阐述目前 JupyterLab 当前存在的 cell 输出保持问题的原因,并且给出一个基于当前 Jupyter 架构的解决方案。
这个问题,简单描述为就是当我们在 JupyterLab Notebook 中执行一个耗时代码,如果此时我们因为某些原因刷新或者关闭重新打开了页面,我们就永远无法继续跟踪输出了。
我们通过一个例子来更直观地感受:
JupyterLab 在 NodeBook 中运行如下的代码:
1 | import time |

如果此时你强制刷新页面,就会发现标准输出丢失,并且这个时候没有办法再找到。
如果只关注解决方案,可以直接滚动至下文解决方案部分
%%capture var 来捕获一些 stderr,通过 var.show() 来显示,同样的,这个也是只能等到执行完 cell 之后。sys.stdout = open('my_log.log', 'w')总之,解决这个问题的主要困境为:
我们这里经过权衡,设计出一套不依赖 RTC 功能解决该问题的方案。
我们主要需要改动一下几点:
改动的范围主要是 JupyterLab 前端 packages 下的各个模块,后端部分需配合做少量修改。
接下来我们依次详细说明以下改动
JupyterLab 的 workspace 实际上就是你打开多个 JupyterLab 之后,URL 中 /lab/workspaces/*** 后面的那一串,JupyterLab 会针对不同的 Workspace 存储不同的布局等信息,不过笔者认为,Jupyter 的 Workspace 还是有一定缺陷的,接下来会进行详细分析。
在同一个浏览器里面:
每当你打开一个 jupyter 页面,它会做这样几件事情:
defaultping,并 window.addEventListener('storage') 来接受其他页面的 pongwindow.addEventListener('storage') 监听 ping 并返回 pong,携带自己的 workspace 名字abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789 中随机取一个随机数,组成 auto-随机数, 放到 workspace 参数里面,重启整个页面因此:
因为一共就 62 个随机数,假设所有的随机数用完了,会发生什么?
是的,JupyterLab 会直接无限循环 Reload, 无法使用,经过实测,确实如此。
在不同的浏览器里面:
他们完全互不知情,因此:
不同浏览器直接打开的第一个 workspace 都叫做 default,后续的更多 tab 是否冲突看运气。
当你在不同的浏览器 JupyterLab,并且在 JupyterLab 里面打开了一些东西,之后原来浏览器 Tab 刷新,就会被影响到了,这个问题在多用户集群场景下会更加显著。
同时,JupyterLab 后端会把 workspace 配置持久化到文件中,不过自身没有配置文件清理逻辑
所以,针对 Workspace 我们整体的改动为:
这部分可以结合下文“扩展阅读”,来更加深入地了解。
目前,当我们打开一个 Notebook 的时候,默认 Jupyter 就会新开一个 Kernel,然后前端 Notebook 通过建立 websocket(有一个叫做 KernelConnection 的类来管理 websocket) 来和 Kernel 通信。
同一个 Notebook,会随机生成一个 uuid,这个 uuid 是在 KernelConnection 管理类创建的时候生成的:
1 | export class KernelConnection implements Kernel.IKernelConnection { |
然后对于非广播的消息(这里比如 kernel 的状态,它就是一个广播消息,而 python 代码块的具体执行,它是一个非广播消息,这一点更多也可以参考下文扩展阅读),是只会发送到这个 uuid 对应的 KernelConnection 客户端。
也因此,当目前刷新页面之后,这个 uuid 就变了,这个时候就无法对接到之前的非广播消息。
那么 JupyterLab 为什么不保持 uuid 不变呢,主要说法是:
所以,如果我们改变 uuid 的策略,需要在保证 Workspace 的唯一的前提下,设计连接 websocket 的 uuid 为 workspaceId - notebookId
在上一点我们更改了一个 Notebook 连接 kernel 的 uuid,但是对于其中一个 cell 的 uuid 还是随机的,这会导致即使 kernel 收到了消息,也无法得知这个消息是属于哪个 cell。
我们梳理下目前 cell 执行 python 代码的过程,接下来我们简化这个过程:
1 | export function createMessage<T extends Message>(options: IOptions<T>): T { |
这条消息会调用上文提到的 KernelConnection 的实例方法 sendShellMessage,sendShellMessage 会把消息发送给后端,同时维护一个基于 msgId 的绑定关系。
后端 Kernel 执行完之后返回消息。返回的消息和发送的消息比较类似,比较重要的字段是 parent_header,parent_header 即发送的消息体中的 header 字段,其中存储 msgId。
Notebook 客户端收到消息后,通过 parent_header 中的 msgId 找到对应的 cell(这里实际上是执行一个 callback,callback 通过闭包捕获之前 cell 的相关引用),然后更新状态。
也就是说,当我们更改 cell 的 msgId 计算方式的时候,实际上大多数时候上面的逻辑都是没有变化的,只是在刷新页面之后,因为这个时候之前的回调等逻辑是不存在的,我们需要手动找到这个 cell,然后把结果输出。
基于上文我们 Workspace 和 uuid 的更新设计,我们已经可以把一个页面的一条消息对应到一个 cell 中,并且这里的绑定信息是可推断、可持久化、非随机的,也就是说,我们已经有了完成恢复功能的能力。
不过实际上要完成这个逻辑,还要加的内容非常多,Notebook cell 执行相关的逻辑本身状态判断较多且调用复杂,我们新加直接输出的逻辑不仅需要融入到现有的逻辑中,而且需要考虑各种边界情况,无异于做一次重构。
也因为第四点过于复杂,笔者并没有完全验证,只是依靠控制台输出做了一些分析验证可行性的工作。
本文提供的解决方案,能够解决我们遇到的输出丢失问题,不过同时,它也是有一些副作用的,比如:
文件 uuid 的概念,所以当文件名称变了,这个 uuid 也会跟着变化,这里的方案可能是:为了方便更加深入了解 JupyterLab Client->Kernel 的架构,我们补充一些扩展信息。
当前的 Jupyter 的 Notebook-Kernel 是一个多对一的架构,也就是说,我们可以开多个页面连接到相同的一个 Kernel,同时每个页面都执行不同的 cell,它们是都可以正确的和输出对应起来的。

同时,对于一些比如 kernel 状态的信息,是会广播给所有客户端的。
目前 Jupyter 通过 jupyter_client 这个包来和 kernel 进行管理和通信,这个包虽然叫做 client,但是是在 server 端,使用的,主要是和 jupyter 的 kernel 通信。
默认使用的是 ipykernel
启动代码:
1 | ['/path/to/bin/python', '-m', 'ipykernel_launcher', '-f', '/path/to/Jupyter/runtime/kernel-85259599-797f-4b21-b701-2a63c96fbe10.json'] |
kernel 文件中会存储通信端口等一些信息:
1 | { |
jupyter_client 使用 zmq 来做和 kernel 之间的通信。
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。