Показаны сообщения с ярлыком php. Показать все сообщения
Показаны сообщения с ярлыком php. Показать все сообщения

21 декабря, 2009

IoC контейнеры

Если вы пишете на объектно-ориентированном языке, то вы должны быть знакомы с "джентльменским набором" принципов, которые очень полезны при написании кода. К таким принципам относятся: single responsibility principle, open-closed principle, interface segregation principle и другие. Общий эффект их использования заключается в том, что количество сущностей (классов и интерфейсов) в системе стремительно растет. В этом есть как положительные, так и отрицательные стороны.

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

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

Два представления конфигурации

В правильно построенной объектно-ориентированной системе нет конфигурации. Довольно революционное заявление, верно? Суть в том, что с точки зрения самого кода несущего полезную нагрузку конфигурации не должно существовать. Одни сущности требуют в качестве зависимостей другие (опираясь на интерфейс). Где, кто и как будет инициализировать зависимости, — вопрос не имеющий к пользовательскому коду никакого отношения.

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

Именно так должны вести себя все сущности. Другими словами, они не должны сами заниматься поиском и разрешением своих зависимостей.

Можно сказать иначе. С точки зрения самой системы во время выполнения, ее конфигурация задается графом объектов. У нас есть граф объектов по которому туда сюда бегают сообщения. Конечное поведение системы зависит от того как именно сконфигурирован этот граф.

Это очень удобное описание системы с точки зрения компьютера, но не с точки зрения человека. Человеку нужен какой-то текстовый файл, в котором можно было бы подправить настройки подключения к БД, цвет шрифта, timeout подключения к внешнему сервису и т.д.

Давайте остановимся на минутку и подумаем что это значит. Мы имеем два способа которые описывают поведение системы: граф объектов и конфигурационные файлы.

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

Это вам ничего не напоминает? Object-relational impedance, но только в области конфигурирования.

IoC контейнеры

Возможно вы уже задались вопросом, если системе нужен граф объектов, то может стоит описывать конфигурацию в виде графа объектов? Тогда можно было бы один раз написать транслятор превращающий конфигурацию в тот самый runtime граф, который необходим для работы системы.

Такими трансляторами и являются IoC контейнеры.

Здесь я рассматриваю только те IoC контейнеры, которые позволяют описывать конфигурацию отдельно от самого кода. Библиотеки, называющиеся IoC контейнерами, и не выполняющие вышеуказанное требование, таковыми не являются, так как не выполняют самой главной задачи IoC контейнера, — изоляции production кода от механизма локализации и удовлетворения зависимостей.

У себя в проекте мы написали свой IoC контейнер. Фактически это порт Spring beans на PHP. Согласен, звучит странно. Но spring beans, по моему мнению, идеологически верен, так как он не требует менять код для своего использования.

Конфигурация в нашем случае описывается примерно следующим образом:

<?xml version="1.0" encoding="utf8"?>
<beans xmlns="http://farpost.com/slr/injector">
  <bean id="masterConnection" class="MySqlConection">
    <property name="host" value="hostname" />
    <property name="user" value="john" />
    <property name="password" value="secret" />
    <property name="db" value="orders" />
  </bean>
    
  <bean id="userRepository" class="SqlUserRepository">
    <constructor-arg ref="masterConnection" />
  </bean>
</beans>

Фактически, это эквивалентно следующему коду:

$masterConnection = new MySqlConnection();
$masterConnection->setHost("hostname");
$masterConnection->setUser("john");
$masterConnection->setPassword("secret");
$masterConnection->setDb("orders");

$userRepository = new SqlUserRepository($masterConnection);

Многословно? Да, наверное. Но это дешевле чем каждый раз писать код создающий объекты на основании конфигурации, когда у вас сотни таких объектов.

Существует несколько способов, как можно сделать вышеприведенный XML гораздо более лаконичным. Некоторые из них мы уже опробовали и они работают вполне хорошо, некоторые мы только хотим опробовать. Здесь я не буду углубляться в детали. Если вам интерестно, дайте мне знать, быть может это стоит отдельной заметки.

