Привет, Хабр! Меня зовут Анатолий Саблин, я java-разработчик и техлид команды эксплуатации в СИБУР Диджитал. Сегодня я расскажу про типовые сложности, которые возникают на этапе эксплуатации продукта, как они влияют на работоспособность и как их решить. И особенно рассмотрю:

●      Что важно в разработке программного продукта помимо самого кода;

●      Как сделать релиз и не сломать прод;

●      Какая модель работы больше подходит для промышленной эксплуатации.

Упрощаем эксплуатацию на этапе разработки

Говоря о программном продукте, я подразумеваю не только сам код программы, но и логи, мониторинг, окружение и его переменные, бэкапы и пользовательские гайды. То есть программа — это просто код, а продукт — это код и все, что его касается. Если смотреть на разработку под таким углом, подход меняется: сразу начинаешь видеть механизм работы с продуктом после его вывода на прод.

В жизненном цикле продукта доля активной разработки составляет 5-10%, пару процентов — тестовая эксплуатация, а все остальное — промышленная, которая может длиться 10-15 лет. Если забываешь что-то учесть на этапе разработки, во время эксплуатации эта забывчивость может дорого обойтись.

Например, есть приложение, в котором пользователям начисляются бонусы. Есть алгоритм, который считает эти самые бонусы и раскидывает.
Алгоритм протестировали — все работает корректно. Поэтому принципы расчета в лог решили не выводить.

Но потом на этапе эксплуатации начинают прилетать заявки с вопросами «А почему пользователю X начислили такую сумму, а не другую?»или «А когда было начисление?». Или ещё хуже: например, началась гонка из-за многопоточности, которую тесты не отловят, или произошло лишнее начисление бонусов, и нужно понять, когда были начисления. Без логов на эти вопросы уже не ответить.

Если вы не хотите идти по плохому пути и проглатывать исключения, ошибки нужно отлавливать. А при отсутствии логов и правил применения алгоритма, сделать это сложно (если не невозможно), долго и потому дорого.

Техническая админка как волшебная пилюля

Самый недооцененный инструмент работы с программным продуктом — техническая админка. Она помогает отслеживать работу системы и даже вносить изменения. Конечно, есть метрики мониторинга системы, с их помощью мы оцениваем техническое состояние. Но для эксплуатации важны еще и бизнес-процессы.

Например, есть процесс, в котором у объекта может быть несколько состояний. На дашборд админки мы выводим данные о том, сколько объектов находятся в каждом состоянии, и сразу видим, когда какое-то состояние становится “узким местом” — в нем накапливается слишком много объектов.

В админку можно частично вывести управление функциональностью. Например, в приложении пользователи регистрируются по email, а мы хотим подключить дополнительный способ — номер телефона. Для этого идем в код и дорабатываем модуль регистрации, выводя в админке оба способа с возможностью их включить/отключить. Это гораздо удобнее, чем срочно править код, а потом обновлять приложение, если появятся новые вводные. 

А ещё с помощью админки вторая и третья линия саппорта могут быстро оценить, как система в целом работает, и вносить изменения, не закапываясь в БД.

Ошибки прода, с которыми вы не столкнетесь при разработке и тестировании

Во время промышленной эксплуатации обновить код не всегда просто. Приведу пример: бесшовный релиз, продукт работает круглосуточно, в качестве БД используем PostgreSQL. В приложение нужно добавить столбец, в котором содержатся данные по умолчанию. Добавляем команды:

1) ALTER TABLE tbl ADD COLUMN flag; — добавление столбца в catalog;

2) DEFAULT FALSE  — блокировка таблицы и добавление индекса с возможностью полной перезаписи.

На тестовом сервере все отрабатывает, выкатываем на прод. Первые три попытки неудачные — функциональность не срабатывает, на четвертой падает прод. В чем причина?

Проблема в самой миграции, и с ней мы сталкиваемся только во время эксплуатации. Первая команда, которая добавляет столбец, ничего не ломает. А добавление DEFAULT FALSE приводит к тому, что индексация перестраивается, и происходит апдейт прежних значений. Апдейт таблицы длиной в 600-700 тысяч строк в PostgreSQL – операция сложная, которая может длиться 20 минут и положить прод. Что и произошло (пример был из практики)...

В последних версиях PostgreSQL эта операция уже исправлена и не вызывает глобальный update, так что случай привожу лишь как пример того, что промышленная эксплуатация вносит свои коррективы, не всегда очевидные на этапе разработки.

Или другой кейс: для работы с периодическими задачами мы в бэкенде использовали Scheduler/CronJob, но по непонятной причине происходило дублирование записей. Разобравшись, мы поняли, что бесшовный релиз подразумевает в первую очередь выпуск нового экземпляра, и только потом останавливается старый. В этот короткий промежуток времени есть два экземпляра бэкенда, там отрабатывают шедулеры и дублируют записи. Ошибка не обязательно кроется в работе шедулеров, она может касаться и пакетной обработки.

Что делать? Выносить повторяющиеся задачи отдельно через Kubernetes Cronjob или c помощью pattern/consumer producer, который обеспечит одновременную обработку данных в таблице.

Как работать с релизами, чтобы не было больно

Хоть ошибки, о которых я рассказал, и появляются только во время эксплуатации, с ними тоже можно справиться. Во-первых, приложения должны работать в нескольких экземплярах. Чтобы при масштабировании не было проблем, и ничего не упало.

Во-вторых, стоит придерживаться простых правил при обновлении продукта.

●      Учитывать, что во время обновления работают обе версии приложения — новая и старая. Мгновенных релизов не бывает;

●      Чем релиз меньше, тем проще его контролировать.

