多实例多用户的 Jupyterhub

前言

  Jupyter, 想必大家对这个项目都耳熟能详吧。因为能够实时交互、支持异构计算、部署简单、几乎无运维成本,所以得到了很多人的青睐。笔者的身边也有很多从事科学研究的人选择了 Jupyter 作为编写 Python 的工具,当然也有一部分人选择了 PyCharm。不过笔者还是比较喜欢 VS Code,简单的纯文本编辑功能,利用丰富的插件市场来添加各种想要的功能,无缝支持远程开发,简直就是理想中的编辑器了。但是,今天还是要来考虑一下 Jupyter,毕竟 JupyterLab 的服务功能也是非常强大的。

  在 Jupyter 出现之前,也有可以替代 Python 自带的 Python Shell 的 IPython。笔者在早期也曾使用过,体验还不错。其实,Jupyter 就是2014 年从 IPython 中衍生出来的,所以从 IPython 过渡到 Jupyter 毫无困难。如果说 IPython 是为了 Python 而量身定制的话,那么 Jupyter 则是为包括 Julia、Python、R 在内的几十种编程语言(详情连接)的交互式数据科学和科学计算而生的。

  早期的 Jupyter 只包含 Jupyter Kernels 和 Jupyter Notebook,其中 Jupyter Kernels 是用于支持编程语言的内核,Jupyter Notebook 是基于 Web 的交互式计算环境,前身是 IPython Notebook。现在的 Jupyter 除了这两者以外还有 JupyterHub、JupyterHub API 和 JupyterLab。JupyterHub 是一个用于 Jupyter Notebook 的多用户服务器。它通过生成、管理和代理许多单一的 Jupyter Notebook 服务器来支持多用户。JupyterHub API 是以 REST 风格向开发者们提供的 API 接口,可以完成一系列对 Jupyter 的操作,比如生成用户环境、配置环境等。JupyterLab 号称是 Jupyter 项目的下一代用户界面,它以一个灵活且强大的用户界面向用户提供经典的 Jupyter Notebook、终端、编辑器、文件浏览器、丰富输出等模块,俨然像是朝着现代化的理想编辑器的目标进发的。

  无论之前的 Jupyter 是什么样子,现在的 JupyterLab 已经是和曾经的 Cloud9 (一款先进的在线代码编辑器,现已被 AWS 收购)一样的支持多用户多实例的代码运行平台。对于从事科学研究的团队来说,使用 JupyterLab 搭建一个内部科学计算平台成为了可能。当然,个人用户还是可以选择使用 Anaconda 或者 PIP 来安装单用户版本。

搭建

  说到搭建平台自然而然想到了使用 Docker,既可以保证用户对自己所需的软件或环境可以修改,又保证不同用户之间互不干扰、宿主机与 Jupyter 之间互不干扰。虽说 Jupyter 官方提供了一个使用 Docker 来部署 Jupyter 各个产品的 文档网站,但不得不说即使看了这个文档也很难搞清楚到底怎么部署一套 JupyterLab。可能唯一有用的就是 Jupyter 官方提供的镜像构建 Dockerfile 集合 吧。

  JupyterLab 提供两种方式启动多用户多实例:

  • DockerSpawner 方式:每个用户独享一个 Docker 实例,能有效隔离用户。
  • SystemSpawner 方式:共享同一个 Docker 实例,以系统用户身份运行。

  事实上,既然我们选择了用 Docker 来部署,自然而然应该选择 DockerSpawner 方式了。JupyterLab 中主要实现多用户多实例功能的是 JupyterHub 模块(如下图)。JupyterHub 模块为整个 JupyterLab 对外提供了一个共同的 HTTP 接口,并可以进行用户鉴权和为通过鉴权的用户创建一个新的 Docker 实例。笔者在这里主要是使用 Gitlab 方式鉴权登录,图中涉及到 Admin 以及数据库这里不作探讨。

架构图 JupyterHub Design

  以下为搭建所需的文件的列表:

文件列表 Files

构建 Jupyter Notebook 实例镜像

基础镜像 base-notebook

  这里的基础镜像可以根据需要自行选择,与 jupyter/docker-stacks 相比镜像构建 Dockerfile 有些内容做了修改,本目录下其他文件和 base-notebook 目录 一致。

# Dockerfile
ARG ROOT_CONTAINER=nvidia/cuda:10.2-devel-ubuntu18.04

ARG BASE_CONTAINER=$ROOT_CONTAINER
FROM $BASE_CONTAINER

ARG NB_USER="ubuntu"
ARG NB_UID="1000"
ARG NB_GID="100"

# Fix DL4006
SHELL ["/bin/bash", "-o", "pipefail", "-c"]

USER root

# Install all OS dependencies for notebook server that starts but lacks all
# features (e.g., download as all possible file formats)
ENV DEBIAN_FRONTEND noninteractive
RUN apt-get update \
    && apt-get install -yq --no-install-recommends wget htop vim bzip2 ca-certificates sudo locales fonts-liberation run-one \
    && apt-get clean \
    && rm -rf /var/lib/apt/lists/*

RUN echo "en_US.UTF-8 UTF-8" > /etc/locale.gen && \
    locale-gen

# Configure environment
ENV CONDA_DIR=/opt/conda \
    SHELL=/bin/bash \
    NB_USER=$NB_USER \
    NB_UID=$NB_UID \
    NB_GID=$NB_GID \
    LC_ALL=en_US.UTF-8 \
    LANG=en_US.UTF-8 \
    LANGUAGE=en_US.UTF-8
ENV PATH=$CONDA_DIR/bin:$PATH \
    HOME=/home/$NB_USER

# Copy a script that we will use to correct permissions after running certain commands
COPY fix-permissions /usr/local/bin/fix-permissions
RUN chmod a+rx /usr/local/bin/fix-permissions

# Enable prompt color in the skeleton .bashrc before creating the default NB_USER
RUN sed -i 's/^#force_color_prompt=yes/force_color_prompt=yes/' /etc/skel/.bashrc

# Create NB_USER wtih name ubuntu user with UID=1000 and in the 'users' group
# and make sure these dirs are writable by the `users` group.
RUN echo "auth requisite pam_deny.so" >> /etc/pam.d/su \
    && sed -i.bak -e 's/^%