三、JupyterHub 离线部署(适配 K8s v1.28.0)

1. 下载适配版本的 Helm Chart 与离线镜像

1.1 下载 JupyterHub Helm Chart v3.1.0(替换原 2.0.0)

参考文档通过百度网盘分享 Chart 包,此处替换为 3.1.0 版本:

bash






# 方式1:离线包上传(推荐,复刻参考文档离线逻辑)
# 联网环境下载:https://artifacthub.io/packages/helm/jupyterhub/jupyterhub/3.1.0,获取 jupyterhub-3.1.0.tgz
# 上传到 K8s master 节点的 /data/s0/kubernetes/helm 目录

# 方式2:若临时联网,可直接拉取
[root@k8s-master /data/s0/kubernetes/helm]# helm pull jupyterhub/jupyterhub --version 3.1.0

# 解压 Chart(后续需查看镜像列表)
[root@k8s-master /data/s0/kubernetes/helm]# tar -xzvf jupyterhub-3.1.0.tgz

1.2 下载离线镜像(按参考文档 “镜像保存 - 传输 - 加载” 流程)

参考文档通过 docker pull/tag/save 制作离线镜像,此处替换为适配 v3.1.0 与 K8s v1.28.0 的镜像列表,需在联网环境执行以下命令

镜像名称

适配版本

联网环境下载 & 保存命令

jupyterhub/configurable-http-proxy

4.6.0

docker pull jupyterhub/configurable-http-proxy:4.6.0 && docker save -o chp-4.6.0.tar $_

jupyterhub/k8s-hub

3.1.0

docker pull jupyterhub/k8s-hub:3.1.0 && docker save -o k8s-hub-3.1.0.tar $_

jupyterhub/k8s-image-awaiter

3.1.0

docker pull jupyterhub/k8s-image-awaiter:3.1.0 && docker save -o image-awaiter-3.1.0.tar $_

jupyterhub/k8s-network-tools

3.1.0

docker pull jupyterhub/k8s-network-tools:3.1.0 && docker save -o network-tools-3.1.0.tar $_

jupyterhub/k8s-secret-sync

3.1.0

docker pull jupyterhub/k8s-secret-sync:3.1.0 && docker save -o secret-sync-3.1.0.tar $_

jupyterhub/k8s-singleuser-sample

3.1.0

docker pull jupyterhub/k8s-singleuser-sample:3.1.0 && docker save -o singleuser-sample-3.1.0.tar $_

registry.k8s.io/kube-scheduler

v1.28.0

docker pull registry.k8s.io/kube-scheduler:v1.28.0 && docker save -o kube-scheduler-1.28.0.tar $_

registry.k8s.io/pause

3.9

docker pull registry.k8s.io/pause:3.9 && docker save -o pause-3.9.tar $_

traefik

v2.10.4

docker pull traefik:v2.10.4 && docker save -o traefik-2.10.4.tar $_

jupyter/datascience-notebook

2024.01.08

docker pull jupyter/datascience-notebook:2024.01.08 && docker save -o datascience-20240108.tar $_

1.3 加载镜像到所有 K8s 节点(复刻参考文档多节点逻辑)

  1. 打包传输:将上述所有 .tar 镜像文件打包为 jupyterhub-chart-images-v3.1.0.tgz,上传到 K8s master、node1、node2 节点的 /data/s0/kubernetes/helm 目录。
  2. 解压加载(所有节点执行,参考文档路径):




    bash





