Бэкенд mysql_cluster нужен для использования Django в схеме с master-slave репликацией MySQL. Он умеет переключать глобальное соединение Джанго с БД между мастером и slave-репликами, и тем самым позволяет использовать стандартный ORM.
Этот бэкенд мы написали для "Куда все идут", и решили выложить в открытый доступ. Про особенности использования читайте на страничке проекта, а в этом посте я изложу подробности реализации и архитектуры.
Stateful архитектура
Считается, что "Джанго не работает с несколькими БД". Но более точно будет сказать, что по умолчанию ORM Джанго использует единый враппер соединения с БД (бэкенд). Это означает, что:
- в обход ORM можно обращаться куда и как угодно (кстати, именно так и поступает джанговское кеширование при хранении кеша в БД)
- бэкенд можно заменить своим и организовывать там пулинг, репликации и все что угодно, и это будет работать для соединений из ORM
Еще более точно надо заметить, что "можно заменить своим" бэкенд стало можно недавно в транке. Есть ли это в текущей стабильной версии 0.96, я, честно говоря, просто не помню...
Однако проектирование бэкенда натыкается на некоторые трудности. Дело в том, что протокол общения Джанго с бэкендом довольно беден. В частности, ORM не сообщает бэкенду многих вещей, которые могли бы оказаться полезными:
- нужно ли соединение только для выборки или апдейта
- от имени какой модели будет делаться операция
- происходит ли она в контексте какого-то веб-запроса
Фактически вся суть протокола, опуская всякие служебные "close", "commit", "rollback", состоит в операции "дай курсор!" Что потом будет происходит с этим курсором, в общем-то, непредсказуемо.
Mysql_cluster обходит эту таинственность ORM'а тем, что переключение между мастером и репликами делается из внешнего кода и хранится в виде состояния. То есть работа строится примерно так:
from django.db import connection # connection -- тот самый бэкенд
# по умолчанию работает мастер-соединение
connection.use_slave()
# все курсоры с этого момента создаются из слейв-соединения
connection.revert()
# вернулись к предыдущему состоянию
Главный минус этого "императивного" решения в том, что оно рушит барьеры абстракции. Вы не можете написать совершенно отвлеченную функцию, которая хочет писать в БД, и быть уверенными, что она работает всегда. Потому что если ее вызовет что-то в то время, когда активно слейв-соединение, она упадет.
За это меня все время ругает Андрей, и даже пророчит, что не пройдет и двух месяцев, как кто-нибудь наткнется на какой-нибудь трудноуловимый баг, связанный с этой императивностью. Теоретически, он прав. Но это единственное решение, которое я смог придумать, которое работает извне Джанго, не трогая ее код. Кроме того, не все так страшно на самом деле :-).
Почему оно все таки работает
Поскольку Джанго — это именно веб-фреймворк, большая часть кода под его управлением работает не произвольно, а внутри веб-запросов, которые обладают полезными для нашей задачи свойствами:
- выполняют одно законченное логическое действие, часто в транзакции
- изолированы друг от друга
Первое условие означает, что по ходу запроса не надо метаться между мастером и репликами, а достаточно выбрать что-то одно в начале, исходя из того, что будет делать запрос. Второе условие означает, что состояния соединений в параллельных запросах не будут мешать друг другу. Поэтому в большинстве случаев все решается или middleware или декоратором для view, которые переключают бэкенд в нужное состояние до конца запроса.
Но есть и меньшинство случаев.
Второстепенные операции
Бывают операций с БД, которые выполняются независимо от основной логики работы запроса. Например создание сессиий, запись какой-нибудь статистики, прозрачная регистрация пользователя где-нибудь внутри системы. Они могут случаться в произвольные моменты времени, в том числе и в тех запросах, которые работает через слейв-соединение.
Для таких случаев специально оставлена возможность просто вручную включить мастер-соединение, сделать все что нужно, и вернуть состояние бэкенда к предыдущему. И для вот этого "вернуться к предыдущему" бэкенд хранит не одно текущее состояние, а стек состояний. Иначе бы для возврата к предыдущему состоянию клиентскому коду пришлось бы запоминать его в отдельной переменной, что неудобно.
В "Куда все идут" такого кода — ровно два места. Одно — это создание профиля для каждого нового пользователя, и там все решилось одним перекрытием метода save() модели профиля. Второе — это работа с сессиями. Сессии, на самом деле, такой классический случай второстепенных операций, что мы даже включили в mysql_cluster замену стандартной джанговской модели сессий, которая работает точно так же, только явно переключается в мастер для всех операций.
Не-REST архитектуры
Вторая часть меньшинства случаев заключается в том, что многие разработчики игнорируют REST-архитектуру HTTP, и воспринимают веб-запросы не как основную схему, управляющую состоянием приложения, а как некоторую деталь реализации. Этот подход часто встречается у GUI-программистов, когда они начинают делать веб, и подходят к этому с вопросами типа: "как на клик по <select>
выполнить мою питоновую функцию". В такой схеме одна логическая операция может обслуживаться несколькими запросами, и каждый из них не будет знать, зачем он делает то, что делает. Другими словами, здесь уровень бизнес-логики поднимается с уровня веб-запроса на более высокий и обслуживается комбинацией кук и отслеживания текущего состояния в БД.
Mysql_cluster для таких случаев не работает. Но тут моя совесть чиста. Сама Джанго в явном виде не поощряет такой стиль приложений, и для этого лучше пользоваться другими фреймворками. Кажется Seaside и ASP.NET его проповедуют, если я не путаю.
Почему не MySQL Proxy
Довольно много меня спрашивали разные люди, почему я не хочу воспользоваться решениями, которые распределяют запросы между мастером и репликой, смотря на тип запроса: select'ы в одну сторону, все остальное — в другую. Самый известный (мне) продукт, который это умеет — MySQL Proxy. Самое приятное в нем то, что это совсем внешнее решение, которое не требует изменения кода.
Однако нам это не подходит, потому что мы работаем с транзакциями. И, предвосхищая советы их выкинуть, скажу сразу, что без них невозможно надежно работать с денормализованными даными: сплошь и рядом будут случаться ситуации, когда авторитетные данные будут разъезжаться с предрассчитанными.
С транзакциями же возможны ситуации, когда некий update-запрос меняет данные в базе, а потом в той же транзакции следующие select-запросы эти данные используют. Проксирующее решение, которое про наши транзакции ничего не знает, направит select в другое соединение, и там этот select новых данных не увидит. Я, к сожалению, не могу привести какого-то реального примера, но возможность кажется мне очень реальной, и мне страшно :-).
Другая проблема с внешним прокси, что он не знает, что даже некоторые read-only транзакции иногда хочется выполнять в мастер-базе. Речь идет о ситуациях, когда POST-форма после сабмита возвращает HTTP-редирект на страницу с обновленными данными. А поскольку реплики обновляются не мгновенно, может возникнуть (и возникает) ситуация, когда эта страница считается из еще не обновленной реплики, и пользователь увидит, что его действие не сработало. Для борьбы с этим эффектом в обслуживающих middleware и декораторах mysql_cluster'а есть специальная логика, которая заворачивает в мастер следующий GET'а после POST'а с редиректом.
Кластеризация реплик
Последний вопрос, который я хотел осветить — это работа с несколькими репликами. Сам mysql_cluster про несколько реплик ничего не знает, для него все они — одно соединение. Потому кластеризацию реплик делается с помощью некого внешнего балансера. Там никакой зависимой от приложения логики не надо.
Что конкретно используется для этого в Яндексе, признаться, не знаю. А поскольку я сейчас в отпуске, то и пойти спросить трудновато :-). Может быть кто из яндексоидов в комментариях подскажет.
Ну а в целом — скачивайте, пользуйтесь, делитесь впечатлениями.
P.S. Что-то дернуло посчитать размер mysql_cluster. Оказалось — всего 9 КБ, даже этот пост занимает больше — 14 КБ :-)
Комментарии: 7
Спасибо!
P.S. Было бы неплохо на странице проекта mysql_cluster (в русской версии) дать линк на этот пост.
P.P.S openid-логин не сработал, пришлось пост заново набивать... :(
mysql_cluster не очень хорошее название. У MySQL есть свой cluster. По названию можно подумать что у Вас это патч для работы django с MySQL cluster
Ваня, мне вот что интересно: если сваливается slave и происходит изменение структуры БД, то потом восстановить слейв проблемно.
Были проблемы с этим?
Сам не знаю, потому что у нас этим отдельные админы занимаются (и я очень рад, что мне не нужно точно знать, как работает вся эта репликация :-) ). В мою бытность ничего такого не было. Но с другой стороны, если и произойдет, то можно реплику оторвать из кластера, перенаполнить по новой, синхронизировать и вернуть в кластер обратно.
Использование кода примерно следующего вида
должно снять часть недостатков императивного подхода. Как минимум, не нужно будет всегда обрамлять код в try-finally. А Андрей, безусловно, прав - рано или поздно он даст о себе знать.
Да нет, этот синтаксис никак от них не избавляет. Точно так же внутри with теоретически может вызваться какой-то сигнал, который дернет какую-нибудь update'ящую функцию, которая до того момента всегда думала вызываться в мастере.
Да черт с ним, с этим подходом :-). Сам по себе он сложности сильно не добавляет. Я, честно говоря, не знаю вообще ни одного надежного механизма абстрагировать репликации. Они, к сожалению, в принципе обладают таким свойством, что заставляют "следить за руками". Поэтому для написания надежного кода единственный нормальный практичный выход — держать всю эту структуру достаточно простой и писать меньше кода, работающего с БД, в отрыве от основной логики.