Jupyter 二次开发思路(1)

上篇文章介绍了 Jupyter 生态及重要组件的原理。基于之前的内容,本文介绍 Jupyter 二次开发的思路。首先介绍项目的需求,接着进一步介绍架构设计,进行demo的实现,最后进行总结。

需求

实现图数据管理分析 BI 平台的 Notebook Service,具备数据的探索、执行分析任务、sql 操作、spark 操作等功能。这个平台目前是单租户的架构,是 to b 的。一般是公司的数据分析团队使用,一个团队一般是十几个人,写代码用到 Notebook 的可能也就那么几个人。所以对性能和可用性没有像 to c 的产品要求那么高。

总结下来,我们的 Notebook Service 应该具备下面的功能:

  • 提供给用户开箱即用的环境
  • 隔离用户的环境,避免互相污染
  • 接入 spark、adb、oss 等平台资源供用户进行探查、分析

目前打算先快速把架构搭起来,先使用自带的 IPython 内核,后面再接入平台的对象存储、数据库和 spark 集群等资源。

架构设计

经过调研,决定采用下面组件来组合实现:

  • 前端 使用经典的 Jupyter Notebook 组件,因为项目不需要太多的功能,只需要实现简单的 Notebook 功能就行。
  • 服务端 使用 Jupyter Server 来转发前端的请求给内核执行。
  • 内核 负责执行代码,将结果返回服务端。暂时使用 IPython Kernel,后面再接入平台的对象存储、数据库和 spark 集群等资源。
  • 部署 使用 Jupyter Hub 组件,用于为实现多用户提供 Notebook,即不同用户使用不同的 Jupyter Server 和 Kernel。
    • Authenticators:实现自定义登录鉴权,可以通过自定义 Authenticator 类并在配置文件中指定来实现。
    • Spawners:用户登录时,Jupyter Hub 会用用户启动一个新实例(Jupyter Server + Kernel)。启动实例是通过 Spawner 实现的。官方提供了多种 Spawner 的实现,包括:本机新的Notebook Server进程、本机启动Docker实例、K8s系统中启动新的Pod、YARN中启动新的实例等等。这些实现本身是可配置的。如果不符合需求,也可以自己开发全新的 Spawner。后续我们需要接入 spark 和 数据库等资源,可以基于官方提供的 Spawner 进行定制,来接入资源。

整个架构如下图所示。不同的客户端通过 JupyterHub 进行登录验证后,可以通过 Jupyter Notebook 前端,访问对应的实例。每个实例即 K8s 的 pod,不同 pod 之间的资源是隔离的。

notebook-architecture

安装部署

下面在本地部署一个 Jupyter Hub,从各个组件的源码进行编译。虽然各个组件都可以通过扩展的方式去开发,但是后期如果有复杂的架构的话,可能需要修改相关的源码,所以通过在本地去编译各个组件的源码,去部署 Jupyter Hub。在本地完成开发后,可以打包成镜像,然后参考 社区的教程 部署到 K8s 上。

# 安装 IPyKernel
pip install ipykernel -i https://pypi.tuna.tsinghua.edu.cn/simple

# 编译 Jupyter Server
git clone https://github.com/jupyter-server/jupyter_server.git
cd jupyter_server
pip install -v -e .
cd ../

# 编译 Jupyter Lab 前端
# 需要注意的是,直接编译 Jupyter Lab,会自动安装 IPyKernel 和 Jupyter Server 等依赖
# 我们将这些步骤放在前面先编译安装了,方便理解
git clone https://github.com/jupyterlab/jupyterlab.git
cd jupyterlab
# 如果执行 pip install -v -e . 报错,就先尝试执行 yarn install 安装前端的依赖
yarn install
pip install -v -e .
# build 前端资源
jupyter lab build
cd ../

# 安装 Jupyter Hub 的 proxy,它的作用是将用户的请求路由给对应的 Jupyter Server
npm install -g configurable-http-proxy --registry=https://registry.npmmirror.com

# 编译 Jupyter Hub
git clone https://github.com/jupyterhub/jupyterhub.git
cd jupyterhub
pip install -v -e .
cd ../