# 解压镜像包
[root@k8s-master /data/s0/kubernetes/helm]# tar -xzvf jupyterhub-chart-images-v3.1.0.tgz -C ./chart-images
# 加载所有镜像
[root@k8s-master /data/s0/kubernetes/helm/chart-images]# docker load -i chp-4.6.0.tar
[root@k8s-master /data/s0/kubernetes/helm/chart-images]# docker load -i k8s-hub-3.1.0.tar
[root@k8s-master /data/s0/kubernetes/helm/chart-images]# docker load -i image-awaiter-3.1.0.tar
[root@k8s-master /data/s0/kubernetes/helm/chart-images]# docker load -i network-tools-3.1.0.tar
[root@k8s-master /data/s0/kubernetes/helm/chart-images]# docker load -i secret-sync-3.1.0.tar
[root@k8s-master /data/s0/kubernetes/helm/chart-images]# docker load -i singleuser-sample-3.1.0.tar
[root@k8s-master /data/s0/kubernetes/helm/chart-images]# docker load -i kube-scheduler-1.28.0.tar
[root@k8s-master /data/s0/kubernetes/helm/chart-images]# docker load -i pause-3.9.tar
[root@k8s-master /data/s0/kubernetes/helm/chart-images]# docker load -i traefik-2.10.4.tar
[root@k8s-master /data/s0/kubernetes/helm/chart-images]# docker load -i datascience-20240108.tar

# 镜像标签修正(适配 Chart 期望的仓库名,参考文档“tag 调整”逻辑)
[root@k8s-master ~]# docker tag registry.k8s.io/kube-scheduler:v1.28.0 k8s.gcr.io/kube-scheduler:v1.28.0
[root@k8s-master ~]# docker tag registry.k8s.io/pause:3.9 k8s.gcr.io/pause:3.9

2. 配置 JupyterHub(config.yaml 适配调整)

参考文档的 config.yaml 需针对 K8s v1.28.0 和 Chart v3.1.0 调整,关键配置如下(路径:/data/s0/kubernetes/helm/config.yaml):

yaml






# 应用名称覆盖(与参考文档一致)
fullnameOverride: "jupyterhub"

# 离线环境禁用镜像拉取密钥(参考文档逻辑)
imagePullSecret:
  create: false
  automaticReferenceInjection: false

# Hub 核心配置(适配 Chart v3.1.0)
hub:
  revisionHistoryLimit: 1
  config:
    JupyterHub:
      admin_access: true
      admin_users:
        - zyp  # 参考文档管理员用户,按需修改
      authenticator_class: dummy  # 测试用虚拟认证,生产需替换
  service:
    type: ClusterIP  # 内部服务,通过 Proxy 暴露
  db:
    type: sqlite-pvc  # 参考文档 SQLite 存储
    pvc:
      accessModes: [ReadWriteOnce]
      storage: 2Gi
      subPath: sqlite
      storageClassName: sqlite-pv  # 与后续 PV 匹配
  image:
    name: jupyterhub/k8s-hub
    tag: "3.1.0"  # 适配 Chart 版本
    pullPolicy: IfNotPresent  # 离线环境用本地镜像

# Proxy 配置(参考文档 NodePort 暴露,调整镜像版本)
proxy:
  service:
    type: NodePort
    nodePorts:
      http: 30081  # 与参考文档一致的外部端口
  chp:
    revisionHistoryLimit: 1
    image:
      name: jupyterhub/configurable-http-proxy
      tag: "4.6.0"  # 适配镜像版本
      pullPolicy: IfNotPresent
  https:
    enabled: false  # 测试环境禁用 HTTPS(参考文档逻辑)

# 单用户服务器(适配新镜像与存储)
singleuser:
  networkTools:
    image:
      name: jupyterhub/k8s-network-tools
      tag: "3.1.0"
      pullPolicy: IfNotPresent
  storage:
    type: static  # 参考文档静态存储
    static:
      pvcName: notebook-pvc  # 与后续 PVC 匹配
      subPath: "{username}"  # 按用户名隔离
    capacity: 10Gi
    homeMountPath: /home/jovyan  # 参考文档挂载路径
  image:
    name: jupyterhub/k8s-singleuser-sample
    tag: "3.1.0"
    pullPolicy: IfNotPresent
  # 科学环境选择(参考文档双环境逻辑,更新镜像版本)
  profileList:
    - display_name: "Sample Environment"
      description: "Basic Python (参考文档默认环境)"
      default: true
    - display_name: "DataScience Environment"
      description: "Python + R + Julia (参考文档扩展环境)"
      kubespawner_override:
        image: jupyter/datascience-notebook:2024.01.08
        pullPolicy: IfNotPresent
  cpu:
    guarantee: 0.5  # 参考文档资源保障
  memory:
    guarantee: 1G
  defaultUrl: "/lab"  # 参考文档 JupyterLab 界面