Использование IoC контейнеров дает следующие выгоды:

  • единый формат описания конфигурации вне зависимости от подсистемы;
  • автоматический lazy для всех объектов;
  • декларативное управление зависимостями, — не требуется менять код для изменения поведения системы;
  • гибкое управление зависимостями, — в отличии от singleton и toolkit/registry разным клиентам можно подпихивать разные имплементации.

Последний пункт имеет далекоидущие последствия. Очень часто бывает что разным клиентам нужны разные имплементации одного и того же интерфейса. Например, в большинстве случаев клиенты используют различного рода data provider'ы обернутые в кеширующие декораторы. Тем не менее, некоторые клиенты в силу специфики задач могут генерировать большое количество cache miss'ов. Становится разумным просто отключить кеш для этих клиентов. Если ссылка на data provider хранится в singleton'е или реестре (registry), то подменить ее для конкретных клиентов становится просто невозможно.

Конечно, теперь конфигурация становится сложнее. Но в любой сложной системе конфигурация будет самой сложной ее частью. Я думаю смело можно сказать, что сложность большинства систем определяется сложностью их конфигурации. От этой сложности никуда не деться. Но мы можем обеспечить себя инструментами позволяющими справляться с этой сложностью. IoC контейнер это, по моему мнению, один из таких инструментов.

Стоит ли?

Решать вам. Идея применения IoC контейнера в скриптовых языках, может показаться абсурдом. Тем не менее, существует рубикон перевалив через который становится понятно, чтос точки зрения сложности эксплуатации разницы между скриптовыми языкам и всеми остальными не существует.

Поэтому ответ на вопрос "стоит ли?" заключается в вопросе "перевалили ли вы через этот рубикон?".


23 октября, 2009

Оптимистическая блокировка

Shared state, как известно необходимо защищать. Иначе параллельные потоки могут его "поломать". Это относится и к web-приложениям. Несмотря на отсутствие вменяемой поддержки параллелизма в большинстве web-ориентированных языков (PHP, Python, Ruby), concurrency в web-приложениях хватает. Запросы приходят на web-сервер параллельно, исполняются на разных процессорах параллельно и т.д. По этой причине следующий код некорректен:

$bulletin = $manager->findBulletinById($request->id);
$bulletin->setHits( $bulletin->getHits()+1 );
$manager->saveBulletin($bulletin);

Два параллельных потока могут одновременно загрузить объявление, одновременно инкрементировать счетчик, и записать объявление обратно. Это приведет к тому, что один инкремент потеряется. В общем случае, если N потоков выполняют данный код одновременно, может быть потеряно до N - 1 update'ов.

Существует два принципиально разных способа обеспечения целостности данных: пессимистическая и оптимистическая блокировка. Пессимистическая блокировка исходит из предположения, что если мы выполняем код, конкурентное выполнения которого может привести к "поломке" данных, то необходимо исключить его конкурентное исполнение. То есть сериализовать потоки в этой точке. Достигается это или при помощи distributed lock'ов или транзакций в БД.

У нас в системе есть небольшая библиотека позволяющая реализовать в коде исключающую блокировку. Ее использование выглядит примерно следующим образом:

$lock = LockManager::getLock("bulletin:{$request->id}");
try {
  $bulletin = $manager->findBulletinById($request->id);
  $bulletin->setHits( $bulletin->getHits()+1 );
  $manager->saveBulletin($bulletin);
  $lock->release();
} catch ( Exception $e ) {
  $lock->release();
}

Пессимистическая блокировка схожа с принципом Мерфи. Она предполагает, что если что-то плохое может случится, это обязательно случится. В отличии от пессимистической, оптимистическая блокировка предполагает что во время обновления записи в БД мы будем единственными кто ее меняет. В большинстве случаев, так и есть, так что оптимизм оправдан. Тем не менее, во время UPDATE'а мы проверяем наверняка изменилась ли запись с момента ее чтения. И если изменилась, то мы обязаны прочитать последнюю версию записи из БД и повторить нашу операцию с ней.