回顾一下之前的简化架构图,会发现上述命令将需要用到的重要组件都安装到了。 JupyterHub 管理多个用户,每个用户都有独立的单用户服务器实例,包括 Jupyter Lab 界面、Jupyter Server 和 IPython 内核。用户通过 JupyterLab 交互界面编写和执行代码,Jupyter Server 处理请求并与内核通信执行代码,最终将结果返回给用户,实现了多用户的交互式计算环境。

Jupyter 架构简化图

上面安装好了 Jupyter 的本地环境后,接下来介绍如何跑起来。

# 创建一个文件夹用来存放配置文件
mkdir jupyterhub-config && cd jupyterhub-config
# 生成配置文件
jupyterhub --generate-config

按下面的配置进行修改,各个配置注释都有解释,也可以参考这里

# 使用 PAM 进行用户身份验证,用户通过操作系统中的有效用户名和密码进行验证
c.JupyterHub.authenticator_class = 'jupyterhub.auth.PAMAuthenticator'

# 让能通过验证的用户成功访问 Hub
c.Authenticator.allow_all = True

然后执行下面命令。

# 以 jupyterhub_config.py 中的配置运行jupyterhub
jupyterhub -f jupyterhub_config.py

控制台会类似输出下面的内容。

需要注意的是 8081 端口是 Hub API 的监听端口,用于与其他组件(如 Proxy 和 Spawner)进行通信的接口,只对 JupyterHub 的内部通信开放,不直接对外提供服务。

8000 端口是 JupyterHub Proxy 的监听端口,负责将用户的请求从这个端口转发到对应的 Jupyter Server 服务,这个端口通常对外开放,用户通过这个端口访问 JupyterHub 的 Web 界面。所以我们在浏览器打开 http://127.0.0.1:8000,输入电脑的登录账号和密码就可以登录成功。

[I 2024-08-15 18:25:27.339 JupyterHub app:3307] Running JupyterHub version 5.2.0.dev
[I 2024-08-15 18:25:27.339 JupyterHub app:3337] Using Authenticator: jupyterhub.auth.PAMAuthenticator-5.2.0.dev
[I 2024-08-15 18:25:27.339 JupyterHub app:3337] Using Spawner: jupyterhub.spawner.LocalProcessSpawner-5.2.0.dev
[I 2024-08-15 18:25:27.339 JupyterHub app:3337] Using Proxy: jupyterhub.proxy.ConfigurableHTTPProxy-5.2.0.dev
[I 2024-08-15 18:25:27.342 JupyterHub app:1882] Writing cookie_secret to /jupyterhub_cookie_secret
[I 2024-08-15 18:25:27.764 alembic.runtime.migration migration:215] Context impl SQLiteImpl.
[I 2024-08-15 18:25:27.764 alembic.runtime.migration migration:218] Will assume non-transactional DDL.
[I 2024-08-15 18:25:27.768 alembic.runtime.migration migration:623] Running stamp_revision  -> 4621fec11365
[I 2024-08-15 18:25:27.827 JupyterHub proxy:556] Generating new CONFIGPROXY_AUTH_TOKEN
[I 2024-08-15 18:25:27.844 JupyterHub app:3376] Initialized 0 spawners in 0.003 seconds
[I 2024-08-15 18:25:27.846 JupyterHub metrics:373] Found 0 active users in the last ActiveUserPeriods.twenty_four_hours
[I 2024-08-15 18:25:27.846 JupyterHub metrics:373] Found 0 active users in the last ActiveUserPeriods.seven_days
[I 2024-08-15 18:25:27.846 JupyterHub metrics:373] Found 0 active users in the last ActiveUserPeriods.thirty_days
[W 2024-08-15 18:25:27.846 JupyterHub proxy:748] Running JupyterHub without SSL.  I hope there is SSL termination happening somewhere else...
[I 2024-08-15 18:25:27.846 JupyterHub proxy:752] Starting proxy @ http://:8000
18:25:28.383 [ConfigProxy] info: Proxying http://*:8000 to (no default)
18:25:28.384 [ConfigProxy] info: Proxy API at http://127.0.0.1:8001/api/routes
18:25:28.763 [ConfigProxy] info: 200 GET /api/routes
[I 2024-08-15 18:25:28.763 JupyterHub app:3690] Hub API listening on http://127.0.0.1:8081/hub/
18:25:28.765 [ConfigProxy] info: 200 GET /api/routes
[I 2024-08-15 18:25:28.765 JupyterHub proxy:477] Adding route for Hub: / => http://127.0.0.1:8081
18:25:28.767 [ConfigProxy] info: Adding route / -> http://127.0.0.1:8081
18:25:28.768 [ConfigProxy] info: Route added / -> http://127.0.0.1:8081
18:25:28.768 [ConfigProxy] info: 201 POST /api/routes/
[I 2024-08-15 18:25:28.768 JupyterHub app:3731] JupyterHub is now running at http://:8000

