24 августа, 2009

Что если цепь рванет?

Сегодня со мной произошел форс-мажор. Я приехал на работу на велосипеде так что, когда рабочий день закончился, я сел на вел и сопровождаемый прохладным ветерком покатил в центр города. Далеко уехать не получилось. Буквально через 40-50 метров я услышал хруст и педаль под ногой резко провалилась. Оказалось, что я поломал крепление заднего переключателя.

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

Моего веса хватило, чтобы хрупкий металл на котором переключатель крепится к раме не выдержал. Таким образом, разорвавшаяся цепь спровоцировала ситуацию, которая конструктивно не должна была произойти — высокая нагрузка на задний переключатель и его крепление. Конструктивно не должна была произойти... Лично меня как потребителя это не особо утешает. Не буду лукавить, в этом инциденте виноват в первую очередь я сам. Цепи просто так не лопаются. Не так давно во время поездки на остров Русский у меня был еще один инцидент с этой цепью. В тот день я ее починил при помощи подручных средств. Мне следовало было сменить ее по приезду домой. Теперь цепь стоимостью 300-500 рублей спровоцировала ущерб на 6000-8000 рублей. Но давайте не будем искать виноватых. Наверное, вы уже спрашиваете себя: “Это конечно прекрасно, но мы то тут причем?”. Благодаря этой ситуации в моей памяти всплыл пост Ивана Сагалаева, который не так давно написал небольшую статью о том, что “кеш не хак”. В целом, я согласен с постулатами которые излагает Иван. Кеширование действительно стало неотъемлемой частью большинства web-проектов. Все знают что такое кеш, во всех языках программирования уже есть интерфейсы к memcached и т.д. А это значит, что кеш, действительно не хак — это наши суровые будни. Но это лишь одна сторона медали. Вторая ее сторона, на мой взгляд, заключается в том, что кеш для некоторых программистов является тем самым “прямым ходом цепи, на котором нет нагрузки”. Когда в приложении появляется кеш, так легко пойти на поводу у собственной лени и сказать: “ну ладно, здесь информация быстро попадет в кеш и все будет оки доки”. Но фактически каждый раз когда вы себе это позволяете вы превращаете кеш в неотъемлемое звено вашего приложения, без которого оно возможно уже не будет работать. Существуют ситуации, когда это обоснованно. Когда кеш действительно является неотъемлемой частью системы. В этом случае он, как и все другие критичные звенья системы, дублируется. Вы дублируете свой кеш? Или может сохраняете его в persistent store чтобы предотвратить эффект “холодного старта”? Хорошо, если так. Полагаясь на кеш, вы не должны складывать с себя обязательств по обеспечению нужного уровня производительности и масштабируемости вашего storage’а. В противном случае, “когда цепь рванет” вы можете потерять что-нибудь более важное чем “просто цепь”. Например, вы можете потерять базу данных, которая не справилась с большим потоком конкурентных запросов. Конечно же, в этом случае вы скажете: “высокая нагрузка на этот агрегат конструктивно не предусмотрена”. Пользователям от этого не легче, скажу я вам. Безусловно, ситуации бывают разные, и возвращаясь к моей маленькой “трагедии”, я понимаю инженеров shimano, которые скорее всего гнались за низким весом (а это тоже очень важно) и поэтому сделали крепеж из хрупкого металла. Но это не меняет главного... В любой момент времени вы должны себе отдавать отчет в том, что произойдет с приложением, если “рванет ваша цепь”.

18 августа, 2009

О проблемах растущего размера

Все таки интернет индустрия развивается. Теперь на каждом углу высоконагруженные проекты, миллионы пользователей, требования к high-availability, масштабиремости. Причем вот ведь странно. К масштабиремости всегда относятся как-то однобоко. Иногда ее путают с производительностью. А иногда не понимают что масштабируют.

Dan Pritchett, один из архитекторов eBay (теперь уже бывший), еще в 2006 году написал отличное эссе "You scaled your what?" на тему векторов масштабирования (scaling vectors). Действительно, существует масса направлений которые требуют внимания при интенсивном росте проекта, а следственно и сложности решаемых в проекте проблем. А это значит, что употребляя слово "масштабируемость", мы должны отдавать себе отчет в том, в каком контексте мы употребляем это слово.

Transactional scalability

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

Data scalability

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

Важно понимать, что цена хранения одного мегабайта данных зависит от цены потери одного мегабайта данных. При росте dataset'а, использовать один и тот же подход для хранения данных разной важности крайне неэффективно. Как и в случае с transactional scalability типичным ответом на data scalability является горизонтальное масштабирование.

Operational scalability

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

