28 февраля 2019

Дмитрий Сыроватский

Back-end разработчик

VMmanager 6. Как избавиться от непрактичного и сохранить полезное

Меня зовут Дмитрий, я разработчик в ISPsystem. Недавно мы выпустили в бета-тестирование новую версию панели управления виртуальными машинами. Сегодня я расскажу, как мы решали, что взять из старого продукта, а от чего лучше отказаться. Пройдусь по самым важным для нас вопросам: библиотека для работы с libvirt, поддержка различных ОС при установке продукта, переход от монолита к микросервисам, развёртывание виртуальных машин.
В статье речь идёт о VMmanager. Это система управления, развёртывания и мониторинга виртуальных машин на основе виртуализации KVM и OVZ. Пятое поколение вышло в 2012 году. С тех пор интерфейс сильно устарел, а централизованная архитектура мешала развивать продукт. Пришло время делать новую версию.

Работа с libvirt: рассматриваем варианты, выбираем библиотеки

Как инструмент управления KVM-виртуализацией в нашем продукте используется libvirt. В 2012 году для работы с ним была выбрана библиотека, написанная на C, так было удобнее для той команды разработки. Как результат — большой объем кода, написанный на C++, вызывающий С-библиотеку, которая реализует непосредственную работу с libvirt.
И вот мы на пороге нового проекта оглядываемся назад и осматриваем наш продукт, взвешиваем, стоит ли брать то или иное решение/технологию; что зарекомендовало себя, а что нужно запомнить и никогда не повторять.
Садимся и делаем ретроспективу многолетней работы над предыдущей версией продукта. Запасаемся терпением, берём стикеры и пишем три вида бумажек:
  1. Что удалось в продукте? Что хвалили пользователи? На что ни разу не слышали жалоб? Что нравилось самим?
  2. Что не удалось? С чем постоянно были проблемы? Что мешало работе, и почему начали новую ветку?
  3. Что можно изменить? Что просят пользователи? Что хотят изменить члены команды?
В группе людей, рьяно портящих бумагу, должны быть как те, чьё тесное общение с продуктом исчисляется столетиями, так и те, у кого может быть свежий взгляд на продукт. Не забываем Feature Request и продукт-менеджера. Готовые стикеры клеим на доску, они нам обязательно помогут.
Вернёмся к истории. Осматриваем кусок кода, где C++ 98 стандарта мирно соседствует с вызовами С-библиотеки. Вспоминаем, что на дворе 2018 год и решаем оставить его в покое. Но как повторить функциональность работы с виртуальными машинами (ВМ), сделав код более компактным и удобным для работы?
Изучаем вопрос, понимаем, что какое бы решение и на каком языке мы ни выбрали, это будет обёртка над C-библиотекой. Как интересный вариант стоит отметить библиотеку на Go от DigitalOcean, она использует протокол RPC для общения с libvirt напрямую, но у неё есть свои недостатки. Мы остановились на Python-библиотеке.
В результате получили скорость написания кода, простоту использования и чтения. Стоит пояснить эти красивые слова.
  1. Скорость. Теперь мы можем быстро прототипировать определённую часть работы с доменом прямо из консоли на отладочном сервере, без пересборки основного приложения.
  2. Простота. Вместо вызова множества C++ методов в неком хендлере, мы имеем вызов Python-скрипта с передачей параметров.
  3. Отладка также максимально быстра и безболезненна. На мой взгляд, в перспективе это может нести интересный пользовательский опыт. Представьте, системный администратор, недовольный, что его виртуальные машины ждут shutdown перед destroy, идёт и переопределяет скрипт для метода host_stop. Может мне за вас еще и панель писать?
Как итог, мы получили простой и удобный инструмент для работы с виртуальными машинами на уровне сервера.

Распространение продукта: отказываемся от множества пакетов и переходим на Docker