# 调度器配置(关键:适配 K8s v1.28.0)
scheduling:
  userScheduler:
    revisionHistoryLimit: 1
    image:
      name: k8s.gcr.io/kube-scheduler
      tag: "v1.28.0"  # 必须与 K8s 版本一致(参考文档原 v1.20.15)
      pullPolicy: IfNotPresent
  userPlaceholder:
    image:
      name: k8s.gcr.io/pause
      tag: "3.9"  # 适配 K8s v1.28
      pullPolicy: IfNotPresent

# 离线环境禁用预拉取(参考文档逻辑)
prePuller:
  hook:
    enabled: false
  continuous:
    enabled: false

# 空闲清理(参考文档 1 小时超时逻辑)
cull:
  enabled: true
  timeout: 3600
  every: 600

3. 配置 PV/PVC(复刻参考文档 NFS 存储逻辑)

参考文档使用 NFS 提供持久化存储,需在 K8s v1.28.0 中保持路径一致,创建 pvs.yaml(路径:/data/s0/kubernetes/nfs/pvs/pvs.yaml):

yaml






# 1. SQLite 数据库 PV(参考文档 hub 存储)
apiVersion: v1
kind: PersistentVolume
metadata:
  name: sqlite-pv1  # 参考文档 PV 名称
spec:
  nfs:
    server: k8s-node1  # 参考文档 NFS 服务器(按需修改)
    path: /data/s0/kubernetes/nfs/pv1  # 参考文档路径
    readOnly: false
  capacity:
    storage: 2Gi  # 与 config.yaml 匹配
  accessModes:
    - ReadWriteOnce
  storageClassName: sqlite-pv  # 与 config.yaml 匹配
  persistentVolumeReclaimPolicy: Retain  # 参考文档数据保留策略

---
# 2. 单用户存储 PV(参考文档用户数据存储)
apiVersion: v1
kind: PersistentVolume
metadata:
  name: notebook-pv2  # 参考文档 PV 名称
spec:
  nfs:
    server: k8s-node1
    path: /data/s0/kubernetes/nfs/pv2  # 参考文档路径
    readOnly: false
  capacity:
    storage: 200Gi  # 参考文档大容量存储
  accessModes:
    - ReadWriteMany  # 多节点共享(参考文档逻辑)
  storageClassName: single-notebook  # 与 PVC 匹配
  persistentVolumeReclaimPolicy: Retain

---
# 3. 单用户存储 PVC(参考文档绑定逻辑)
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: notebook-pvc  # 与 config.yaml 匹配
  namespace: jhub  # 与 JupyterHub 同命名空间
spec:
  accessModes:
    - ReadWriteMany
  storageClassName: single-notebook
  resources:
    requests:
      storage: 20Gi  # 参考文档申请容量

执行创建命令(复刻参考文档步骤):

bash






# 1. 在 NFS 服务器(k8s-node1)创建目录(参考文档路径)
[root@k8s-node1 ~]# mkdir -p /data/s0/kubernetes/nfs/pv1 /data/s0/kubernetes/nfs/pv2
[root@k8s-node1 ~]# chmod 777 /data/s0/kubernetes/nfs/pv1 /data/s0/kubernetes/nfs/pv2

# 2. 创建命名空间(参考文档 jhub 命名空间)
[root@k8s-master ~]# kubectl create ns jhub

# 3. 应用 PV/PVC 配置
[root@k8s-master /data/s0/kubernetes/nfs/pvs]# kubectl apply -f pvs.yaml

# 验证(参考文档验证逻辑)
[root@k8s-master ~]# kubectl get pv -n jhub  # 查看 PV 状态
[root@k8s-master ~]# kubectl get pvc -n jhub  # 确保 PVC 为 Bound 状态

4. 启动 JupyterHub 并验证(复刻参考文档流程)

4.1 执行 Helm 安装(参考文档命令格式)

bash






