Minikube 本地部署 Jupyter 集群
背景
Jupyter 是兼備優秀的編程體驗和交互體驗的在線 IDE,它能讓你的代碼不再受限于自己的電腦環境,也不只是編程語言本身,而是可以結合數學公式、Markdown、命令行等各種語法和指令,在更高性能的服務器以更靈活的交互方式,隨處運行,隨時可見結果。也正因為這些特性,它在教學、大數據分析、機器學習等領域都能助開發者一臂之力

在最近一年維護和使用 Jupyter 服務的過程中,我部署 Jupyter 的方式也經歷了從 linux 服務器單點,到 minikube 本地部署模擬集群,再到生產環境 k8s 正式部署集群的升級過程。之所以要將 Jupyter 從單點升級成集群,主要是為了解決單點會遇到的可靠性、可擴展性和可維護性等問題

上面的問題要解決,首先需要一套可以管理集群的基礎服務,這個服務就是 Kubernetes ,而借助 Jupyter 官方提供的 helm chart,幾行配置幾條指令,就可以部署成集群了,“理論上”整個過程可以非常順滑
當然,上線生產之前必不可少的過程是測試。在這篇文章中,我們就以自己電腦作為基本環境,借助 minikube 和 k8s 生態下的工具, 部署一套類似生產環境的 Jupyter
事不宜遲,讓我們先從了解 Jupyter 服務開始,一步步深入了解如何在本機玩轉 Jupyter 集群吧
Jupyter 生態
Jupyter 在組件通信、頁面交互和后臺運行環境上,都具有各司其職的組件,這也形成了 Jupyter 獨一無二的生態

若從最關鍵的用戶使用場景入手,最重要的組件有以下四個:
-
JupyterHub: 登錄頁面,只提供簡單的用戶信息輸入框( 不過默認的 Spawner 和 Authticator 也在其源碼中 )
-
JupyterLab: 新一代的 Notebook 編輯器,界面功能更豐富,且支持通過插件系統擴展更多功能(如 git、資源查看器等)
-
Spawner: 登錄 JupyterHub 后實際運行的 IDE(主要是 Notebook 或者 JupyterLab ),IDE 可以自定義運行環境: 跑在本地、 k8s 或者 yarn 等等。一個用戶 IDE 進程也可以稱作 Jupyter Server
-
Ipykernel: 用戶運行每一個 Notebook,又會在 Spawner 運行的服務器上啟動一個個負責運行代碼的進程。它既是進程,也是服務器上預裝的不同開發環境( 比如可選擇在不同 Python 版本作為運行代碼的環境 )
只是簡單介紹這些組件的概念,并不能講清楚它們之間的協同和交互機制。相信通過接下來的實踐,它們各自的發揮的作用都會在我們面前清晰展現出來
環境準備
既然要在 k8s 部署 Jupyter,第一步當然需要把 k8s 先啟動起來
在自己電腦啟動 k8s 可選的工具有 minikube, kind, k3s, microk8s 等,它們實際啟動集群的過程是差不多的。這里筆者從功能的完善程度考慮,選擇了 minikube
在文章最后會提到 k3s 也可作為在電腦資源受限情況下的備選
minikube
minikube 是一個在本地快速啟動 k8s 集群的工具,由 kubernetes 社區開源,支持多種虛擬化運行環境(Docker、Hyper-V、VirtualBox、podman 等)。它基于一個輕量級虛擬機 coreboot 啟動,因此 minikube 也支持跨平臺
minikube 可執行文件可以直接從 官方倉庫 或者 官方網站 下載
Docker 鏡像源配置
注: 由于 minikube 網絡插件的依賴,會導致非 root 權限運行的 podman 無法啟動 minikube,筆者為測試 root 的 podman,目前還是建議使用 Docker
Docker 的具體的安裝方式這里就不贅述了。不過還是要提一下鏡像源的配置
網易云、中科大等之前常用的比較官方的鏡像源現在都用不了,其他第三方可用的鏡像源可以 參考這里 或 這里, 目前可用的部分鏡像源如下:
// vim /etc/docker/daemon.json
{
"registry-mirrors": [
"https://docker-0.unsee.tech",
"https://docker.1ms.run",
"https://docker.m.daocloud.io"
"https://docker.xuanyuan.me",
]
}
注: 一切網絡問題都不是等等就能好的,再等也是浪費時間,這也是在開始就要把國內鏡像源這種信息告訴大家的原因
minikube 安裝和啟動
啟動 minikube 遇到的第一個坑,就是經典的國內網絡問題: 下載鏡像和二進制文件時不太順暢
首先 minikube 運行的基礎系統鏡像 kicbase 會按照 docker.io、 gcr.io 和 github release 的順序嘗試下載。然而這些下載地址都在外網,都可能下載不成功
其次,minikube 還會下載 kubectl, kubelet 和 kubeadm 這三個用于訪問和管理 k8s 集群的可執行文件(下載后放在虛擬機內部)在設置了 image-mirror-country=cn 參數后,下載地址會被替換成 kubernetes.oss-cn-hangzhou.aliyuncs.com,但是這個地址并沒有提供這些文件,所以還需要配置正確的 binary-mirror 參數
(不得不說坑是真的多啊)
正是因為有這些問題,建議通過源碼編譯來生成 minikube 二進制文件,方便快速修正鏡像下載問題
# linux 下載 minikube
curl -LO https://github.com/kubernetes/minikube/releases/download/v1.36.0/minikube-linux-amd64
# windows
curl -LO https://github.com/kubernetes/minikube/releases/download/v1.36.0/minikube-windows-amd64.exe
# github 加速
curl -LO https://ghfast.top/https://github.com/kubernetes/minikube/releases/download/v1.36.0/minikube-linux-amd64
curl -LO https://ghfast.top/https://github.com/kubernetes/minikube/releases/download/v1.36.0/minikube-windows-amd64.exe
# 安裝 minikube
sudo install minikube-linux-amd64 /usr/local/bin/minikube
# 通過源碼編譯 minikube
git clone https://github.com/kubernetes/minikube
git checkout tags/v1.36.0
## 替換 docker.io/kicbase/stable 下載鏡像源
sed -i "s#\"docker.io/#\"docker.1ms.run/#g" pkg/drivers/kic/types.go
## 編譯
make
mv out/minikube /usr/local/bin/
# 啟動 k8s 1.33.1 版本
minikube start --kubernetes-version=v1.33.1
# “最終版”啟動 k8s 參數 (適配國內網絡)
# --v 和 --log_file: 增加日志輸出,并打印到指定日志文件中,方便定位問題
minikube start --kubernetes-version=v1.33.1 --iso-url https://ghfast.top/https://github.com/kubernetes/minikube/releases/download/v1.35.0/minikube-v1.35.0-arm64.iso --image-mirror-country=cn --image-repository=registry.cn-hangzhou.aliyuncs.com/google_containers --binary-mirror https://files.m.daocloud.io/dl.k8s.io/release --v=8 --log_file /tmp/test.log
# 擴展: 限定運行資源、磁盤大小等
minikube start --cpus=2 --memory=4096 --disk-size=20g ...
# 擴展: 指定鏡像下載路徑
export MINIKUBE_HOME=/opt/minikube
順利啟動 minikube 后,我們在命令行可以通過 minikube 虛擬機內部的 kubectl 來訪問集群,或者為了操作更方便可以在主機也安裝 kubectl( minikube 啟動時已經在本機設置好了 ~/.kube/config )
# kubectl 安裝
curl -LO https://dl.k8s.io/release/v1.33.0/bin/linux/amd64/kubectl
curl -LO https://dl.k8s.io/release/v1.33.0/bin/windows/amd64/kubectl.exe
# 國內源
curl -LO https://files.m.daocloud.io/dl.k8s.io/release/v1.33.0/bin/linux/amd64/kubectl
curl -LO https://files.m.daocloud.io/dl.k8s.io/release/v1.33.0/bin/windows/amd64/kubectl.exe
# 或者通過 k8s 源碼安裝
git clone https://github.com/kubernetes/kubernetes
make kubectl
mv _output/local/bin/linux/amd64/kubectl /usr/local/bin
# 獲取所有 pod
kubectl get pods --all-namespaces
# 使用 minikube 自帶的 kubectl
minikube kubectl -- get pods --all-namespaces
通過 kubectl get pods -A 查看所有 pod,可以看到 minikube 啟動了一套標準的 k8s 組件,簡單說明每個組件的作用

