…или рассказ о self service на JupyterHub для дата саентистов

Всем привет, сегодня я расскажу о том, как мы переехали на наш велосипед в виде JupyterHub, и он оказался удобным. У нас в компании работают ~20 дата саентистов и в своей работе они используют множество Open Source-инструментов: Airflow, Hadoop, Hive, Spark и т.д. Но в данной статье речь пойдет исключительно о JupyterHub, точнее говоря о боли, которая преследовала администраторов, и как мы успешно ее побороли.
Почему мы выбрали JupyterHub
JupyterHub — это тот же Jupyter, только ставится он на отдельный сервер и работает как клиент-серверное веб-приложение.
Преимущества тут очевидны:
-
Вам не нужно беспокоиться об установке Jupyter’а и его окружения;
-
Не тратятся локальные ресурсы на вычисления;
-
Серверные мощности обычно выше локальных.
Но есть и недостатки:
-
Ресурсы сервера делятся на всех пользователей. По сути кто первый – того и тапки;
-
Одна среда на всех: вы будете пользоваться только тем ПО, которое установлено на сервере.
-
Обновление через боль. Установка нового ПО или обновление существующего требует согласования со всеми пользователями JupyterHub’а.
Просто представьте насколько задача усложнится, если на ваших серверах нет интернета. А политика безопасности настолько забюрократизирована, что процедура установки ПО будет съедать всё ваше время.

В игру вступает Kernel
Частично вышеописанные проблемы решаются с помощью kernel’ов — виртуальных сред (venv). Вы устанавливаете в них необходимые пакеты, затем переносите их на JupyterHub, после чего данное ядро становится доступным для выбора в интерфейсе лэптопа. А весь код, написанный на лэптопе, будет работать именно в этом окружении.
Но на практике оказалось, что kernel’ы оказались еще бо́льшей бедой: их также необходимо поддерживать и регулярно обновлять, плюс со временем они обрастали зависимостями и legacy-кодом. А до бесконечности создавать новые kernel’ы невозможно.
Все это вызывало негатив со всех сторон: дата саентисты не могли получить своевременный доступ к нужному ПО. А администраторам приходилось постоянно что-то досогласовывать, устанавливать и переустанавливать. Так продолжать мы не могли, поэтому мы решили оптимизировать работу дата саентистов.
Что придумали
Чтобы избавить саентистов (и админов) от боли, мы поставили перед собой следующие цели:
-
Установить/обновить любое ПО можно без привлечения администраторов;
-
Установить/обновить ПО можно в любой момент;
-
Установленное одним пользователем ПО не должно влиять на работу остальных пользователей;
-
Установленное ПО не должно негативно влиять на сервер в целом.
После исследования мы решили использовать связку Jupyterhub + Docker, а kernel’ы собирать в GitLab CICD, чтобы затем доставлять их на сервера Jupyterhub.

Схема работы
Схема работы следующая: для каждого пользователя в GitLab создана отдельная папка и, когда пользователю необходимо создать новый kernel, он:
-
Создает в своей папке новый проект (папку);
-
Создает файл requirements.json и описывает в нем:
2.1 Название kernel’а
2.2 Имя docker-образа (скачивается из DockerHub’а, либо с нашего локального репозитория, где хранятся наши кастомные образы).
2.3 Python-библиотеки для установки и их версии
-
В случае необходимости редактирует Dockerfile;
-
Запускает CICD-процесс, в котором:
4.1 Собирается ядро;
4.2 Выполняются команды из Dockerfile.
4.3 Устанавливаются библиотеки из requirements.json.
4.4 Ядро копируется на сервер JupyterHub.