如果运行的时候发现没有 Kernel,那么可以通过如下命令来安装 Kernel,然后重启

# 查看有没有安装内核
jupyter kernelspec list
# 如果没有的话,添加 IPyKernel
# 这条命令将当前 Python 环境作为一个新的内核安装到 Jupyter 中,安装在用户级别目录
python -m ipykernel install --user --name 内核名称 --display-name "内核显示名称"
# 注意,这条命令是用来删除已经安装的内核。如果需要删除内核的话,执行下面命令。
jupyter kernelspec remove 内核名称

自定义认证

调试

首先介绍如何在 Pycharm 中将 Jupyter 跑起来,方便进行开发调试(如果你不是 Python 开发的话,相信对你会有帮助😄)。

添加如下面图片中的运行配置,需要注意的需要是以 module 的方式去运行项目的入口 jupyter/app.py 文件,然后设置以下命令参数 -f xx,最后设置项目的工作目录为 jupyterhub 项目的根目录。然后就可以愉快的运行调试了👍。

Pycharm run/debug configuration

下面解释一下为什么需要以 module 的方式去运行入口文件。

  • 以 scripy 方式运行文件相当于直接使用 python xxx

  • 以 module 方式运行文件相当于使用python -m xxx

  • 路径处理: 运行 script 时,Python 会根据文件的物理路径直接执行,而运行 module 时,Python 会处理模块路径并将模块的顶级目录添加到 sys.path

  • 导入方式: 在 module 运行时,Python 可以处理相对导入,而 script 运行时则不能直接处理相对导入(因为脚本是作为独立的 __main__ 执行的,没有包的上下文)。

  • 适用场景: script 方式适合单文件或测试文件的简单执行,而 module 方式则适合复杂的包结构,尤其是涉及包内相对导入或模块内部的命名空间管理时。

这两种方式的选择取决于你的项目结构和需要执行的代码位置。在大型项目或需要调试模块化代码时,使用模块方式通常更合适。如果喜欢通过命令行进行调试的话,也可以使用 pdb (类似 gdb)来调试项目。

实现

由于我们对 Jupyter Notebook 进行了二次开发,需要将其作为一个服务集成到我们的图数据分析平台中。因此,我们需要自定义JupyterHub 的认证机制,使其与平台中的认证机制一致。目前,我们的图数据分析平台采用基于 JWT 的令牌认证方式。因此,我们需要在 JupyterHub 中实现基于 JWT 的自定义认证机制。

下面是认证过程的时序图,过程如下所述:

  • 用户访问入口: 用户通过浏览器访问 Jupyter Hub,最初的请求到达 Proxy

  • 跳转 SSO: 如果验证 token 失败,Proxy 会将请求重定向到 SSO 系统进行认证。

  • SSO 登录: 用户完成 SSO 登录,SSO 系统将结果回调到 Hub

  • 实例申请和创建: Hub 接收回调后,为用户请求在 本地创建一个新的 Jupyter Server 实例。

  • 实例创建: 创建实例后,返回相关信息给 Hub

  • 配置和跳转: Hub 设置转发规则,然后将用户的请求重定向到 <username> 路径。

  • 访问 JupyterLab: 用户浏览器访问 <username> 路径,加载 Jupyter Lab 界面。

  • 代码执行和结果返回: 用户在 Jupyter Lab 中请求执行代码,转发给 Jupyter Server 执行,最终返回执行结果给用户浏览器。