-
CoreDNS: k8s 集群內部的 DNS 服務,負責提供 k8s 內服務的訪問地址,前身是 kube-dns,現在發展成 CNCF(Cloud Native Community Foundation 云原生基金會) 中的獨立項目
-
ETCD: k8s 的元數據服務,記錄了當前集群的所有節點、配置、服務、密鑰等等信息
-
kube-apiserver: 提供k8s資源( pod, deployment, service 等)的 restful api 接口
-
kube-controller-manager: 通過 api 對集群資源進行管理和控制,在資源出現異常的時候自動恢復
-
kube-proxy: 在每個 k8s 節點(node)上運行,負責 監測 apiserver 收到的用戶請求,并將請求轉發到具體的 Pod 進行處理
-
kube-scheduler: 負責將 Pending 狀態的 pod 分配到最合適的 node 上運行
-
storage-provisioners: 在服務需要掛載存儲目錄的時候,自動從本地分配一塊目錄空間給服務使用
dashboard
dashboard 是 kubernetes 官方提供的 k8s 集群看板,通過它能夠看到集群的基本狀態、資源、日志等,盡管功能簡單,還是建議在首次啟動時候打開它,方便后續查看 pod 日志定位問題
minikube 內置 dashboard, 執行 minikube dashboard 即可打開


helm
在不使用 k8s,只用 docker 如何啟動,最簡單的方案通常會用到 docker compose,在 docker-compose.yaml 配置文件中編寫集群每個節點部署的服務和配置,比如 flink 的官方示例,一個 jobmanager 和一個 taskmanager 組成的集群