VMmanager 5 распространяется как набор linux-пакетов. Поддерживаются CentOS 6/7 и до недавнего времени Debian 7. О чём это говорит? Это значит, больше сборочных серверов для CI/CD, больше тестирования, больше внимания к коду. Надо помнить, что когда в официальном репозитории CentOS 7 qemu версии 1.5.3, в CentOS 6 — это 0.12.1. При этом пользователь может использовать репозитории, в которых версия этого пакета значительно выше. Это значит, надо поддерживать различные версии api при работе с ВМ, в частности, при миграции. Надо помнить про разницу инициализаторов (init, systemd), учитывать разницу в названиях пакетов и утилит. Те утилиты, которые работают в CentOS, не сработают в Debian либо их версии в официальных репозиториях сильно разнятся. На каждый push нужно собрать пакеты для всех версий, и протестировать их тоже желательно не забыть.
Всё это в новом продукте нас не устраивает. Чтобы не поддерживать различную логику, отказываемся от нескольких систем и оставляем только CentOS 7. Проблема решена? Не совсем.
Мы также не хотим перед установкой проверять версию операционной системы, доступны ли необходимые утилиты, какие правила установлены в SELinux, не хотим переконфигурировать файервол и списки репозиториев. Хочется раз — и всё, по щелчку уничтожить каждого второго развернуть всё окружение и сам продукт. Сказано — сделано, проект завёрнут в докер-контейнер.
Теперь достаточно сделать:

# docker pull vmmanager
# docker run -d vmmanager:latest

Панель запущена и готова к работе.
Конечно, я утрирую, пользователь должен установить себе Docker, да и контейнер у нас не один, и в настоящее время VMmanager запускается в swarm-режиме как SaaS-сервис. О том, с чем мы столкнулись, выбрав Docker, и как это решали, можно написать отдельную статью.
Важен сам факт, насколько можно упростить разработку, а главное — развёртывание вашего продукта, install.sh которого некогда занимал 2097 строк.
Как итог:
  1. Гомогенная среда установки продукта упрощает программный код, уменьшает затраты на сборку и тестирование.
  2. Распространение приложения как докер-контейнера делает развёртывание простым и предсказуемым.

Архитектура: отказываемся от монолита в пользу микросервисов, или нет

Пятая версия продукта представляет собой большую монолитную систему с устаревшим стандартом С++. Как следствие — проблемное внедрение новых технологий и сложность рефакторинга legacy-кода, плохое горизонтальное масштабирование. В новой ветке решили использовать микросервисный подход как один из способов избежать подобных проблем.
Микросервисы — это современный тренд, у которого хватает как плюсов, так и минусов. Постараюсь изложить своё видение сильных сторон этой архитектуры и расскажу о решении проблем, которые она привносит в проект. Стоит отметить, что это будет первый взгляд на микросервисную архитектуру на практике со стороны обычного разработчика. Аспекты, о которых я, вероятно, не упомяну, раскрывает хорошая обзорная статья.

Положительные стороны

Небольшой сервис даёт много возможностей. Помимо удобства написания, тестирования и отладки, микросервисы привнесли в проект новый язык программирования. Когда ваш проект представляет собой монолит, сложно представить, что в один прекрасный день вы попробуете переписать его часть на другом интересном вам языке. В микросервисной архитектуре — пожалуйста. Помимо языка программирования, вы также можете попробовать новые технологии, с той лишь оговоркой, что всё это будет обоснованным для бизнеса. Мы, например, написали часть микросервисов на Golang, сэкономив при этом порядочное количество времени.
Масштабирование команд. Мы можем разделить множество людей, которые раньше комитили в один репозиторий и старались удержать в голове структуру монолита, на несколько команд. Каждая команда будет заниматься своим сервисом. Кроме того, вхождение нового человека в работу происходит намного проще и быстрее, благодаря ограниченному контексту, в котором ему предстоит работать. С другой стороны, людей-агрегаторов всемирных знаний, у которых всегда можно узнать про любой аспект огромной системы, становится меньше. Возможно, со временем я пересмотрю своё отношение к этому пункту.
Независимая деградация. Независимую деградацию я бы отнёс и к положительным и к отрицательным сторонам микросервисов, ведь кому нужно ваше приложение, если лежит, к примеру, сервис авторизации? При этом это всё-таки положительная сторона. Раньше сбор статистики с нескольких сотен виртуальных машин заставлял хорошенько напрягаться наш монолит, в момент пиковой нагрузки ожидание выполнения запроса пользователя возрастало в разы. Отдельный сервис сбора статистики может собирать её, не затрагивая остальные сервисы, при этом его ещё можно масштабировать, добавив нового железа или увеличив количество сборщиков той самой статистики. И можем даже выделить отдельный сервер под Graphite, куда этот сервис статистику и записывает. С монолитом, где одна база, такое невозможно.

