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 контейнера в скриптовых языках, может показаться абсурдом. Тем не менее, существует рубикон перевалив через который становится понятно, чтос точки зрения сложности эксплуатации разницы между скриптовыми языкам и всеми остальными не существует.

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


20 декабря, 2009

Диагностика OutOfMemoryError подручными средствами

Один мой коллега является адептом философии "дефолтных настроек". Эта философия пропагандирует следующий подход: не пытайтесь менять environment под свои нужды, — просто научитесь пользоваться стандартным environment'ом.

Несмотря на то что сам по себе этот подход довольно спорен, в нем есть свои плюсы. Умение решать задачи штатными средствами особенно выручает когда необходимо быстро продиагностировать какую-то проблему, а у вас под рукой нет настроенного environment'а. Например, вы временно работаете за другой машиной, или географически отдалены от вашего милого сердцу, прекрасно настроенного environment'а. Поэтому, я считаю, очень важно уметь диагностировать типовые проблемные ситуации пользуясь только штатными утилитами. Так что давайте посмотрим как мы можем диагностировать memory leak'и в java, когда у вас под рукой нет ничего кроме JDK.

Итак, в логах вы нашли OutOfMemoryError. Что делать? Во-первых, надо уяснить чего делать не надо. Ни в коем случае не надо перезапускать процесс. Сделав это вы потеряете весь heap приложения, а в нашем случае heap — это единственная улика, которая может натолкнуть вас на причины OOM. Вам надо сделать heap dump. Это позволит понять кто занимает память, а также почему эта память не была конкретно высвобождена garbage collector'ом.

Самый простой способ сделать dump — это использовать утилиту jmap из пакета JDK.

jmap -dump=format=b,file=heap-dump.hprof $PID

Вот теперь, когда вы сделали dump, вы можете смело возвращать систему к жизни и перезапускать JVM, если это требуется.

Перед доставкой dump'а на вашу машину советую его пережать, так как heap dump'ы очень хорошо жмутся. Когда dump будет на вашей машине возникает вопрос. А каким образом вообще понять что там у "не внутря"?

С последними версиями JDK поставляется приложение VisualVM, которое содержит в себе в том числе и memory profiler. Загружаем heap dump в VisualVM и открываем вкладку "Classes".

На этой вкладе мы видим распределение памяти в dump'е по классам объектов. В данном случае большего всего памяти занимает тип char[]. Видимо кто-то хранит много строчек в памяти. Кто же это?

Дважды щелкаем на типе и переходим в instance view. Здесь мы видим все экземпляры данного типа, а также кто на них ссылается, а следственно и то, почему GC их не собрал. Просматриваем несколько экземпляров.

В моем случае большинство ссылок на строку удерживается базой данных H2 при помощи soft reference. Немного погуглив можно узнать, что H2 использует soft reference для хранения кеша базы данных. Отличительной особенностью soft ссылок является то, что JVM собирает их только тогда, когда ей не хватает памяти (перед генерацией OOM). Это делает soft ссылки довольно удобным механизмом для различного рода кешей. Тем не менее JVM не гарантирует что она успеет собрать все soft ссылки перед генерацией exception'а. Что, судя по всему, и происходит в моем случае.

Также стоит отметить, что VisualVM может сам находить ближайших GC root, удерживающий данный instance от garbage collector'а.

Это избавляет вас от необходимости сайгаком прыгать по дереву referent'ов в поисках ближайшего GC root'а.

Auto dump

Иногда бывает так, что причиной OutOfMemoryException служит не нехватка памяти, а другие причины. Например, если JVM видит, что она тратит большую часть процессорного времени на сборку мусора, а не на выполнение собственно приложения, она генерирует следующий exception.

java.lang.OutOfMemoryError: GC overhead limit exceeded

В зависимости от того как написано приложение оно может остаться живо, и даже продолжать выполнять свои функции. Причем уже через несколько минут heap может быть чистенький и без лишнего мусора (GC не зря жрал так много процессорного времени, и в конце концов собрал весь мусор).

Отладка затрудняется, — у вас нет heap'а, хоть на кофейной гуще гадай. В этом случае, стоит перезапустить приложение с ключом -XX:-HeapDumpOnOutOfMemoryError. Это заставит JVM сделать heap dump автоматически перед тем как кидать в бедное приложение OOM'ом. После следующего подобного инцидента у вас появится пища для размышлений.

Runtime статистика

Часто бывает так, что у программиста появляется теория относительно того, почему возникает memory leak. Например, зная список последних изменений кодовой базы, можно предположить что проблема локализована в каком-то конкретном участке системы. В этом случае, вам снова может помочь jmap. Эта утилита позволяет просмотреть количество экземпляров и занимаемую ими память по типам.

bazhenov@home core$ jmap -histo 11507 | egrep "(#instances|---|java.util.LinkedList)"
 num     #instances         #bytes  class name
----------------------------------------------
 110:           506          20240  java.util.LinkedList$Entry
 166:           220           8800  java.util.LinkedList
1021:             5            240  java.util.LinkedList$ListItr

Вы также можете легко посчитать суммарную память занимаемую типами определенного пакета.