但這種方式對于生產環境部署大規模集群的目標,就有點力所不及了。自動發現和恢復故障節點、自動擴縮容、動態升級和配置變更等方面的支持,還是 k8s 更擅長
當然 k8s 這套生態的門檻也比較高,相關的組件和配置也更多,如何在 k8s 也像 docker compose 一樣能絲滑一鍵啟動集群呢?Helm 就是我們的答案
Helm 是 CNCF 社區孵化的集群服務編排和部署工具,它將一個服務在 k8s 部署需要的服務以及依賴的資源,抽象成了可以通過 golang template 批量生成的配置,配置的格式也統一為 yaml
從 2016 年發展至今,Helm 已經成為了在 k8s 平臺管理服務發行包和發布服務的主流標準
Helm 的幾個概念這里也簡單介紹下,稍做了解就好
-
chart: 服務定義,包括服務名、服務版本、依賴 k8s 版本
-
value: 啟動服務所需所有資源的配置默認值,可在啟動時加上 -f new.yaml 覆蓋默認配置文件
-
template: k8s 相關資源( deployment, service, pvc, configmap 等)的配置模板,和 values 結合生成完整的資源清單
-
release: chart 可以發布到 k8s 的版本
-
repo: 用于存放所有 chart release 打包文件的服務器

大部分支持云原生的開源組件,官方都會提供 Helm chart,讓我們只修改少量的配置,即可完成集群部署
Helm 的安裝也非常簡單,只需下載一個二進制文件
# linux
curl -LO https://get.helm.sh/helm-v3.18.2-linux-amd64.tar.gz
# mac
brew install helm@stable
curl -LO https://get.helm.sh/helm-v3.18.2-darwin-arm64.tar.gz
# windows
choco install kubernetes-helm
https://get.helm.sh/helm-v3.18.2-windows-amd64.zip
Jupyter
環境準備工作終于萬事俱備,各位久等,我們終于要來啟動 Jupyter 服務了
這里我們需要用到 Jupyter 官方提供的 chart zero-to-jupyterhub-k8s
啟動服務
# 在本地添加 jupyter 官方 repo
helm repo add jupyterhub https://hub.jupyter.org/helm-chart
helm repo update
# 啟動
# vim start.sh
RELEASE=jhub
NAMESPACE=jhub
VERSION=4.2.0
# 徹底清理上次啟動 Jupyter 的所有資源
helm uninstall ${RELEASE} -n ${NAMESPACE}
kubectl delete daemonsets,replicasets,services,deployments,pods,jobs,rc,ingress,persistentvolumes,persistentvolumeclaims --all --namespace=jhub
# 將 Jupyter 組件安裝到 jhub 的命名空間(namespace)下
helm upgrade --cleanup-on-fail \
--install $RELEASE jupyterhub/jupyterhub \
--namespace $NAMESPACE \
--create-namespace \
--timeout 600s \
--version $VERSION
成功啟動后查看 jhub 這個 namespace 下相關的 pod 如下:

默認方式啟動后,再通過執行 minikube service proxy-public -n jhub --url ,即可隨機開放一個端口以供訪問 JupyterHub 登錄頁面了
![]()
Hub 默認使用 DummyAuthenticator 作為用戶登錄校驗器 ( values.yaml 中的 hub.config.JupyterHub.authenticator_class 默認值 ),所以我們可以使用任意用戶名 + 任意密碼登錄
登錄后,默認的界面不是最經典的 Notebook 而是功能更現代更豐富的 JupyterLab( JupyterHub 在 2.0 版本后就將用戶登錄后默認跳轉 url 指向了 /lab,參考配置 : c.Spawner.default_url )

Jupyter on k8s
相較于直接通過容器或者運行 JupyterLab 命令啟動的 Jupyter,這套 Jupyter helm chart 在哪里做了優化呢?我們可以從啟動的 Pod 中一步步探究竟

