07 мая, 2012
Перенос блога
17 апреля, 2012
Один на миллион
Вам наверное приходилось слышать что-то вроде: “Этого не может быть. Вероятность этого менее одной миллионной”. В бытовом понимании одна миллионная это некая несвершимая вероятность. Другими словами, этого просто не может произойти. Но насколько это вeрятное событие, если вы повторите эксперимент миллион раз? Конечно, вероятность наблюдать событие растет если вы повторяете эксперимент много раз подряд. Но все равно мы зачастую подсознательно пытаемся игнорировать такие события считая их невероятными.
Тем не менее, в современных Интернет-системах которые обрабатывают миллионы запросов в сутки не заметить в течение дня событие с вероятностью в одну миллионную довольно сложно. И вот почему. Вероятность что интересующее нас событие (с исходной вероятностью 1/n) не проявит себя ни в одном из n независимых экспериментов равно:
(1 - n–1)n
В этой формуле n–1 — вероятность события которое реализуется в одном эксперименте из n. 1 - n–1 соответственно вероятность что это событие не реализуется в ходе эксперимента. Ну и не тяжело догодаться что (1 - n–1)n это вероятность с которой это событие не реализуется в ходе n независимых экспериментов. Для n = 1 000, если вы его подставите в формулу, итоговое значение вероятности составит 0.368 или 36.8%. Для n = 1 000 000, значение вероятности составит… те же самые 36.8%! Редкость события всегда уравновешивается частотой повторения испытаний.
Обоснование этого возможно контринтуитивного результата кроется во втором замечательном пределе, который большинству из нас преподавали в ходе курса высшей математики.
limn→∞(1 + n–1)n = e
Второй замечательный предел интенсивно используется при решении дифференциальных уравнений, но нас интересует одно из следствий этого предела.
Этот предел говорит нам о том что при растущем n, значение выражения (1 - n–1)n стремится к значению e–1, которое является той самой константой приблизительно равной 0.368.
Я не буду здесь приводить доказательство этого следствия второго замечательного предела. Предел разумного количества формул на один пост достигнут :).
Теперь давайте соберем все это вместе. Предположим что у вас есть некий баг (скажем, race condition в многопоточном приложении), который воспроизводится с вероятностью один на миллион. Фактически что говорит этот предел – если вы обслужите за день миллион запросов то вероятность того что этот баг не порявит себя ни в одном из запросов ~37%. То есть с вероятностью ~63% этот баг даст о себе знать хотя бы один раз за сутки.
Из этого можно сделать еще один вывод. Чем больше запросов вы обрабатываете за сутки, тем более редкие явления вы будете наблюдать в течение суток. 100 000 запросов в сутки? Тогда вероятность 1/100 000 для вас не редкость. 10 000 000? Тогда и одной десятимиллионной вас не удивишь.
Да пребудет с вами нормальное распределение!
05 марта, 2012
О высокой нагрузке
В последнее время все чаще говорят о высоконагруженных приложениях. Нельзя не заметить что это теперь очень популярная, можно даже сказать модная, область знаний. Сам же термин “высоконагруженная система” при этом не имеет в нашей отрасли четкого определения. В этой заметке я хочу привести свои рассуждения по этому вопросу. Я не ставлю перед собой цель дать исчерпывающее определение этого термина. Моя цель, предоставить читателю ключевую информацию о системах такого рода, которая определяет стиль мышления при работе с ними.
Итак, что такое высоконагруженная система? Ответ на этот вопрос стоит начать с описания качественных свойств такого рода систем.
Традиционные качества высоконагруженной системы
Как правило, к таким качествам относят большое количество пользователей и данных. В целом это правда, но тут есть несколько загвоздок:
- это не вся правда;
- приведенные факторы являются количественными, а не качественными.
Ниже на основании предпосылки “много пользователей, много данных” я сформирую список качественных факторов присущих высоконагруженным системам. Начнем с самого простого.
Многопользовательская система
Конечно же высоконагруженное приложение в первую очередь является многопользовательским. То есть в один момент времени с ним работает более чем один человек. Сейчас, в эру стремительного развития Интернета, это тысячи и сотни тысяч человек.
Устойчивая ассоциация высоконагруженных систем с большим количеством пользователей в нашей индустрии появилась давным давно. Ничего принципиально неверного в такой связи нет. Но если высокая нагрузка подразумевает большое количество пользователей, то большое количество пользователей совсем не обязательно подразумевает высоконагруженную систему.
Если посмотреть на статистику Московского метрополитена за 2010 год, то окажется что средняя часовая нагрузка на систему максимальна в диапазоне от 8 до 9 часов утра. За этот час через турникеты проходят приблизительно 720 тысяч человек. Что порождает необходимость не менее 200 раз в секунду проверять статус предъявленных проездных и принимать решение о пропуске того или иного человека через турникет. В Интернете существует масса высоконагруженных ресурсов с подобными показателями пропускной способности. Например, статистика по StackOverflow за тот же 2010-й год показывает что их средняя пропускная способность находится в диапазоне 100-150 хитов в секунду.
Определенно у метрополитена более высокие требования к пропускной способности. Но значит ли это что Московский метрополитен можно считать более “высоконагруженным” чем StackOverflow? Вряд ли, в частности потому что эти две системы оперируют несравнимыми объемами информации, о чем мы поговорим позже.
Я намерено в обоих случаях привел оценку пропускной способности, так как она дает больше информации о нагрузке чем количество пользователей системы. Две разные системы могут подталкивать пользователей к разным паттернам их использования. Это может приводить к абсолютно разным требованиям по пропускной способности для этих систем. Пропускная способность точнее описывает количество работы которую должна уметь выполнять система в единицу времени.
Распределенная система
Высоконагруженные системы являются системами распределенными, то есть работают более чем на одном сервере. Зачастую это десятки и сотни серверов. Требование распределенности вытекает из следующих причин:
- необходимости обрабатывать возрастающие объемы данных;
- необходимости “живучести” системы в случаях отказа части серверов.
Но обо всем по порядку…
Я наверное не ошибусь если скажу что большинство высоконагруженных приложений являются Интернет-приложениями (позже мы еще вернемся к состоятельности этой гипотезы). А отличительной особенностью современного Интернета основанного на концепции Web 2.0 является тот факт, что сами пользователи генерируют данные, которые они сами же в итоге и потребляют. Это приводит к тому, что чем больше у вас пользователей, тем больше потенциальный объем хранимых данных.
Требование обработки больших объемов данных может существенно осложнить жизнь. Под “большим объемом” я подразумеваю такой объем информации, который не может эффективно обработать одна машина. В большинстве случаев, это объем превышающий объем доступной на сервере оперативной памяти. То есть приходится тем или иным образом распределять данные между несколькими машинами, каждая из которых обрабатывает свой небольшой кусочек данных, но делает это эффективно, без page fault’ов (не используя swap) и прочих неприятностей. Необходимость эффективной обработки данных диктуется другим очень важным качеством высоконагруженных систем, – интерактивностью, о котором будет сказано ниже.
Но большие объемы данных – это не все. Ко всему к этому хочется чтобы система работала в режиме 24x7 без остановок и перерывов. Но вот незадача, любое даже самое надежное оборудование иногда выходит из строя. Встает естественная задача обеспечения доступности системы в случаях отказа оборудования.
Тут мы вступаем в область знания распределенных систем, эксплуатация которых редко бывает безоблачной, даже когда вы используете готовые решения. Тем не менее распределенные системы, не смотря на сложность их разработки и поддержки, пожалуй единственный подход позволяющий обеспечить вышеизложенные требования в полной мере.
Позитивная динамика по пользователям и данным
Если ваше приложение представляет хоть какой-то интерес, то даже если ничего не делать, ваша аудитория будет расти просто с ростом аудитории Интернета. Поэтому характерной чертой высоконагруженных систем является не просто большое количество пользователей, но и позитивная динамика количества пользователей.
В контексте реалий Web 2.0 растущее количество пользователей может привести к тому, что такую же позитивную динамику вы будете иметь и по данным. Поэтому в контексте высоконагруженных систем корректней говорить не о большом, а о растущем количестве пользователей и данных.
Интерактивность
И тут мы подходим к своеобразной финальной ноте в аккорде качеств highload систем, если позволите так выразиться. Интерактивность – одно из основополагающих качеств высоконагруженной системы. Интерактивность подразумевает, что пользователь после того как послал запрос сидит и ждет ответа, а люди как известно ждать не любят. При этом большинство Интернет-приложений о которых мы говорим не являются критическими важными в жизни людей. Twitter, Flickr, Facebook и т.д. это все круто конечно, но если они будут отвечать непомерно долго, я найду чем занятся еще. Наша жизнь от них не зависит (прогрессирующие формы задротства у некоторых особо аддиктивных индивидов не в счет), а это значит что эти приложения должны занимать минимум нашего времени. То есть отвечать за приемлимое время. Это контрастирует, например, с системами пакетной обработки данных, в которых время ответа системы вторично.
Из этого правила безусловно есть исключения. Представьте что вы только что совершили покупку в Интернете сообщив свою платежную информацию третьим лицам. Скорее всего вы дождетесь ответа системы, даже если она будет отвечать дольше минуты. Но как известно исключение из правила лишний раз подверждает само правило.
Управление ресурсами
Качество интерактивности очень важно для понимания сути высоконагруженных приложений, потому что по вышеобозначенным причинам разрабатывая такую систему вы должны быть уверены вот в чем:
Каждый раз когда приложение получает очередной запрос, у него должно быть достаточно свободных ресурсов (CPU, память, диск, сеть) для обработки запроса за приемлимое время.
Возможно это и звучит тривиально, но именно данное требование приводит нас к основному посылу данной заметки:
Контроль за ресурсами является неотъемлимой частью работы с высоконагруженным проектом.
Необходимо также учитывать тот факт, что из-за позитивной динамики свободных ресурсов становится меньше с течением времени. В этом заключается “парадокс” высоконагруженных систем. Если вы возьмете высоконагруженный проект и заморозите его разработку (отправите всех разработчиков в бессрочный отпуск), то рано или поздно он перестанет работать.
Высоконагруженная система – это интерактивная распределенная система которая требует постоянного контроля за собственными ресурсами. В противном случае она перестает работать.
Это противоречит обывательскому опыту. Как может само по себе поломаться то, чего не меняли? У программного кода нет срока годности. Причина в том, что со временем системе просто перестанет хватать ресурсов. А нехватка ресурсов провоцируется факторами, часть из которых мы уже рассмотрели:
- рост аудитории;
- рост объема данных;
- изменения паттернов поведения аудитории, в том числе и сезонные.
Если посмотреть темы докладов на конференциях посвященных тематике разработки высоконагруженных систем (например, тот же highload), то большинство тем с которыми выступают докладчики можно свести к двум основополагающим направлениям:
- как решать существующие задачи используя меньше ресурсов (практически все NoSQL БД, неблокирующий I/O, оптимизация и тюнинг);
- как имея больше ресурсов решать пропорционально больший объем задач (message queueing, распределенные вычисления, распределенные БД, параллелизм).
Эту дихотомию я уже затрагивал ранее в заметке Performance vs. Scalability.
Фактически речь на таких конференциях идет в основном о различных способах адекватного управления аппаратными ресурсами в контексте баз данных, языков программирования, операционных систем, алгоритмов, моделей ввода/вывода, вычислительных парадигм и т.д.
Просто подумайте над этим. Большая часть пропогандируемых в настоящее время баз данных под эгидой NoSQL, не предоставляет качественно новых возможностей для разработчиков. Реляционная модель позволяет относительно легко все это реализовать не выходя за рамки одной парадигмы. В этом, я полагаю, и заключается одна из причин почему реляционки приобрели такую популярность и до сих остаются самым распространенным типом БД. Так называемые, пост-реляционные базы данных не являются инструментом для решения качественно новых задач, всего лишь инструментом для более эффективного решения существующих. Именно важность эффективной траты ресурсов стала катализатором популярности NoSQL тематики.
Высоконагруженный проект – это Интернет приложение
Если исходить из того что неотъемлемой частью высоконагруженного проекта является постоянный рост данных и аудитории, то становится понятно почему высоконагруженные проекты – это поголовно Интернет приложения.
В реальной жизни всегда есть некий предел, который не позволяет использовать систему все возрастающему количеству людей. В том же метрополитене человеку требуется некоторое время, чтобы пройти через турникет. Исходя из этого времени, а также общего количества турникетов можно достаточно точно расчитать верхний предел возможной нагрузки. Выглядит очень невероятным чтобы за секунду через один турникет могло пройти 10 человек.
В сфере High Performance Computations приложения могут выполнять просто гигансткое количество операций в единицу времени. Больше чем любое Интернет-приложение. Но это количество зависит только от объема входных данных, а также алгоритма обработки (например, от точности моделирования, если речь идет о моделировании динамических систем). Тяжело придумать причину почему нагрузка на такую систему может вырасти сама по себе без вмешательства персонала ее сопровождающего.
Похоже, что Интернет-приложения это единственная сфера в которой нагрузка есть переменная не имеющая верхнего предела.
Работая в Highload проекте
Но не стоит возводить данный тип проектов на какой-то особый пьедестал. Highload проект это в первую очередь такой же самый проект как и все остальные. Проект в котором бывают и гонки за фичами и deadline’ы и сложности межличностных отношений и все прочие “прелести” процесса разработки ПО. Но говоря о “высоконагруженной составляющей” проекта, работа над ней сводится к тому, что постоянно приходится искать ответы на ресурсно-ориентированные вопросы:
- какие bottleneck’и есть в системе по ресурсам и как их устранить?
- каков запас системы по ресурсам учитывая естественный рост аудитории/данных?
- что делать при нехватке того или иного ресурса? (например, можно за счет RAM сэкономить CPU).
Не удивительно что в такого рода проектах гигантское значение имеет система мониторинга. Она предоставляет массу информации для ответа на такого рода вопросы. Сопровождать и развивать высоконагруженный проект без мониторинга хотя бы основных метрик по всему серверному парку это безрассудство.
Это приводит нас к cписку первостепенных задач которые стоят перед разработчиками в контексте высокой нагрузки:
- создание инфраструктуры предоставляющей качественную обратную связь по утилизации аппаратных ресурсов системы (мониторинг, self-аудит приложений);
- определение звеньев системы нуждающихся в масштабировании в ближайшем времени и выбор стратегии масштабирования этих звеньев;
- определение toolbox’а позволяющего решать типовые задачи эффективно по аппаратным ресурсам (базы данных, очереди сообщений, языки программирования, библиотеки и т.д.).
В состоянии гонки
У вас как у инженера нет прямого влияния на количество пользователей и есть опосредованое влияние на объемы данных. В конце концов, чем больше данных и чем шире аудитория, тем лучше для бизнеса. Больше пользователей – больше возможностей для монетизации. Больше данных – больше конкурентное преимущество, а так же потенциально более высокие темпы роста проекта. Необходимо смирится с непрерывным ростом этих двух метрик.
Что же остается? Не так много на самом деле. Сделать так чтобы возможности системы были всегда на шаг впереди ее потребностей.
29 июня, 2011
Deploy и прочие неприятности
В продолжение предыдущего поста хочу немного рассказать о том как у нас происходит deploy. В прошлый раз мы закончили на том что артефакт доставлен на production и готов к развертыванию. Начинается самое интересное, процесс деплоя.
Но для начала надо немного описать платформу на которую мы деплоим наши приложения. В качестве servlet-container'а мы используем Jetty. Есть два основных типа приложений, которые мы пишем и сопровождаем: web-приложения, которые имеют некий web-интерфейс (это может быть как UI, так и REST интерфейс), а также background сервисы, которые как правило работают асинхронно (мы активно используем ActiveMQ). И здесь нужно подметить первую особенность, мы используем Jetty для обоих типов приложений. Это может показатся странным, так как для background-сервисов мы получаем явный overhead связанный с запуском и работой servlet-container'а. Но этот overhead просто мизерный по сравнению с тем какие плюсы мы получаем. А именно:
- единые утилиты управления lifecycle'ом приложения вне зависимости от его типа (старт, останов, перезапуск);
- стандартные способы задания конфигурации приложения вне зависимости от его типа;
- стандартные форматы распространения приложения вне зависимости от его типа (в нашем случае
.war).
Вобщем, стандартизация на лицо.
У нас своя сборка Jetty в которой кроме самого Jetty, есть еще несколько часто используемых нами библиотек (например: slf4j, logback, mysql драйвер, библиотеки connection pooling'а и некоторые другие). Сборка представлена в виде rpm пакета, что позволяет очень быстро подготавливать новую машину: yum install jdk jetty, готово.
Вас может удивило почему мы ставим на production машины JDK, а не JRE. Это связано с наличием в JDK очень полезных для диагностики утилит, таких как: jmap, jstack, jvisualvm и других. Это выгодно отличает платформу Java от множества других, -- масса утилит для диагностики, которые в большинстве случаев позволяют довольно быстро понять что не так с приложением.
Деплой приложения состоит из следующих шагов:
- распространение бинарника с приложением, а также конфигурации на целевые машины;
- перезапуск servlet container'а;
- deployment тестирование.
Естественно второй и третий шаг выполняются для всех машин по очереди.
Распространение артефакта и конфигурации
Тут все просто, scp доставляет файлы на каждую машину по очереди и ложит каждый в нужное место. Доставляются артефакты, а также конфигурационные файлы.
Перезапуск servlet container'а
Мы не используем hot redeploy, так как он вызывает ряд проблем. В частности, небезизвестную OutOfMemoryError: PermGen. Для перезапуска используются скрипты входящие в стандартную поставку Jetty с некоторыми несущественными дополнениями.
Deployment тестирование
Этот момент стоит описать поподробнее. После того как Jetty запустился было бы неплохо проверить что приложение способно выполнять основные функции. Тот кто деплоит приложение может опечататься в конфигурации, системные администраторы могут забыть "пропилить дырку" в firewall'е для необходимого приложению сервиса, мало ли что еще может произойти.
Задача deployment тестирования проверить что у приложения есть доступ ко всем необходимым сервисам для того чтобы оно выполняло свою работу. Это могут быть базы данных, очереди сообщений, сетевые файловые системы и другое middleware ПО. К счастью для нас, Spring Beans контейнер практически все делает за нас. Если во время инициализации контейнера (частью которого являются подключения к БД и т.д.) происходит ошибка, об этом легко узнать послав GET запрос на любой url, который обрабатывается непосредственно spring'ом. У нас есть соглашение что url /status не занимается приложением и служит для deployment тестирования. Этот url также сообщает какая версия приложения запушена в данный момент. Таким образом, deploy скрипт после того как перезапускает servlet container начинает опрашивать приложение. Если приложение возвращает 200-й статус код, то можно переходить к обновлению следующей машины. Если нет, то процесс деплоя считается неуспешным и прерывается. Тот кто делает deploy может инициировать процедуру отката. Откат мы делаем руками, так как процент "битых" релизов у нас не велик и смысла автоматизировать эту процедуру пока что смысла нет.
Rollout распределенных приложений
Когда приложение запущено в нескольких экземплярах (на нескольких машинах), то после запуска приложения на одном ноде имеет смысл немного подождать прежде чем переходить к следующей машине. Здесь играют свою роль как особенности приложения, так и особенности платформы. Java, как русские -- "долго запрягает, но быстро едет" (начальная загрузка байт кода, создание пула соединений, JIT компилятор etc). Нас такой tradeoff вполне устраивает, поэтому наш deploy скрипт может быть сконфигурирован таким образом, чтобы давать только что запущенному ноду "разогреться" перед тем как принять на себя нагрузку его еще не обновленных коллег. Как правило, 3-5 секунд достаточно.
Деплой на несколько машин (особенно учитывая их "разогрев") порождает интересную проблему. Во время обновления кластера, на нем работает две версии приложения. Некоторых людей этот факт ставит в ступор. Они не могут ужиться с тем, что у приложения в любой момент времени нет строго определенной версии.
Суровая реальность заключается в том, что требование работать одновременно в "нескольких версиях" порождается природой web-приложений. Мы стремимся к тому чтобы наше система всегда была online. Это имеет одно очень важное последствие для процесса deploy'я:
Мы всегда должны иметь возможность откатиться на предыдущую версию системы, так как пусть редко, но бывает что ошибка все же проходит сквозь все рубежи тестирования и попадает на production. Причем rollback должен осуществлятся не медленнее чем rollout.
Это приводит нас к следующему заключению: следующая версия системы всегда должна быть обратно-совместима с предыдущей. Это касается не только кода, но и схемы БД, данных в кеше и т.д.
Оказывается, что когда новая версия системы обратно совместима с текущей, нет ничего страшного в том чтобы некоторое время они поработали вместе.
На практике
Итак, как это все происходит на практике. Разработчик заходит через ssh на машину-координатор, куда автоматически доставляются артефакты приложений. У каждого приложения есть отдельная директория содержимое которой выглядит примерно следующим образом:
-rw-rw-r-- 1 tech tech 101 Jun 24 16:42 .config
-rw-rw-r-- 1 tech tech 1134 Jun 9 13:05 config.properties
-rw-r--r-- 1 tech tech 163 May 16 16:11 .jettyrc
-rw-r--r-- 1 tech tech 30298960 Jun 3 19:41 search-web-frontend-1.0.118.war
-rw-r--r-- 1 tech tech 30299011 Jun 3 21:25 search-web-frontend-1.0.119.war
-rw-r--r-- 1 tech tech 30298949 Jun 13 21:51 search-web-frontend-1.0.120.war
-rw-r--r-- 1 tech tech 30297647 Jun 14 12:38 search-web-frontend-1.0.121.war
-rw-r--r-- 1 tech tech 30297689 Jun 15 11:50 search-web-frontend-1.0.122.war
-rw-r--r-- 1 tech tech 30298356 Jun 15 12:13 search-web-frontend-1.0.123.war
-rw-r--r-- 1 tech tech 30298678 Jun 15 12:32 search-web-frontend-1.0.124.war
-rw-r--r-- 1 tech tech 30732203 Jun 23 11:45 search-web-frontend-1.0.125.war
-rw-r--r-- 1 tech tech 30732204 Jun 23 11:55 search-web-frontend-1.0.126.war
Здесь есть несколько файлов о которых стоит рассказать поподробнее. Файл .config это обыкновенный ini-файл хранящий имена всех машин на которых установлено приложение, а также некоторые другие настройки развертывания. config.properties — это properties-файл, который содержит настройки приложения. .jettyrc содержит startup опции виртуальной машины на которой стартует Jetty.
За довольно длительное время мы перепробовали различные способы работы с конфигурационными файлами приложений. Мы хранили их в development системе контроля версий. Мы создавали для них отдельный репозиторий в зоне production. Текущая схема нам нравится больше всего. Она позволяет автоматически бекапить все конфигурационные файлы приложений, что безусловно необходимо, а также не связываться с излишей сложностью VCS систем для управления "двумя файлами из 10 строчек каждый".
Если запустить команду deploy без аргументов, то она выведет текущее состояние нодов:
$ deploy
http://search-service1:8080 - Ok com.farpost.search:search-web-frontend 1.0.126
http://search-service2:8080 - Ok com.farpost.search:search-web-frontend 1.0.126
http://search-service3:8080 - Ok com.farpost.search:search-web-frontend 1.0.126
Благодаря maven во все наши сборки автоматически попадает информация о версии, а также номере билда.
Если же передать команде deploy имя артефакта, то начнется его развертывание на production машинах.
Дальнейшие соображения
Текущий процесс нас вполне устраивает, тем не менее у нас есть идеи как сделать его еще лучше.
Runtime обновление конфигурации логгирования
Мы используем logback в качестве библиотеки логгирования. Существенным ее плюсом является то что она позволяет менять конфигурацию логгирования на лету. Достаточно просто поменять XML файл с конфигурацией. Было бы неплохо иметь возможность распространять конфигурацию логгирования на машины без перезагрузки самого приложения.
Дифференцирование конфигурации различных нодов
В распределенной системе разные ноды могут иметь идентичную сборку, но разную конфигурацию, обуславливающую требуемую разность в их поведении. Сейчас у нас пока что нет возможности задать разную конфигурацию для разных нодов. Учитывая что конфигурация задается в виде properties файлов, сделать такого рода дифференциорание не сложно.
Partial deploy
Иногда бывает необходимо обновить не весь кластер, а только один нод из кластера. В будущем, я думаю мы сделаем такую возможность.
Вот пожалуй и все. А как деплоите приложения вы? ;)
17 апреля, 2011
Fair lock
Не так давно у нас на собеседовании был кандидат, который произвел довольно хорошее впечатление, поэтому было решено предложить ему более сложную задачу, которую обычно мы не спрашиваем. Вот ее немного видоизмененный вариант:
Переделайте следующий код оставив его многопоточным таким образом, чтобы лампочки зажигались и гасли строго по очереди и в любой момент времени должна быть включена только одна лампочка:
package me.bazhenov.bulb;
public class Main {
public static void main(String[] args) {
new Thread(new Bulb("first")).start();
new Thread(new Bulb("seconds")).start();
}
}
public class Bulb implements Runnable {
private final String name;
public Bulb(String name) {
this.name = name;
}
public void run() {
Thread self = currentThread();
while(!self.isInterrupted()) {
System.out.println(name + " bulb is on");
try {
sleep(300);
} catch (InterruptedException e) {
self.interrupt();
}
System.out.println(name + " bulb is off");
}
}
}
Кандидат предложил использовать ReentrantLock в FairSync режиме. В первом приближении эта идея может показаться рабочей. Передать общий лок в оба оъекта типа Bulb и синхронизироватся там на нем. Тем не менее, этот подход не работает. Если мы заглянем в документацию к классу, то увидим следующее описание:
The constructor for this class accepts an optional fairness parameter. When set true, under contention, locks favor granting access to the longest-waiting thread. Otherwise this lock does not guarantee any particular access order. [...] Note however, that fairness of locks does not guarantee fairness of thread scheduling.
FairSync не гарантирует отсутствие race condition'а между потоками. Единственное что он гарантирует это то, что лок возьмет поток который ждал на локе дольше всего. Отсутствие контроля за CPU шедулером не дает нам гарантии что в момент когда поток отпускает лок его оппонент уже попытался сделать acquire на этом же локе (что необходимо для того чтобы сработал FairSync в этой задаче).
К сожалению у меня нет под рукой соответствующего железа, но я подозреваю что на однопроцессорной машине разницы между FairSync и NonfairSync вообще не будет, так как у параллельного потока не будет возможности поставить в очередь заявку на acquire, чтобы при следующем unlock'е его заявка была обслужена.
Правильное же решение задачи остается на совести читателя :)
09 апреля, 2011
Маленький Билд и его друзья
Помните фильм "Пятый элемент"? Там была сцена, когда Зорг опрокидывает стакан на пол и роботы тут же начинают уборку помещения. Всего лишь одно маленькое действие привело в жизнь десяток машин, которые сразу же начали подметать и мыть полы, а в конце еще и налили воды хозяину.
Что-то похожее происходит в коллективе с налаженным build процессом, когда разработчик коммитит изменения в систему контроля версий.
О пользе налаженного процесса билда известно много. Но "путь на production" не прост. После того как код написан и перед тем как он будет запущен на production'е было бы неплохо сделать следующие вещи:
- откомпилировать код если вы используете компилируемый язык (удивил, да?);
- прогнать модульные и интеграционные тесты если они у вас есть;
- выполнить статический анализ кода и/или другие проверки которые позволяет выполнить ваша платформа и которые имеют смысл для вашей команды (code style check, code coverage, цикломатическая сложность, dependency matrix и т.д.);
- собрать артефакт приложения (желательно в виде одного файла), содержащий в себе весь код и ресурсы необходимые для запуска приложения на целевой платформе;
- выполнить приемочное тестирование, если оно у вас есть;
- опубликовать артефакт в репозитории (предположительно локальном), чтобы другие члены комманды могли им воспользоватся в своих целях (особенно актуально, если вы разрабатываете билиотеку, а не приложение);
- в случае если вы разрабатываете приложение а не библиотеку, произвести deploy в окружение staging тестирования чтобы команда и менеджеры могли оценить текущее состояние проекта.
Этот список действий не догма и варьируется от потребностей проекта. Я же расскажу что и как делаем мы. Сразу отмечу что у нас в проекте используется две платформы: PHP и Java. Я преимущественно буду описывать как мы собираем Java проекты, так как за счет поддержки инструментария билд процесс там получается более целостный и вразумительный. Я думаю, этот опыт будет полезен читателям.
Итак код закоммичен в систему контроля версий. С этого момента начинается его маленькое путешествие на production. Но для начала давайте познакомимся с главными героями. Для того чтобы успешно проводить автоматизированною сборку проекта необходим определенный инструментарий.
В главных ролях
Система контроля версий (VCS)
Какую VCS систему использовать решать вам. Это может быть старый проверенный временем subversion, или модно-распределенный git/mercurial. Но она должна быть. Если на вашем календаре уже 2011 год и вы все еще не пользуетесь какой-либо системой контроля версий, то я настоятельно советую вам переоценить принципы согласно котрым вы принимаете решения.
В контексте обсуждения процесса сборки выбор системы контроля версий настолько неважен, что я даже не буду говорить какую используем мы :)
Система сборки проектов (build tool)
Основная задача системы сборки состоит в автоматизации задач связанных с созданием релиза из исходных кодов. Среди программистов работающих с интепретируемыми языками распространено мнение что исходный код и есть релиз. Связано это судя по всему с тем, что в таких языках нет выделенной фазы компиляции. Я считаю, что даже в этом случае разделение между исходниками и релизами нужно, потому что вне зависимости от того на каком языке программирования написано приложение, релиз должен обладать следующими свойствами, которыми не обладают исходные коды:
- любой релиз в отличии от исходных кодов должен работать. Подтверждатся это должно каким-либо видом тестирования (хотя бы ручным). Исходные коды же могут находится некоторое время в нерабочем состоянии (например, во время рефакторинга);
- релиз может содержать third party библиотеки и программное обеспечение, которое разрабатывается и поддерживается третьими лицами. Хранить все это в системе контроля версий может быть не самым удобным решением;
- платформа на которой работает приложение может иметь ограничения на формат релиза. Например, может быть строго определен формат архива с приложением (например, rpm или deb). Хранить исходные коды в VCS в том же формате может быть очень неудобно c точки зрения разработки.
Выражаясь математически: релиз = ƒ(исходники).
Для сборки мы используем Apache Maven, -- это довольно зрелая и продвинутая система сборки, которая впрочем не так проста в изучении как ее более "легковесные" братья вроде ant и gradle (хотя ant — это скорее бабушка, а gradle внучатый племянник). Gradle в последнее время получил довольно много положительных отзывов и продолжает набирать популярность, поэтому вам определенно стоит посмотреть на него, если вы определяетесь с вопросом выбора системы сборки Java-проекта.
Для того чтобы использовать maven более эффективно, у нас есть определенная экосистема для его поддержки.
Во-первых, это репозиторий артефактов о котором я расскажу ниже.
Во-вторых, у нас есть общийэ POM-дескриптор для всех проектов компании, который определяет настройки компилятора, сразу делает доступными библиотеки повсеместно используемые в нашей компании (TestNG, hamcrest, logback и т.д.), а также настраивает плагины для статического анализа кода и логгирования и т.д.
В третьих, у нас есть несколько прототипов проектов (archetype в терминологии maven), которые позволяют одной командой из консоли создать новый проект. Эдакий hello world с уже настроенным логированием, Jetty для тестирования, ActiveMQ и Spring Integration для обработки сообщений, Spring в качестве web-framework'а и много еще чем. Все это очень сильно упрощает старт, особенно людям не знакомым с премудростями настройки всего этого "зоопарка".
Репозиторий артефактов
Еще один вопрос связанный с системой билда — куда ложить его результат? После того как мы собрали приложение и протестировали его, нам надо опубликовать релиз. Необходимо общее централизованное место где бы хранились все артефакты, чтобы любой разработчик знал где искать последний релиз продукта. Позже вы можете достать его и использовать для deploy'я на production или в окружение staging тестирования. Если вы разрабатываете библиотеку то релиз нужен другим членам команды, для того чтобы использовать ее в своем приложении.
В простейшем случае роль репозитория может играть web-сервер. Его настройка для этих задач не займет у вас много времени. Либо это может быть FTP-сервер или сетевой диск доступный всем разработчикам. Так же эту роль может играть continious integration сервер, речь о котором пойдет ниже.
В более сложных случаях удачным решением может быть специализирванное ПО. Особенно это актуально если вы используете Maven, который специфицирует формат репозиториев и протокол работы с ними (поверх HTTP).
Мы используем Artifactory в качестве репозитория, который выполняет несколько ролей в нашей экосистеме:
- хранит артефакты делая их (и их исходники) доступными для использования любым программистом в любое время;
- кеширует third party библиотеки используемые программистами. Если вы начинаете новый проект и хотите использовать какую-то библиотеку, с высокой долей вероятности она уже есть в локальном репозитории и вы получите ее моментально не дожидаясь загрузки из интернета.
Continuous Integration сервер
Не смотря на грозное название, CI-сервера по своей сути — это триггеры билд процесса которые предоставляют разработчикам удобный способ контроллировать процесс и результат сборок. Они делают несколько вещей:
- позволяют конфигурировать политику запуска сборки (при коммите в VCS, через заданные интевалы времени, после успешной сборки зависимого проекта, вручную, и т.д.);
- предоставляют возможность следить за процессом сборки публикуя ее логи через web-интерфейс;
- предоставляют отчеты по результатам билда (проваленные тесты, предупреждения статического анализатора и т.д.);
- строят тренды на основании истории сборок (количество тестов, время затраченное на исправление билда, процент успешных сборок и т.д.);
- могут успешно играть роль репозитория артефактов.
Строго говоря использование CI-сервера не обязательно. Более того, на начальных этапах я бы рекомендовал делать сборки на машинах разработчиков. Это позволит сэкономить вам время в процессе допиливания build-процесса, которое на начальных стадиях неизбежно.
Мы до сих пор не используем CI-сервер для сборки билиотек. Библиотеки собираются и релизятся самим программистами. Несмотря на то что этот подход имеет ряд недостатков, он существенно проще.
В качестве CI-сервера мы используем TeamCity, бесплатной версией которой мы полностью довольны. Одна из привлекательных фич этого продукта состоит в том что он предоставляет неплохую статистику по билдам, включая success rate сборок, количество тестов и т.д.
Поезд отправляется
Итак как это все происходит на практике.
За системой контроля версий следит continuous integration сервер. Как только он замечает какие либо изменения, он делает checkout свежей версии и инициирует процесс сборки проекта.
TeamCity позволяет очень детально настроить с какими параметрами будет запущен build. Вы можете предопределить значения системных переменных нужных вашему процессу билда. Эти переменные могут использоваться например для того чтобы специфицировать на какой БД будет выполнятся тестирование.