Представьте себе простое web-приложение работающее на одном сервере и использующее в качестве базы данных простые текстовые файлы. Такое приложение может представлять из себя буквально пару десятков скриптов. Вряд ли бы у вас возникли проблемы с организацией процесса разработки и тестирования такого приложения. А что если это система распределена по 15 серверам, написана на разных языках и использует самое различное middleware обеспечение? Как бы вы вообще запустили бы такую систему на машине разработчика? И стали бы вы вообще это делать?

Если не уделять внимания этому вектору, то при росте инфраструктурной сложности он вам нанесет "ответный удар", не сомневайтесь.

Feature time-to-market scalability

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

Умение при помощи растущего коллектива решать пропорционально растущий объем задач — это искусство.

Я назвал всего четыре вектора. Есть и другие, такие как: power scalability, deployability и другие. Более того внутри компании могут быть свои scaling vectors, которые обусловлены спецификой ее функционирования.

Конечно же (и это уже классика жанра), вы не сможете добиться максимума по всем направлениям — необходимо выбирать. Надо понимать, какие направления для вас являются приоритетными в краткосрочной перспективе, и что может изменится в долгосрочной. Поэтому, когда вы в следующий раз будете употреблять слово "масштабируемость", задумайтесь — масштабируемость чего?


17 августа, 2009

Pipelining

Существует одна очень старая и эффективная техника — конвейерная обработка данных (pipelining). Ее смысл заключается в том, что разные физические исполнители, которые могут работать не блокируя друг друга (хвала DMA), такие как: процессоры, жесткие диски, сетевые карты, — должны работать не блокируя друг друга. Это позволяет повысить их утилизацию, и, если правильно все организовать, не допустить перегрузки отдельно взятых исполнителей. Отличительной особенностью этой техники является то, что она используется в микропроцессорах с незапамятных времен, и сейчас является неотъемлемой частью любого суперскалярного процессора.

Представьте себе следующий код:

while ( !isInterrupted() ) {
  byte[] data = performComplexIOOperation();
  performComplexProcessing(data);
} 

Довольно типичная ситуация. Сначала мы читаем данные, потом обрабатываем. Если каждая из фаз (чтение/обработка) в отдельности требует много времени, то этот код не сможет полностью утилизовать ни процессор ни подсистему ввода/вывода. Что мы можем сделать для того, чтобы более полно утилизовать ресурсы машины, на которой работает приложение?

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

Вполне очевидно, что ни IO, ни CPU не загружены на 100%. Причиной является то, что один поток работает сразу с двумя физическими исполнителями, не давая возможности этим исполнителям работать параллельно. В тот момент, когда процессор обрабатывает данные, жесткому диску нечем заняться. Пока жесткий диск читает данные, у процессора нет работы. Вы можете сказать: "Так ведь процессор не просто ждет. Он ждет данных от жесткого диска". Все верно, но чего ждет жесткий диск пока процессор обрабатывает очередную порцию данных?

Это приводит нас к следующим двум правилам:

  • поток занимающий процессор не должен иметь причин для блокировки;
  • поток не занимающий процессор может иметь только один ресурс, который может служить причиной его блокировки (конечно все иначе, если вы используете какую-либо из моделей неблокирующего ввода/вывода).

Другими словами, поток управления должен работать только с одним исполнителем. Можете это считать принципом единичной ответственности для многопоточного программирования.

Мы можем вынести чтение в отдельный поток и связать потоки при помощи очереди задач.

class ReadingThread implements Runnable {

  private final Queue<ByteBuffer> queue;
  [...]
  public void run() {
    while ( !isInterrupted() ) {
      queue.add( performComplexIOOperation() );
    }
  }
}

class ProcessingThread implements Runnable {

  private final Queue<ByteBuffer> queue;
  [...]
  public void run() {
    while ( !isInterrupted() ) {
      ByteBuffer buffer = queue.poll();
      performComplexProcessing(buffer);
    }
  }
}

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

Теперь мы имеем загруженный жесткий и один загруженный процессор. Что мы можем сделать еще?

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

Стоит отметить, что java предоставляет все необходимые синхронизационные примитивы для реализации конвейерной обработки данных, такие как: конкурентные очереди и пулы потоков.

Закрепление за отдельным исполнителем отдельного потока дает ряд преимуществ:

  • не имея других причин для блокировки кроме как на этом исполнителе, мы способны утилизовать его полностью. По-крайней мере, до тех пор пока коммуникационный механизм (например, очередь при помощи которой передаются данные между потоками) не станет bottleneck'ом;
  • так как не существует большого количества потоков работающего с исполнителем мы не допускаем его перегрузки.
Второй аспект очень важен. Вы должны понимать, что большое количество потоков вредит системе. В случае процессора это высокий context switch'инг, что пагубно влияет на производительность. В случае жесткого диска мы можем забыть про sequential read.

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

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 запросам).

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