-
hub: JupyterHub,提供登錄頁面
-
continuous-image-puller: 在新的 node 加入 k8s 集群的時候,自動在該 node 拉取 Jupyter 所需要的鏡像,避免用戶在登錄后因拉取 Jupyter 服務鏡像長時間等待
-
proxy: JupyterHub 的上層代理, 即 configurable-http-proxy
-
user-scheduler: 通過 k8s 的調度策略讓 Jupyter 相關的 Pod 盡量分配到同個節點,資源更緊湊
-
singleuser.storage: 每個用戶首次啟動時都會自動申請一個 pv, 后續用戶重新登錄,重啟 notebook 也能使用之前已經保存的文件
-
singleuser.cpu & singleuser.memory: 單個 Notebook 的資源限制, 默認對 cpu 無限制,內存最多 1G
功能概述
在 JupyterLab 界面你能看到的幾個基本功能有 Notebook 、Console 和 Terminal 等
具體功能有機會可以單開一篇詳細介紹,這里只提幾點注意的
-
你在其中一個 Jupyter Server 中編寫的 Notebook,啟動其他鏡像也能看到,因為是同一個目錄
-
打開 Terminal 并執行 pwd 你會看到每個用戶的主目錄都是 /home/jovyan ,這是因為 Notebook 鏡像設置了默認用戶就是 jovyan ,但因為每個用戶都會申請獨立的存儲掛載到 Notebook,所以實際目錄是分開的
-
Notebook 快捷鍵和 ipykernel 語法非常強大,強烈建議在深度使用 Notebook 之前先了解這些技巧
自定義配置
那么到這里我們已經通過 minikube + helm 在本地成功啟動了類生產環境啟動的一個 Jupyter 集群了,不妨先喝杯茶?? 因為接下來又是一個重頭戲了
我們需要先轉換一下思路,從 Jupyter 的運維者變成它的使用者,來看看 Jupyter 會有哪些實際的需求,我們又可以怎樣修改 Jupyter chart 的配置,讓它實現這些需求
如果你想了解 Jupyter chart 每一項配置的詳細說明,可以翻閱官方文檔 Customization Guide 和 Customizing User Environment
values.yaml
values.yaml 包含了整個 Jupyter 服務的大部分默認配置,其他配置都由這里衍生。它的配置內容會通過 k8s secret 分發到 JupyterHub 的 Pod 中,對后續 Hub 的啟動和 Notebook 的啟動產生作用
這里列舉其中一部分配置的作用:
-
db: JupyterHub 默認使用 sqlite 作為元數據庫,用于記錄服務狀態、登錄用戶信息和token等。Hub 啟動前需要申請 PV 存儲 sqlite db 文件,Hub 重載后數據不會丟失
-
hub: 定義 JupyterHub 的登錄方式、登錄后的 Notebook 鏡像選擇列表等
-
culler: 不活躍用戶進程超時后自動清理,默認打開,超時時間1小時
-
singleuser: 定義用戶登錄后默認使用的 Notebook 鏡像,以及擴展鏡像選擇等
修改啟動鏡像源
通過 helm install 首次啟動 Jupyter,需要拉取的鏡像也大多在外網,但主機配置的 /etc/docker/daemon.json 對 minikube 虛擬機是不生效的。我們可以手動在配置中把這些鏡像都替換成國內源,下一節設置 Notebook 啟動鏡像時也會用到這種方式來給鏡像下載加速(放心,這一步之后,就再沒有鏡像下載的問題了)
這里列幾個常見的鏡像下載地址替代方式
-
docker.io -> docker.1ms.run / docker-0.unsee.tech / docker.m.daocloud.io
-
quay.io -> quay.m.daocloud.io
-
gcr.io -> gcr.m.daocloud.io
-
ghcr.io -> ghcr.m.daocloud.io
-
registry.k8s.io -> registry.cn-hangzhou.aliyuncs.com/google_containers / k8s.m.daocloud.io
在 values.yaml 中修改方式如下:
# vim config.yaml
hub:
image:
name: quay.m.daocloud.io/jupyterhub/k8s-hub
proxy:
chp:
image:
name: quay.m.daocloud.io/jupyterhub/configurable-http-proxy
secretSync:
image:
quay.m.daocloud.io/jupyterhub/k8s-secret-sync
scheduling:
userPlaceholder:
image:
name: registry.cn-hangzhou.aliyuncs.com/google_containers/pause
userScheduler:
image:
name: registry.cn-hangzhou.aliyuncs.com/google_containers/kube-scheduler
prePuller:
hook:
image:
name: quay.m.daocloud.io/jupyterhub/k8s-image-awaiter
pause:
image:
name: registry.cn-hangzhou.aliyuncs.com/google_containers/pause
singleuser:
image:
name: quay.m.daocloud.io/jupyterhub/k8s-singleuser-sample
networkTools:
image:
name: quay.m.daocloud.io/jupyterhub/k8s-network-tools
Notebook鏡像選擇列表
默認的 Notebook 鏡像為 jupyterhub/k8s-singleuser-sample,它自帶 jupyter 運行所需的基礎環境,但是對于實際的開發場景,肯定還需要安裝很多其他 Python 依賴的。在 Notebook 鏡像中提前裝好 Python 依賴,減輕用戶操作負擔,是非常經典的需求
好在官方的鏡像倉庫 jupyter/docker-stacks 已經為我們提供了一些擴展鏡像作為參考,有適用于數據開發的 scipy-notebook、適用于機器學習的 tensorflow-notebook 等。我們可以修改 singleuser.profileList 把這些鏡像加進來,給用戶更多的開發環境選擇
# vim config.yaml
singleuser:
profileList:
- display_name: "singleuser"
description: "singleuser default"
default: true
kubespawner_override:
image: 'docker-0.unsee.tech/jupyterhub/singleuser:5.3'
- display_name: "scipy"
description: "scipy"
kubespawner_override:
image: 'docker-0.unsee.tech/jupyter/scipy-notebook:python-3.11'
- display_name: "tensorflow"
description: "tensorflow"
kubespawner_override:
image: 'docker-0.unsee.tech/jupyter/tensorflow-notebook:python-3.11.6'
這里也是將每個鏡像都設置了具體的 tag 名,避免每次啟動時都需要從鏡像倉庫拉取 latest 鏡像
注: hub.extraConfig 也可以設置 c.KubeSpawner.profile_list,兩種方法最后生成的 Hub 配置文件都是類似的,區別在于 prePuller 是否會解析鏡像列表。前者添加的鏡像在 Jupyter chart 啟動階段不會預拉取( 參考 _helpers-daemonset.tpl 文件和 Relevant image sources ),這會導致在 k8s 節點未下載過的鏡像,用戶啟動后需要先等待鏡像下載完成才能進入 JupyterLab 界面,等個幾分鐘也是可能的,體驗很差
在 profileList 添加多個鏡像后,登錄 JupyterHub 就不會立刻跳轉 Notebook 了,而是先展示這些鏡像列表,給用戶選擇