Затем в работу вступает Maven. Он компилирует исходники, запускает модульные тесты. Если они провалились, то генерирует отчеты по проваленным тестам, которые будут затем показаны на персональной странице билда, а сам билд завершается и считается проваленным:
Если тесты проходят, то "состав движется дальше". Следующим шагом является сборка артефактов. В терминах continious integration артефакт это любой результат процесса сборки, главная черта которого — воспроизводимость. Для нас это означает что артефакт является процессом работы машины, а не ручных действий человека.
Обратите внимание, что в смежных областях знаний, например в configuration management, термин "артефакт" имеет немного отличные значения.
Воспроизводимость артефакта — очень важное его свойство. Оно позволяет в любой момент времени из любого среза VCS попытатся собрать приложение и посмотреть что из этого получится.
Воспроизводимость артефакта служит хорошей поддержкой для так популярных в наше время итеративных методов разработки, смысл которых состоит в идее "давайте начнем стрелять, а потом будем корректировать огонь". Когда патроны дешевле чем время, это хороший подход. Налаженный процесс автоматического производства артефактов в этом контексте можно сравнить с автоматическим станком по производству патронов. Чем лучше он у вас отточен, тем дешевле ваши патроны, и тем быстрее вы можете стрелять и получать фидбек критически важный для следующей итерации.
После того как артефакт собран Maven принимается за интеграционное и приемочное тестирование. Здесь необходимо отметить зачем разделение между модульными, интеграционными и приемочными тестами.
Модульные тесты тестируют классы в изоляции от других частей системы, а также в изоляции от внешних систем, таких как базы данных и веб-сервисы. Их задача состоит в том чтобы давать программисту быстрый фидбек относительно того, работает система или нет безотносительно того верно ли она интегрируется с внешними источниками данных. Фактически, любой программист должен иметь возможность сделать checkout исходников проекта, запустить модульные тесты на абсолютно не подготовленной машине (например, без базы данных) и тесты должны выполнится. Единственная причина почему они могут не выполнится, это если в коде приложения допущена ошибка.
Таким образом, у модульных тестов не должно быть зависимостей, отличных от тех которые тест может удовлетворить сам. По личному опыту могу сказать: тех кто не соблюдаете это правило ждет не очень приятное будущее.
В сухом остатке. Модульные тесты должны:
- выполнятся быстро. Потолок 15-20 секунд, затем их просто перестают использовать. Здесь нужно обратить внимание на то, что модульные тесты должны выполнятся именно на стороне программиста и как можно чаще. И конечно же обязательно перед коммитом в систему контроля версий. На CI-сервере они выполнятся лишь для полноты тестирования;
- быть изолированными от внешнего окружения и выполнятся даже на машине без сети.
В отличии от модульных, интеграционные тесты проверяют как приложение дружит с внешними системами, и как следствие, обладают следующим рядом особенностей:
- могут требовать довольно сложное окружение для своего выполнения (конкретную БД, возможно даже конкретной версии);
- могут быть довольно медленными, так как включают в себя взаимодействие по сети, зачастую с системами производительность которых находятся под контролем третьих лиц;
- являются гораздо более хрупкими чем модульные, потому что опираются на заранее установленное окружение, а также на программное обеспечение которое скорее всего меняется без оглядки на ваше конкретное приложение.
Тем не менее тестировать все равно надо, поэтому такие тесты выделяются в отдульную группу и запускаются на CI-сервере. Настроить и поддерживать в актуальном состоянии сложное окружение проще один раз на сервере, чем десять раз на машинах разработчиков. Разработчик может запустить интеграционные тесты у себя, но для этого он должен настроить хотя бы часть окружения на своей машине, что может быть довольно непросто. Частично эти проблему можно решить при помощи виртуализации и таких инструментов как puppet.
Обычно интеграционные тесты на стороне разработчика запускаются только в случае если менялся интеграционный код системы и как правило только тесты на изменившуюся часть системы. С точки зрения maven (а именно, maven-surefire-plugin) интеграционные тесты отличаются от модульных только постфиксом (*Test у модульных, *IT у интеграционных). После того как тесту задан соответствующий постфикс, он автоматически начинает запускатся на нужной фазе сборки проекта.
Если с интеграционными тестами все хорошо, то запускаются приемочные. Приемочные тесты проверяют систему по типу черного ящика. Если вы разрабатываете web-приложение, то оно запускается на web-сервере и тест под видом обыкновенного пользователя начинает ходить по нему и проверять его работоспособность. Пожалуй, это один из самых сложных видов тестирования. В нем довольно легко наделать ошибок, которые могут существенно увеличить стоимость поддержки тестовой инфраструктуры. Это в конце концов делает процесс тестирования менее эффективным. Впрочем, тема приемочного тестирования выходит далеко за рамки моего поста, поэтому я не буду заострять на этом сейчас внимание. Людям искушенным очень советую прочитать книгу Джеза Хамбла и Девида Ферли Continuous Delivery.
Ну а мы отправляемся дальше. Следуюшая остановка — статический анализ кода. Мы используем findbugs для анализа кода. Статические анализаторы действительно находят ошибки и потенциальные уязвимости, так почему бы не делать это при каждом изменении наших исходных файлов.
Можно указать TeamCity чтобы при превышении опредленного порога по количеству найденных проблемных ситуаций, билд считался проваленным.
Но, данный шаг можно порекомендовать не всем. Во-первых, для этого требуется существенная поддержка инструментов. Например, для php нет ничего подобного. Во-вторых, получаемая от статического анализатора информация должна оцениваться прагматически. Помните что анализатор показывает риски с формальной точки зрения. У каждого риска есть вероятность материализации и стоимость исправления. Вероятность материализации — это вероятность с которой риск превратится в проблему (то есть вы сможете наблюдать его воочию). Стоимость исправления — это сколько ресурсов (например, в виде человеко часов) нам надо будет потратить если риск все же материализуется в проблему. Перемножив эти два числа мы получим математическое ожидание проблемы. Может оказаться так, что математическое ожидание проблемы гораздо меньше чем стоимость внесения изменений в код здесь и сейчас. Некоторые проблемы дешевле исправлять пост фактум.
После того как статический анализ кода законечен, закончен и процесс непосредственно сборки. Теперь нам осталось опубликовать артефакт в общедоступный репозиторий и можно рапортовать об успешности билда.
Деплой у нас происходит в два этапа. Первый этап это деплой в локальный репозиторий артефактов. Второй этап включает в себя автоматическую доставку приложения на production сервера. Обратите внимание, доставку, но не deploy. Deploy осуществляется только по инициативе и под контролем разработчика. Таким образом, если разаботчик работает над проектом search-service версии 1.0 и на TeamCity успешно выполнился билд под номером 134, то на production кластере в папке проекта автоматически появляется файл search-service-1.0.134.war, который содержит в себе все необходимое для работы приложения и готов к развертыванию по команде разработчика. Это позволяет свести к минимуму участие человека в процессе билда. Человек привлекается только там где его внимание и возможность принимать решения не может заменить машина.
Вот пожалуй и все. Некоторые моменты остались за кадром. Например, как происходит запуск (deploy) приложения на production серверах. Но это тема совсем другого разговора.
Буду рад услышать отзывы и success stories читателей.
14 ноября, 2010
Как я собирал NAS
NAS у меня исполняет несколько обязанностей:
- файловое хранилище "жирного" контента (фильмы, etc);
- сервер для Apple TimeMachine (бекапы);
- качает и раздает торренты;
- "самопальный" мониторинг сетевой активности с использованием rrdtool;
- локальный git-репозиторий.
Мой опыт использования NAS начался с QNAP TS-109 PRO II.
Довольно удачная однодисковая железка, которая прослужила мне верой и правдой более года. У нее несколько минусов:
- собственно всего один диск;
- нестандартный встроенный linux;
- ARM-based процессор.
- установка компилятора и development библиотек тоже довольно нетривиальна (видимо проблема курицы и яйца);
- будучи программистом с серьезным отношением к тестированию я отношусь к той категории людей которые считают, что если ты сам компилируешь пакет, ты должен его сам и тестировать. А мне этого совсем не хотелось.
Таким, довольно эволюционным путем, примерно через полтора года я пришел к мысли что пора менять storage. На этот момент на рынке было несколько device'ов, которые привлекли мое внимание:
Моя вера в x86 на этом поприще росла с каждой новостью связанной с Intel Atom. Поэтому два первых девайса, опять таки построенные на базе ARM процессора, отсеялись сами собой. Два последних представляют уже больший интерес, но анализируя их спецификации я пришел к мысли что я бы мог сам собрать более подходящую мне железку.
Hardware
Я начал подискивать подходящий miniITX корпус. Мягко говоря это не просто, особенно во Владивостоке. Большинство корпусов в этом форм факторе нацелены на создание nettop'ов: имеют максимум 2 корзины под HDD, кучу лишних фентифлюшек вроде headphones output на фасаде и т.д. Мне же хотелось собрать железку не имеющую ничего лишнего и при этом "на вырост", чтобы через пару лет не пришлось менять снова.
В конце концов я нашел корпус, который меня полностью удовлетворил — Chenbro ES3469.
Этот корпус специально создавался под NAS, поэтому обладает довольно характерными особенностями:
- четыре корзины для 3.5" HDD с поддержкой горячей замены и индивидуальной индикацией питания и data activity;
- один внутренний слот для 2.5" HDD (очень удобно так как все четыре жестких можно отдать под хранение данных, а ОС поставить на отдельный пятый жесткий);
- внешний блок питания, что позволяет сэкономить место внутри и избавится от одного лишнего вентилятора (внешний БП без активного охлаждения);
- довольно "богатая" индикация на передней панели.
Сам корпус сделан довольно качественно, внутри разделен на два отсека. В одном отсеке находятся жесткие диски с двумя вентиляторами работающими на выдув, во втором материнская плата и вся коммутация.
Немного огорчило меня то, что одна из корзин сразу не заработала. Сначала я грешил на backplane, но слава богу дело было в неисправном SATA кабеле, который легко поддается замене. Замена правда осуществляется не самым простым образом, — для того чтобы добраться до отсека с корзинами и до backplane'ов, надо буквально говоря разобрать весь корпус, что не очень удобно. Что уж тут поделать, издержки форм фактора.
Следующим шагом стал выбор материнской платы. С ней тоже все оказалось не просто. Надо было найти Atom-based miniITX материнскую плату с полностью пассивным охлаждением и минимум 5 SATA портами! Немного странные требования для форм фактора miniTX, не находите? Такая все же нашлась — Supermicro X7SPA-H:
- двухядерник Intel Atom D510 1.6Ghz с поддержкой HT;
- 6 SATA портов;
- два SO-DIMM DDR2 667MHz модуля, максимум 4Gb.
- две гигабитные сетевые карты;
- полностью пассивное охлаждение;
- довольно простая видеокарта Intel GMA3150, что в моем случае является плюсом, будет меньше кушать и греться;
- поддержка power on after failure, что для девайсов такого класса очень полезно.
Вобщем, эта материнская плата подошла в моем случае просто идеально. Причем, материнская плата и корпус, что называется, соответствуют друг другу. Например, у обоих есть поддержка индикации power fail и chassis intrusion.
Правда, тут произошел казус. Сразу материнка не завелась: питание есть, вентилятор на корпусе вращается, а power LED не горит и стандартного писка о прохождении POST check'а нету. Отключил всю периферию, проверил что нигде ничего не замыкает, проверил память, — один черт, не заводится. Сбросил CMOS, — не заводится. Причем, если достать RAM, то матерится о ее отсутствии на всю квартиру. В конце концов, на третий день, когда вера в то что материнка жива окончательно иссякла, она при самых странных обстоятельствах (во время очередной операции "выткни-воткни оперативку") завелась. То ли чудо, то ли фокус... То ли я два или три десятка раз подряд неверно вставлял оперативную память.
Дальше стало проще. В качестве жестких я выбрал Western Digital линейки Caviar Green, так как они хорошо себя зарекомендовали еще со времен моего первого NAS'а — тихие и холодные. Сейчас у меня стоят два: WD10EADS и WD10EARS. Насколько я понимаю, отличаются они только объемом кеша 32/64Mb.
Так как я старался собрать максимально тихий device, один из вентиляторов в отсеке с жесткими я отключил, а на второй поставил zalman'овский регулятор скорости вращения. Эмпирическим путем была подобрана скорость, которая дает тихую работу. Температура при этом держится в районе 40-42 градусов, что меня вполне устраивает.
Software
Теперь немного об используемом программном обеспечении. Я "на короткой ноге" с Ubuntu, поэтому я выбрал именно этот дистрибутив. Так же, наслушавшись страшных историй о том как восстанавливают данные с RAID-зеркал, я сразу принял решение использовать файловую систему с поддержкой избыточности вместо аппаратного RAID'а.
Собственно, сейчас таких файловых системы две: BTRFS и ZFS. Первая находится на стадии беты и не предназначена для production использования. Вторая довольно стабильная и широко используемая, но в linux реализована в рамках проекта FUSE из-за лицензионных ограничений (изначально ее разработала Sun для ОС Solaris). По совокупности факторов я решил использовать ZFS.
ZFS вещь довольно интересная. Она позволяет собрать из списка блочных девайсов виртуальный том с заданными настройка избыточности. У нее есть ряд преимуществ перед аппаратным RAID'ом:
- в зеркале могут участвовать абсолютно разные жесткие (разного размера);
- выход из строя блочного девайса не запрещает клиентам читать и писать на том, до тех пор пока есть хотя бы одна живая реплика;
- rebuild тома (scrub в терминах ZFS) тоже не блокирует клиентов, они по прежнему могут читать и писать;
- настройки избыточности можно задавать отдельно для директорий. То есть вы можете хранить все в двух копиях, а особо важные для вас папки в трех (для этого том должен состоять как минимум из трех физических дисков).
Все фичи кроме последней я вынуждено "протестировал" в ходе эксплуатации. Так что, да, это действительно работает. Для проверки последней у меня просто не было мотивации, у меня нет настолько важных данных.
Создание ZFS тома делается очень просто:
zpool create tank raidz1 /dev/sda /dev/sdb
После создания, том будет автоматически примонтирован в /tank. Теперь можно заливать на него информацию. Просмотр статуса по виртуальным томам делается следующими командами:
zpool status
zpool iostat -v
Перебалансировка и починка тома осуществляется при помощи команды:
zpool scrub tank
В интернете есть множество информации о ZFS. Если хотите использовать ZFS, советую обратится обратится к официальному мануалу.
Надо понимать что такая гибкость не дается бесплатно. ZFS прожорлив по памяти, поэтому советую оснастить машину гигабайтом, а лучше двумя гигабайтами оперативной памяти. Я так и сделал, благо она дешевая.
С ZFS у меня произошел только один негатвиный инцедент. Во время начальной заливки данных на том, oomkiller'у не понравилось что драйвер ZFS ест много памяти, и он его убил :). Это издержки того что ZFS работает в user space. После reboot'а все стало нормально, ничего чинить не пришлось. Больше такого у меня не повторялось.
Так же, я использую NAS как сервер для Apple Time Machine, поэтому мне надо было настроить его так, чтобы мой mac mini воспринимал его как AFP сервер. В интернете есть полно мануалов, одним из которых я и пользовался.
Вот пожалуй и все что касается софта.
В итоге
Atom все же поменял мое представление о том, какой должен быть домашний файловый сервер. На базе атома вполне можно собрать тихий и холодный storage, и спихнуть на него "домашнюю работу". При этом это все это будет работать под управлением стандартного дистрибутива вашей любимой ОС (хоть windows). А сама железка мне обошлась даже немого дешевле чем Acer easyStore H340.
31 октября, 2010
HighLoad++ 2010 - впечатления
В этом году я побывал на HighLoad++. Event довольно интересный, поэтому я попытаюсь вкратце описать свои впечатления, а так же основные тенденции наблюдаемые на конференции.
Во-первых, стоит отметить, что тезисы докладов и презентации доступны на официальном сайте конференции.
Я живу во Владивостоке, поэтому мне пришлось пролететь очень большое расстояние чтобы попасть на конференцию. Этим частично обусловлено мое отношение к конференции как к таковой.
Начнем с приятного. На конференции действительно были специалисты с которыми было интересно общаться. Чувствовалось что люди знают о чем говорят и эту информацию они получили не из мануалов в интернете, а из своей ежедневной практики.
Организация была вполне приемлемая: был WiFi (который впрочем почему-то не ловил мой телефон) и достаточно еды (на которую у меня впрочем не было времени). Расписание было составлено таким образом, что самые интересные доклады параллельно не шли, а это радует.
Основные понравившееся доклады
Performance tweaks and tools for Linux / Joe Damato
В этом докладе рассказывается об основных инструментах профилирования под Linux — lsof, strace, ltrace, oprofile. Но об этом знают если не все, то почти все. Интересен этот доклад другим. Joe будучи Ruby хакером создал набор довольно интересных инструментов для профилирования Ruby приложений при помощи этих низкоуровневых инструментов.
Секреты Фейсбука: как выдержать 50 миллионов запросов в секунду / Robert Johnson
Robert Johnson — ведущий инженер по разработке ПО в компании Facebook (а по совместительству, как и я, владелец Jackson'а). Роберт много говорил не только о технической стороне масштабирования, но и об организационных мерах и том, каких направлений должен придерживаться коллектив участвующий в разработке высоконагруженного приложения. Основные посылы следующие:
- большому проекту рано или поздно прийдется столкнутся с горизонтальным масштабированием;
- все изменения должны быть инкрементальными (нет изменений, которые не могут быть отменены);
- если вы не можете что-то измерить, вы не можете этим управлять;
- работа в небольших независимых коллективах (от 3 до 7 человек) над разными проектами (фотографии, личные сообщения и т.д.);
- люди ответственные за что-либо должны иметь контроль над этим.
В личном общении с Робертом я смог узнать следующие вещи:
- социальные графы очень тяжело partition'ить. Я спросил не пытались ли они заимствовать опыт MMORPG проектов? По идее у них схожие задачи, но гораздо более жесткие требования к latency. Роберт сказал, что это интересная идея, но ничего подобного они не пробовали делать. Так же он не особо верит в том что алгоритмы кластеризации графов смогут помочь им решить эту проблему в общем виде и достичь при этом вменяемой производительности;
- их custom'ный протокол репликации в mysql, обошелся им в десяток строк кода, что и обусловило использование такого, на мой взгляд, странного решения;
- в довольно большом проекте в угоду производительности коллектива в целом приходится принимать такие решения, как писать HipHop. Насколько я понял, Роберт не особо верит в его широкий adoption за пределами Facebook. Это довольно специфические решение проблемы в их специфическом окружении.
Быстрое развертывание шаблонов и статики в Mail.ru / Николай Кондратов
Несмотря на немного "попсовое" название, доклад был довольно интересный. Ребята рассказывали о том, как они организовали работу верстальщиков и какие инструменты они им дали для развертывания приложения на сервере. Основная особенность больших систем не только в высокой нагрузке, но и в очень сложных процедурах развертывания системы. Последний факт почему-то редко получает много внимания, а зря.
В mail.ru особенности процесса следующие:
- шаблоны пишутся на псевдоязыке и позже могут быть транслированы в C/Perl;
- шаблоны и статика доставляются на production отдельно от кода приложения, тем самым код и шаблоны могут меняться независимо. Схема выглядит довольно хрупко. На мой вопрос: "как вы избегаете несовместимых изменений между кодом и шаблонами?", они ответили: "мы внимательно следим за этим";
- у каждого разработчика есть свой сетевой диск (который монтируется при помощи FUSE). Этот диск связан с отдельным для разработчика доменом, на котором крутится приложение;
- артефакт с шаблонами и статикой доставляется на production машину, с которой посредством p2p сети (torrent) распространяется по frontend'ам. Исходная машина работает в режиме суперсидирования (то есть каждый блок отдается только единожды). Ребята объяснили такое решение тем, что в противном случае у них заканчивается network bandwidth на исходной машине (размер артефакта порядка 100Mb).
Progressive downloads and Rendering / Stoyan Stefanov (Yahoo)
Yahoo, как известно "помешана" на оптимизации на клиентской стороне. Этот доклад всецело посвящен такого рода оптимизациям. Много говорилось про user perception и то как люди оценивают производительность. О том что стабильность производительности (среднеквадратичное отклонение) важнее, чем абсолютные значения latency. Про аттрибуты async и deffered, которые предоставляет HTML5. Про то что такое chunked response и как его использует Google и Amazon. Как CSS может значительно снизить производительность вашего web-приложения. Доклад довольно довольно последовательный, так что очень советую посмотреть слайды а также видео.
Я из этого доклада я вынес следующее: performance is a feature (этот тезис всплывал на конференции не раз). Если вы хотите чтобы ваше приложение быстро рендерилось на стороне клиента (что очень важно еще и в связи с ростом процента мобильных клиентов), то к этому вопросу над подходить как к фиче. Несмотря на расхожее мнение, добавить такого рода оптимизации в уже существующее приложение написанное без оглядки на вопросы client side performance, может быть довольно непросто.
Thinking Clearly About Performance / Carry Millsap
"Пушка" первого дня. Несмотря на относительно низкую практическую ценность доклада, я считаю что излагаемая в нем информация очень важна, — она вправляет мозги. В докладе излагается последовательный, целостный, математический подход к оценке производительности и пропускной способности. Поэтому вдвойне жалко что зал был почти пустой: человек 15-20.
Основные постулаты:
- пропускная способность и время отклика связаны опосредованно. Узнать одну метрику по другой, вы можете только в ряде частных случаев. В общем случае, вы должны измерять обе метрики независимо.
- арифметическое среднее не самый удачный способ оценки производительности. Percentile значения лучше.
- важна не только абсолютная мера производительности, но и стабильность этой меры (дисперсия времени отклика);
- для того чтобы достичь стабильной производительности, необходимо контролировать утилизацию исполнителей (для чего в теории систем массового обслуживания есть соответствующий мат. аппарат);
- существуют некоторые фундаментальные законы, которые ограничивают максимальную пропускную способность. Вы должны знать их.
Я очень советую прочитать одноименную публикацию, чтобы получить более полное представление об этом материале.
Закулисы
И все же большая часть информации была получена не на докладах, а за кулисами. Именно там была возможность пообщаться с опытными людьми: как докладчиками, так и просто участниками конференции. Из этого общения я вынес для себя несколько моментов:
- Sphinx очень популярен. Этим фактом, впрочем, никого не удивишь, но тем не менее полезно было получить тому подтверждение от людей реально его использующих;
- Redis тоже довольно популярное решение, что для меня стало новостью. Я встретил по меньшей мере пятерых человек из разных компаний, которые используют это хранилище;
- стандартная библиотека Scala изобилует довольно "детскими" багами как функциональными, так performance-related. Если вы хотите использовать Scala в крупном проекте, есть смысл подумать об использовании стандартной библиотеки Java, как бы грустно это ни звучало;
- системы асинхронного обмена сообщениями тоже получили довольно широкий adoption. Очень много кто использует те или иные реализации очередей (в основном memcacheQ и Redis);
- и несмотря на это, практически никто не знаком с EIP, основополагающим трудом в этой области;
- вопреки расхожему мнению, вопрос найма специалистов стоит остро в любой точке нашей страны (а возможно и планеты). Владивосток, будучи относительно небольшим городом, в этом отношении не особо хуже Москвы.
Ложка дегтя
Впрочем, не все так безоблачно. До сих пор, я намеренно игнорировал негативные моменты связанные с конференцией:
- некоторые доклады были попросту скучные, а некоторые я считаю вредные. Абсолютизм пропагандируемый в докладе "Практическое создание крупного масштабируемого web 2.0 проекта с нуля" это на мой взгляд один из худших феноменов, который может настигнуть специалиста. Масштабируемость в этом докладе провозглашается чуть ли не самодостаточным свойством системы ради которого и "крутится Земля". Попытки аудитории обратить внимание докладчика на то что иногда существуют определенные требования к гарантии в отношении consistency (которые влияют на принимаемые решения и выбор инструментов) не увенчались успехом;
- некоторые участники в личном общении беззастенчиво утоляли собственные комплексы. Один участник в течении 10 минут пытался мне объяснить что настраивать GC в Java очень просто. Объяснения сводились примерно к следующему. В Java есть один стоящий GC — Parallel GC, все остальное — ошибка природы. Серьезно к такому мнению относится не получается (на самом деле, это еще один частный случай абсолютизма описанного в предыдущем пункте);
- в рамках конференции был круглый стол об использовании SMP систем. Intel прислала на это событие не технического специалиста, а, если я не ошибаюсь, менеджера по связям с общественностью. Стоит ли говорить, что это не тот кого мы ждали;
- вообще, в конце докладов оставалось очень мало времени на вопросы. Иногда доходило до смешного, в конце доклада организаторы говорили: "у нас есть время на два вопроса". Два вопроса?! Вы издеваетесь?! По факту, обсуждение докладов осуществлялось за кулисами в компании 5-10 особо заинтересованных участников и самого автора.
В сухом остатке
Участие в конференции составило 15 000 рублей. Для иногородних еще стоимость билетов и проживания в городе. Поэтому я бы дал следующий совет тем, кто собирается поехать на HighLoad++ в будущем. И пожалуй я это выделю в отдельный абзац.
Основной источник информации на такого рода event'ах — это личное общение.
Не стоит ехать на конференцию если вы собираетесь ограничиться прослушиванием докладов или вы социофоб. Необходимо знакомится с участниками, общаться, задавать вопросы, выражать свое мнение и провоцировать людей на дискуссии. Только тогда участие в конференции будет экономически целесообразным.
Вот наверное и все. Если у вас есть еще какие-либо вопросы, с удовольствием отвечу на них.
30 августа, 2010
Keep it simple, stupid
Простота. Краеугольный камень нашей профессии. Наверняка вам приходилось слышать от своих коллег: “это можно сделать гораздо проще”. Или вы говорили это вашим коллегам: “смотри, можно сделать так. Это ведь гораздо проще”, а в ответ получали взгляд полный непонимания, как бы говорящий вам: “и это ты считаешь проще?”.
В программной инженерии определенно произошла инфляция слова “простота”.
Простота — это расстояние
Не существует общепринятого определения простоты. Тем не менее, если мы заглянем в словарь то увидим, что очень часто рядом со словом простота фигурируют такие понятия как: “предсказуемость”, “прямолинейность”, “ясность”, “очевидность”.
Что мы считаем простым? Процессы и объекты чье поведение легко предсказать. Если “оно” ведет себя так как я от “него” ожидаю, значит я понимаю как “оно” работает и скорее всего смогу “его” изменить не поломав. В противоположность этому, если “оно” ведет себя непредсказуемо, значит я не понимаю основополагающих принципов лежащих в “его” основе. И любая попытка “его” изменить может окончится таким же непредсказуемым результатом. В этом случае процесс постулируется слишком сложным.
Такие “предсказания” возможны благодаря тому что в голове человека есть ментальная (поведенческая) модель описывающая этот процесс или объект. И чем сильнее соответствует модель реальности, тем проще расценивается объект. Так что да, простота есть расстояние. Расстояние между ожидаемым и действительным.
Мера простоты — понятие субъективное
Теперь это должно быть очевидно. У двух людей могут быть различные поведенческие модели, а значит и своя мера простоты относительно того или иного процесса. Если вы считаете какой-то процесс простым, это значит что у вас в голове есть удачная модель происходящего. Именно она позволяет вам эффективно понимать и управлять происходящим. Мнение другого человека по этому же вопросу может быть диаметрально противоположенным, и это может обосновываться, например, отсутствием опыта работы в какой-либо сфере.
Любой взрослый человек без проблем восстановит порядок букв в алфавите, но это не значит что алфавит объективно прост. Это всего лишь значит что вам его вбили в голову в школе. На самом деле в порядке следования букв в алфавите нет никакой логики и очевидности. Объективно алфавит сложен и непонятен. Я думаю латинский алфавит по памяти восстановят гораздо меньше людей, даже при том что порядок сильно пересекается с кириллицей.
Но более интересна обратная ситуация. Если вы считаете какой-то процесс сложным, это еще не означает что процесс объективно непредсказуем и непонятен. Это может означать что у вас просто нет подходящей поведенческой модели. Возможно не стоит клеймить все вокруг сложным, возможно просто стоит... научится?
Например, если вы считаете что следующий код слишком сложен, так это просто потому что вы не знакомы с функциональным программированием.
def qsort: List[Int] => List[Int] = {
case Nil => Nil
case pivot :: tail =>
val(lessThan, greaterOrEqualThan) = tail partition(_ < pivot)
qsort(lessThan) ::: pivot :: qsort(greaterOrEqualThan)
}
Это приводит нас к заключению, что если мы хотим создать простой продукт, мы обязаны учитывать ментальные особенности людей которые будут работать с продуктом. И тут возможны два варианта:
- создавать процессы близкие к ожиданию пользователей;
- создавать в головах пользователей новые поведенческие модели (учить пользователей).
Последний продукт просто ужасен, верно? Или нет? Я незнаю. Мне ясна посылка этого комикса, — существует тенденция к чрезмерному усложнению вещей (или их недостаточному упрощению, как вам больше нравится).
Но что, если мы говорим об экспертной системе где сложность является частью предметной области, а не следствием особенностей имплементации и человек работающий с ней должен быть в нее очень хорошо погружен. Скажем... пилот реактивного лайнера.
Это кабина еще советского Ту-154М, реактивного самолета выпуска середины 80-х. Обычный человек не сможет идентифицировать большинство приборов, рычагов управления и их назначения. Значит ли это что интерфейс сложен для тех для кого он создавался, для пилотов?
Тенденции к упрощению существуют и в авиации, так что мы можем поискать более простой реактивный лайнер (так сказать, "для нубов"). Давайте посмотрим на кокпит широкофюзеляжного A-380, который начал эксплуатироваться в 2007 году. Считается что A-380 более прост в управлении. Об этом говорит даже тот факт что экипаж A-380 составляет два человека (против 4 человек в Ту-154М).
Аналоговых приборов почти не осталось, им на смену пришли цифровые приборы и LCD панели. Рычаги управления стали больше похожи на джойстик. Основным органом управления, судя по всему, стала клавиатура. Но стал ли он проще? Для пилотов может быть, для всех остальных никакой разницы. Если вы чувствуете себя как слон в посудной лавке в кабине Ту-154, тоже самое будет в кабине A-380.
Конечно, если вы можете создать продукт который будет решать задачи пользователя и при этом будет простым как электрочайник, то это просто прекрасно. Но есть сферы где человеку нужен полный контроль над происходящим и масса информации чтобы принять верное решение. И в этом случае может быть не лучшей идеей сводить интерфейс к одной кнопке в UI/одному методу в API и т.д.
Простота многомерна
Но вернемся к программированию. Простота в отношении программного кода обманчиво проста. Эта простота (или ее отсутствие) может проявляться в разных контекстах, в зависимости от процесса разработки.
Простота использования
Если речь идет об API или бибилиотеке, то как просто ее развернуть на клиенте и начать использовать?
Простота написания
Насколько просто писать код с использованием той или иной технологии. Как много кода прийдется писать? Как много ошибок я могу допустить в процессе написания? Здесь все просто, и зачастую именно об этом виде простоты идет речь когда два программиста спорят о простоте. Но это далеко не единственное (и иногда не главное) качество.
Простота тестирования
Люди по своей природе не надежны. Наша интуиция не всегда хорошо работает, иногда мы ленимся, иногда теряем концентрацию. Поэтому, мое жизненное кредо заключается в том, что там где это возможно вопросами верификации корректности должны заниматься машины. Человек не всегда может подтвердить что код все еще работает увидеть что код уже не работает. Машина может.
Именно поэтому для меня простота тестирования (тестируемость) — очень важное качество кода. Если я пишу код и не могу сделать так, чтобы верификацией работоспособности за меня занималась машина, то скорее всего этого кода не должно быть.
Простота верификации
Автоматическое тестирование не панацея конечно же, особенно если мы говорим о параллельных приложениях, которые очень тяжело тестировать. Насколько просто вы можете загрузить в голову коллеги такую ментальную модель при помощи которой он мог бы сказать: “да, я уверен, этот код работает”? Это и есть простота верификации.
Простота понимания
Последнее по порядку, но не по значению. Насколько код очевиден, насколько легко понять что он делает, и как он это делает, а следственно и насколько легко изменить его. Простота понимания и верификации очевидно связаны, потому как тяжело верифицировать то, чего ты не понимаешь. Тем не менее я склонен считать это разными факторами.
Это не исчерпывающий список. Вы можете изобразить все это на spider графике. Как и всегда, достичь максимума по всем направлениям вряд ли получится. Надо определить приоритеты и следовать им. Кому-то важнее простота написания, кому-то простота тестирования. Но в любом случае этот выбор должен быть осмыслен. Это то, что в наши дни очень тяжело найти — осмысленный выбор. В большинстве случаев выбор делается неявно в угоду упрощения процесса которым разработчик занимается именно сейчас. Это свойственно всем людям, — идти по пути наименьшего сопротивления.
Проблема в том, что упрощений в чистом виде не так много. Чаще приходится иметь дело с разменом. Упрощая одно, вы автоматически усложняете другое. Например, упрощение тестирования зачастую связанно с “расслоением” системы и усложнением ее понимания. Это размен на который мы идем, неважно отдаем мы себе в этом отчет или нет. Вопрос в том, выгоден ли этот размен для нас? Но это тема совсем другого разговора...
10 августа, 2010
Lock vs. Lease
Представьте себе типичную задачу, есть поток событий и этот поток событий надо разделить между несколькими пользователями системы. Типичный пример: модерация. Скажем, есть три модератора, которые просматривают добавляемый пользователями контент (допустим рекламные кампании). Мы хотим “распилить” поток событий между модераторами, чтобы они не делали одну и ту же работу дважды.
В самом простом случае, мы можем использовать mq в любом его проявлении. Пользователи просто берут сообщение из головы и обрабатывают его. Эта схема хорошо работает, но к сожалению не всегда применима. Например, если пользователи имеют возможность указывать критерии событий которые хотят обрабатывать. Последнее требование очень часто в том или ином виде всплывает в подобных задачах, потому что люди (точно так же, как и процессоры) плохо переносят context switch. Например, если я настроился на модерацию авто тематики, то я фигурально выражаясь загрузил в кеш ключевые слова относящиеся к этой тематике и буду очень расстроен если мне подсунут объект с тематикой относящийся к сотовым телефонам.
Окей, мы не можем использовать mq, что тогда? Далее приходит на ум следующее — хранить события в реляционной таблице (чтобы иметь возможность фильтрации) и брать shared lock на каждое из событий/объектов при их обработке. В данном случае lock’и позволяют исключить дублирующую обработку события. Ничего криминального в использовании локов для решения этой задач нет, но если вы начнете имплементировать подобное решение, то вы очень быстро прийдете к тому что lock’и обладают семантикой отличной от той которая необходима вам. И вот почему...
Lock timeout
Если мы говорим про web-приложение, то необходимо понимать что факт закрепления события за конкретным пользователем должен иметь timeout. HTTP — это stateless протокол. После того как вы закрепили событие за пользователем, он может просто закрыть браузер и пойти по своим делам. Если мы хотим чтобы любое событие было рано или поздно обработано, мы обязаны аннулировать устаревшие локи.
Но традиционный механизм lock’ов не имеет timeout’ов, и это решение вполне обосновано, так как обычно lock’и или используются внутри одного адресного пространства (а следственно проблемы исчезновения lock owner’а нет) или используются в distributed окружении, но имеют механизм autorelease’а (при разрыве соединения текущего владельца блокировки и координатора автоматически происходит release).
Это означает в традиционных use case’ах использовании lock’ов между текущим владельцем лока и координатором всегда есть канал связи, чего нет в нашем случае.
Immediate locking
В нашем примере, если мы не можем взять lock на событие это означает что данное событие уже кто-то обрабатывает. В этом случае нам надо просто пропустить это событие и попытаться заблокировать следующее. Но в большинстве случаев использования lock’ов, если lock уже взят, то клиент вынужден ждать своей очереди. Это вполе логично, так как действия выполняемые в разных потоках представляют собой различные не связанные между собой действия. Все они должны быть выполнены последовательно, для чего и используется lock.
Фактически в данной задаче нам нужен не инструмент сериализации нескольких потоков (чем и является лок), а некий флаг благодаря которому мы могли бы исключить из обработки события которые уже находятся в процессе обработки.
Lease
Проблема в том, что люди видя эту семантическую разницу пытаются расширить интерфейс локов для того чтобы покрыть им данный use case использования. Это опасно, так как в механизм добавляется элемент autorelease’а, который легко может нарушить целостность данных. Представьте, что если при традиционном использовании локов (скажем, параллельном изменении коллекции элементов) autorelease произойдет до того момента когда владелец лока успеет выполнить операцию?
Вместо того чтобы пытаться приспособить локи там, где они не нужны, давайте посмотрим в сторону другого решения. Я лично считаю поиск аналогий в реальном мире или смежных областях очень полезным занятием. Это позволяет лучше осознать задачу и процессы лежащие в ее природе.
Помните DHCP? Это протокол автоматической конфигурации сетевого стека компьютера. Сегодня его можно найти буквально в любом домашнем маршрутизаторе. DHCP позволяет компьютеру автоматически получать IP-адрес и автоматически конфигурировать сетевой стек в зависимости от топологии сети в которой находится компьютер.
Обратите внимание, когда компьютер получает IP-адрес по DHCP, он не посылает запрос “дайте мне IP 192.168.1.15”. Он посылает запрос: “дайте мне любой свободный адрес”. В ответ он получает lease. Lease — это аренда адреса. Аренда имеет timeout, по истечении которого, если lease не был продлен, то аренда аннулируется и IP-адрес возвращается в список доступных. Вполне подходит под нашу задачу, верно?
В случае DHCP, клиенту по большому счету все равно с каким IP-адресом работать. В нашей задаче клиенту все равно какие именно события обрабатывать. Главное чтобы они удовлетворяли формальным требованиям о которых мы говорили раньше.
Модель аренды гораздо ближе к природе задачи. Во-первых, timeout’ы, которые нам просто необходимы, лежат в природе механизма аренды. Во-вторых, аренда не подразумевает владения объектом. Фактически в этот же момент с этим же объектом может происходить какая-то другая операция (например, индексация), которая скорее всего имеет возможность выполнятся параллельно, а значит данные находящиеся в объекте не являются предметом защиты от конкурентного доступа. Это сигнализирует о том что локи в данном случае не уместны. В этой задаче lock’и могут служить только инструментом обеспечения целостности списка арендованных объектов, но не более того.
P.S. интересно услышать к какими способами решения схожих задач приходилось сталкиваться вам?



