Мир изменился. Я чувствую это в воде, вижу в земле, ощущаю в воздухе. Всё, что когда-то существовало, ушло, и не осталось больше тех, кто помнит об этом.
Из фильма «Властелин колец: Братство кольца»
В интернете существует 100500 статей и докладов на тему «как мы пилили монолит», и у меня нет желания написать еще одну. Я попробовал пойти немного дальше и рассказать, как изменения технологий привели к появлению абсолютно нового продукта (спойлер: мы писали коробку, а написали платформу). Статья во многом получилась обзорной, без технических подробностей. Подробности будут позже.
Речь пойдет о панели управления сайтом и сервером Vepp. Это продукт ISPsystem, где я руковожу разработкой. О возможностях новой панели читайте в другой статье, здесь — только про технологии. Но вначале, как обычно, немного истории.
Часть 1. Надо что-то менять
Наша компания пишет софт для автоматизации услуг
Но, менялись не только мы, менялись наши клиенты и конкуренты.Если 10-15 лет назад владелец сайта был неплохо технически подкован (другие интернетом интересовались слабо), то сейчас это может быть человек, никак не связанный с ИТ. Появилось множество конкурирующих решений. При таких условиях делать просто работающий продукт недостаточно, надо, чтобы им было легко и приятно пользоваться.
Героический редизайн
Это изменение стало во всех смыслах наиболее заметным. Всё это время интерфейс наших продуктов оставался практически неизменным и унифицированным. Мы уже писали про это отдельно — взгляд со стороны UX. В пятом поколении продуктов API определял внешний вид форм и списков. Что, с одной стороны, позволяло реализовывать многие вещи без привлечения frontend-разработчиков, с другой, рождало очень сложные комплексные вызовы, иногда затрагивающие большую часть системы, и сильно ограничивало возможности по изменению интерфейса. Ведь любое изменение — это неминуемо изменение API. И всё, привет интеграциям!
Например: создание пользователя в ISPmanager также может приводить к созданию FTP пользователя, почтового домена, почтового ящика, DNS записи, сайта. И всё это делается атомарно, а следовательно, блокирует изменения в перечисленных компонентах до полного завершения операции.
В новом API мы перешли на маленькие и простые атомарные операции, а сложные комплексные действия оставили на откуп Frontend разработчиков. Это позволяет реализовывать сложный интерфейс и менять его, не затрагивая базовый API и, следовательно, не ломая интеграции.
Для frontend-разработчиков мы реализовали механизм пакетного выполнения запросов с возможностью выполнения обратных действий в случае ошибки. Как показывает практика в большинстве случаев сложные запросы — это запросы создания чего-либо, а создание довольно легко отменить, выполнив удаление.
Уменьшение времени отклика и мгновенные уведомления
Мы отказались от долгих модифицирующих запросов. Предыдущие версии наших продуктов могли подвисать на длительное время, пытаясь выполнить запрос пользователя. Мы решили, что на каждое действие сложнее банального изменения данных в БД мы будем создавать в системе задачу и отвечать клиенту как можно быстрее, позволяя ему продолжать работу, а не смотреть на бесконечный процесс загрузки.
Но, что делать, если результат вам нужен, что называется здесь и сейчас? Думаю, многим знакома ситуация, когда вы раз за разом перезагружаете страницу в надежде, что операция вот-вот завершится.
В новом поколении продуктов мы используем websocket для мгновенной доставки событий.
Первая реализация использовала longpoll, но frontend разработчикам такой подход был неудобен.
Внутреннее взаимодействие по HTTP
HTTP как транспорт победил. Сейчас в любом языке HTTP-сервер реализуется за секунды (если гуглить, то за минуты). Даже локально проще сформировать HTTP-запрос, чем городить свой протокол.
В предыдущем поколении расширениями (плагинами) были приложения, которые при необходимости запускались как CGI. Но чтобы написать долгоживущее расширение приходилось сильно постараться: писать плагин на C++ и пересобирать его при каждом обновлении продукта.
Поэтому в шестом поколении мы перешли на внутреннее взаимодействие по HTTP, а расширения, по сути, стали маленькими WEB серверами.
Новый (не совсем REST) API
В предыдущих поколениях продуктов мы передавали все параметры через GET или POST. Если с GET особых проблем нет — его размер невелик, то в случае с POST не было никакой возможности проверить доступ или перенаправить запрос до его полного вычитывания.
Представляете, как это грустно: принять несколько сотен мегабайт или гигабайт, а потом обнаружить, что их влил неавторизованный пользователь или что теперь их надо переложить вооон на тот сервер!
Теперь имя функции передается в URI, а авторизация исключительно в заголовках. И это позволяет проводить часть проверок до вычитывания тела запроса.
Кроме того, функции API стали более простыми. Да, теперь мы не гарантируем атомарность создания пользователя, почты, сайта и тому подобного. Зато даём возможность комбинировать эти операции так, как это необходимо.
Мы реализовали возможность пакетного выполнения запросов. По сути, это отдельный сервис, который принимает список запросов, и последовательно выполняет их. Он же может сделать откат уже выполненных операций, в случае возникновения ошибки.
Да здравствует SSH!
Еще одно решение, которое мы приняли исходя из предыдущего опыта, — работать с сервером только через SSH (даже если это локальный сервер). Изначально в VMmanager и ISPmanager мы работали с локальным сервером, а уже потом сделали возможность добавить дополнительные — удаленные. Это привело к необходимости поддержки двух реализаций.
И когда мы отказались от работы с локальным сервером, исчезли последние причины использовать нативные библиотеки пользовательской операционной системы. С ними мы мучаемся с основания компании. Нет, в нативных библиотеках есть свои плюсы, но минусов больше.
Безусловный плюс — меньшее потребление как диска, так и памяти, а для работы внутри
Старые привычки
Изменив подходы и перейдя на новые технологии, мы продолжали действовать по-старому. Это приводило к сложностям.
Хайповая тема с микросервисами не прошла мимо нас. Мы тоже решили разделить приложение на отдельные компоненты-сервисы. Это позволило ужесточить контроль над взаимодействием и проводить тестирование отдельных частей приложения. И, дабы еще жестче контролировать взаимодействие, мы разложили их по контейнерам, которые, впрочем, всегда можно было собрать в одну кучу.
В монолитном приложении вы можете без особого труда получить доступ практически к любым данным. Но даже если вы разделяете продукт на несколько приложений и оставляете их рядом, они, как живые, могут образовывать связи. Например, в виде общих файлов или непосредственных запросов друг к другу.
Переход на микросервисы не был простым. Старый паттерн «напишем библиотеку и подключим её везде, где надо» преследовал нас довольно долго. Например, у нас есть сервис, отвечающий за выполнение «долгих» операций. Изначально он был реализован в виде библиотеки, подключаемой к тому приложению, которому это требовалось.
Еще одну привычку можно описать следующим образом: зачем писать еще один сервис, если можно научить этому существующий. Первое, что мы отпилили от своего монолита — механизм авторизации. Но затем появился соблазн пихать этот сервис все общие компоненты, как в COREmanager (базовый фреймворк для продуктов пятого поколения).
Часть 2. Совмещая несовместимое
Вначале читающие и пишущие запросы выполнялись одним процессом. Как правило, пишущие запросы — блокирующие запросы, но при этом очень быстрые: записал в базу, создал задачу, ответил. С читающим запросом история другая. Создавать на него задачу сложно. Он может генерировать довольно объемный ответ, и что делать с этим ответом, если клиент за ним не вернется? Сколько его хранить? Но при этом обработка читающих запросов отлично распараллеливается. Данные различия приводили к проблемам с перезапуском таких процессов. Их жизненные циклы попросту несовместимы!
Мы разделили приложение на две части: читающую и пишущую. Правда, вскоре выяснилось, что это не очень удобно с точки зрения разработки. Чтение списка ты делаешь в одном месте, редактирование в другом. И тут главное — не забыть поправить второе, если изменяешь первое. Да и переключения между файлами как минимум раздражают. Поэтому мы пришли к приложению, которое запускается в двух режимах: читающем и пишущем.
Предыдущее поколение наших продуктов активно использовало потоки. Но, как показала практика, они нас не сильно спасали. Из за множества блокировок нагрузка на CPU редко переваливала за 100%. Появление большого числа отдельных достаточно быстрых сервисов позволило отказаться от многопоточности в пользу асинхронной и однопоточной работы.
В процессе разработки мы пробовали использовать потоки совместно с асинхронностью (boost::Asio это позволяет). Но данный подход скорее привносит в ваш проект все недостатки обоих подходов, чем дает какие-то видимые преимущества: придется сочетать необходимость контроля при доступе к разделяемым объектам и сложность написания асинхронного кода.
Часть 3. Как мы писали коробку, а написали платформу
Все сервисы разложены по контейнерам и работают с клиентским сервером удаленно. А зачем тогда ставить приложение на сервер клиента? Вот какой вопрос я задал руководству, когда пришло время упаковывать получившийся продукт для установки на сервер.
Что такое платформа? Сперва мы развернули SaaS — сервис, работающий на наших серверах и позволяющий настраивать ваш сервер. Если вы пользовались какой-либо панелью управления сервером и покупали её самостоятельно — это решение для вас. Но провайдерам оно не подходит:, они не готовы предоставлять доступ на сервера своих клиентов сторонней компании, и я их очень хорошо понимаю. Тут возникают вопросы как безопасности, так и отказоустойчивости. Поэтому мы решили отдать им наш SaaS целиком, чтобы они могли развернуть его у себя. Это как Amazon, который вы можете запустить в своём собственном дата-центре и подключить к своему биллингу. Мы назвали такое решение — платформа.
Первое развертывание прошло не очень гладко. На каждого активного пользователя мы поднимали отдельный контейнер. Docker-контейнеры поднимаются быстро, вот только service discovery срабатывает не мгновенно: не предназначено оно для динамического поднятия/остановки контейнеров в течение секунды. И с момента поднятия до момента, когда сервисом можно пользоваться, иногда проходили минуты!!!
Я уже писал, что пользователь
После того как он всё же смог попасть в консоль, ему надо установить панель. А это операция тоже небыстрая. А если что-то пойдет не так? Например, РосКомНадзор может заблокировать сервера, с которых скачиваются пакеты для вашей ОС. С такими ошибками пользователь останется один на один.
Вы можете возразить, что в большинстве случаев пользователь получает уже установленную хостером панель, и сказанное ранее его не касается. Возможно. Но панель, работающая на сервере, потребляет оплаченные вами ресурсы (занимает место на диске, жрет ваш процессор и память). Её работоспособность напрямую зависит от работоспособности сервера (упал сервер — упала панель).
Еще могут быть те, кто не пользуется никакими панелями и думает: меня это не касается. Но, возможно, на вашем сервере, если он у вас есть, какая-то панель всё же стоит и жрёт ресурсы, а вы ей просто не пользуетесь?
«Так это даже замечательно, вы помогаете нам продавать ресурсы», — говорят российские хостеры. Другие добавляют: «Зачем нам тратить свои ресурсы на развертывание платформы, которая потребляет их больше, чем отдельно стоящая панель?»
На этот вопрос есть несколько ответов.
- Повышается качество обслуживания: вы можете контролировать версию панели — оперативно обновлять её при появлении нового функционала или обнаружении ошибок; можете анонсировать акции прямо в панели.
- В масштабах инфраструктуры вы экономите диск, память и процессор, так как часть процессов запускается только для обслуживания активных пользователей, а часть обслуживает множество клиентов одновременно.
- Поддержке не надо анализировать, является ли поведение особенностью конкретной версии панели или ошибкой. Это экономит время.
Еще многие наши клиенты покупали лицензии пачками и затем жонглировании ими, перепродавая своим клиентам. Больше нет необходимости заниматься этой, в принципе, бессмысленной работой. Ведь теперь это один продукт.
Кроме того, мы получили возможность использовать тяжелые решения, предлагая недоступный ранее функционал. Например, сервис для получения скриншотов сайта требует запуска безголового chromium. Я не думаю, что пользователь очень обрадуется, если из-за подобной операции у него закончится память и застрелился, скажем, MySQL.
В заключение хочется отметить, что мы учимся на чужом опыте и активно нарабатываем собственный. Сборка логов, docker, service discovery, всевозможные задержки, ретраи и очереди… Сейчас и не вспомнишь всего, что пришлось освоить или изобрести заново. Все это не делает разработку проще, но открывает новые возможности и делает нашу работу более увлекательной.
История еще не завершилась, но то, что получилось, можно посмотреть на сайте Vepp.
Автор: Александр