Новое ядро сразу становится доступными для работы в JupyterHub’е. А в случае необходимости дата саентист самостоятельно правит параметры своего ядра и пересобирает его.
Что это нам дало
Теперь большая часть работы выполняется дата саентистами без привлечения администраторов. После 10 тысяч сборок kernel’ов мы сэкономили массу времени на процедурах согласований и самой установке.
Эффективность обоих сторон увеличилась, а админы привлекаются крайне редко —только для решения сложных вопросов. Цели 1 и 2 выполнены.
Доступ в интернет для скачивания библиотек мы реализовали посредством прокси-сервера, с которого разрешено обращаться только к репозиторию pip. Все ПО работает исключительно внутри контейнера. Что бы там не произошло — это никак не повлияет на работу других пользователей. Так мы закрыли вопрос с целями 3 и 4.
А теперь пара технических моментов:
Пример конфига ядра
kernel.json
{ "argv": [ "/usr/bin/docker", "run", "--network=host", "--rm", "-v", "{connection_file}:/connection-spec", "-v", "/home/anikishin/work:/root/work", "************/docker/registry/anikishin_dataflow:latest", "python", "-m", "ipykernel_launcher", "-f", "/connection-spec" ], "display_name": "anikishin_dataflow", "language": "python", "env": {} }
Использование —network=host объясняется тем, что во время работы pyspark на машине открывается случайный порт и кластер Hadoop должен иметь доступ к клиенту.
Пример сборки ядра
$ LOGIN=`echo "${GITLAB_USER_LOGIN}" | awk '{print tolower($0)}'` $ echo -e "export PATH_TO_KERNEL=/${LOGIN}/${KERNEL}\nLOGIN=${LOGIN}\nKERNEL=${KERNEL}" >.env $ source ./.env $ PYTHON_VERSION=`/bin/python3 ${CI_PROJECT_DIR}/parser.py ${CI_PROJECT_DIR}/${PATH_TO_KERNEL}/requirements.json python_version` $ sed "s/PYTHON_VERSION/${PYTHON_VERSION}/g" ${CI_PROJECT_DIR}/${PATH_TO_KERNEL}/Dockerfile FROM python:3.8-slim WORKDIR /root/work COPY requirements.txt /tmp/requirements.txt RUN pip install --upgrade -r /tmp/requirements.txt$ sed -i "s/PYTHON_VERSION/${PYTHON_VERSION}/g" ${CI_PROJECT_DIR}/${PATH_TO_KERNEL}/Dockerfile $ /bin/python3 ${CI_PROJECT_DIR}/parser.py ${CI_PROJECT_DIR}/${PATH_TO_KERNEL}/requirements.json libs > ${CI_PROJECT_DIR}/${PATH_TO_KERNEL}/requirements.txt $ echo "IMAGE_NAME=`/bin/python3 ${CI_PROJECT_DIR}/parser.py ${CI_PROJECT_DIR}/${PATH_TO_KERNEL}/requirements.json image_name`" >> ./.env $ echo "IMAGE_VERSION=`/bin/python3 ${CI_PROJECT_DIR}/parser.py ${CI_PROJECT_DIR}/${PATH_TO_KERNEL}/requirements.json image_version`" >> ./.env $ source ./.env $ docker build --no-cache -t ************:5005/docker/registry/${LOGIN}_${IMAGE_NAME}:${IMAGE_VERSION} ${CI_PROJECT_DIR}/${PATH_TO_KERNEL}/ Step 1/4 : FROM python:3.8-slim 3.8-slim: Pulling from library/python 42c077c10790: Already exists f63e77b7563a: Pulling fs layer 5215613c2da8: Pulling fs layer 9ca2d4523a14: Pulling fs layer e97cee5830c4: Pulling fs layer e97cee5830c4: Waiting 9ca2d4523a14: Verifying Checksum 9ca2d4523a14: Download complete f63e77b7563a: Verifying Checksum f63e77b7563a: Download complete 5215613c2da8: Verifying Checksum 5215613c2da8: Download complete f63e77b7563a: Pull complete 5215613c2da8: Pull complete 9ca2d4523a14: Pull complete e97cee5830c4: Verifying Checksum e97cee5830c4: Download complete e97cee5830c4: Pull complete Digest: sha256:0e07cc072353e6b10de910d8acffa020a42467112ae6610aa90d6a3c56a74911 Status: Downloaded newer image for python:3.8-slim ---> 61c56c60bb49 Step 2/4 : WORKDIR /root/work ---> Running in 4baf6a21fb37 Removing intermediate container 4baf6a21fb37 ---> 0f5165f4c567 Step 3/4 : COPY requirements.txt /tmp/requirements.txt ---> 40490bed96d2 Step 4/4 : RUN pip install --upgrade -r /tmp/requirements.txt ---> Running in a79389decbc4 Collecting ipykernel Downloading ipykernel-6.13.1-py3-none-any.whl (133 kB) ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 133.2/133.2 KB 1.4 MB/s eta 0:00:00 Collecting ipython Downloading ipython-8.4.0-py3-none-any.whl (750 kB) ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 750.8/750.8 KB 7.4 MB/s eta 0:00:00 Collecting numpy Downloading numpy-1.22.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (16.9 MB) ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 16.9/16.9 MB 50.1 MB/s eta 0:00:00 Collecting psutil Downloading psutil-5.9.1-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl (284 kB) ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 284.7/284.7 KB 24.1 MB/s eta 0:00:00 Collecting tornado>=6.1 Downloading tornado-6.1-cp38-cp38-manylinux2010_x86_64.whl (427 kB) ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 427.5/427.5 KB 38.0 MB/s eta 0:00:00 Collecting packaging Downloading packaging-21.3-py3-none-any.whl (40 kB) ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 40.8/40.8 KB 5.0 MB/s eta 0:00:00 Collecting matplotlib-inline>=0.1 Downloading matplotlib_inline-0.1.3-py3-none-any.whl (8.2 kB) Collecting nest-asyncio Downloading nest_asyncio-1.5.5-py3-none-any.whl (5.2 kB) Collecting debugpy>=1.0 Downloading debugpy-1.6.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl (1.8 MB) ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 1.8/1.8 MB 71.2 MB/s eta 0:00:00 Collecting traitlets>=5.1.0 Downloading traitlets-5.2.2.post1-py3-none-any.whl (106 kB) ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 106.8/106.8 KB 18.8 MB/s eta 0:00:00 Collecting jupyter-client>=6.1.12 Downloading jupyter_client-7.3.3-py3-none-any.whl (131 kB) ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 132.0/132.0 KB 18.9 MB/s eta 0:00:00 Collecting pygments>=2.4.0 Downloading Pygments-2.12.0-py3-none-any.whl (1.1 MB) ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 1.1/1.1 MB 68.7 MB/s eta 0:00:00 Collecting backcall Downloading backcall-0.2.0-py2.py3-none-any.whl (11 kB) Collecting pickleshare Downloading pickleshare-0.7.5-py2.py3-none-any.whl (6.9 kB) Collecting prompt-toolkit!=3.0.0,!=3.0.1,<3.1.0,>=2.0.0 Downloading prompt_toolkit-3.0.29-py3-none-any.whl (381 kB) ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 381.5/381.5 KB 41.8 MB/s eta 0:00:00 Collecting pexpect>4.3 Downloading pexpect-4.8.0-py2.py3-none-any.whl (59 kB) ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 59.0/59.0 KB 12.4 MB/s eta 0:00:00 Collecting decorator Downloading decorator-5.1.1-py3-none-any.whl (9.1 kB) Collecting jedi>=0.16 Downloading jedi-0.18.1-py2.py3-none-any.whl (1.6 MB) ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 1.6/1.6 MB 82.1 MB/s eta 0:00:00 Requirement already satisfied: setuptools>=18.5 in /usr/local/lib/python3.8/site-packages (from ipython->-r /tmp/requirements.txt (line 2)) (57.5.0) Collecting stack-data Downloading stack_data-0.2.0-py3-none-any.whl (21 kB) Collecting parso<0.9.0,>=0.8.0 Downloading parso-0.8.3-py2.py3-none-any.whl (100 kB) ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 100.8/100.8 KB 23.1 MB/s eta 0:00:00 Collecting python-dateutil>=2.8.2 Downloading python_dateutil-2.8.2-py2.py3-none-any.whl (247 kB) ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 247.7/247.7 KB 36.0 MB/s eta 0:00:00 Collecting jupyter-core>=4.9.2 Downloading jupyter_core-4.10.0-py3-none-any.whl (87 kB) ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 87.3/87.3 KB 21.7 MB/s eta 0:00:00 Collecting entrypoints Downloading entrypoints-0.4-py3-none-any.whl (5.3 kB) Collecting pyzmq>=23.0 Downloading pyzmq-23.1.0-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.whl (1.1 MB) ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 1.1/1.1 MB 68.4 MB/s eta 0:00:00 Collecting ptyprocess>=0.5 Downloading ptyprocess-0.7.0-py2.py3-none-any.whl (13 kB) Collecting wcwidth Downloading wcwidth-0.2.5-py2.py3-none-any.whl (30 kB) Collecting pyparsing!=3.0.5,>=2.0.2 Downloading pyparsing-3.0.9-py3-none-any.whl (98 kB) ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 98.3/98.3 KB 19.7 MB/s eta 0:00:00 Collecting executing Downloading executing-0.8.3-py2.py3-none-any.whl (16 kB) Collecting asttokens Downloading asttokens-2.0.5-py2.py3-none-any.whl (20 kB) Collecting pure-eval Downloading pure_eval-0.2.2-py3-none-any.whl (11 kB) Collecting six>=1.5 Downloading six-1.16.0-py2.py3-none-any.whl (11 kB) Installing collected packages: wcwidth, pure-eval, ptyprocess, pickleshare, executing, backcall, traitlets, tornado, six, pyzmq, pyparsing, pygments, psutil, prompt-toolkit, pexpect, parso, numpy, nest-asyncio, entrypoints, decorator, debugpy, python-dateutil, packaging, matplotlib-inline, jupyter-core, jedi, asttokens, stack-data, jupyter-client, ipython, ipykernel Successfully installed asttokens-2.0.5 backcall-0.2.0 debugpy-1.6.0 decorator-5.1.1 entrypoints-0.4 executing-0.8.3 ipykernel-6.13.1 ipython-8.4.0 jedi-0.18.1 jupyter-client-7.3.3 jupyter-core-4.10.0 matplotlib-inline-0.1.3 nest-asyncio-1.5.5 numpy-1.22.4 packaging-21.3 parso-0.8.3 pexpect-4.8.0 pickleshare-0.7.5 prompt-toolkit-3.0.29 psutil-5.9.1 ptyprocess-0.7.0 pure-eval-0.2.2 pygments-2.12.0 pyparsing-3.0.9 python-dateutil-2.8.2 pyzmq-23.1.0 six-1.16.0 stack-data-0.2.0 tornado-6.1 traitlets-5.2.2.post1 wcwidth-0.2.5 WARNING: Running pip as the 'root' user can result in broken permissions and conflicting behaviour with the system package manager. It is recommended to use a virtual environment instead: https://pip.pypa.io/warnings/venv WARNING: You are using pip version 22.0.4; however, version 22.1.2 is available. You should consider upgrading via the '/usr/local/bin/python -m pip install --upgrade pip' command. Removing intermediate container a79389decbc4 ---> 942cfb92669c Successfully built 942cfb92669c Successfully tagged ************:5005/docker/registry/anikishin_proj3:latest $ docker push ************:5005/docker/registry/${LOGIN}_${IMAGE_NAME}:${IMAGE_VERSION} The push refers to repository [************:5005/docker/registry/anikishin_proj3] ca238036b879: Preparing 5083b2b128f1: Preparing 92487648c84b: Preparing 9df5b2f53554: Preparing 590db2877d9d: Preparing 3d5419adeeb6: Preparing 2c9f341968bc: Preparing ad6562704f37: Preparing 2c9f341968bc: Waiting 3d5419adeeb6: Waiting ad6562704f37: Waiting 92487648c84b: Pushed 5083b2b128f1: Pushed 590db2877d9d: Pushed 2c9f341968bc: Pushed 9df5b2f53554: Pushed 3d5419adeeb6: Pushed ca238036b879: Pushed latest: digest: sha256:bc36a9bcc6be914a9b7f8ee6ea6c940409f32c57a528c521651442235309239a size: 1996 Running after_script 00:00 Saving cache 00:00 Uploading artifacts for successful job 00:00 Uploading artifacts... Runtime platform arch=amd64 os=linux pid=27359 revision=c5874a4b version=12.10.2 ./.env: found 1 matching files Uploading artifacts to coordinator... ok id=38302 responseStatus=201 Created token=************ Job succeeded
Какие проблемы могут возникнуть
У нас все получилось не сразу. По пути возникали проблемы, из-за которых мы не смогли быстро перейти на новую схему:
-
Нет онбординга
Понадобилось некоторое время на обучение дата саентистов работе с докер-образами;
Изначально у коллег не было понимания, что данные в контейнере не сохранятся если их не писать в специальную директорию. Плюс ваши дата саентисты должны знать, что простого pip install может оказаться недостаточно: в контейнере должны быть установлены дополнительные зависимости, если этого требует Python-модуль.
Решение: сделайте инструкцию, проводите онбординг новых сотрудников, помогайте в случае проблем со сборкой кернелов.
-
Сохранность данных
Поскольку kernel’ы работают в докер-образах, то их перезагрузка приводит к потере всех сохраненных данных.
Решение: создайте отдельную общую папку на сервере, которая монтируется к докер-образу и в которую сохраняются все необходимые дата саентисту артефакты.
-
Ограничение ресурсов
Без лимитов и рычагов один дата саентист может навалить такую нагрузку, что другие не смогут нормально работать:

Это один контейнер с kernel.
И так может случиться не один раз

Решение: мониторьте нагрузку по CPU\RAM. Когда получаете алерт определяйте кто грузит машину и идите наказывать виновника попросите коллегу сбавить обороты.
-
Версионность
Как мы пришли к тегу #latest: изначально мы планировали версионировать все создаваемые докер-образы. Но дата саентисты стали делать такое множество версий своих образов, что в результате место в Docker registry быстро закончилось.
Решение: используйте версионность только для продуктивных процессов.
Планы на развитие
Резюмируя: мы получили современное решение, дата саентисты больше не дергают админов, они рады что могут самостоятельно править свои kernel в любое время, не прибегая к помощи со стороны.
Сейчас мы думаем о том, как реализовать удобное и гибкое ограничение ресурсов для контейнеров. Если у вас есть идеи как это сделать — напишите в комментариях.
ссылка на оригинал статьи https://habr.com/ru/company/rostelecom/blog/689596/
Добавить комментарий