Если выкатываем большое обновление — боли не избежать. Поэтому стоит планировать релизы и дробить их, даже в рамках одного спринта. Если возникнут проблемы — мы знаем, куда смотреть. Это ускоряет идентификацию ошибок и их исправление. Например, у меня однажды был опыт работы с приложением, для которого мы накатывали релизы в течение всего дня в формате “одна задача — один релиз”.

●      Большие релизы лучше разбивать на последовательность небольших.

Например, для миграции данных можно запланировать такие итерации: в первом релизе выкатываем новую схему данных, во втором — код, который умеет работать со старыми и новыми данными, в третьем — функциональность их замены. А в конце переключаем логику обработки.

●      Планировать обратную совместимость релизов минимум на одну версию, чтобы в случае проблем можно было безболезненно откатиться к рабочей.

●      Помнить о том, что на проде данные могут быть битыми. Если обновление касается правил или ограничений работы с ними, данные нужно проверить и продумать инструмент исправления.

Приведу пример: при разработке в поле для мейлов забыли поставить маску для проверки корректности ввода. Когда решили ее внедрить, в БД уже накопилась куча некорректных данных. В таком случае нам нужен механизм для корректировки мейлов, который показывает кривые данные и задает логику их исправления. 

●      Код, который выполнил свою функцию, – хороший код.

Когда мы работаем с программным продуктом в промышленной эксплуатации, мы чаще всего не можем сделать вообще все сразу и надолго. Использовать код, который только один раз выполнит свою функцию — это в целом нормально. Такой подход упрощает работу.

Ну и наконец, важно помнить, что на проде произойти
может все что угодно. Обязательно случатся ошибки, появление которых спрогнозировать невозможно. Таков закон Мерфи!

Как управлять проектом во время эксплуатации

SCRUM vs CANBAN

Во время разработки отлично работает SCRUM-подход, а вот для эксплуатации он не всегда подходит. В SCRUM стандартно выделяют 30% времени на исправление ошибок. Но ошибки запланировать невозможно. В рамках одного спринта вы можете жить спокойно, а в следующем все вдруг ломается, и для тушения пожаров нужно выделить чуть ли не 100% времени команды. Допустимый вариант — выделять время на работу с ошибками, опираясь на аналитику предыдущего релиза. 

SCRUM также отводит время для погашения тех. долга, обычно это 10-20%. Иногда тех. долг маленький, а иногда нужно переписать половину системы, и запланированных 20% на это не хватит. 

Еще один минус работы SCRUM на этапе эксплуатации — невозможность обеспечить SLA. Например, по SLA проблемы в приложении должны устраняться в течение 8 часов. Но если команда загружена, то ресурсов и времени может не хватить.

Мне кажется эффективным подход, когда с продуктом работают две команды. Одна занимается исключительно разработкой и деплоем и работает по SCRUM, вторая поддерживает эксплуатацию системы и работает по KANBAN.

У второй команды нет спринтов и планирования, но есть приоритеты. При появлении критичной ошибки, команда бежит ее чинить, а если ошибок нет — занимается тех. долгом. Такой подход помогает быстро и адекватно реагировать на ошибки и обеспечивать SLA.

В KANBAN критерии приоритетных задач нужно согласовывать заранее. В этом поможет регламент, который определит последовательность действий при работе с ошибками. Например, критичные задачи — это если бизнес-процесс не работает, workaround-a для него не существует, и вы получили больше 10 жалоб на ошибку. Если workaround есть и жалоб немного, такая задача не считается критичной.

То есть, команда разработки работает по SCRUM-модели и чинит только те ошибки, которые вылезли на тестировании. А третья линия эксплуатации работает по KANBAN-доске, занимается тех. долгом и тушит пожары. Каждые пару спринтов команды могут меняться – это удобно, потому что так меньше рутины, люди не так устают от своих задач, и у них есть возможность поймать ошибки собственной разработки и поработать с ними.

Три линии поддержки

Из пресловутого закона Мерфи можно вывести, что наша цель, помимо уменьшения числа ошибок, – оперативное исправление форс-мажоров. Здесь помогает распределение задач по работе с инцидентами внутри службы поддержки.

  1. Первая линия принимает на себя основную часть запросов пользователей, ее цель —  максимально сузить воронку.

  2. Вторая линия больше занимается эксплуатацией самой системы, принимает заявки и обращения первой линии, если та не может ответить на вопросы пользователей. Еще вторая линия следит за приложением, отвечает за его доступность и восстанавливает в случае ЧП.

  3. Третья линия, – как правило, самая малочисленная, – уже занимается вопросами и заявками второй линии, следит за кодовой базой и чинит ошибки.

Например, произошло ЧП, система не доступна. Первая линия принимает на себя первый удар: оповещает о недоступности системы, объясняет пользователям, что произошло. Вторая линия в это время пытается восстановить работоспособность: наращивает мощности, чинит сеть и пр., может даже каждые полчаса перезапускать систему, чтобы она не зависала. Третья линия в это время пишет багфикс или добавляет кэши. У каждой линии свои цели и понятные задачи, от которых ничего не отвлекает.

Выводы

Вроде всё рассказал. Надеюсь, было полезно! А для всех, кто сразу скроллит к концу, сделал выжимку в три пункта:

●      Продукт — это код и все, что его касается. Закладывайте инструменты, которые сделают дальнейшую поддержку продукта более простой и удобной;

●      На проде вы всегда будете сталкиваться с ошибками, которых в разработке не было. Поэтому используйте гибкий подход к релизам: дробите крупные на более мелкие, запускайте новые фичи последовательно, помните про возможные ошибки в данных;

●      Отделяйте разработку от эксплуатации и распределяйте задачи поддержки на три линии. Это поможет быстрее и качественнее работать с инцидентами.

Кстати, делитесь вашими историями об "огненных" релизах в комментариях :) Такой опыт бесценен!

Комментарии (0)