Реализация

Реализуется это довольно просто. Достаточно хранить с каждой записью в БД идентификатор версии и при записи проверять что он не изменился и менять его. Алгоритм выглядит следующим образом.

public function saveBulletin(Bulletin $bulletin) {
  $connection->prepareStatement("UPDATE bulletins SET version = version + 1 ... ".
    "WHERE id = :id AND version = :version")
  ->int('id', $bulletin->getId())
  ->int('version', $bulletin->getVersion())
  ->execute();
  if ( $connection->rowsAffected() <= 0 ) {
    throw new ConcurrentModificationException();
  }
}

В данном методе при обновлении мы проверяем, что версия не изменилась, а это значит, что и запись в БД никто не менял. Если версия изменилась, мы обязаны известить об этом клиента.

Но тут есть одна загвоздка. Что будет делать клиент с этим exception'ом?

try {
  $manager->saveBulletin($bulletin);
} catch ( ConcurrentModificationException $e ) {
  // Huh?!
}

По идее, клиент должен заново прочитать объявление из БД, заново выполнить свою операцию и заново сохранить объявление. И вполне возможно что... заново получить exception, заново прочитать объявление и... Нет, так не пойдет.

В случае, если вы реализуете оптимистическую блокировку, то модель должна взять на себя логику сохранения объектов, иначе вы опухнете писать клиентов. Модель должна предоставить иной, более удобный интерфейс для оперирования над объектами. Используя возможности PHP/5.3 можно сделать следующее:

$manager->processBulletin($request->id, function(Bulletin $b) {
  $b->setHits( $b->getHits()+1 );
});

Как видно, в данном случае клиент не загружает и не сохраняет объявление. А это значит, что и с конкурентными изменениями ему иметь дело не требуется, — все это ответственность модели. Реализовать в модели цикл сохранения объекта с обработкой ошибок — дело техники.

Преимущества "оптимистов над пессимистами"

Не блокирует клиентов, которые не меняют состояние

Представьте себе такой код:

$bulletin = $manager->findBulletinById(...);
if ( $bulletin->getText() != $request->text ) {
  $bulletin->setText($request->text);
  $manager->saveBulletin($bulletin);
}

В случае пессимистической блокировки мы обязаны взять lock перед загрузкой объявления. Но возможно, что нам даже менять его не потребуется. Тем не менее, мы потенциально можем быть заблокированы на lock'е, даже если ничего не будем менять. В случае оптимистической блокировки мы просто не сохраняем объявление.

Избавляет клиента от необходимости заботится о lock'ах

В случае оптимистической блокировки клиентский код проще и этого кода требуется меньше. Меньше кода → меньше проблем.

Гарантировано защищает данные

Когды вы оперируете lock'ами могут случиться четыре типа проблем:

  • вы возьмете слишком мало локов (поломанные данные);
  • вы возьмете слишком много локов (deadlock, starvation);
  • вы возьмете не те локи (поломанные данные);
  • вы возьмете те локи, но не в том порядке (deadlock).
Если вы хотите чтобы пессимистическая блокировка корректно работала в больших системах, требуется чтобы программисты, которые пишут клиентский код, четко осознавали суть конкурентных процессов, были очень внимательны и чтобы у них было 5 килограммов мозга. Скорее всего у них, как и у большинства других нормальных людей, мозг весит только 3 килограмма. Так что не стоит спихивать на них задачу модели, а именно — обеспечение целостности данных.

Худшее что может случится в случае оптимистической блокировки — клиент получит exception. Худшее что может случится в случае пессимистической блокировки — вы "поломаете" данные.

Что вы выбираете?

02 августа, 2009

Возожности mysqlnd в PHP/5.3

Если вы не знаете, то с PHP/5.3 поставляется новый mysql драйвер — mysqlnd. У него есть несколько особенностей и воможностей, которые отличают его от libmysql.