筆者平時對接負責數據開發的同學比較多,scipy-notebook 比較適合他們的開發場景,鏡像中安裝的依賴也非常實用,做自定義鏡像的時候也可以考慮將它們加進來,分類如下:
-
數據可視化: altair, bokeh, matplotlib, seaborn
-
數據采集: beautifulsoup4, sqlalchemy
-
數據計算: bottleneck, numba, dask, statsmodel, sympy, patsy, scikit-learn, scipy
-
數據導出/格式化: openpyxl, pandas, pytables, h5py, protobuf, dill
-
Lab插件: jupyterlab-git
登錄密碼
基于默認的 DummyAuthenticator 我們也可以對登錄方式稍做限制,比如限制哪些用戶允許登錄、規定必須使用哪個登錄密碼
hub:
config:
Authenticator:
admin_users:
- can_login_user_admin
allowed_users:
- can_login_user_normal
DummyAuthenticator:
password: login_password
JupyterHub:
authenticator_class: dummy
對接 ldap
盡管可以限定登錄密碼,但 DummyAuthenticator 的校驗方式依然太簡單太不安全了,所有用戶都使用同一密碼。對公司來說肯定需要專門的身份認證服務的,比如 LDAP
在本地我們想快速啟動 LDAP 可以通過 osixia/docker-openldap 封裝的鏡像快速啟動,然后 Jupyter 通過 host.minikube.internal 域名來訪問暴露在宿主機的 LDAP 端口
# 啟動 ldap 容器, 并預設 cn=myreadonly,dc=example,dc=com 作為可以檢索所有用戶信息的只讀用戶
docker run --name openldap --network ldap_network --hostname openldap -e LDAP_DOMAIN=example.com -e LDAP_ADMIN_PASSWORD=readonly_pwd -e LDAP_READONLY_USER=true -e LDAP_READONLY_USER_USERNAME=readonly -e LDAP_READONLY_USER_PASSWORD=readonly -p 30389:389 -p 30636:636 -d osixia/openldap:1.5.0
hub:
config:
JupyterHub:
authenticator_class: ldapauthenticator.LDAPAuthenticator
LDAPAuthenticator:
lookup_dn: true
use_lookup_dn_username: false
use_ssl: false # 登錄時不使用 ssl
tls_strategy: insecure # 登錄時不使用 tls
lookup_dn_search_password: myreadonly # 配只讀用戶名
lookup_dn_search_user: cn=myreadonly,dc=example,dc=com
lookup_dn_user_dn_attribute: cn
server_address: host.minikube.internal # 可以訪問到主機的域名
server_port: 30389
user_attribute: givenName
user_search_base: ou=users,dc=example,dc=com
allow_all: true # 允許所有用戶登錄
關于 LDAP 如何添加用戶,可以參考 Creating users in an LDAP。先創建一個組織 ou=users,dc=example,dc=com ,再在這個組織下創建具體的用戶 uid=具體用戶名,ou=users,dc=example,dc=com
指定開放端口
注: 這種通過 nodePorts 暴露端口的方法,適用于直接在機器上部署的 k8s 集群(比如通過 sealos 工具部署 )。但 minikube 是在虛擬機之上啟動的 k8s,此方法不適用
proxy:
service:
type: NodePort
nodePorts:
http: 30080 # 注意: nodePort 受 apiserver 的 service-node-port-range 參數限制,默認必須在 30000-32767 之間
https: 30443
但是 minikube 是通過虛擬機的方式啟動 k8s ,和主機并不屬于同一個網絡環境,需要通過 minikube service 或者 kubectl port forward 指令開放到宿主機
# 隨機開放端口
minikube service proxy-public -n jhub --url
# 通過 kubectl 指令,將 Hub 端口映射到主機的指定端口
# 加上 --address='0.0.0.0' 參數以供內網其他電腦訪問
kubectl port-forward svc/proxy-public 30080:80 -n jhub
黑暗主題
作為一個黑暗模式強迫癥, 筆者不管是用什么 IDE 第一時間都是先把主題設置成 dark mode,Jupyter 也不例外
(不過有一個說法是黑暗模式對眼睛不太好)
JupyterLab 默認主題是 light mode,為了讓自己能在每次重新啟動 Jupyter 都自動使用黑暗模式,需要把默認主題修改成 dark mode
從界面的 Settings -> Theme 修改主題的話,會在 /home/jovyan/.jupyter/lab/user-settings/@jupyterlab/apputils-extension/themes.jupyterlab-settings 這個配置文件中新增一項 "theme" 屬性來實現配置變更。那么要修改默認主題配置,第一反應就是在構建鏡像時直接把這個文件修改了。但這個文件路徑有一個特殊點在于它是 PV 的掛載路徑( /home/jovyan ),而 PV 默認又是會從本地申請一個新的目錄掛載進去的,這會導致我們在鏡像中打入到這個路徑的配置文件最終是找不到的
所以這個配置的初始化,不應該放在鏡像構建階段,而是在 Pod 啟動時,也就是初始化 Pod 階段完成
因此思路變成了在鏡像中添加一個初始化配置的腳本
# vim images/singleuser-sample/initandstartjupyter
config_home=${HOME}/.jupyter/lab/user-settings/@jupyterlab/apputils-extension
mkdir -p ${config_home}
# 設置主題配置為 dark mode
echo '{"theme": "JupyterLab Dark"}' > ${config_home}/themes.jupyterlab-settings
# 啟動 jupyter
jupyterhub-singleuser
然后修改 Dockerfile 的 CMD,在啟動 JupyterLab 前先執行這個腳本 (但關于這個 CMD 又有一個坑 請看后續)
# Dockerfile
# 在 dockerfile 設置 pip index 加速鏡像構建
ENV PIP_INDEX_URL=https://pypi.tuna.tsinghua.edu.cn/simple
# 拷貝腳本 initandstartjupyter 到鏡像內,并設置權限
COPY --chmod=0755 initandstartjupyter /
CMD [ "/initandstartjupyter" ]