Отрицательные стороны

Контекст запроса. Весь мой debugging в монолите сводился к двум запросам в консоли:

# tail -n 460 var/vmmgr.log | grep ERR
# tail -n 460 var/vmmgr.log | grep thread_id_with_err

Готово! Я могу отследить весь запрос, от его поступления в систему до возникновения ошибки.
Но как же быть теперь, когда запрос путешествует из микросервиса в микросервис, сопровождаясь дополнительными вызовами соседних сервисов и записями в различные БД? Для этого мы реализовали request info, которое содержит идентификатор запроса и информацию о пользователе или сервисе, который его произвёл. Так становится проще отследить всю цепочку событий, но приходит желание написать сервис агрегации логов, у нас ведь, в конце концов, микросервисная архитектура. Можно также посмотреть в сторону Elasticsearch, этот вопрос открыт и будет решён в скором времени.
Несогласованность данных. Данные в микросервисах децентрализованы, нет единой базы, в которой хранится вся информация. Обдумывая эту статью, я перебирал в уме основные взаимодействия между микросервисами — где мы можем получить дубли, где используем межсетевые транзакции — и понял, что проблему несогласованности мы решили монолитом.
Мы действительно построили монолит с одной основной базой, завернув в него большую часть транзактивных действий. А вокруг монолита собрали все микросервисы, которые не влияют на консистентность основных данных. Исключение составляет связка сервис авторизации + монолит. Проблема в данном случае состоит в том, что база основного приложения не содержит пользователей как таковых, их ролей и дополнительных параметров, всё это находится в сервисе авторизации.
Пользователь системы может работать с виртуальными машинами в монолите, при этом в сервисе авторизации у него могут измениться права, или его вовсе заблокируют. Система должна на это своевременно среагировать. В этой ситуации консистентность данных достигается проверкой пользовательских параметров перед выполнением любого запроса.
Что касается остальных микросервисов, невозможность зарегистрироваться в сервисе статистики никак не влияет на работу виртуальной машины, и это действие всегда можно повторить. Отлично, делаем микросервис сбора статистики. А вот сервис define domain (создание виртуальной машины посредством libvirt) свет так и не увидит, так как кому нужна болванка машины без её фактического существования.

Развёртывание ВМ: установка из образов взамен установки по сети

В пятой версии продукта развёртывание виртуальной машины занимает довольно продолжительное время по настоящим меркам. Причиной этому служит установка операционной системы по сети.
Для Centos, Fedora, RedHat — это kickstart-метод:
  1. Создаём kickstart-файл.
  2. Указываем ссылку на файл ответов в параметрах ядра linux inst.ks=<ссылка на файл kickstart>.
  3. Запускаем kickstart-установку.
Kickstart-файл довольно гибок, в нём вы можете описать все шаги установки, начиная от её метода и выставления часового пояса, заканчивая разбивкой диска и настройкой сети. Параметр url в наших шаблонах указывает на то, что установка происходит с удалённого сервера.
Для Debian и Ubuntu — метод preseed:
Он схож с предыдущим, этот метод также построен вокруг конфигурационного файла и его содержимого. В нём мы тоже настроили установку по сети.
Аналогична и установка для FreeBSD, только вместо kickstart-файла — shell-скрипт собственного производства.

Положительные стороны подхода