bazhenov@home core$ jmap -histo 11507 | grep "org.netbeans" | awk '{SUM += $3} END {print SUM/1024 "K"}'
1745.28K

Да пребудет с вами сила дефолтных настроек.


06 декабря, 2009

Гарантия доставки сообщений и ее последствия

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

Почему же многие очереди сообщений не гарантирую порядок? И так ли он вообще важен — этот порядок доставки?

Почему многие системы очередей не гарантируют порядок доставки?

Здесь потребуется договорится об обозначениях. Для любых двух событий A и B запись “A→B” означает, что событие A происходит перед событием B. Людям знакомым с Java Memory Model эта запись должна быть известна как отношение happens-before.

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

image #1

Исходя из этого мы можем определить порядок доставки сообщений следующим образом. Для двух любых сообщений m1 и m2 для которых выполняется условие receive(m1)→receive(m2) (то есть брокер получил сначала сообщение m1 потом m2), если гарантируется что dispatch(m1)→dispatch(m2), значит очередь сообщений гарантирует порядок доставки.

Тем не менее гарантия в отношении порядка доставки мало что дает. Даже если сообщения доставлены в порядке (in-order), consumer’ы могут обработать сообщения в порядке отличном от порядка доставки (out-of-order).

image #2

Как любит говорить один мой коллега: “Тому есть тысяча причин”. Машины на которых работают consumer’ы могут быть разной конфигурации: у них могут быть разные процессоры, разный объем памяти, разная по производительности подсистема I/O. Но даже если они идентичны, у вас нет контроля над детерминизмом cpu и I/O scheduler’ов. Любая машина может отказать или начать медленно работать из-за большого количества pagefault’ов и т.д. Все это говорит о том, что порядок доставки сообщений не имеет ничего общего с порядком их обработки. А ведь именно порядок обработки, а не доставки должен интересовать нас в первую очередь.

Порядок обработки сообщений мы можем определить следующим образом. Для двух любых сообщений m1 и m2 для которых выполняется условие receive(m1)→receive(m2), если гарантируется что ack(m1)→ack(m2), значит очередь сообщений гарантирует порядок обработки.

Но соблюсти это правило очередь сообщений может только одним способом. Путем форсирования порядка ack(m1)→dispatch(m2). Другими словами, брокер не должен отправлять следующее сообщение пока предыдущее не будет обработано. Это подразумевает следующую картину.

image #3

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

Получается что порядок доставки сам по себе ничего не значит, — важен порядок обработки, предоставление гарантий в отношении которого убивает на корню пропускную способность.

А важен ли порядок обработки?

Иногда да. Есть ситуации когда порядок обработки очень важен. Предыдущая заметка иллюстрирует один из таких случаев. Но вы должны понимать, что любой порядок в web-приложении является имитацией. В любом web-приложении клиенты посылают запросы параллельно. Эти запросы параллельно идут по сети, параллельно обрабатываются frontend’ами, параллельно идут на backend’ы и так же параллельно ответы идут обратно к пользователям.

Место где порядок обработки начинает проявляться — это реляционная база данных. Если два потока в транзакции модифицируют одни и те же кортежи, база данных останавливает выполнение одного из потоков до тех пор пока другой не завершит свою работу. База данных линеаризует выполнение нескольких потоков таким образом, что в системе появляется порядок обработки. Но вместе с появлением порядка обработки испаряется пропускная способность. К тому же тот порядок который навязывает база данных может не совпадать с порядком поступления запросов на frontend. Для двух любых запросов req1 и req2 связанных порядком receive(req1)→receive(req2) соблюдение правила transaction(req1)→transaction(req2) не гарантируется.

В свою очередь, это означает что сложность, которая присуща решению продемонстрированному в предыдущей заметке, не является сложностью присущей асинхронным системам обработки сообщений. Это сложность присущая web-приложениям в целом. И даже если бы вы исключили MQ-систему из транка обработки и, скажем, посылали бы сообщения через SOAP или напрямую писали в базу данных, вам все равно пришлось бы реализовывать те же механизмы, чтобы обеспечить сохранность данных.

Любое web-приложение — это конкурентная система в природе которой порядок обработки запросов отсутствует.

Некоторых людей такая ситуация не устраивает. Отчасти являясь идеалистами (что особенно характерно для программистов) они не могут ужиться с мыслью, что те или иные процессы протекают стохастично и не имеют порядка в своей природе. Попытки навязать этот порядок обрекают web-приложение на деградацию пропускной способности. Вы можете наблюдать этот синдром повсюду — начиная от многопоточного программировния и использования mutex’ов до баз данных и протоколов XA-транзакций. В свое время Dan Pritchett написал об этом отличное эссе — “Chaotic Perspectives”.

Если вы web-программист, то я вас поздравляю. Судьба сделала вам подарок. В других отраслях программистам приходится прилагать титанические усилия для распараллеливания задач. У вас же большая часть процессов протекает и так параллельно. Все что вам надо, — смирится с мыслью, что порядок обработки запросов в системе не детерминистичен. С этим бессмысленно бороться, этим надо уметь пользоваться.