接下來我們需要構建新的 singleuser 鏡像。為了不覆蓋默認鏡像,我把鏡像名設置成 jupyterhub/singleuser_withdark
關于構建的鏡像 tag 要注意最好不要用 latest,因為 latest 會導致啟動時每次都向官方鏡像倉庫拉取最新的( kubespawner 的參數 image_pull_policy 的默認策略 ),而這個鏡像是我們在本地構建的,官方倉庫并沒有,會導致報錯
# 注意: 需要先將 docker 運行環境切換到 minikube 虛擬機,否則構建的鏡像是在主機的 docker 中,minikube 虛擬機是訪問不到的
# 切換到 minikube 的虛擬機 docker 環境
eval $(minikube docker-env)
# 構建鏡像
docker build -t jupyterhub/singleuser_withdark:1.0.0 /Volumes/dream/workspace/router1/coding/zero-to-jupyterhub-k8s/images/singleuser-sample
# 退出 minikube 的 docker 環境
eval $(minikube docker-env -u)
最后一步是把 withdark 鏡像添加到 profileList 中
# config.yaml
# 修改全局 cmd,這里對 singleuser.profileList 也是生效的
singleuser:
cmd: "/initandstartjupyter"
# 或者在需要配置黑暗主題模式的鏡像才添加 cmd, 此時 singleuser.cmd 可以不配置,保持默認。實際啟動時會以 kubespawner_override.cmd 為準(參考 jupyterhub_config.py 中設置 c.Spawner.cmd 的邏輯 )
profileList:
- display_name: "singleuser dark"
description: "singleuser dark"
kubespawner_override:
image: 'jupyterhub/singleuser_withdark:1.0.0'
cmd: '/initandstartjupyter'
為什么在 kubespawner_override 或者 singleuser 中還需要設置 cmd 呢,前面在 Dockerfile 不是已經覆蓋了嗎?這是因為 singleuser.cmd 在 values.yaml 中是有默認值的( jupyterhub-singleuser ),并不是空值,即使不聲明 cmd,Dockerfile 的 cmd 也是會再被覆蓋掉
(還是要認真看默認配置啊...)
共享磁盤
除了功能性的需求,有的需求是和體驗優化相關的,這些需求比較隱蔽,但實現之后往往會有事半功倍的效果
譬如對于第一次使用 Jupyter 的用戶,登錄 Jupyter 他想做的第一件事并不是寫代碼,而是最好直接就有個教程,教他怎么使用 Notebook,就不用再跑去翻閱官方文檔了
這時如果他能直接看到類似 donnemartin/data-science-ipython-notebooks,以 Notebook 為展現形式的教學工程,他就可以直接把代碼運行起來,甚至還能了解到 Notebook 的更高階用法,體驗非常好

有兩種方案可以實現這個效果
-
直接把教程代碼打入到 Notebook 鏡像中,好處是方案實現起來比較容易,不過鏡像體積會增大
-
每個 Notebook 都共享一個存放教程代碼的只讀目錄(避免相互修改覆蓋),好處是不會影響鏡像體積,但正因為目錄只讀,所以用戶只能查看,不能修改這些代碼
這兩種方案各有優劣,實際場景下,肯定有的用戶是希望可以一邊修改代碼一邊運行的,因此第一種方案更合適
不過通過第二種方案,我們還能了解到如何通過 extraVolumes 配置共享磁盤的方法,因此這里稍做介紹
首先我們需要創建一個 PVC ( PersistentVolumeClaim ) 作為共享存儲,并設置為只讀權限
# vim shared-pvc.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: jupyterhub-shared-volume
namespace: jhub
spec:
accessModes:
- ReadOnlyMany # 只讀
resources:
requests:
storage: 2Gi
執行 kubectl apply -f shared-pvc.yaml 然后在 singleuser.storage.extraVolumes 和 extraVolumeMounts 在 /home/jovyan 下面添加一個共享目錄,掛載到這個共享存儲
singleuser:
storage:
extraVolumes:
- name: jupyterhub-shared
persistentVolumeClaim:
claimName: jupyterhub-shared-volume
extraVolumeMounts:
- name: jupyterhub-shared
mountPath: /home/jovyan/shared
配置好之后重啟 Jupyter chart,我們就能測試看到多個用戶登錄后,在各自的根目錄下都有 shared 共享目錄的效果了

