Привет, Хабр! На связи Егор, бэкенд-разработчик из команды Портфолио в Т-Банке. Мы занимаемся актуализацией данных компаний и периодически внедряем новые подходы в наши процессы разработки.
В последнее время мы часто выпускаем новую функциональность, используя метод канареечных релизов. Хочу рассказать о том, как мы это делаем. У себя на проекте мы используем Camunda, поэтому в статье разберем, как более безопасно выпускать новые версии bpmn-схемы на прод, минимизируя влияние багов на пользователей.
Статья написана с учетом того, что читатель уже знаком с Camunda и имеет опыт разработки приложений на этом движке.
Контекст проекта
Для начала хочу немного погрузить вас в контекст проекта:
Spring-Boot-приложение с встроенным движком Camunda. Пишем на Kotlin.
Слушаем топики Kafka и по событиям из них запускаем bpmn-процессы.
Периодически переделываем процессы так, что от старых схем почти ничего не остается. Новые фичи могут состоять из 10—15 мелких задач, которые мы релизим на прод по отдельности (TBD у нас). Включаются на проде они все разом, когда все задачи пройдут тестирование.
Главная мотивация для использования канареечных релизов — желание снизить количество пользователей, которые могут наткнуться на баг после запуска новой функциональности. Особенно если каждая ошибка приводит к обращению клиента в техподдержку и требует ручных действий от команды разработки.
Поэтому после разработки новой версии bpmn-схемы нам нужно иметь возможность отправлять на нее только небольшой процент от всех пользователей. План такой:
Добавить возможность запускать новые процессы на определенной версии схемы. Если клиенту «повезет» протестировать на себе новую функциональность, то мы направим его на самую свежую версию схемы. Всех остальных — менее удачливых — будем отправлять на стабильную версию.
Найти способ выбирать группу пользователей, для которой будет доступна новая функциональность. В этом нам поможет Togglz, но также рассмотрим варианты, как обойтись без него.
Начнем с того, что научимся отделять стабильную версию от свежей.
Определяем версию схемы для запуска процесса
В Camunda есть два параметра, которые обозначают версию схемы:
Version — счетчик, который однозначно определяет версию схемы. Не может повторяться, каждое изменение увеличивает номер версии на единицу.
Version tag — строка, дает возможность пометить определенную версию схемы тэгом, например ‘release.v.1’. В отличие от поля version может повторяться, то есть может быть две версии схемы с разным version и одинаковым version tag.
Для поиска нужной схемы можно использовать их оба, но недостаток поля version в том, что оно, скорее всего, будет различаться в зависимости от окружения. Пока разработчик воюет со схемой на локальной машине, количество неудачных версий может уйти за сотню. Потом, когда тестировщик три раза отправит задачу на доработку, на QA-окружении будет только три версии. Ну и на прод долетит всего одна рабочая (пока не найдется новый баг) версия схемы.
Для поиска мы будем использовать параметр version tag. По умолчанию Camunda всегда запускает процессы на самой свежей версии схемы. Такой вариант не подойдет, если мы захотим разбить задачу на части и релизить их по отдельности, ведь мы не хотим пускать трафик на недоделанную схему.
Перед началом работы над новой фичей наша задача — пометить тэгом стабильную версию основной схемы и всех вызываемых в ней подпроцессов:
Проставляем version tag на основной схеме, которую планируем изменять.
Проставляем version tag для всех схем, которые вызываются через Call Activity. В параметрах вызова указываем Binging = version tag и нужный тэг.
Если на схеме несколько дорожек(Participant), нужно зафиксировать version tag и для них.
Теперь нужно написать код, который находит нужную схему в зависимости от того, включена ли фича для текущего пользователя:
fun startProcess(processName: String) {
if (featureService.isEnabled(NEW_EXPERIMENTAL_FEATURE)) {
// последняя версия c новым функционалом
runtimeService.startProcessInstanceByKey(processName)
} else {
// ищем стабильную версию
var schemaWithTag = repositoryService.createProcessDefinitionQuery()
.processDefinitionKey(processName)
.versionTag("v.2.stable")
.orderByProcessDefinitionVersion()
.desc()
.listPage(0, 1)
.firstOrNull()
if (schemaWithTag == null) {
// если схема с таким тэгом не нашлась, берем последнюю
schemaWithTag = repositoryService.createProcessDefinitionQuery()
.processDefinitionKey(processName)
.latestVersion()
.singleResult()
}
runtimeService.startProcessInstanceById(schemaWithTag.id)
}
}
Важно помнить, что Camunda разрешает деплоить схемы с одинаковыми version tag. Это будет проблемой, если не поднять verson tag при следующей правке схемы. Функция singleResult() выдаст ошибку, если в БД сохранено несколько схем с таким тэгом. Поэтому при поиске версии нужно дополнительно отсортировать схемы по дате и взять самую первую в списке.
Важно! Фильтр latestVersion() не работает в сочетании с фильтром versionTag("v.2.stable"), поэтому вместо него можно использовать сочетание orderByProcessDefinitionVersion() + desc() + listPage(0, 1)
При фиксации стабильной версии важно не забыть задеплоить ее на все окружения. Если этого не сделать, мы потеряем возможность запускать процессы на этой версии, так как ее попросту нет в базе. С такой ситуацией можно легко столкнуться, если на проекте есть несколько тестовых стендов, каждый из которых имеет свою собственную БД. Мы дописали логику, которая выберет свежую версию по умолчанию, чтобы избежать ошибки.
На этом этапе мы добавили возможность выбирать версию схемы для запуска нового процесса. Дальше нам нужно разделить пользователей на тех, кого отправлять на свежую версию, а кого — на стабильную.
Включаем функциональность только для части пользователей
Для выделения группы пользователей тестирования новой функциональности мы используем фреймворк Togglz. Он дает возможность включать и выключать функциональность через встроенную админку. А еще можно указать процент пользователей, для которых будет включена эта фича — как раз то, что нам нужно для настройки канареечных релизов.
Когда происходит проверка на включение фичи, Togglz определяет решение на основании данных текущего пользователя, взятых, как правило, из ThreadLocal-переменных. Эти данные заполняются в момент, когда есть возможность узнать, какой пользователь вызвал эту функциональность. Для приложения на Camunda это можно сделать перед началом выполнения делегата или Service Task. Если приложение — классический сервер на Spring, то заполнять контекст можно через дополнительный фильтр в Spring Security. Есть несколько готовых решений от Togglz, все они реализуют интерфейс UserProvider.
Вольный пересказ того, так Togglz определяет, включена ли фича для текущего пользователя:
fun isEnabledForCurrentUser(): Boolean {
val currentUserKey: String = userProvider.getCurrentUser().getName()
val releasePersent: Int = 20
return (currentUserKey.hashCode() % 100) < releasePersent
}
В качестве ключа текущего пользователя можно выбрать его ID или логин.
releasePersent — процент от пользователей, которым нужно запустить новую функциональность. Togglz позволяет задать его в UI-админке и сохранить значение в БД. Если Togglz не подходит, можно вынести это значение в конфигурацию или в переменные окружения, чтобы быстро отключить функциональность, если что-то пойдет не так.
Можно заметить, что результат проверки, по сути, зависит от двух последних цифр в хэшкоде от имени текущего пользователя. Привязка к хэшкоду позволяет получить одинаковый результат при многократном выполнении этой функции. Здесь нельзя использовать рандом, потому что тогда пользователи столкнутся с тем, что новая функциональность будет то пропадать, то снова появляться при повторении запроса.
Если нет желания подключать Togglz только ради канареечных релизов, можно реализовать функцию распределения пользователей на группы самостоятельно. Если не критично получить разный результат при повторном запросе, можно сделать еще более простой вариант:
fun isEnabled(): Boolean {
val releasePersent: Int = 20
return (RandomUtils.nextInt(0, 100)) < releasePersent
}
Наш опыт внедрения
В нашей команде канареечные релизы быстро прижились и даже были встроены в регламент по выпуску крупных фич. На мой взгляд, сильно повлияла простота внедрения. Ведь если возможность выкатить фичу на часть пользователей дается практически бесплатно, то почему бы ее не использовать?
На скрине из админки можно увидеть историю разработки новой фичи в одном из наших bpmn-процессов:
1) На версии 27 мы приступили к разработке новой фичи
2) С помощью versionTag зафиксировали 28 версию как стабильную. Во время разработки все новые процессы стартовали именно на ней. На сырых схемах(версии 29-35) процессы не запускались
3)Когда все задачи прошли этап тестирования на QA, мы убедились что фича готова к выпуску и включили флаг на 10% пользователей. Остальные 90% процессов продолжили запускаться на стабильной 28 версии
4) Если на проде обнаруживается баг мы выключаем фичу и бежим исправлять. Если все идет хорошо, то постепенно увеличиваем процент раскатки фичи
Такой подход дает шанс на ошибку за счет небольшого влияния на пользователей. Как бы подробно ни были расписаны постановки к задаче и как бы хорошо ни был протестирован код, все равно остается риск упустить один из сценариев или не угадать ход мысли пользователя.
Пример из нашей работы. Мы заложили логику, что клиенту могут продлить роль подписанта в компании, но не учли, что сотрудники поддержки могут обновить роль через удаление старой и создание новой. Для них это равнозначные действия, но для нас событие об удалении роли — сигнал, что клиента нужно заблокировать. Что мы и делали, исполняя свой долг перед многоуважаемым регулятором!
Выкатив фичу на 10%, мы получили только 5 обращений в поддержку о несправедливой блокировке и сразу выключили обратно. Не имея возможности менять процент раскатки фичи на ходу, нам бы пришлось делать срочный хотфикс и потом еще вручную разбирать репорты с почты, которых было бы в 10 раз больше.
Для нас канареечные релизы версий схем стали удачной находкой. Нельзя сказать, что этот подход подойдет для всех проектов. Все зависит от конкретной функциональности, ее потребителей, уровня критичности сервиса и многих других факторов. Например, такой подход будет неудобен в случаях, когда:
На проекте всего две-три большие bpmn-схемы, которые постоянно изменяются всеми разработчиками в команде. Когда мы зафиксируем стабильную версию схемы и начнем работу над новой фичей, другим разработчикам нужно будет постоянно откатывать коммит до той стабильной версии и вносить правки в нее. А разработчику новой фичи потом придется накатывать свою версию обратно.
Если все фичи по объему небольшие, возня с фиксированием схем будет занимать столько же времени, что и разработка новой функциональности. В таких ситуациях, кажется, лучше подойдут обычные if-else-развилки в коде или на bpmn-схеме.
Если вы тоже используете канареечные релизы у себя на проекте — буду рад увидеть ваши истории о том, как они вас спасали, или, может быть, наоборот — только добавили вам хлопот.
Комментарии (4)
anaxita
22.01.2025 11:04у нас бизнесу как то пытались продать камунду как «да продакт будет схему кликать как емк нравится», по факту я быстро поняли что это разработка будет делать и забили на камунду тк команду погружать в новую. кинологию не хотелось и не сильно надо было
Camundaslav Автор
22.01.2025 11:04Камунду продавали еще до моего прихода в команду, так что тут рассказать не смогу ничего интересного
Согласен с тем, что камунда это для разработчика в первую очередь. У нас даже на этапе СА схемы процессов рисуются "примерно", потому что разработчики потом все равно будут часть схемы переделывать. Да и все же большая часть логики все равно лежит внутри делегатов и Service Task
DenSigma
Насколько я понимаю, канареечные релизы можно применять для фич типа интерфейса или вывода какой-то информации. Но как можно выкатывать изменения в бизнес-логике для части пользователей? У одних пользователей счета формируются одним образом, а у других - по-другому? А они не бегают с изумленными воплями, почему у одних система работает одним образом, а у других - по-другому?
Camundaslav Автор
Хороший вопрос, у меня нет на него четкого ответа, но могу описать свое видение:
Такой подход подойдет точно не для всех фичей. Думаю можно придумать много ситуаций, когда раскатка фичи только на 10% приведет к неконсистентным данным, и что нужен выкат обязательно на всех пользователей сразу
Но вот хороший пример фичи для запуска через канареечный релиз — добавление новой нотификации. Если они будут прилетать только малой части клиентов, то кажется в этом ничего страшного нет. Тем более что раньше эти нотификации не приходили вообще никому.