notebook-login-sequence-chart

下面将介绍如何实现 Jupyter Hub 的自定义认证。

我们使用 DockerSpawner 来创建新的 Jupyter Server 实例,会为每个用户创建一个容器来运行 Jupyter Server。所以,Jupyter Hub 会和用户容器里的 Server 通信。如果 Jupyter Hub 部署在本地的话,和容器通信是不方便的,所以我们将 Jupyter Hub 放在容器里面部署,将 Jupyter Hub 容器 和 用户实例容器放在一个 network 里面,这样这些容器就可以相互通信。也许你会有疑问,Jupyter Hub 容器里面没有安装 docker,DockerSpawner 如何创建容器呢?我们可以将本地主机的 Docker 守护进程的 Unix 套接字文件(/var/run/docker.sock)挂载到 Jupyter Hub 容器中的相同位置,这样容器内的应用可以像主机上的任何 Docker 客户端一样,与 Docker 守护进程通信,执行 Docker 命令。

首先,我们通过编写一个 Dockerfile 来安装 Jupyter Hub,我们希望能在本地连接这个容器进行开发调试,所以我们像之前一样,自己编译这些组件。下面是 Dockerfile 的内容。

# 使用 Python 3.12 基础镜像
FROM python:3.12

# 安装必要的系统依赖,包括 curl、git、vim 和 SSH 服务器
RUN apt-get update && apt-get install -y \
    git \
    vim \
    curl \
    openssh-server \
    apt-transport-https \
    ca-certificates \
    gnupg-agent \
    software-properties-common \
    && rm -rf /var/lib/apt/lists/*

# 创建工作目录
WORKDIR /srv/jupyterhub

# 配置SSH服务器
RUN mkdir /var/run/sshd

# 设置root用户的密码(注意:这是不安全的,仅用于开发环境)
RUN echo 'root:password' | chpasswd

# 允许root用户通过SSH登录
RUN sed -i 's/#PermitRootLogin prohibit-password/PermitRootLogin yes/' /etc/ssh/sshd_config

# 禁用PAM限制
RUN sed -i 's@session    required   pam_loginuid.so@session    optional   pam_loginuid.so@g' /etc/pam.d/sshd

# 暴露必要的端口,包括SSH端口22
EXPOSE 22 8000

# 启动SSH服务并进入交互式 shell
CMD service ssh start && bash

执行下面的命令构建镜像。

docker build -t jupyterhub-dev .

执行下面的命令创建 network。

docker network create jupyterhub

执行下面的命令运行容器,映射本地存储和端口。

docker run -it --name jupyterhub-dev \
	-v /var/run/docker.sock:/var/run/docker.sock \
	--net jupyterhub \
  -v /Users/tom/notebook-service-demo/docker-image/source-code:/srv/jupyterhub \
  -p 8000:8000 \
  -p 16022:22 \
  jupyterhub-dev

然后在本地的 /Users/docker-image/source-code 目录下,clone Jupyter Server、Jupyter lab 和 Jupyter Hub 的源码。执行 docker attach jupyterhub-dev 进入容器的命令行,按照“安装部署”小节中的内容进行编译刚刚 clone 下来的源码。注意编译 Jupyter Lab 的时候,如果报错,先尝试运行下 yarn install

接下来,在本地的 Pycharm 中打开映射的文件夹里的 JupyterHub 源码,然后通过 SSH 使用容器内的编译器。这样就可以愉快的使用容器内的环境进行开发了,本地更新的代码也会同步到容器内。按照上面“调试”小节中的内容去操作就行。

下面是实现自定义认证的代码。

我们先编写一个自定义认证器,用于实现 JWT 的验证逻辑。

import jwt
from jupyterhub.auth import Authenticator
from tornado import gen
from jwt.exceptions import ExpiredSignatureError, InvalidTokenError
import redis

class JWTAuthenticator(Authenticator):
  secret = "your_secret"
  authKeyFormat="auth_token_key:{}"

  # 初始化 Redis 连接
  def __init__(self, **kwargs):
    super().__init__(**kwargs)
    # 根据 Redis 配置进行修改,host 为运行 redis 的容器名
    # 如果 Redis 在容器中启动,则需要加入和 Jupyter Hub 容器所在的 network 内
    self.redis_client = redis.StrictRedis(host='redis', port=6379, db=0)

  def get_user_id_from_token(self, token):
    try:
      # Debug log for the token being decoded
      self.log.info(f"Decoding token: {token}")

      payload = jwt.decode(token, self.secret, algorithms=['HS384'])
      self.log.info(f"Decoded payload: {payload}")

      user_id = payload.get("sub")
      if user_id:
        return user_id
      else:
        self.log.warning("No user ID found in token")
        return None
    except ExpiredSignatureError:
      self.log.warning("Token has expired")
      return None
    except InvalidTokenError as e:
      self.log.warning(f"Invalid token: {e}")
      return None
    except Exception as e:
      self.log.warning(f"Error decoding token: {e}")
      return None

  @gen.coroutine
  def authenticate(self, handler, data=None):
    token = handler.request.headers.get('Authorization', None)
    if not token:
      self.log.warning("No Authorization header provided")
      return None

    user_id = self.get_user_id_from_token(token)
    authKey = self.authKeyFormat.format(user_id)
    if user_id:
      # 从 Redis 中获取 user_id 的值
      user_exists = self.redis_client.exists(authKey)
      if user_exists:
        return user_id
      else:
        self.log.warning(f"User ID {user_id} not found in Redis")
        return None
    else:
      return None

然后编写配置文件使用上述自定义认证器。

from jupyterhub.auth import LocalAuthenticator
from tornado import gen
import os
import sys

# 获取配置文件所在目录的绝对路径
current_dir = os.path.dirname(os.path.abspath(__file__))
print(current_dir)
# 添加当前目录到 Python 路径
sys.path.insert(0, current_dir)

from jwt_authenticator import JWTAuthenticator
from dockerspawner import DockerSpawner

# Jupyter Hub 的配置文件

c = get_config()  # noqa

# 设置所有用户都可以登录
c.Authenticator.allow_all = True

# 使用 SimpleAuthenticator 进行基于操作系统用户的认证
c.JupyterHub.authenticator_class = JWTAuthenticator

# 设置默认启动 JupyterLab,而不是经典的 Jupyter Notebook 界面
c.Spawner.default_url = '/lab'

# 使用 DockerSpawner
c.JupyterHub.spawner_class = DockerSpawner

# DockerSpawner 的配置
# 设置每次创建实例使用的镜像
c.DockerSpawner.image = 'jupyter/minimal-notebook'
# 设置使用 docker 的自定义网络
c.DockerSpawner.network_name = 'jupyterhub'

# 我们需要 hub 在容器中监听所有 IP
c.JupyterHub.hub_ip = '0.0.0.0'
# 用于连接 hub 的主机名/IP
# 这通常是 hub 容器的名称
c.JupyterHub.hub_connect_ip = 'jupyterhub-dev'

# 禁用 XSRF 跨域保护,运行跨域请求
c.JupyterHub.tornado_settings = {
  'xsrf_cookies': False,
}

然后使用上面的配置文件启动 Jupyter Hub 。通过 apifox 发送下面的请求,将获取的 cookie 存入到浏览器上的 http://localhost:8000 上,打开 http://localhost:8000/hub/。如果可以成功打开,就是成功啦😄。

apifox-1

总结

这篇文章介绍了 Jupyter 二次开发的初步思路。首先介绍项目的需求。接着我们的进一步介绍架构设计,使用了哪些组件。然后介绍了如何编译安装部署 Jupyter Hub 及相关组件。最后在 docker 上实现了 Jupyter Hub 的自定义的 JWT 认证。

需要注意的是,目前的我们的架构会存在下面这些问题,这些问题也是之后需要优化。

  1. hub 单点故障会导致全部实例使用不了
  2. 文件存在实例本地,hub 挂掉就找不到了
  3. 升级比较复杂,需要给挨个实例升级

参考

Zero to JupyterHub with Kubernetes

Jupyter Hub Authenticators

字节博客

美团博客

  • 19
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值