Обратная совместимость и изменение схемы базы данных

5 декабря 2014

Каждый раз одно и то же. Программист что-то там понаписал, его коллега что-то там наревьювил, код вмержен, сторипоинты засчитаны, все здорово, все просто замечательно. Тот факт, что общение с приложением происходит по какому-то протоколу, или что у базы данных есть какая-то схема, почему-то вспоминается в самый последний момент.

Проще всего объяснить проблему на примере с протоколами. Вот допустим, что приложение предоставляет REST API. Мы в какой-то момент решили что REST — это плохо, и нужно все переписать на Thrift поверх вебсокетов. Что же делает программист? Само собой разумеется, выкидывает из приложения REST и добавляет Thrift поверх вебсокетов. На практике обычно происходит какая-то модификация протокола, замена одних концов на другие, переименование полей в JSON’е, и так далее, но это не суть важно. Протокол изменяется. Так почему программист в этой ситуации неправ?

Потому что у приложения есть клиенты, и временами их сильно больше одного. Переход на новый протокол, тестирование соответствующих версий клиентов и их выкатка занимают какое-то время, нередко — весьма существенное. Это означает, что сервер нельзя катить до тех пор, пока клиенты не перейдут на новый протокол. Притом, все клиенты. И катить сервер придется вместе с этими клиентами. Но это только пол беды. Какие-то фичи завязаны на новый протокол. В итоге у приложения очень быстро появляется две основные ветки — одна с новым протоколом, и одна со старым. Что еще веселее, ветки эти нужно время от времени как-то мержить и разрешать конфликты.

В лучшем случае команда приходит к тому, что поддерживает только одну ветку, с новым протоколом, в старую же мержатся только критические фиксы. Как можно было избежать проблемы? Да просто не выпиливать старый протокол!

Если нужна поддержка Thrift — добавляем поддержку. Более того, неплохо бы еще добавить два флага, один для включения и выключения REST, второй — для включения и выключения Thrift. Что это даст? Возможность плавно перевести всех клиентов на новый протокол. И если окажется, что с ним что-то не работает, откатиться к старой версии. Вообще, можно сначала перевести на Thrift наиболее лояльных пользователей (например, самих сотрудников компании), убедиться, что все работает, затем потихоньку переводить остальных пользователей. Если вдруг в логах начинают сыпаться ошибки, откатываемся. Если все хорошо, переводим 100% пользователей. Если все ОК, REST отключаем. Если нигде ничего не сломалось, в следующем билде выпиливаем REST.

Аналогичная история нередко случается со схемой базы данных. Допустим, что у пользователя есть name, и мы решили разбить его на first_name и last_name. Пишется скрипт, который пол часа мигрирует базу, а код переписывается в расчете на то, что у пользователя есть first_name и last_name. Мало того, что приложение нужно целиком остановить на время миграции, так еще и откатиться к более ранней версии становится невозможно. А как еще можно изменить схему базы данных?

Да очень просто. Добавляются поля first_name и last_name. При чтении данные берутся из first_name и last_name. Если же они не указаны — из name. Во время обновления профиля name пишется по старой логике, а также пишутся first_name и last_name. При этом в фоне потихоньку работает скрипт, который выполняет миграцию старой схемы в новую. В итоге (1) никакого даунтайма, (2) новая версия приложения может жить одновременно со старой, (3) если что, всегда можно откатиться к предыдущей версии приложения, все будет работать, как раньше. Когда переход выполнен и все хорошо, name можно в фоне грохнуть.

В действительности, с базами данных есть еще по крайней мере один нюанс. Насколько я припоминаю, в некоторых РСУБД добавление нового столбца может быть долгой операцией. Но эта проблема вполне решаема, например, путем введения столбца, содержащего в себе JSON. PostgreSQL, например, может вполне эффективно работать с JSON, строить по нему индексы, и так далее. Более же общее решение данной проблемы заключается в введении новой таблицы, использовании тригеров и той же плавной фоновой миграции.

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

Ведь совсем несложно просчитать на пару шагов вперед, к чему приведет сломанная обратная совместимость! Или может просто я чего-то не понимаю в жизни?

Метки: , .


Вы можете прислать свой комментарий мне на почту, или воспользоваться комментариями в Telegram-группе.