不過,第一次創建的 PV 里面還是空目錄,還需要放置教程代碼。方法稍微有點繞,因為這個目錄對于 Jupyter 用戶來說是只讀的,所以 Jupyter 上沒有權限上傳,需要利用 storage-provisioner 掛載本地目錄的機制,登錄 minikube 虛擬機,找到這個共享目錄再下載代碼
# 登錄 minikube 虛擬機
minikube ssh
# 進入默認的 stroage-provisioner 掛載目錄
cd /var/hostpath-provisioner/jhub/jupyterhub-shared-volume
# 安裝 git
sudo apt -y update
apt -y install git
# 下載教程代碼
git clone https://github.com/donnemartin/data-science-ipython-notebooks
限制資源
在 k8s 部署的另一個特性也是可以對資源進行合理的限制,避免其中一個用戶運行的腳本占用太多資源,影響了其他用戶正常使用
# 全局限制用戶使用 Notebook 的 CPU、內存和磁盤資源
singleuser:
cpu:
limit: 1
guarantee: 1
memory:
limit: 1G
guarantee: 1G
storage:
capacity: 1Gi
# kubespawner 對鏡像進行限制
# 對于一些要使用 GPU 的鏡像,可以將資源限制適當調大
profileList:
- display_name: "singleuser"
description: "singleuser default"
default: true
kubespawner_override:
image: 'docker-0.unsee.tech/jupyterhub/singleuser:5.3'
cpu_limit: 1
mem_limit: '1G'
mem_guarantee: '512M'
然后啟動 singleuser 的默認鏡像,執行一段會申請超過 1G 內存空間的程序,我們會發現 notebook 直接提示觸發 kill 和自動重啟了

注: 如果你通過 top 指令或者是 psutil 庫查看系統總資源,會發現和實際限制的配置不一致,比如限制 1G 內存,實際看到的還是有十幾G。這是因為 top 和 psutil 看到的都是整個虛擬機的資源,而 k8s 是通過 cgroup 來限制 pod 資源的,cgroup 相關的資源限制文件在 /sys/fs/cgroup 目錄下
對接外部數據庫
用來存儲元數據的數據庫默認為 sqlite,盡管會使用 PV 進行持久化,但真的要去查數據的時候還是不如外部數據庫方便的
因此如果你本地還部署了 mysql,可以考慮把 JupyterHub 使用的數據庫切換成 mysql
# 配置使用 mysql 作為數據庫
hub:
db:
type: mysql
url: mysql+pymysql://root:root_pwd@host.minikube.internal:3306/jupyter # 連接開放在宿主機330端口的 mysql
upgrade: true # 首次啟動時需要設置 upgrade: true 以初始化數據庫
culler
culler 是 jupyter 用來定期清理 Jupyter Server 進程的工具,它的清理機制是判斷用戶上次在 JupyterLab 界面有活動的間隔時間,是否有活動,和這個用戶是否有運行中的代碼無關,即使用戶依然有代碼未結束,長時間未操作頁面,進程也會被清理
默認的清理判斷超時時間為 3600s ,也就是一小時, 但這對于實際的數據分析和機器學習等場景肯定是不夠,因此可以適當調大一些
注意:
cull:
enabled: true
timeout: 604800 # 7 days
every: 300
支持用戶啟動多個 notebook
有些用戶會有在多套 Python 環境下同時開發代碼的需求,比如一套 Python 用來做數倉查詢,另一套 Python 環境用于 BI 制作。可以通過 allowNamedServers 配置來支持
hub:
allowNamedServers: true
namedServerLimitPerUser: 5

不過從體驗上來看并不是很方便,用戶需要先跳轉到 control panel ,對已啟動的 server 命名,才能啟動新的 server
admin ui
最后介紹的自定義配置,是對運維比較有幫助,在定位具體用戶反饋問題時可以用上的 admin ui
hub:
config:
Authenticator:
admin_users:
- adminuser1
- adminuser2

