В данной статье делюсь замеченной на практике проблемой. Смысл проблемы заключается в использовании противоречащих подходов на уровне 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 подход при записи в базу данных.
Имеется сущность Person с атрибутами
id
,userName
,dateOfBirth
иphoneNumber
В сущность Person добавляется новый атрибут -
email
, дополняем модель в коде и скрипт миграции, который будет включать в себя добавление поляemail
в таблицуПри релизе первым шагом мы выполняем миграцию схемы:
ALTER TABLE Person ADD email varchar(255);
На данном этапе проблем нет, поскольку все экземпляры сервиса используют старую версию кода в которой атрибут
email
не используется ни для чтения, ни для записиВыполняем развертывание новой версии кода сервиса используя стратегию Rolling deployment или же Canary deployment
Первый экземпляр сервиса с новой версией завершил инициализацию и начал обрабатывать часть клиентских запросов
На новый экземпляр сервиса пришел запрос на запись Person A, мы сохраняем запись с новым полем
email
На один из старых экземпляров сервиса пришел запрос на запись Person A, с изменением атрибута
dateOfBirth
и поскольку старый экземпляр сервиса не знает об атрибутеemail
, в соответствующее поле таблицы записали null
В результате, после добавления последней записи (id=3)
старым экземпляром сервиса, атрибут email у Person A утерян и в дальнейших транзакциях использоваться не будет. Проще говоря, запись одного и того же пользователя записала новая версия и затем сразу же переписала старая версия сервиса, с потерей значения нового атрибута email
.
id | userName | dateOfBirth | phoneNumber | |
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.
Источники
Kubernetes Deployments: Rolling vs Canary vs Blue-Green, Pavan Belagatti, https://dev.to/pavanbelagatti/kubernetes-deployments-rolling-vs-canary-vs-blue-green-4k9p
Designing Data-Intensive Applications, Martin Kleppmann, https://www.oreilly.com/library/view/designing-data-intensive-applications/9781491903063/