Первое не очень интерестно. Теперь при fetch'e результата не происходит копирования из памяти libmysql в память, находящуюся под управлением zend engine. Фактически весь result set находится в памяти zend engine. Это значит что выборки превышающие по размеру memory limit теперь таки будут генерировать out of memory. Наверное, это представляет ценность для владельцев shared хостинга, но для тех у кого выделенный сервер это имеет мало пользы.

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

  • распараллеливать чтение/запись;
  • реализовать SLA в отношении запросов к БД.

Распараллеливание записи может быть полезным, когда логическая операция записи физически приводит к обновлению информации хранящейся на нескольких серверах. Особенно это может быть выгодно, когда ваш процесс большую часть времени занят ожиданием подтверждения от БД об этой самой записи (i/o bound). Потребность "писать сразу в несколько мест" может возникнуть, например, в случае, если вы делаете программное зеркало вашей базы данных. На первый взгляд это может показатся глупостью, почему бы просто не настроить репликацию? Тем не менее, существуют ситуации когда application layer репликация имеет преимущества над mysql-репликацией. Также распараллеливание записи может быть полезно в случае осуществления сложных механизмов sharding'а. Иногда объект должен быть записан не в один shard, а в несколько (это уже не совсем зеркало). Такие операции записи хорошие кандидаты для распараллеливание (по-крайней, мере до тех пор пока shard'ы находятся на физически разных серверах).

Распараллеливание чтения может быть полезно в случае реализации scatter-gather. Если у вас сложное разбиение данных по серверам, то может возникнуть ситуация когда при построении SELECT запроса неизвестно какому серверу его адресовать (представьте что вы пытаетесь извлечь данные с фильтрацией по полю, которое не учавствует в критериях разбивки) — соответствующие данные "распылены" по всем серверам. В этом случае необходимо послать SELECT запрос всем серверам (scatter) и затем обьединить все результаты в один result set (gather). Вообще, эта техника очень дорога и лучше делать так, чтобы вам не приходилось ее использовать. Например, выбрать разбиение, которое позволит обращатся только к одному серверу. Тем самым вы увеличите data locality и благотворно повлияете на availability, так как теперь для обслуживания запроса вам нужны не все сервера, а всего один. Но если уж без scatter-gather не обойтись, то будет неплохо хотя бы отправлять запросы серверам параллельно, а не последовательно. В этом случае общее время ожидения ответа будет равно времени ожидания самого медленного сервера, а не сумме времени ожидания всех серверов.

И конечно же SLA. Лично нам этого очень сильно нехватало. Если запрос нас не блокирует, то технически мы можем и не ждать результатов его выполнения. Совсем не ждать смысла конечно нет, а вот не ждать дольше чем строго определенное количество времени, бывает очень полезно. Я уже писал раньше про fail fast. Всякий раз когда вы делаете более менее сложный запрос к БД вы не можете быть уверены сколько времени займет выполнение этого запроса. А пользователи ждать не любят. Все наверное были свидетелями как БД при растущей нагрузке все медленее и медленее обрабатывает существующие запросы. В такой ситуации новые запросы дают эффект снежного кома. С другой стороны, вполне возможно, что результат нам не так уж и нужен. Ну подумаешь не покажем пользователю на странице поиска сколько у него новых сообщений в "личке", — он не для этого поиск инициировал. Неблокирующие запросы позволяют реализовать поведение когда мы спрашиваем у БД: "сколько у пользователя личных сообщений?" и недождавшись ответа в оговоренный timeout как бы говорим: "не очень-то и хотелось" и просто скрываем блок с личными сообщениями. Более того, можно реализовать схему в которой клиент не дождавшись ответа, через отдельный control connection, прибьет свой собственный запрос, чтобы он не генерировал дополнительную нагрузку на БД, — ей судя по всему и так не сладко, раз она не успела ответить вовремя (конечно, все это не работает с DML запросам).

Это все. Остальноесомнительные мелочи.