Сегодня Spark — отраслевой стандарт среди инструментов обработки данных. Его часто используют в связке с Hadoop, однако Hadoop не очень подходит для работы в облаке. Альтернативой может быть Kubernetes, однако самостоятельно его настраивать и конфигурировать очень сложно. Чтобы упростить ситуацию и помочь пользоваться всеми преимуществами технологий, не сталкиваясь с трудностями, мы сделали в VK Cloud Spark в Kubernetes. Для работы с ним не нужна глубокая экспертиза в K8s.
Меня зовут Алексей Матюнин, я ведущий программист команды разработки ML Platform в компании VK Cloud. Расскажу, почему мы решили делать Spark в Kubernetes, с какими сложностями столкнулись и как их обходили, а также что получили в итоге.
Материал подготовлен по мотивам моего выступления на конференции VK Data Meetup.
Базово про Apache Spark
Apache Spark — Open-Source-фреймворк для распределённых вычислений, который стал популярным благодаря высокому быстродействию и эффективности при обработке больших данных. Также, что немаловажно, эта технология активно развивается, имеет большое комьюнити и хорошую документацию. Фреймворк часто используют для машинного обучения, когда нужно быстро обработать большой объём информации. Также он подходит для обработки потоковых данных в реальном времени, что делает его незаменимым инструментом. Сегодня Apache Spark де-факто стал стандартом при работе с большими объёмами данных.
При чём здесь Kubernetes?
Исторически Spark ориентирован на работу с Hadoop — многие специалисты по ряду причин до сих пор предпочитают работать со Spark именно на основе Hadoop, например из-за существующей инфраструктуры, и, как правило, On-premise.
А переезд приложений в облачную инфраструктуру — это тренд, которому в том числе следуют и крупные компании. И Kubernetes тут очень кстати, так как на его основе чаще всего разворачивают приложения в облаке. А ещё он даёт ряд преимуществ:
-
Изоляция сред. При развёртывании в Hadoop-кластере возникают сложности при версионировании Spark: необходимость перехода на новую версию Spark всегда сопряжена с трудностями на стороне команд администраторов и Data Science. В случае с Kubernetes такой проблемы нет, каждый сотрудник может создать для себя отдельное окружение, которое будет работать в независимом контейнере, и упаковать в него Spark-приложение со всем кодом и любыми зависимостями.
-
Управление ресурсами. Запуская Spark в K8s, мы можем гибко управлять объёмом ресурсов, выделяемых для каждого Spark-приложения и прочих приложений, работающих в кластере.
-
Гибкое масштабирование. Kubernetes в облаке позволяет задействовать автомасштабирование кластера с учётом текущей нагрузки. Например, увеличивать количество ядер, когда вычислительная нагрузка растёт, и автоматически уменьшать, когда такие мощности больше не нужны. Это позволяет экономически эффективно использовать ресурсы, но в то же время нативно справляться даже с пиковыми нагрузками.
Проработка идеи и ресерч
Основной нашей целью было сделать Spark в Kubernetes максимально доступным и простым в использовании, не требующим длительной и сложной настройки. В идеале — чтобы запустить сервис можно было одной кнопкой.
В поисках решения мы начали изучать документацию, гайды и опыт других команд. Также мы нашли в открытом доступе достаточно подробные инструкции по установке Spark в Kubernetes и не один раз вручную проходили все шаги. Надо сказать, что в базовом варианте каждый может запустить себе Spark по, например, вот этому руководству.
Но для удобного запуска Spark-приложений этого ещё не достаточно. Мы обнаружили, что самостоятельная установка Spark в Kubernetes сопряжена и с другими сложностями:
- для конфигурации K8s нужна экспертиза;
- сложный механизм запуска Spark-приложений;
- надо решать вопрос интеграции с S3-хранилищем;
- важно определиться в необходимости дополнительных компонентов, например Docker Registry, Spark History Server.
Поскольку мы хотели сделать сервис в первую очередь удобным для пользователей и вынести все сложности за скобки, мы решили полностью автоматизировать установку и конфигурации.
Начало работы над реализацией Spark в Kubernetes
Первым делом мы приступили к проработке общей архитектурной схемы и определения основных компонентов будущего сервиса. Как обычно, на начальном этапе мы не видели всех подводных камней и не понимали, сколько это займёт времени.
На схеме со стороны пользователя изображены компоненты, которые мы посчитали необходимыми для полноценной работы:
- Private Docker Registry, где уже находятся подготовленные Docker-образы для приложений с разными версиями Spark. Также пользователь может хранить собственные сборки и запускать на их основе свои приложения. Docker Registry защищён авторизацией и TLS/SSL-сертификатом;
- S3 Bucket VK Cloud создаётся при запуске сервиса и используется для хранения датасетов и логов Spark-приложений;
- Spark History UI — сервис внутри Kubernetes, позволяющий в удобном виде просматривать логи завершённых Spark-приложений. Для авторизации используются учётные данные от личного кабинета VK Cloud (как и для доступа в Spark UI);
- Python Client Library — клиентская библиотека для удобного запуска и управления Spark-приложениями. Может работать из любого окружения.
Рассмотрим подробнее все компоненты, конфигурацию Kubernetes и установку Spark.
Установка Spark Operator
Начали мы с установки Spark Operator. Это оркестратор, который работает внутри Kubernetes и отвечает за управление самим Spark и Spark-приложениями. В соответствии с официальной документацией Google, для его установки достаточно всего двух команд:
$ helm repo add spark-operator
https://googlecloudplatform.github.io/sparkon-k8s-operator
$ helm install my-release sparkoperator/spark-operator --namespace sparkoperator --create-namespace
И тут нет подвохов, эти команды действительно установят Spark Operator, но нюанс в том, что в сборку этого компонента не добавили файлы зависимостей для интеграции с S3-хранилищем, а для работы сервиса в облаке это критически важно. Из этого появляется дополнительный объём работы, и в итоге, чтобы решить эту проблему, надо распутать целый клубок задач:
- пересобрать Docker-образ Spark Operator с зависимостями, причём важно указать совместимые версии;
- перед установкой создать S3-бакет и ключи доступа;
- создать secrets с ключами в K8s;
- указать secrets и новый образ при установке Spark Operator.
Для установки одного компонента — излишне сложный алгоритм (кстати, со сборкой образа Spark такая же история). Поэтому в своей реализации мы хотели избавить пользователей от подобных трудностей.
Подготовка кластера K8s
Мы решили начать с простой схемы, в которой главную роль выполняет бэкенд ML-платформы. Первым через внутренний сервис облака Kubernetes-as-a-Service запускается кластер Kubernetes. Причём это отдельный K8s на виртуальных машинах, где пользователю доступны все ресурсы процессора, памяти и диска.
Далее бэкенд ML Platform создаёт S3 Bucket и генерирует ключи доступа, после запуска кластера подключается и выполняет все действия по настройке и установке Spark Operator из подготовленного образа и интегрирует с S3-хранилищем.
На этом этапе пользователь уже получает настроенный Kubernetes со Spark и может запускать свои первые Spark-приложения.
Запуск Spark-приложений
Spark-приложения в Kubernetes представляют собой некий исполняемый файл, который написан на каком-нибудь из языков программирования: Python, Java, Scala или другом. И чтобы приложение запустить, нужно каким-либо образом подложить исполняемый файл внутрь Docker-контейнера Spark-приложения и выполнить следующие шаги:
- использовать kubeconfig для установки соединения с K8s;
- описать конфигурацию приложения в YAML-формате (манифест);
- применить манифест с помощью команды
kubectl apply –f manifest.yaml
.
Чтобы понять все прелести механизма запуска приложений, надо в деталях рассмотреть пару способов загрузки файла и взглянуть на YAML-манифест.
Доставка кода через ConfigMap
Один из способов — доставка кода через ConfigMap, который позволяет сохранять внутри Kubernetes текстовую информацию. Надо:
- создать config_map с кодом;
- написать manifest.yaml, указав ранее созданный config_map и путь, по которому исполняемый файл будет доступен внутри контейнера;
- выполнить команду
kubectl apply -f manifest.yaml
.
Доставка кода через S3-бакет
Этот способ подходит, если исполняемый файл находится в S3-хранилище. Для доставки файла с кодом в Kubernetes надо:
- создать secrets с ключами от S3;
- написать manifest.yaml, указав переменные окружения для доступа к S3, а также путь до исполняемого файла в хранилище;
- выполнить команду
kubectl apply -f manifest.yaml
.
Алгоритм понятен, но у него есть существенный недостаток: при таком подходе пользователям нужно для каждого приложения создавать YAML-манифесты и вникать во все нюансы настроек. Поскольку нам было важно сократить такие сложности, мы решили автоматизировать этот механизм и разработали специальный компонент, который назвали «клиентская библиотека» (Python Client Library).
Python Client Library
Это библиотека на Python, которую легко установить командой
pip Install
. Она работает из любого окружения и при этом не требует прямого доступа к кластеру Kubernetes. Клиентская библиотека, в свою очередь, делает запросы в бэкенд ML-платформы, и уже бэкенд подключается к Kubernetes и выполняет все необходимые действия. При использовании библиотеки запуск приложений значительно упрощается. В первом случае для запуска и доставки кода через ConfigMap понадобятся две строки кода.
В первой строке получаем манифест с параметрами по умолчанию, а во второй — запускаем приложение. При этом если указать локальный файл, то он «под капотом» загрузится в ConfigMap, после чего приложение запустится.
А во втором способе добавляется ещё одна строка, которая указывает путь к файлу в S3-хранилище:
После выполнения команд Spark-приложения запустятся и начнут свою работу. И для удобства отслеживания выполнения мы добавили в библиотеку возможность просмотра логов приложения и ивентов кластера.
Теперь клиенту даже не понадобится прямой доступ к кластеру — достаточно клиентской библиотеки, в которой есть вся нужная функциональность. С её помощью можно том числе находить ошибки и их причины.
В самом YAML-манифесте мы заполнили все значения по умолчанию. Это предельно упрощает запуск Spark-приложения и позволяет не вникать в глубокие настройки. Вместе с тем мы не ограничиваем возможность редактирования: при необходимости можно внести нужные изменения, переопределить или добавить переменные. Например, можно поменять количество ядер, которое будет использовать приложение, указать дополнительные параметры для Spark, добавить свои переменные окружения, а также выбрать Docker-образ с нужной версией Spark. Всё это также легко сделать через клиентскую библиотеку.
Docker Registry
Следующий компонент, который мы реализовали, — это Private Docker Registry. Он отвечает за хранение Docker-образов и интегрирован с кластером K8s и Spark. У приватного Registry есть ряд важных преимуществ. Во-первых, в нём уже есть собранные нами образы Spark, в которых установлены все необходимые расширения. Но ещё важнее, что пользователи могут использовать свои наработки, не выкладывая образы в публичный доступ. И конечно, важно, чтобы сам сервис был защищён авторизацией и SSL-соединением.
В нашей схеме Docker Registry запускается на отдельной виртуальной машине. Бэкенд ML-платформы создаёт доменное имя, выписывает сертификат, подключается к кластеру Kubernetes и выполняет интеграцию между Docker Registry, Kubernetes и самим Spark. Сразу после этого пользователь может загружать свои Docker-образы или использовать те, которые мы собрали и сделали доступными по умолчанию.
В нашей реализации легко переключаться между Docker-образами. Например, если изначально использовался образ по умолчанию, а потом был загружен собственный. Для этого достаточно переопределить параметр
image
и указать имя нужного образа с версией Spark — при запуске Kubernetes скачает и запустит этот образ.И нет ничего страшного, если вы не хотите вникать или даже знать, что существует Docker Registry: всегда можно оставить в манифесте параметры по умолчанию, и всё будет работать из коробки.
Spark History Server UI
Чтобы сделать нашу реализацию Spark в Kubernetes полноценной и удобной, мы также предусмотрели Spark History Server — веб-приложение, которое служит для отображения логов в структурированном и интерпретируемом для пользователя виде. Например, с его помощью можно понять, как приложение отработало, сколько времени это заняло, и увидеть детальную информацию о состоянии и работе Spark executors.
Интеграция происходит через S3 Bucket. Spark-приложение в процессе работы записывает логи, а Spark History Server читает и показывает их.
С установкой Spark History Server UI трудностей не возникло, и для этого было необходимо выполнить следующие действия:
- создать S3-бакет и ключи доступа;
- создать secrets в K8s;
- выполнить деплой Spark History Server UI в K8s.
Но без нюансов всё равно не обошлось. Например, в этом компоненте нет авторизации «из коробки», а для обеспечения безопасности и ограничения доступа нужно отдельное доменное имя и соединение, защищенное при помощи сертификата. Чтобы нивелировать этот недостаток, нам пришлось сделать свой сервис авторизации, который интегрирован с личным кабинетом VK Cloud. Чтобы попасть в Spark History Server, достаточно ввести данные от учётной записи личного кабинета.
Помимо сервиса авторизации, мы добавили в схему Ingress Controller, который отвечает за управление трафиком внутри Kubernetes. При правильной настройке он проверяет авторизацию всех входящих запросов и устанавливает защищённое соединение. Благодаря такой комбинации компонентов пользователи могут получать защищённый доступ к Spark History Server.
Token при использовании библиотеки
В качестве некой вишенки на торте мы добавили такую функцию, как токены доступа. Их главная цель — чтобы при работе с клиентской библиотекой не нужно было указывать логин и пароль от личного кабинета, так учетные данные не подвергаются риску утечки. Иначе мы бы поставили под угрозу все запущенные клиентом сервисы в облаке.
Такая возможность очень важна, так как библиотеку можно использовать как из пайплайна обработки данных, так и с персонального компьютера сотрудника, у которого может не быть доступа к учётной записи VK Cloud.
Для удобства мы добавили две роли: администратора и обычного пользователя. Например, администратор может создавать дополнительные токены для других пользователей прямо из клиентской библиотеки, просто указав имя, время жизни и роль.
from mlplatform_client import MLPlatform
from mlplatform_client.serializers.auth import MLPTokenType
ADMIN_REFRESH_TOKEN = "<значение токена доступа с ролью Администратор>"
mlp = MLPlatform (ADMIN_REFRESH_TOKEN)
register_token = mlp.create_register_token(
client_name = "<имя токена доступа>",
access_ttl = "<время жизни регистрационного токена>",
refresh_ttl = "<время жизни токена доступа>",
token_type = <роль токена доступа>)
print(register_token)
Работу с токенами мы вынесли в отдельный сервис — Token Manager, который генерирует токены. Таким образом, все запросы из клиентской библиотеки проходят авторизацию в бэкенде и в зависимости от ролевой модели получают разрешение на выполнение действия.
Итоги
Работа со Spark в Kubernetes — один из способов сократить издержки и получить ряд значимых преимуществ. Но при самостоятельном развёртывании сервиса неизбежна встреча с подводными камнями, граблями и сложностями.
В своей реализации мы не смогли уйти от сложной схемы с десятками компонентов, но все сложности мы оставили на стороне бэкенда — в процессе работы со Spark в Kubernetes пользователю не приходится с ними сталкиваться.
Вот и получилась изначальная архитектурная схема, в которой каждый элемент очень важен, для того чтобы сервис был простым и удобным, как мы это себе представляли. Наша реализация сервиса уже проходит бета-тестирование.
Сегодня мы зарелизили бету сервиса Cloud Spark на платформе VK Cloud. Это облачный сервис на основе Managed Kubernetes и Apache Spark для распределенной пакетной и потоковой обработки данных, работы с ML и аналитикой. Вы можете помочь сделать Cloud Spark лучше и принять участие в его развитии. Тестируйте сервис, присылайте нам отзывы и предложения — и мы реализуем их.