Чем быстрее процесс разработки, тем быстрее развивается технологическая компания.
К сожалению, современные приложения работают против нас — наши системы должны обновляться в режиме реального времени и при этом никому не мешать и не приводить к простоям и перерывам. Развертывание в таких системах становится сложной задачей и требует сложных пайплайнов непрерывной поставки даже в маленьких командах.
Эти пайплайны обычно имеют узкое применение, медленно работают и не отличаются надежностью. Разработчики должны сначала создать их вручную, а потом управлять ими, и компании часто нанимают для этого целые команды DevOps.
От скорости этих пайплайнов зависит скорость разработки. У лучших команд развертывание занимает 5–10 минут, но обычно все делается гораздо дольше, и для одного развертывания требуется несколько часов.
Почему конвейеры непрерывной поставки такие медленные?
Допустим, есть у нас веб-приложение Python и мы уже создали замечательный и современный пайплайн непрерывной поставки. Для разработчика, который каждый день занят этим проектом, развертывание одного незначительного изменения будет выглядеть примерно так:
Внесение изменений
Создание новой ветки в git
Внесение изменений за переключателем функции
Модульное тестирование для проверки изменений с переключателем функции и без
Пул-реквест
Коммит изменений
Отправка изменений в удаленный репозиторий на github
Пул-реквест
Сборка CI выполняется автоматически в фоновом режиме
Ревью кода
Еще несколько ревью, если нужно
Слияние изменений с мастером git.
CI выполняется на мастере
Установка фронтенд-зависимостей через npm
Сборка и оптимизация ресурсов HTML+CSS+JS
Прогон во фронтенде модульных и функциональных тестов
Установка зависимостей Python из PyPI
Прогон в бэкенде модульных и функциональных тестов
Тестирование интеграции на обоих концах
Отправка ресурсов фронтенда в CDN
Сборка контейнера для программы Python
Отправка контейнера в реестр
Обновление манифеста Kubernetes
Замена старого кода новым
Kubernetes запускает несколько экземпляров нового контейнера
Kubernetes ждет, чтобы экземпляры стали работоспособными
Kubernetes добавляет экземпляры в балансировщик нагрузки HTTP
Kubernetes ждет, пока старые экземпляры перестанут использоваться
Kubernetes останавливает старые экземпляры
Kubernetes повторяет эти операции, пока новые экземпляры не заменят все старые
Включение нового переключателя функции
Новый код включается только для себя, чтобы убедиться, что все нормально
Новый код включается для 10% пользователей, отслеживаются операционные и бизнес-метрики
Новый код включается для 50% пользователей, отслеживаются операционные и бизнес-метрики
Новый код включается для 100% пользователей, отслеживаются операционные и бизнес-метрики
Наконец, вы повторяете всю процедуру, чтобы удалить старый код и переключатель
Процесс зависит от инструментов, языка и использования архитектур, ориентированных на сервисы, но в общих чертах он выглядит так. Я не упомянул развертывания с миграцией баз данных, потому что для этого нужно тщательное планирование, но ниже я расскажу, как с этим разбирается Dark.
Здесь много компонентов, и многие из них запросто могут тормозить, сбоить, вызывать временную конкуренцию или обрушивать рабочую систему.
А раз эти пайплайны почти всегда созданы для особого случая, на них сложно полагаться. У многих бывают дни, когда код невозможно развернуть, потому что в Dockerfile есть проблемы, в одном из десятков сервисов произошел сбой или нужный специалист в отпуске.
Хуже того, многие из этих шагов вообще не делают ничего полезного. Они нужны были раньше, когда мы развертывали код сразу для пользователей, но теперь у нас есть переключатели для нового кода, и эти процессы разделились. В итоге шаг, на котором развертывается код (старый заменяется на новый), теперь стал просто лишним риском.
Конечно, это очень продуманный пайплайн. Команда, создавшая его, не пожалела времени и денег для быстрого развертывания. Обычно пайплайны развертывания куда медленнее и ненадежнее.
Реализация непрерывной поставки в Dark
Непрерывная поставка настолько важна для Dark, что мы с самого начала нацелились на время меньше секунды. Мы перебрали все шаги пайплайна, чтобы удалить все лишнее, а остальное довели до ума. Вот как мы удаляли шаги.
Джесси Фразель (Jessie Frazelle) придумала новое слово deployless (не требующий развертывания) на конференции Future of Software Development в Рейкьявике
Мы сразу решили, что Dark будет основан на концепции «deployless» (спасибо Джесси Фразель за неологизм). Deployless означает, что любой код моментально развертывается и готов к употреблению в продакшене. Конечно, мы не пропустим неисправный или неполный код (принципы безопасности я опишу ниже).
На демонстрации Dark нас часто спрашивали, как мы ухитрились так ускорить развертывание. Странный вопрос. Люди, наверное, думают, что мы придумали какую-то супертехнологию, которая сравнивает код, компилирует его, упаковывает в контейнер, запускает виртуальную машину, на холодную запускает контейнер и все в таком духе, — и все это за 50 мс. Вряд ли это возможно. Но мы создали специальный движок развертывания, которому все это и не нужно.
Dark запускает интерпретаторы в облаке. Допустим, вы пишете код в функции или обработчике HTTP или событий. Мы отправляем diff в абстрактное синтаксическое дерево (реализацию кода, которую внутренне использует наш редактор и серверы) на наши серверы, а затем запускаем этот код, когда поступают запросы. Так что развертывание выглядит просто как скромная запись в базу данных — моментальная и элементарная. Развертывание происходит так быстро, потому что включает самый минимум.
В будущем мы планируем сделать из Dark компилятор инфраструктуры, который будет создавать и запускать идеальную инфраструктуру для высокой производительности и надежности приложений. Моментальное развертывание, конечно, никуда не денется.
Безопасное развертывание
Структурированный редактор
Код в Dark пишется в редакторе Dark. Структурированный редактор не допускает синтаксических ошибок. По сути, в Dark даже анализатора нет. Пока вы вводите текст, мы напрямую работаем с абстрактным синтаксическим деревом (AST), как Paredit, Sketch-n-Sketch, Tofu, Prune и MPS.
У любого незавершенного кода в Dark есть допустимая семантика выполнения, примерно как typed holes в Hazel. Например, если вы меняете вызов функции, мы храним старую функцию, пока новая не станет пригодной.
У каждой программы в Dark есть свой смысл, поэтому незавершенный код не мешает завершенному работать.
Режимы редактирования
Вы пишете код в Dark в двух случаях. Первый: вы пишете новый код и являетесь единственным пользователем. Например, он в REPL, и другие пользователи никогда не получат к нему доступ, или это новый маршрут HTTP, на который вы нигде не ссылаетесь. Тут можно работать без всяких мер предосторожности, и сейчас вы примерно так и работаете в среде разработки.
Вторая ситуация: код уже используется. Если через код проходит трафик (функции, обработчики событий, базы данных, типа), нужно соблюдать осторожность. Для этого мы блокируем весь используемый код и требуем использовать более структурированные инструменты для его редактирования. О структурных инструментах расскажу ниже: переключатели функций для обработчиков HTTP и событий, мощная платформа миграции для баз данных и новый метод управления версиями для функций и типов.
Переключатели функций
Один из способов убрать лишнюю сложность в Dark — устранить несколько проблем одним решением. Переключатели функций выполняют много разных задач: замена локальной среды разработки, ветки git, развертывание кода и, конечно, традиционный медленный и контролируемый выпуск нового кода.
Создание и развертывание переключателя функции выполняется в нашем редакторе за одну операцию. Он создает пустое пространство для нового кода и предоставляет элементы управления доступом к старому и новому коду, а также кнопки и команды для постепенного перехода на новый код или его исключения.
Переключатели функций встроены в язык Dark, и даже незавершенные переключатели выполняют свою задачу — если условие в переключателе не выполнено, будет выполняться старый заблокированный код.
Среда разработки
Переключатели функций заменяют локальную среду разработки. Сегодня командам сложно следить за тем, чтобы все использовали одинаковые версии инструментов и библиотек (средства форматирования кода, линтеры, диспетчеры пакетов, компиляторы, препроцессоры, инструменты тестирования и т. д.) С Dark не нужно устанавливать зависимости локально, управлять локальной установкой Docker или принимать другие меры, чтобы обеспечить хотя бы подобие равенства между средой разработки и продакшеном. Учитывая, что такое равенство все равно невозможно, мы даже притворяться не будем, что стремимся к нему.
Вместо того, чтобы создавать клонированную локальную среду, переключатели в Dark создают новую песочницу в продакшене, которая заменяет среду разработки. В будущем мы также планируем создавать песочницу для других частей приложения (например, моментальные клоны базы данных), хотя пока это не кажется таким уж важным.
Ветки и развертывания
Сейчас есть несколько способов ввода нового кода в системы: ветки git, этап развертывания и переключатели функций. Они решают одну проблему в разных частях рабочего процесса: git — на этапах перед развертыванием, развертывание — в момент перехода со старого кода на новый, а переключатели функции — для контролируемого выпуска нового кода.
Самый эффективный способ — переключатели функций (заодно самый простой для понимания и использования). С ними можно полностью отказаться от остальных двух методов. Особенно полезно удалить развертывание — если мы все равно используем переключатели функций, чтобы включить код, то шаг перевода серверов на новый код только создает лишние риски.
Git сложно использовать, особенно новичкам, и это здорово его ограничивает, но зато у него есть удобные ветки. Мы сгладили многие недостатки git. Dark редактируется в реальном времени и обеспечивает возможность совместной работы в стиле Google Документов, чтобы не приходилось отправлять код и можно было реже выполнять перебазирование и слияние.
Переключатели функций лежат в основе безопасного развертывания. Вместе с моментальными развертываниями они позволяют быстро тестировать концепты в маленьких фрагментах с низким риском вместо того, чтобы применять одно крупное изменение, которое может обрушить систему.
Версионирование
Для изменения функций и типов мы используем версионирование. Если вы хотите изменить функцию, Dark создает новую версию этой функции. Потом вы можете вызвать эту версию с помощью переключателя в обработчике HTTP или событий. (Если это функция глубоко в графе вызовов, по ходу создается новая версия каждой функции. Может показаться, что это чересчур, но функции не мешаются, если вы их не используете, так что вы этого даже не заметите.)
По этим же причинам мы версионируем типы. О нашей системе типов мы подробно рассказывали в предыдущем посте.
Благодаря версионированию функций и типов вы можете вносить изменения в приложение постепенно. Можно проверить, что каждый отдельный обработчик работает с новой версией, не нужно сразу вносить все изменения в приложения (но у нас есть инструменты, чтобы быстро это сделать, если захотите).
Это гораздо безопаснее, чем полное развертывание всего сразу, как это происходит сейчас.
Новые версии пакетов и стандартная библиотека
Когда вы обновляете пакет в Dark, мы не заменяем сразу использование каждой функции или типа во всей базе кода. Это небезопасно. Код продолжает использовать ту же версию, что использовал, а вы обновляете использование функций и типов до новой версии для каждого отдельного случая с помощью переключателей.
Скриншот части автоматического процесса в Dark, показывающий две версии функции Dict::get. Dict::get_v0 возвращал тип Any (от которого мы отказываемся), а Dict::get_v1 возвращает тип Option.
Мы часто предоставляем новую функцию стандартной библиотеки и исключаем старые версии. Пользователи со старыми версиями в коде сохранят доступ к ним, но новые пользователи не смогут их получить. Мы собираемся предоставить инструменты, чтобы переводить пользователей со старых версий на новые за 1 шаг, и опять с помощью переключателей функций.
Dark также предоставляет уникальную возможность: раз мы выполняем ваш рабочий код, мы можем сами тестировать новые версии, сравнивая выходные данные для новых и старых запросов, чтобы сообщить вам об изменениях. В итоге обновление пакетов, которое часто выполняется вслепую (или требует тщательного тестирования в целях безопасности), представляет гораздо меньше рисков и может происходить автоматически.
Новые версии Dark
Переход с Python 2 на Python 3 растянулся на десятилетие и до сих пор остается проблемой. Раз мы создаем Dark для непрерывной поставки, нужно учитывать эти изменения языка.
Когда мы вносим небольшие изменения в язык, мы создаем новую версию Dark. Старый код остается в старой версии Dark, а новый код используется в новой версии. Для перехода на новую версию Dark можно использовать переключатели или версии функций.
Это особенно полезно, учитывая, что Dark появился совсем недавно. Многие изменения в языке или библиотеке могут оказаться неудачными. Постепенное версионирование языка позволяет нам делать незначительные обновления, то есть мы можем не спешить и отложить многие решения о языке, пока у нас не будет больше пользователей, а значит и больше информации.
Переписать код для поддержки нового и старого форматов
Преобразовать все данные в новый формат
Удалить старый доступ к данным
В итоге миграция базы данных затягивается и требует много ресурсов. И у нас накапливаются устаревшие схемы, ведь даже простые задачи, вроде исправления имени таблицы или столбца, не стоят затраченных усилий.
У Dark есть эффективная платформа миграции баз данных, которая (мы надеемся) настолько упростит процесс, что вы перестанете его бояться. Все хранилища данных в Dark (хранилища пар «ключ-значение» или постоянные хеш-таблицы) имеют тип. Чтобы перенести хранилище данных, вы просто присваиваете ему новый тип и функцию отката и наката для преобразования значений между двумя типами.
Доступ к хранилищам данных в Dark осуществляется через версионированные имена переменных. Например, хранилище данных Users изначально будет называться Users-v0. Когда создается новая версия с другим типом, имя меняется на Users-v1. Если данные сохранены через Users-v0, а вы обращаетесь к ним через Users-v1, применяется функция наката. Если данные сохранены через Users-v1, а вы обращаетесь к ним через Users-v0, применяется функция отката.
Экран миграции базы данных с именами полей старой базы данных, выражениями наката и отката и инструкциями для включения миграции.
Используйте переключатели функций, чтобы направить вызовы к Users-v0 в версию Users-v1. Это можно делать по одному обработчику HTTP за раз, чтобы снизить риски, а еще переключатели работают для отдельных пользователей, чтобы вы могли проверить, что все работает, как ожидалось. Когда пользователей Users-v0 не останется, Dark преобразует все оставшиеся данные в фоновом режиме из старого формата в новый. Вы этого даже не заметите.
Тестирование
Dark — это функциональный язык программирования со статической типизацией и неизменяемыми значениями, поэтому поверхность тестирования у него значительно меньше по сравнению с объектно-ориентированными языками с динамической типизацией. Но тестировать все равно надо.
В Dark редактор автоматически запускает модульные тесты в фоновом режиме для редактируемого кода и по умолчанию прогоняет эти тесты для всех переключателей функций. В будущем мы хотим с помощью статических типов автоматически выполнять фаззинг кода, чтобы находить баги.
Кроме того, Dark выполняет вашу инфраструктуру в продакшене, а это открывает новые возможности. Мы автоматически сохраняем HTTP-запросы в инфраструктуре Dark (пока мы сохраняем все запросы, но потом хотим перейти на выборку). Мы тестируем по ним новый код и проводим модульные тесты, и при желании вы можете легко преобразовать интересные запросы в модульные тесты.
От чего мы избавились
Раз у нас нет развертывания, но есть переключатели функций, около 60% пайплайна развертывания остается за бортом. Нам не нужны ветки git или пул-реквесты, сборка бэкенд-ресурсов и контейнеров, отправка ресурсов и контейнеров в реестры или шаги развертывания в Kubernetes.
Сравнение стандартного пайплайна непрерывной поставки (слева) и непрерывная поставка Dark (справа). В Dark поставка состоит из 6 шагов и одного цикла, а традиционная версия включает 35 шагов и 3 цикла.
В Dark в развертывании всего 6 шагов и 1 цикл (шаги, которые повторяются несколько раз), в то время как современный пайплайн непрерывной поставки состоит из 35 шагов и 3 циклов. В Dark тесты запускаются автоматически, и вы этого даже не видите; зависимости устанавливаются автоматически; все, что связано с git или Github, больше не нужно; собирать, тестировать и отправлять контейнеры Docker не нужно; развертывание в Kubernetes больше не нужно.
Даже оставшиеся шаги в Dark стали проще. Поскольку переключателями функций можно управлять одним действием, не приходится второй раз проходить через весь пайплайн развертывания, чтобы удалить старый код.
Мы как могли упростили поставку кода, сократив время и риски непрерывной поставки. А еще мы значительно упростили обновление пакетов, миграции баз данных, тестирование, управление версиями, установку зависимостей, равенство между средой разработки и продакшеном и быстрые и безопасные апгрейды версий языка.