管理員可以在管理頁面進行的操作主要有:
-
查看已登錄用戶
-
管理用戶組
-
查看所有運行的 server 狀態
-
停止任意 server
-
登錄其他用戶的 server
最后一點在定位問題時特別有用: 當用戶反饋 "我執行一段 Python 代碼有報錯",但是同樣代碼在你自己的 Notebook 環境卻是正常跑的,原因有可能是他自己裝了其他的 Python 依賴有問題,那你就可以登錄他的 Notebook 去定位
擴展
以下是筆者在安裝和使用 Jupyter 過程中的其他經驗分享
Helm 本地倉庫
在通過 Helm 安裝集群的時候,我們依然會遇到老朋友: 網絡不佳,導致拉取 repo 要很久的問題,于是我就去找 Helm repo 是否也有統一的國內源
但發現它不像各個語言的依賴倉庫,基本都有大廠維護的國內源。Helm repo 并沒有 “官方” 源,每個開源組件自己維護自己的 repo
所以對于大公司來說一般會自建 helm repo,定期同步開源組件官方 repo。當然筆者想的是先解決眼前的問題,看看如何將 Jupyter 的官方 chart 打包,并直接通過本地啟動文件服務器的方式,開放到本地使用,先解決眼前的 Jupyter repo 更新慢的問題
# 通過 python http.server 模擬文件服務器
# 正式環境建議通過 httpd 或者 nginx 等服務
cd ~/modules/helm_repo
nohup python3 -m http.server 30088 > python_http.log 2>&1 &
# 添加本地 helm repo
helm repo add myrepo http://localhost:30088
# 拉取官方 Jupyter chart
git clone https://github.com/jupyterhub/zero-to-jupyterhub-k8s
# 切換到 4.2.0 的 tag
# 注意: 這一步很重要,因為 Chart.yaml 以及 values.yaml 等文件中有 chart version 和 鏡像 tag 相關的配置
# 這兩個的默認值 ( 0.0.1-set.by.chartpress 和 4.2.1-0.dev.git.7 ) 是無法直接啟動 jupyter 服務的
# 需要切換到已發布的一個 tag,再通過官方的 chartpress 工具設置
cd zero-to-jupyterhub-k8s
git checkout tags/4.2.0
# 初始化 chart version 等配置
pip3 install chartpress
./tools/generate-json-schema.py
chartpress --no-build
./tools/set-chart-yaml-annotations.py
# 打包 chart package 格式為壓縮包
helm package jupyterhub # 生成 jupyterhub-4.2.0.tgz
cp jupyterhub-4.2.0.tgz ~/modules/helm_repo
# 生成 index.yaml 這里會記錄整個 helm repo 提供的 chart 列表
helm repo index . --url http://localhost:30088
# 更新 helm repo, 后續執行 helm install 安裝組件時只會讀取本地已緩存的 index.yaml 文件
helm repo update
# 只更新本地的 myrepo
helm repo update myrepo
# 確認是否能搜到 jupyter chart
helm search repo myrepo
# 通過自建 helm repo 中的 Jupyter chart 啟動
helm --install $RELEASE myrepo/jupyterhub ...
chartpress 是官方提供的一鍵構建 Jupyter chart 及其所需鏡像的工具。添加參數 --no-build 可以只設置 chart 配置文件中的 tag,使得代碼可以打包成 release package,不會觸發鏡像構建
更輕量的啟動 k8s 工具
本機啟動 k8s 的工具,除了 minikube 還有更輕量的 k3s、microk8s 和 kind 等,它們對比 minikube 的優勢主要體現在體積更小,占用資源更少,因此更適合在嵌入式環境中(如樹莓派)使用
筆者在本地測試了 k3s 的安裝,在 mac 或者 windows 需要先通過 multipass 或者其他方式啟動一個虛擬機,再啟動 k3s, 在 linux 安裝過程則不需要依賴虛擬機,有興趣可以參考官方文檔 Quick-Start Guide
sealos
假設公司已經為你提供了3臺以上虛擬機,你已經可以啟動真正的 k8s 集群了,sealos 是一個選擇
有個小細節要注意,啟動集群參數中還需要加上 local-path-provisioner 組件參數,否則 Hub 啟動會卡在為 sqlite 申請 PV 的步驟
sealos gen registry.cn-shanghai.aliyuncs.com/labring/kubernetes:v1.29.0 registry.cn-shanghai.aliyuncs.com/labring/helm:v3.8.2 registry.cn-shanghai.aliyuncs.com/labring/calico:v3.24.1 registry.cn-shanghai.aliyuncs.com/labring/local-path-provisioner:v0.0.28 --masters master_node --nodes worker_nodes
其他擴展方向
成功使用 minikube 在本地搭建 Jupyter 集群之后,沿著這兩個組件和集群部署的思路,我們還可以發掘更多值得動手測試的需求
-
Bitnami: Bitnami 提供和云廠商合作,提供可靠的云服務的企業,19年被 VMware 收購。它的開源倉庫針對很多開源組件實現了 Helm chart ,比如 flink、airflow 等,可以作為參考(不過也看到有吐槽它的 Rabbitmq chart 做得不怎么樣 )
-
JupyterLab 相關的各種可以提升體驗的插件,以及 Jupyter AI
-
對 Jupyter 更加貼近企業實踐的改造, 可以參考字節這篇文章,其中 Session 持久化和 Kernel 持久化對于優化用戶體驗都非常有用
-
學習更多 Notebook 的高級用法,參考 data-science-ipython-notebooks,以及 快捷鍵 和 ipykernel 使用技巧

浙公網安備 33010602011771號