当前位置: 首页 > 科技观察

一文读懂DataLeap中的Notebook

时间:2023-03-12 10:33:44 科技观察

一、概述Notebook是一个支持REPL模式的开发环境。所谓“REPL”就是“读取-求值-输出”循环:输入一段代码,立即得到相应的结果,继续等待下一次输入。它通常使探索性开发和调试更加容易。在笔记本环境中,您可以交互式地编写代码、运行代码、查看输出、可视化数据和查看结果,使用起来非常灵活。在数据开发领域,Notebook被广泛应用于数据清洗与转换、数值模拟、统计建模、数据可视化、机器学习模型的构建与训练等方面。但显然,数据开发仅靠Notebook是不够的。在火山引擎DataLeap数据研发平台上,我们提供了任务开发、发布调度、监控运维等一系列能力。我们在数据研发平台中新增了Notebook作为任务类型,让用户在拥有Notebook交互开发体验的同时,也能享受到一站式大数据研发管理套件带来的便利。如果还不够直观,想象一下这样的场景:借助交互式操作和可视化图表,快速调试并完成一个Notebook。简单梳理一下代码,根据使用的数据配置上游任务依赖,启动周期调度,挂断告警。之后,这个任务你就基本不用操心了:不用每天手动去检查上游数据是否就绪;不用每天都点击运行,因为调度系统会自动为你执行Notebook;执行失败会有报警,可直接登录平台处理;如果上游数据有错误,可以要求他们发起深度回溯,统一修改。2.模型选择2019年底决定支持notebook任务时,考察了很多notebook的实现,包括Jupyter、Polynote、Zeppelin、Deepnote等,JupyterNotebook是Notebook的传统实现。它拥有极其丰富的生态和庞大的用户群体。相信很多人都用过这款软件。事实上,在字节跳动数据平台发展初期,就有针对内部用户在物理机集群(基于多用户解决方案JupyterHub)上统一部署Jupyter。考虑到用户使用习惯和强大的生态,Jupyter最终成为了我们的选择。JupyterNotebook是一个网络应用程序。一般认为它有两个核心概念:Notebook和Kernel。notebook是指代码文件,一般存放在文件系统中,后缀为ipynb。JupyterNotebook后端提供了管理这些文件的能力,用户可以通过JupyterNotebook的页面创建、打开、编辑和保存Notebook。在Notebook中,用户以Cell的形式编写代码,并通过Cell运行代码。Notebook文件的具体内容格式请参考TheNotebook文件格式(https://nbformat.readthedocs.io/en/latest/format_description.html)。Kernel是Notebook中代码的实际运行环境,是一个独立的进程。每个“运行”动作都会产生执行单个Cell代码的效果。具体来说,“运行”就是将Cell中的代码片段通过JupyterNotebook后端以特定格式发送给Kernel进程,然后从Kernel接收特定格式的返回,反馈给页面。这里所说的“具体格式”可以参考MessaginginJupyter(https://jupyter-client.readthedocs.io/en/stable/messaging.html)。在DataLeap数据研发平台中,开发过程的核心是任务。用户可以在项目下的任务开发目录下创建子目录和任务,像IDE一样通过目录树管理自己的任务。笔记本也是一种任务。用户可以启动一个独立的任务Kernel环境,像开发其他普通任务一样使用Notebook。3、技术路线在Jupyter的生态中,除了Notebook本身,我们还注意到了很多其他的组件。当时,JupyterLab正在逐渐取代传统的JupyterNotebook界面,成为新的标准。JupyterHub被广泛使用,是多用户笔记本的答案。EnterpriseGateway(EG)脱胎于JupyterKernelGateway(JKG),提供了我们需要的RemoteKernel(前述独立任务Kernel环境)能力。2020年上半年,我们基于以上三个组件进行了二次开发,在字节跳动数据研发平台上发布了Notebook任务类型。整体结构预览如图所示。在JupyterLab的前端方面,我们选择了基于更现代的JupyterLab(https://jupyterlab.readthedocs.io/en/stable/getting_started/overview.html)进行改造。我们去掉了它外围的view,只留下中间的Cell编辑区,嵌入DataLeap数据开发页面。为了更好地匹配DataLeap的视觉风格,从2020年下半年到2021年初,我们也有针对性地改进了JupyterLab的UI。这包括将JupyterLab使用的代码编辑器从CodeMirror统一为DataLeap数据研发使用的MonacoEditor,以及接入DataLeap提供的Python&SQL代码智能补全功能。此外,我们还开发了定制化的可视化SDK,让用户在笔记本上计算的PandasDataframe可以连接到DataLeap数据研发提供的数据结果分析模块,直接在里面进行一些简单的数据探索笔记本。JupyterHubJupyterHub(https://jupyterhub.readthedocs.io/en/stable/)提供可扩展的认证能力和环境创建能力。首先,由于用户数量众多,为每个用户设置单独的Notebook实例是不切实际的。所以我们决定将Notebook实例按DataLeap项目进行拆分,共享一个实例给项目下的用户(即一个项目其实就是JupyterHub中的一个用户)。这也符合DataLeap的项目权限体系。请注意,此处的“Notebook实例”在我们的配置下会拉出一个用于运行JupyterLab的环境。此外,由于我们将使用远程内核,因此此环境中不提供运行内核的能力。在认证鉴权方面,我们让JupyterHub请求我们业务后台提供的验证接口,判断登录用户是否拥有对应DataLeap项目请求的权限,从而实现权限体系的对接。在环境搭建上,我们通过OpenAPI对接字节跳动内部的PaaS服务,为每个使用Notebook任务的DataLeap项目分配一个JupyterLab实例,对应一个PaaS服务。由于直接新建一个服务的过程又长又慢,我们额外实现了池化,预启动一批服务,在有新项目的用户登录时直接分配。企业网关Jupyter企业网关(https://jupyter-enterprise-gateway.readthedocs.io/en/latest/)提供了在分布式集群(包括YARN、Kubernetes等)内部启动Kernel的能力,成为Notebook进入集群Kernel的代理。在原生Notebook系统中,Kernel是JupyterNotebook/JupyterLab中的一个本地进程;对于开启了Gateway功能的Notebook实例,所有与Kernel相关的功能请求,比如获取Kernel类型、启动Kernel、运行Cell、中断等,都会被代理到指定的Gateway,然后Gateway被代理到特定集群中的Kernel,形成RemoteKernel模式。这样做的好处是Kernel和Notebook是分离的,不会互相影响:比如一个Kernel运行超出物理内存限制,不会导致其他同时运行的Kernel挂掉,即使它们是全部通过同一个笔记本实例使用。EG本身提供的Kernel类型与字节跳动内部系统不完全兼容,需要我们自己修改添加。我们先把字节跳动内部的YARN集群以SparkKernel的形式连接起来。Kernel以PySpark的形式以Cluster模式运行在SparkDriver上,提供默认的SparkSession。用户可以通过Driver上的Kernel直接启动和运行Spark相关的代码。同时,为了满足Spark用户的使用习惯,我们额外提供了在同一个Kernel中交叉运行SQL和Scala代码的能力。2020年下半年,随着云原生的浪潮,我们也接入了字节跳动的云原生K8s集群,为用户提供一个PythononK8sKernel。我们还扩展了许多自定义功能,例如支持自定义图像和SparkKernel的自定义Spark参数。在稳定性方面,在当时的版本中,EG存在异步性不足的问题。在YARN场景下,单个EG进程只能运行十几个Kernel。我们发现了这个问题,完成了处处需要的异步逻辑改造,保证服务的并发。另外,我们利用字节跳动内部负载均衡(nginx七层代理集群)能力,部署多个EG实例,并规定单个JupyterLab实例的流量始终打在同一个EG实例上,实现了基本的HA。4.架构升级当使用Notebook的项目与日俱增时,我们发现运行的PaaS服务太多,之前的架构造成部署麻烦。全面升级JupyterLab比较痛苦。虽然有升级脚本,但是通过API操作升级服务可能会因为镜像构建失败等原因出现卡顿现象。所以每次全量升级后,都会进行人工巡检,检查升级状态,卡住的升级单手动点击下一步。同时,由于升级不同的服务不会复用相同配置的镜像,有多少服务就需要构建多少镜像。当服务数量达到一定数量时,我们的批量升级请求可能会压垮内部的镜像构建服务。JupyterLab需要根据用户增长(项目增长)不断扩展。一旦预启动的资源池不够用,就会有新项目的用户打开Notebook,需要经历整个JupyterLab服务创建和环境拉起的过程,速度慢,影响体验。而且,大量使用JupyterLabs之后,遇到badcase的概率增加了。有些问题不容易重现,而且非常偶发。重启/迁移可以解决,但遇到时,会极大影响用户体验。运维困难。当用户JupyterLab可能出现问题时,为了找到对应的JupyterLab,我们需要根据项目对应JupyterHub用户,然后根据用户找到JupyterHub记录的serviceid,然后去PaaS平台查找服务并进入webshel??l。当然,也有资源浪费。尽管每个实例很小(1c1g),但数量很多;有些项目并不总是使用笔记本,但JupyterLab仍在运行。稳定性有问题。一方面,JupyterHub是单点的,升级需要先开后停,有挂掉的风险。另一方面,EG入站流量通过特定的负载均衡策略,这本身就是让JupyterLab固定一个请求到一个EG。EG升级后,JupyterLab请求的终端也会随之改变。极端情况下,可能会导致Kernel多次启动。基于简化运维成本、降低架构复杂度、提升用户体验的考虑,2021年上半年,我们对整体架构进行了改进。在新架构中,我们主要做了以下改进,大致简化为下图:去掉JupyterHub,将JupyterLab改为多实例无状态常驻服务,实现多用户认证对接DataLeap。改造原本落在JupyterLab本地的数据存储,包括自定义配置、Session维护、代码文件读写。EG支持持久化Kernel,将Kernel的远程环境的元信息持久化到远程存储(MySQL)上,这样重启的时候可以重新连接,JupyterLab可以知道某个Kernel需要通过哪个EG连接。Authentication&Security单用户JupyterNotebook/JupyterLab的认证比较简单(其实JupyterLab直接复用了JupyterNotebook的代码)。比如使用默认命令启动时,会自动生成token,同时浏览器会自动启动。有了令牌,你就可以任意访问这个Notebook。其实JupyterHub也起到维护token的作用。前端会发起API请求获取token,然后将获取到的token请求通过JupyterHub代理带到真实的Notebook实例。而我们直接在JupyterNotebook中添加了Auth功能,在JupyterLab单实例上实现了这套认证(此时使用的是DataLeap服务颁发的Token)。最后,由于所有用户都会共享同一组JupyterLabs,我们还需要禁止对一些接口的调用,以保证系统安全。最典型的接口包括关闭服务(Shutdown)、修改配置。后续notebook需要的配置会被前端保存在浏览器中。Code&SessionPersistenceJupyterNotebook使用文件管理器(https://github.com/jupyter-server/jupyter_server/blob/main/jupyter_server/services/contents/filemanager.py)来管理Contents相关的读写(主要针对我们Notebook代码文件),原生行为是将代码存储在本地,同一份代码不能在多个服务实例之间共享,并且在迁移过程中可能会丢失代码。为了避免代码丢失,我们的做法是将代码按项目存放在OSS上,直接读写。同时解决了其他一些由于代码文件元信息丢失和并发编辑导致的问题。比如多个页面访问同一个代码文件时,会从OSS获取最新的代码。当用户保存时,前端会获取最新的代码文件,并比较该文件的修改时间是否与前端保存的一致。如果没有,说明已经存储了其他页面,会提示用户选择覆盖还是恢复。Notebook使用Session来管理用户与Kernel的连接。比如前端通过POST/session接口启动Kernel,通过GET/session查看当前运行的Kernel。在Session处理方面,原生Notebook使用原生sqlite(在内存中),见代码(https://github.com/jupyter-server/jupyter_server/blob/main/jupyter_server/services/sessions/sessionmanager.py)。虽然我们不明白这个有什么意义(毕竟重启原来的Notebook,一切都没有了),但是我们继续用这个原生的表结构往前走,引入sqlalchemy连接各种数据库,移动session数据到MySQL。另一方面,因为我们启动的部分Kernel涉及到SparkonYARN,所以启动速度不是很理想,所以我们前期加了一个功能。如果内核已经在特定路径中启动,请等待它启动而不是重新启动它。一个新的。该功能最初是使用in-memoryset实现的,现在也移植到数据库中,通过sqlalchemy访问。内核持久性和访问在远程内核场景中,JupyterLab需要知道它的内核之一在哪个EG上。在之前项目一个JupyterLab的状态下,我们简单的通过负载均衡的方式来处理这个问题:即一个Server总是只访问同一个Gateway。但是,当JupyterLab成为无状态服务后,用户并不总是只访问一个JupyterLab,因此不能保证用户总是访问Kernel所在的EG。还有一种情况是当JupyterLab或者EG重启的时候,上面的Kernel会关闭。当我们升级相关服务时,总是需要通知用户准备重启Kernel。因此,为了实现升级对用户不敏感,我们在EG层开发了persistentKernel的特性。KernelGateway在启动Kernel时,会记录一些关于Kernel的元信息,包括启动参数、用于连接Kernel的IP/Port等。有了这些信息,当内核网关重新启动并且远程内核没有关闭时,就有了重新连接的方法。本来这个信息默认是维护在内存dict中的,开源仓库有存储在本地文件中的方案;在此方案的基础上,我们扩展了自研的MySQL存储方案。在多实例场景下,每个EG实例仍然会接管自己的部分Kernel,并记录每个Kernel由谁接管(probing、cullidle、connectionusage等)。在它关闭之前,需要清除接管信息,以便在下次启动或其他实例启动时可以拾取它。为了减少客户端(通常是JupyterLab)随意访问EG的情况,一方面,我们继续使用负载均衡策略,另一方面,在JupyterLab请求Kernel相关操作之前,会先请求EG一次,EG决定哪个EGJupyterLab具体请求IP/Port。当EG服务自身重启或升级时,会在进程退出前清除接管信息。当页面继续被访问时,JupyterLab服务会随机分发相应的请求,其他EG服务会继续接管。收入结构升级简化后,整个Notebook服务的稳定性得到了极大的提升。由于实现了用户无感升级,不仅提升了用户体验,也降低了运维成本。部署成本也大大降低,包括节省计算能力和人力。由于剥离了内部依赖,我们可以将这个架构部署在各种公有云和私有化场景中。5.调度方案前面我们重点介绍了如何将Jupyter应用嵌入到DataLeap数据研发中。这只会覆盖我们笔记本任务的页面调试功能。其实作为一个调度系统,我们也需要关心一个Notebook任务如何调度。首先,它与所有其他任务类型相同的部分是:当Notebook任务配置的所有上游依赖任务运行完毕后,开始运行Notebook任务。基于任务的版本,我们创建任务的快照,我们称之为任务实例,并将其提交给我们的执行者。对于Notebook任务,在实例运行前,我们会根据Notebook任务对应的版本,从OSS上拷贝一份Notebook代码文件执行。在具体的执行过程中,我们使用Jupyter生态中的nbconvert(https://nbconvert.readthedocs.io/en/latest/)在没有Jupyter应用程序的情况下在后台运行这个Notebook文件,而运行后得到的Notebook文件被送回OSS。nbconvert的工作原理比较简单,复用了Jupyter的底层代码,如下:根据指定的KernelManager或者Notebook文件中的Kernel类型创建对应的KernelManager(https://github.com/jupyter/jupyter_client/blob/main/jupyter_client/manager.py);KernelManger创建一个KernelClient并启动一个Kernel;遍历Notebook文件中的Cell,调用KernelClient执行Cell中的代码;获取输出结果,按照nbformat指定的schema填写NotebookNode,保存。下图是调度Notebook执行的Kernel运行流程和通过调试运行EG的RemoteKernel运行流程对比。可以看出它们的链接没有本质区别,只是在调度执行时不需要交互式的Kernel通信,EG的这些KernelLauncher都是使用embed_kernel来启动同一个进程中的Kernel。往下看,都是用的ipykernel(其他语言的内核也是一样)。6、未来的工作Notebook任务已经成为字节跳动内部使用比较频繁的任务类型。在火山引擎中,我们还可以购买一站式大数据研发管理套件DataLeap,开启交互式分析版,使用DataLeap的Notebook任务。有时候,我们发现自己领先Jupyter社区半步:比如基于asyncio异步优化的EG;例如为Notebook添加Auth功能。但是社区的发展也非常快:比如社区已经将Jupyter后端相关代码的实现统一到了jupyter_server;比如EG作者提出的KernelProvider方案,让jupyter_server直接支持RemoteKernel。所以我们没有就此止步。目前这个Notebook服务和DataLeap开发的其他前后端服务还是有分离的。未来我们希望精简架构,实现彻底的整合,让Notebook不嵌入DataLeap的产品中,而是在DataLeap的数据研发中原生支持,带来更好的性能,同时保留Jupyter自带的所有强大功能生态系统。另一方面,由于DataLeap数据研发平台支持流式数据的开发,我们也希望通过Notebook来满足用户对流式数据的探索、调试、可视化等功能的需求。相信在不久的将来,Notebook一定能够实现流批合一,服务于更广泛的用户群体。