В данной статье делюсь замеченной на практике проблемой. Смысл проблемы заключается в использовании противоречащих подходов на уровне Persistance и DevOps.

Глоссарий

Insert-only - подход при котором для записи не используется UPDATE выражение, только INSERT. В определенных условиях позволяет выполнять запись быстрее, также это позволяет всегда иметь полную историю изменений. Запросы на чтение данных в таблицах с Insert-only подходом выполняются с порядком сортировки по убыванию и применением ограничения LIMIT или же TOP.

Rolling deployment - постепенное обновление всех экземпляров сервиса. Инициализировали один экземпляр новой версии сервиса, завершили один экземпляр старой версии сервиса.

Blue-Green deployment - подразумевает собой наличие двух окружений, одно из которых является текущей версией сервиса (blue), тогда как другое является новой версией сервиса (green). Как только все кандидаты green окружения завершили инициализацию и готовы обслуживать запросы клиентов, происходит переключение с blue окружения на green. В случае неполадок новой версии возможен быстрый откат на сервисы со старой версией, то есть переключение назад на blue окружение.

Canary deployment - техника развертывания новой версии сервиса для определенного подмножества пользователей. Создается экземпляр новой версии сервиса (или несколько), который обрабатывает какой-то процент запросов пользователей, при этом остальные пользователи продолжают обращаться к экземплярам старой версии кода [1].

Описание проблемы

Изначально на данную мысль навела книга Мартина Клеппмана [2]. Привожу отрывок из главы 4.2 "Поток данных через БД”:

"В целом часто случается одновременное обращение к базе ряда разных процессов. Они могут быть несколькими различными приложениями или сервисами либо несколькими экземплярами одного сервиса (выполняемыми параллельно в целях лучшего масштабирования или отказоустойчивости). В любом случае в среде, в которой приложение изменяется, существует вероятность того, что код части обращающихся к базе данных процессов будет более новым, а части - более старым. Такое возможно, например, из-за того, что новая версия в этот момент развертыва-ется с помощью плавающего обновления, вследствие чего часть экземпляров уже обновлена, а часть - нет."

Теперь рассмотрим данный случай шаг за шагом, учитывая что мы используем Insert-only подход при записи в базу данных.

  1. Имеется сущность Person с атрибутами id, userName, dateOfBirth и phoneNumber

  2. В сущность Person добавляется новый атрибут - email, дополняем модель в коде и скрипт миграции, который будет включать в себя добавление поля email в таблицу

  3. При релизе первым шагом мы выполняем миграцию схемы: ALTER TABLE Person ADD email varchar(255);

  4. На данном этапе проблем нет, поскольку все экземпляры сервиса используют старую версию кода в которой атрибут email не используется ни для чтения, ни для записи

  5. Выполняем развертывание новой версии кода сервиса используя стратегию Rolling deployment или же Canary deployment

  6. Первый экземпляр сервиса с новой версией завершил инициализацию и начал обрабатывать часть клиентских запросов

  7. На новый экземпляр сервиса пришел запрос на запись Person A, мы сохраняем запись с новым полем email

  8. На один из старых экземпляров сервиса пришел запрос на запись Person A, с изменением атрибута dateOfBirth и поскольку старый экземпляр сервиса не знает об атрибутеemail, в соответствующее поле таблицы записали null

В результате, после добавления последней записи (id=3) старым экземпляром сервиса, атрибут email у Person A утерян и в дальнейших транзакциях использоваться не будет. Проще говоря, запись одного и того же пользователя записала новая версия и затем сразу же переписала старая версия сервиса, с потерей значения нового атрибута email.

id

userName

dateOfBirth

phoneNumber

email

1

default

11.01.1991 

89213222351

null

2

default

11.01.1991 

89213222351

default@default.com

3

default

01.01.1992

89213222351

null

Тут будет верно заметить что если клиент не пришлет на бэкенд сервис запрос в теле или параметрах которого не включен новый атрибут email, то проблема сразу отпадает. Но с другой стороны поэтапное развертывание таких изменений может подразумевать то, что клиентская версия уже присылает новый атрибут, просто старая версия сервиса его игнорировала. Также возможна ситуация когда получение нового атрибута происходит на стороне самого сервиса в новой версии кода, например через запросы на API других сервисов.

Заключение

Заметить такую проблему постфактум и выявить её причину довольно сложно. Интересно то, что такого рода особые случаи могут остаться незамеченными, поскольку вызваны конфликтующими между собой подходами, которые находятся на разных уровнях и разработчик выполняя довольно тривиальную задачу по дополнении сущности может просто не заметить этого. Поэтому важно чтобы разработчик хотел и мог понимать в деталях разработку сервисов как на уровне баз данных, так и DevOps.

Источники

  1. Kubernetes Deployments: Rolling vs Canary vs Blue-Green, Pavan Belagatti, https://dev.to/pavanbelagatti/kubernetes-deployments-rolling-vs-canary-vs-blue-green-4k9p

  2. Designing Data-Intensive Applications, Martin Kleppmann, https://www.oreilly.com/library/view/designing-data-intensive-applications/9781491903063/

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


  1. Ivan22
    22.09.2023 10:33

    Познал на практике что такое распределенные системы и какие компромисы несет в себе CAP теорема. Если выбираешь провернуть всё без даунтайма - то есть сохранить Availability - то получаешь отсутствие Consistency, все честно.


  1. FruTb
    22.09.2023 10:33

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


  1. niamster
    22.09.2023 10:33

    Возможно, что Вам следует подумать о KV-storage и хранить данные в формате который не стирает данные которые не может распознать (тот-же Protobuf или даже JSON).

    А еще лучше просто использовать Document-storage.

    Похоже, что Вы используете неправильный инструмент.