Pull to refresh

Yii: устройство ActiveRecord и Шардинг

Reading time 4 min
Views 7.6K
В последнее время на хабре довольно много внимания уделяется фреймворку Yii. Он стал и нашим выбором для крупного проекта. А проблема большинства крупных проектов, как известно, в масштабировании. Не менее известно, что можно легко поставить сотни параллельных nginx и отбалансировать нагрузку на процессор, память, диск и даже канал. А вот с СУБД все гораздо сложнее.

Для того, чтобы заранее побороть эту проблему правильным способом было решено реализовать в Yii поддержку шардинга. Речь под катом пойдет вкратце о том что такое шардинг и подробно о:
  1. Устройстве ActiveRecord в Yii
  2. Реализации на этом устройстве шардинга
  3. Проблемах, которые все еще есть в AR
UPD: перенес в PHP, т.к. наличие расширения для шардинга может склонить чашу весов при выборе фреймворка.

Шардинг


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

server_id := user.id % 2
server_connection = server_id == 0 ? 'dsn1' : 'dsn2'


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

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

server_id := post.user.id % 2
server_connection = server_id == 0 ? 'dsn1' : 'dsn2'


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

Устройство ActiveRecord в Yii


Основной функционал взаимодействия с базой в Yii разбит на два класса: ActiveRecord и ActiveFinder. Первый умеет работать только с одной таблицей, к которой пренадлежит используемая модель и как только встречает with() или join() сразу подменяет себя на ActiveFinder. Который, в свою очередь, умеет работать только с несколькими таблицами и ВСЕГДА строит запрос для JOIN'ов.

Основное соединение с БД для работы AR определяется конфигом Yii. В реализации за него отвечает публичный метод getDbConnection(). При этом ActiveRecord этот метод определяет, а ActiveFinder обращается к ActiveRecord.

Реализация шардинга


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

Все, что нам нужно для того, чтобы реалиовать шардинг в фреймворке — научить ORM выбирать соединение в зависимости от какого-то условия во внешнем мире. Соответственно нам нужно дать ему возможность принять какой-то параметр и заставить выбрать в зависимости от него соответсвтующее соединение.

Интерфейс Yii распологает к цепочным вызовам: Foo::model()->with('...')->find(). Поэтому хорошим способом передать ему «информацию извне» будет реализовать еще один такой метод, который ее запомнит и потом позволит на основании ее принять решение. Знакомьтесь, choose().

Для конечного пользователя это выглядит так: Foo::model()->choose($shard)->find(); Эта переменная запоминается в рамках объекта. И на другие модели никак влиять не будет. Равно как и на другие экземпляры этой же модели. Что приводит нас к двум итогам:

1. Инкапсуляция. Ничего неожиданно не сломается. :)
2. Для каждой модели, где нужно использовать шарды надо вручную дергать choose. :(

Но вернемся к внутреннему устройству: запомненный ключ учитывается переопределенным getDbConnection(). Если ключ есть — надо по нему прогнать алгоритм выбора шарда. Если ключа нет (чуз не вызван или вызван без параметра) — используем стандартное соединение из конфига. Сам алгоритм выбора соединения реализовывается в специальном классе DbConnectionManager. Реализация этого класса достаточно типовая за исключением одной операции «понять какой же dsn надо использовать по этому ключу». Именно его и предлагается имплементировать занаследовав от абстрактного класса, включенного в нашу реализацию.

Итого: choose($shard) -> getDbConnection($shard) -> DbConnectionManager::getConnection($shard) -> DbConnectionManager::_findConnection($shard).

Вот такая получилась цепочка. Базовая поддержка шардинга есть. Кстати, с помощью такой реализации можно еще сделать автоматическое переключение на реплику и много других приятных штук. Еще один плюс такой реализации: полная прозрачность. Пока choose() не вызван, ничего не меняется.

Дальнейшая реализация очень сильно зависит от того, как именно вы будете строить шардинг. Для своего проекта мы реализовали еще один наследующий от ShardedActiveRecord класс, который в 70% мест избавил нас от ручного указывания шарда. Зная, что мы шардим базу по пользователю, ничто не мешает нам вызывать choose() автоматически при его сохранении. Или при сохранении его поста в блог.

При этом теоретически основываясь на подходе Conventions over Configurations расширение можно развить и сделать какой-нибудь базовый простой шардинг полностью работающий из коробки. Возможно следующая статья будет об этом.

В заключение еще раз обращу внимание на то, что описанная выше реализация лежит в архиве на форуме Yii в топике, где она обсуждается. В нее включены не только два класса: абстрактный ConnectionManager и ShardedActiveRecord, но и набор unit-тестов. И из этих юнит-тестов довольно неплохо видно во что превратится выборка объектов, если такую реализацию использовать.

Проблемы, которые все еще есть в AR


Если вы все-таки прочитали статью по ссылке о реализации шардинга в Netlog из первого абзаца, то вы наверняка заметили, что кроме шардинга, ребята описывают интересный подход к кэшированию. Кэширование по-модельно.

По-умолчанию Yii пытается использовать средне-взвешенную политику по использованию JOIN'ов. Он не делает все сотней запросов, но и не старается засунуть все в один. При этом есть метод together(), который позволяет форсировать создание одного цельного запроса. Но нет метода anti-together(), который бы заставил его сначала выбирать id всех сущностей, а потом делать одиночные запросы для выборки каждой из них.

Если отвязаться от memcached и sphinx, такой метод кажется бесполезным и безумным. Но представьте себе, что у нас есть некий поисковый запрос, который вернул нам кучу id. А все сущности (вообще все) по model_$id хранятся в оперативной памяти. И Yii перед поиском конкретной модели по ID в базе пробовал бы ее найти в кэше.

Реализация всего этого не кажется сильно сложной, пока мы не натыкаемся на ActiveFinder и Behaviours. Но это, в общем-то, тоже сабж для отдельной статьи. А тем временем автор фреймворка обещался подумать про личную реализацию (все равно он в AR ориентируется КУДА лучше внешних хакеров) этого в 1.1 ;].
Tags:
Hubs:
+25
Comments 15
Comments Comments 15

Articles