[root@k8s-master /data/s0/kubernetes/helm]# helm upgrade --cleanup-on-fail \
  --install jupyterhub-release ./jupyterhub \  # ./jupyterhub 为解压后的 Chart 目录
  --namespace jhub \
  --values config.yaml  # 自定义配置

4.2 解决参考文档常见错误(适配 v1.28.0)

  • 错误 1ClusterRole "jupyterhub-user-scheduler" exists(参考文档原错误)解决:清理残留资源(命令与参考文档一致):




    bash





kubectl delete clusterrole jupyterhub-user-scheduler
kubectl delete clusterrolebinding jupyterhub-user-scheduler
  • 错误 2kube-scheduler 容器 CrashLoopBackOff解决:确保 scheduling.userScheduler.image.tag 为 v1.28.0,且镜像标签已从 registry.k8s.io 改为 k8s.gcr.io(步骤 1.3 已处理)。

4.3 验证 Pod 状态(参考文档命令)

bash






[root@k8s-master ~]# kubectl get pod -n jhub
# 预期输出(所有 Pod 为 Running)
NAME                                      READY   STATUS    RESTARTS   AGE
jupyterhub-hub-xxxxxxxxx-xxxxx           1/1     Running   0          8m
jupyterhub-proxy-xxxxxxxxx-xxxxx          1/1     Running   0          8m
jupyterhub-user-scheduler-xxxxxxxxx-xxxxx 1/1     Running   0          8m

4.4 访问 JupyterHub(参考文档 NodePort 方式)

  1. 获取访问地址:参考文档用 NodePort 30081,执行命令:




    bash





[root@k8s-master ~]# kubectl get svc -n jhub
# 关键输出(确认 NodePort 为 30081)
NAME                      TYPE        CLUSTER-IP    EXTERNAL-IP   PORT(S)        AGE
jupyterhub-proxy-public   NodePort    10.96.xxx.xxx <none>        80:30081/TCP   10m
  1. 登录验证
  • 浏览器输入 http://<k8s-master-IP>:30081(参考文档访问格式)。
  • 用虚拟认证登录(任意用户名,如 zyp,无需密码,参考文档测试逻辑)。
  • 选择 “DataScience Environment”,点击 “Start”,成功进入 JupyterLab 界面即部署完成。