Данный вариант установки позволяет использовать один шаблон в двух наших продуктах: VMmanager и DCImanager (управление выделенными серверами).
Развертывание виртуальных машин является довольно гибким, администратор панели может просто скопировать шаблон операционной системы и изменить конфигурационный файл по своему усмотрению.
Все пользователи всегда имеют актуальные версии операционных систем, если они своевременно обновляются на удаленном сервере.

Отрицательные стороны

Как показала практика, гибкость установки оказалась не нужна пользователям VMmanager: в сравнении с выделенными серверами, специфические настройки kickstart-файла для виртуальных машин мало кого волновали. А вот ожидание установки ОС было действительно непозволительной роскошью. Обратная сторона актуальности операционных систем состоит в том, что часть инсталлятора находится в сети, а часть — локально в initrd. И их версии должны совпадать.
Это решаемые проблемы. Можно создать пул установленных машин и завести собственный репозиторий для операционных систем, но это влечёт дополнительные расходы.
Как решить эти проблемы, не создавая репозиториев и пулов? Мы выбрали файлы образов операционных систем. Теперь процесс установки выглядит так:
  1. Копирование образа ОС в диск виртуальной машины.
  2. Увеличение основного раздела образа на размер свободного места после копирования.
  3. Базовая настройка (установка пароля, часового пояса и т.д.).
Всё новое — это хорошо забытое старое. Образы ОС мы использовали в VDSmanager-Linux — прародителе VMmanager.
А как же гибкость установки? Практика показала, что большинство пользователей не интересуют специфичные настройки сети и разбивки дисков на виртуальных машинах. А актуальность данных? Её можно достичь наличием образов с актуальными версиями ОС в репозитории, а минорные обновления можно установить в скрипте первоначальной настройки. Таким образом, виртуальная машина будет уже создана и запущена, а зайдя на нее, вы обнаружите запущенным условный yum update.
Взамен мы получаем готовую виртуальную машину, развёртывание которой зависит только от копирования диска, увеличения раздела диска и запуска операционной системы. Реализация данного подхода работы с машинами даёт нам возможность создавать собственные образы и делиться ими. Пользователь может установить на виртуальной машине связку LAMP или какое-либо сложное окружение, затем сделать образ этой машины. Теперь другим людям не придется тратить время на установку необходимых утилит.
Конфигурацию и изменение разделов мы реализовали с помощью утилит из набора libguestfs. К примеру, смена пароля на linux-машине превратилась из 40 строк кода, состоящих из mount, chroot и usermod, в одну строку:

command = "/usr/bin/virt-customize --root-password password:{password} --domain '{domain_name}'".format(password=args.password, domain_name=args.domain_name)

В результате мы сделали получение готовой виртуальной машины максимально быстрым. Стоит сделать ремарку, что с настройкой сети и установкой внутренних скриптов время развёртывания немного увеличилось. Мы решили этот вопрос отображением шагов установки на фронтенде, заполнив таким образом паузу, образовавшуюся между созданием и полной готовностью машины.
Мы также получили более гибкий подход к развертыванию виртуальных машин, на основе которого удобно создавать собственные образы с необходимым окружением.

Что удалось сделать

В шестой версии продукта мы постарались учесть основной недостаток пятой: сложность общения пользователя с продуктом. Мы сократили время выполнения ключевых действий. В совокупности с неблокирующим интерфейсом это даёт возможность работать с панелью без вынужденного ожидания. Контейнеризация сделала процесс установки продукта более простым и удобным. Использование современных технологий и различных языков программирования упростило поддержку и сопровождение как программистам, так и специалистам технической поддержки. Переход на микросервисы позволил добавлять новые фичи быстро и с незначительными ограничениями.
В заключение хочу сказать, что новый продукт — хорошая возможность попробовать другие подходы к разработке, новые технологии. При этом стоит помнить, для чего вы это делаете, что нового это принесет вам и вашим пользователям. Дерзайте!

Дмитрий Сыроватский

Back-end разработчик