四、后续优化(参考文档扩展建议)

  1. HTTPS 配置:参考文档未启用 HTTPS,生产环境需在 proxy.https 中配置证书(如 Let's Encrypt)。
  2. 认证替换:将 authenticator_class: dummy 改为 OAuth(对接 GitHub/GitLab,参考文档 “生产优化” 暗示)。
  3. 镜像定制:基于 jupyter/datascience-notebook 预装业务依赖(如 TensorFlow),减少用户配置(参考文档 “自定义用户环境” 逻辑)。
  4. 监控添加:集成 Prometheus + Grafana 监控 Pod 资源(参考文档未提及,补充适配 K8s v1.28.0 的监控方案)。





后面的维护:


配置文件在/data/s0/kubernetes/helm/config.yaml


kubectl edit configmap jupyterhub-user-scheduler -n jhub
1. 修正 apiVersion 以匹配 Kubernetes v1.28
Kubernetes v1.28 的 KubeSchedulerConfiguration 应使用 v1beta3 版本(v1 版本可能存在兼容性问题),需在配置顶部修改:
yaml
apiVersion: kubescheduler.config.k8s.io/v1beta3  # 原 v1 改为 v1beta3
kind: KubeSchedulerConfiguration
leaderElection:
  resourceLock: endpointsleases
  resourceName: jupyterhub-user-scheduler-lock
  resourceNamespace: "jhub"
2. 移除插件启用 / 禁用冲突
当前配置中,score 插件的 disabled 列表和 enabled 列表重复定义了相同插件(如 NodeAffinity、NodeResourcesFit 等),导致配置解析失败。需删除 disabled 列表中与 enabled 重复的项:
yaml
profiles:
  - schedulerName: jupyterhub-user-scheduler
    plugins:
      score:
        disabled:
          # 仅保留真正需要禁用的插件(删除与 enabled 重复的项)
          - name: NodeResourcesBalancedAllocation
        enabled:
          - name: NodeAffinity
            weight: 14631
          - name: InterPodAffinity
            weight: 1331
          - name: NodeResourcesFit
            weight: 121
          - name: ImageLocality
            weight: 11
完整修改后的 ConfigMap 配置(关键部分)
yaml
apiVersion: v1
data:
  config.yaml: |
    apiVersion: kubescheduler.config.k8s.io/v1beta3  # 修正 API 版本
    kind: KubeSchedulerConfiguration
    leaderElection:
      resourceLock: endpointsleases
      resourceName: jupyterhub-user-scheduler-lock
      resourceNamespace: "jhub"
    profiles:
      - schedulerName: jupyterhub-user-scheduler
        plugins:
          score:
            disabled:
              - name: NodeResourcesBalancedAllocation  # 仅保留此禁用项
            enabled:
              - name: NodeAffinity
                weight: 14631
              - name: InterPodAffinity
                weight: 1331
              - name: NodeResourcesFit
                weight: 121
              - name: ImageLocality
                weight: 11
        pluginConfig:
          - args:
              scoringStrategy:
                resources:
                - name: cpu
                  weight: 1
                - name: memory
                  weight: 1
                type: MostAllocated
            name: NodeResourcesFit
kind: ConfigMap
#  metadata 部分保持不变...
操作步骤
执行 kubectl edit configmap jupyterhub-user-scheduler -n jhub 进入编辑模式。
按上述建议修改 apiVersion 和插件配置。
保存退出后,删除旧的调度器 Pod 以加载新配置:
bash
kubectl delete pod -l component=user-scheduler -n jhub
检查新 Pod 状态和日志,确认是否启动成功:
bash
kubectl get pods -n jhub | grep user-scheduler
kubectl logs <新Pod名称> -n jhub -c kube-scheduler
通过修正 API 版本和移除插件冲突,可解决 kube-scheduler 因配置解析失败导致的 CrashLoopBackOff 问题。




问题分析
Kubernetes 调度器的 leaderElection.resourceLock 字段在 v1.28 版本中仅支持 leases 作为有效值(endpointsleases 已被移除或不兼容)。而当前配置中使用了 endpointsleases,导致调度器启动失败。
解决方案
修改 jupyterhub-user-scheduler ConfigMap 中调度器配置的 leaderElection.resourceLock 为 leases:
编辑 ConfigMap:
bash
kubectl edit configmap jupyterhub-user-scheduler -n jhub
找到 leaderElection 部分,将 resourceLock: endpointsleases 改为 resourceLock: leases:
yaml
leaderElection:
  resourceLock: leases  # 修正为 leases
  resourceName: jupyterhub-user-scheduler-lock
  resourceNamespace: "jhub"
保存退出后,重启调度器 Pod 使配置生效:
bash
kubectl delete pod -l component=user-scheduler -n jhub


检查新 Pod 日志确认是否启动成功:
bash
kubectl logs <新Pod名称> -n jhub -c kube-scheduler
补充说明
此错误与 Kubernetes 版本兼容有关:endpointsleases 可能是旧版本(如 v1.20 及更早)的有效值,但在 v1.28 中已被废弃,仅保留 leases 作为资源锁类型。
修正后,调度器的 leader 选举逻辑将正常工作,解决 CrashLoopBackOff 问题。



kubectl --namespace=jhub describe pod jupyterhub-hub-77b78cdc5b-lq7cr  

kubectl --namespace=jhub get pod jupyter-wangxinquan   -o yaml

kubectl --namespace=jhub delete pod jupyterhub-user-scheduler-786ccb9584-d8vkf



kubectl delete namespace jhub



kubectl create token dashboard-admin -n kubernetes-dashboard

kubectl create ns jhub
kubectl apply -f pvs.yaml

helm upgrade --cleanup-on-fail --install jupyterhub-release ./jupyterhub --namespace jhub --values config.yaml

kubectl edit configmap jupyterhub-user-scheduler -n jhub

kubectl  logs jupyterhub-hub-5d8dd4474f-g75bl  -n jhub -f