05 июня, 2010

About motivation

Does anybody know why we do what we do? Why do we wake up everyday and go to our workplaces? It seems that the most obvious reason is money. We need money to function in the society. We need some funds to make some plans, if you wish.

It turns out that not only money motivate people to do their job. OSS is the proof. Well, yes, we can make money from open source projects. And a lot of companies do this already, but it's not the point. The point is that there is another force motivating people. Most good programmers like some of my colleagues and buddies work not because of money, but because they feel passion to write programs. Money is a required but not sufficient part of motivation.

Daniel H. Pink some time ago gave an excellent talk about motivation factors and how to use them in work process.

Some companies, like Atlassian, show outstanding results in the process of self organizing their employees. They simply said to their developers:

next 24 hours you could do whatever you want with whoever you want, but you should show results to the company within next 24 hours.

Looks familiar, right? Yes, it's some sort of Google's 20% culture.

Nevertheless, so called FedEx days was born (like FedEx Ground day-definite delivery, I believe):

Overall, I'd say we did pretty damn well. People built some kick ass projects, we all learnt about new technologies, and everyone seemed to have a good time.
[...]
Will we repeat the exercise? Definitely. I think it was great thing to do - developers got to play, exercise their minds a little, build some fun stuff and learn about new technologies.

There are a lot of really fun things provided by Atlassian FedExes. Which makes me believe in the words if Daniel Pink:

Pay people enough to take the issue of money off the table.

There is no need to pay people more that that. Instead of raising salary you would better invest more aggressively in creating an attractive and efficient work environment. If you work in a IT company give your team some time to share experience and thoughts about new technologies, provide really good hardware for your employees (it's so cheap anyway), and, holy crap, do not ever shape an Internet connection. I mean it, never.


16 апреля, 2010

ldt или нагрузочное тестирование по-простому

И снова про инструменты разработки. Часто бывает необходимо сравнить производительность/пропускную способность того или иного участка кода, а писать тестирующий код ой как не хочется. А ведь надо всего-то, запустить нужный метод N раз и померять время выполнения.

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

Недолго думая, пишем простой POJO класс описывающий тестовый случай.

package com.blogspot.dotsid.ldt;

public class ArrayIterationTest {

  private int size;
  private int[] data;

  public void setSize(int size) {
    this.size = size;
  }

  public void prepare() {
    data = new int[size];
  }

  public void doTest() {
    for ( int i : data );
  }
}

компилируем исходник и находясь в classpath'е выполняем:

bazhenov@home ldt -z com.blogspot.dotsid.ArrayIterationTest#doTest -n 100 -p "size=1000000"
                     RESULTS
--------------------------------------------------
 Concurrency level             : 1
 Samples count (per thread)    : 100
 Total time                    : 180ms
 Min. time                     : 1ms
 Max. time                     : 5ms
 Throughput                    : 553 tps

В этом тесте мы создали массив размером 1 миллион позиций и проитерировались по нему 100 раз. Как видно мой процессор по этому массиву пробегает со скоростью несколько миллисекунд на одну полную итерацию.

Довольно простой и эффективный инструмент для выполнения нагрузочных тестов на отдельные модули системы. Основные возможности:

  • поддержка многопоточного тестирования;
  • поддержка warm up периода (указанное число первых прогонов теста может не участвовать в измерениях. Это бывает необходимо для обеспечения hot code execution path);
  • поддержка фикстур (prepare/cleanup);
  • sub millisecond accuracy;
  • поддержка maven;
Утилита open source и доступна на github. Еще один маленький и незатейливый инструмент, который полезно иметь под рукой. Ведь именно из таких инструментов и формируется окружение которое позволяет нам работать эффективно. Как говорится, "что нельзя измерить, тем нельзя управлять".


20 марта, 2010

Программисты и железо

Не так давно у нас на работе (Виктор, Игорь, Олег привет вам) состоялась дискуссия на тему: должны ли программисты знать как работает железо на котором выполняются их программы? И я еще раз убедился в том, что большинство программистов придерживаются мнения: "железо само по себе, а я сам по себе". Точка зрения вполне ожидаемая и ничего принципиально неправильного в ней нет, но я хотел бы "копнуть глубже".

Вообще, мне кажется вопрос "стоит ли программистам изучать железо" в чем-то похож на вопрос "стоит ли программистам изучать математику", — ответ неоднозначен. Существует множество вопросов, ответы на которые могут повлиять на окончательное решение: в какой предметной области вы работаете, насколько эти конкретные знания ценны для вас и т.д. Это вопрос инвестиций и прибыли. В данном случае под инвестициями подразумевается в первую очередь ваше свободное время, а под прибылью — профессиональные навыки и знания, которые позволят вам более эффективно делать вашу работу. Тогда вопрос сводится к следующему: чем изучение железа может помочь нам программистам делать свою работу лучше?

Мы с вами живем в эпоху высокоуровневых языков. Программисты большинства отраслей с успехом забыли про unmanaged языки, не говоря уже об ассемблерных вставках. В этом есть свои преимущества. Во-первых, это позволило нам программистам быть более эффективными. При равных доступных ресурсах теперь мы можем решать более сложные задачи. Во-вторых, не требуя от программистов "погружения в железо", мы позволяем им специализироваться в своей области, а также увеличить их количество за счет более короткой learning curve. Последнее впрочем имеет и свои негативные последствия, но я не буду сейчас акцентировать на них внимание.

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

Почему стоит изучать?

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

Возьмите хотя бы возросший интерес, к actor'ам. В общем и целом actor'ы представляют собой модель кооперативной многозадачности. Если вы хорошо помните историю, то кооперативная многозадачность в широких масштабах последний раз "упоминалась" в начале 90-х. У нее есть один существующий минус — она не работает на маленьком количестве исполнителей. Когда у вас один процессор легко может случится так, что какая-либо задача оккупирует его и другие задачи не смогут выполнятся. Тем не менее, у кооперативной многозадачности есть плюсы которые заставили нас опять обратить свой взор на эту модель...

Дело в том, что основная проблема с которой сталкиваются современные модели распараллеливания (которые преимущественно основаны на потоках) — это context switch. Доступ к памяти не является random'ным, чтобы ни говорили разработчики железа. Latency доступа к памяти на современных платформах составляет сотни тактов процессора. За это время процессор может сделать много работы. Эту проблему сейчас адресуют довольно простым путем — кеш, размер которого уже достигает 2Mb и больше на ядро. Проблемы кеша вам уже должны быть известны, — они одни и те же, неважно говорим ли мы о CPU cache или о memcached. Как только вы получаете промах кеша, вы платите performance penalty. Именно cache miss'ы являются источником деградации производительности в случае высокого context switching'а. Об этом говорило очень много умных дядек.

Решение довольно очевидно. Не переключатся между задачами лишний раз, только после завершения всей задачи целиком. Здравствуй кооперативная многозадачность. Не верите мне, поверьте сотрудникам Яндекса. Не зря в Windows существуют fiber'ы.

Справедливости ради, следует заметить, что скорость не единственный (и лично для меня не главный) плюс модели actor'ов. Эта модель гораздо проще в понимании и тестировании чем традиционные thread-based приложения. Но интерес к этой модели начал появляться как раз в тот момент, когда традиционные способы увеличения производительности исчерпали себя и мы начали искать новые методы обеспечения роста. А это означает одну простую вещь — программные абстракции используемые нами меняются в том числе и под "давлением аппаратных факторов".

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

Никто не просит вас уметь программировать на ассемблере и разбираться в opcode'ах микропроцессора. Но осознание того как работает процессор и какие bottleneck'и есть у существующих аппаратных платформ может дать вам очень хороший теоретический background для решения многих задач и ответа на многие вопросы.

Root of all evil

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

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

Tradeoffs

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

  • ОО анализ и проектирование (шаблоны/SOLID принципы);
  • анализ и построение алгоритмов;
  • классические структуры данных;
  • диагностика типичных проблемных ситуаций;
  • тестирование и поддержка legacy систем;
  • умение писать корректный многопоточный код, а также находить ошибки в многопоточном коде (светлое будущее с Clojure, Erlang, Scala, whatever еще не наступило);
  • функциональная декомпозиция;
  • автоматизация процесса разработки;
  • продолжите список...

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

В итоге

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


15 февраля, 2010

Superlinear scalability

Как вы думаете во сколько раз может быть быстрее ваша программа, если вам дадут в два раза более "крутое" железо: в два раза более быстрый процессор, в два раза больше памяти и т.д. Интуитивный ответ — в два раза. Раньше я уже писал о том, что не все так безоблачно. Взвесив все аргументы вы можете сказать: "окей, максимум в два раза". Но что если бы я вам сказал, что при увеличении вычислительной мощности в два раза вы можете получить ускорение больше чем в два раза?

Давайте представим себе обратный случай: у вас есть алгоритм с асимптотической оценкой по времени в O(n). И вы решаете задачу эталонного размера на эталонном компьютере за эталонное время. Увеличим размер задачи в два раза. Скажем вместо 5 миллионов записей надо отсортировать 10 миллионов (например, за O(n) можно сделать топологическую сортировку). Как изменится эталонное время? Увеличится в двое... как минимум. Увеличение времени вдвое нам гарантировано, так как мы выполним в два раза больше операций. Но существуют причины для дополнительного замедления программы. Например, если удвоенный dataset не помещается в оперативную память, то вы получите большое количество page fault'ов и операционной системе прийдется одни страницы выгружать из оперативной памяти на жесткий диск, другие загружать с диска в оперативную память. В общем случае, вы можете получить замедление более чем в два раза.

Но что если теперь мы начнем решать эту "удвоенную задачу" на "удвоенном компьютере"? Во-первых, мы получим двукратный прирост производительности связанный с удвоенной вычислительной способностью нового компьютера. Но также мы избавимся от лишних page in/page out'ов, что безусловно еще более убыстрит нашу программу, так как теперь не надо тратить лишнее время на IO. А это значит что в сумме мы получим более чем двукратный прирост производительности.

Утверждение "при удвоении вычислительных ресурсов можно получить максимум двукратный прирост производительности" имеет силу только в условиях когда задача решается эффективно (то есть не делается лишней работы).


20 января, 2010

Миграция ключей

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

Формально говоря: при заданном количестве серверов N и хеширующей функции ƒ(x) обладающей выходным множеством с мощностью P (log(P) бит, для crc32 P = 232, для md5 P = 2128) какое количество ключей осуществит миграцию в случае, если серверов станет N-1, а для дистрибуции ключей используется значение f(x) % N.

Ответом является формула описывающая зависимость количества мигрирующих ключей от изначального количества серверов и/или количества хранимых ключей.

Допущения: функция ƒ(x) имеет равномерное распределение на всем множестве входных значений, P несравнимо больше N.

Удачи!


17 января, 2010

Слежка за логами

Раньше я уже писал о том, что нам приходится разрабатывать дополнительный инструментарий для себя. Еще одна сфера которую над которой мы плодотворно поработали — это логгирование. Здесь я не буду говорить о пользе логгирования и о том как надо логгировать. В интернете полно информации по этим аспектам. Я хочу рассказать о том, как мы анализируем логи.

Дело в том, что в сутки у нас генерируется порядка 100 мегабайт логов. Часть из этой информации — это информация об ошибках (при средней длине сообщения в 5 килобайт нам достаточно 4 секунд чтобы набрать мегабайт информации), часть это аудиторская информация. Просматривать такой объем информации в виде текстового файла — это просто нереально, поэтому мы создали для себя инструмент позволяющий аггрегировать информацию со всех компонентов системы и производить поиск по ней.

Компоненты системы посылают сообщения по протоколу UDP. UDP был выбран не случайно. Мы не хотим чтобы коллектор логов мог пагубно влиять на production приложение, поэтому мы специально выбрали такой протокол, который позволил бы не блокировать компоненты в случае, если коллектор медленно работает, или вообще отключен. Похожим образом работает syslog-ng, но мы решили все же "изобрести свое колесо", чтобы иметь более изолированное решение и гибкость в определении протокола передачи данных.

В сообщениях которые генерируют компоненты находится следующая информация:

  • тестовое сообщение;
  • stacktrace exception'а (опционально);
  • диагностический контекст (key-value набор аттрибутов).

Это позволяет иметь следующий довольно удобный web-frontend, который предоставляет общую информацию о возникавших проблемах и аудиторских сообщениях.

Для каждого события есть severity (error/warning/info и т.д.), дата последнего возникновения, сколько всего раз происходило это событие, а также application id (символическое имя компонента приславшего сообщение).

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

Бывает так, что стандартных интерфейсов недостаточно. В этом случае есть поиск который позволяет найти какие-то специфические записи.

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

  • at: frontend severity: error — все ошибки произошедшие в компоненте frontend;
  • occurred: last 2 days @user: bazhenov — все сообщения за последние два дня спровоцированные обработкой запроса для пользователя bazhenov;
  • @machine: n25.baza.loc caused-by: slrDbConnectionFailedException — все записи пришедшие с сервера n25.baza.loc содержащие stacktrace exception'а типа slrDbConnectionFailedException.

Прямо здесь "на месте" из события можно создать тикет в нашей issue tracking системе. Вот так мы работаем с логами. А как вы следите за своими логами? :)


16 января, 2010

KV-хранилища

В последнее время в web-программировании появился очередной тренд — Key-Value базы данных. Существует просто великое множество KV-решений, — одно лучше другого. Но так ли они важны и какая от них польза? Разрешите мне немного порассуждать о происхождении KV-хранилищ.

Нет дыма без огня

Недовольство реляционными базами данных начало появляться давно. Оказалось что на некоторых use case'ах они не такие быстрые как хотелось бы. Они слишком сложные для того чтобы большинство программистов понимало как они работают, а следственно и то, как их использовать. Идея изоляции программиста от физических деталей хранения данных с треском провалилась. Если вы не знаете нюансов вашей РСУБД, то эффективно использовать ее на большом dataset'е или при высокой конкурентной нагрузке у вас не получится.

Помните чему вас учили в университете? Первая нормальная форма, вторая, тертья, Бойса-Кодда и т.д. Бурный рост интернет-аудитории с одной стороны и функциональности web-приложений с другой привел к тому, что пришлось пересмотреть паттерны использования реляционных СУБД. Сейчас, для того чтобы получить от реляционной БД приемлемый уровень пропускной способности, необходимо денормализовывать данные, отказываться от распределенных транзакций и проверки целостности по внешним ключам.

Но не стоит спихивать эти проблемы на не дальновидность или не профессионализм разработчиков реляционных СУБД. Насколько хорошо вы себе представляете как работает реляционная СУБД? Давайте проведем quick test. Ответьте на следующие вопросы:

  • вы знаете что такое план выполнения запроса и как БД его строит?
  • вы знаете как БД использует несколько индексов для фильтрации в случае, если нет index'а покрывающего все условия фильтрации?
  • вы знаете как БД использует индексы для сортировки, группировки и join'ов?
  • вы знаете сильные и слабые стороны hash и btree индексов?
  • вы знаете разницу между nested loop join, merge join и hash join?

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

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

KV-storage

Однажды приведя свою систему к тому состоянию, когда большинство выборок в ней производится по первичному ключу, вы можете задаться вопросом: а на кой черт мне здесь реляционная СУБД? Действительно, все эти статистические выкладки по cardinality индексов, эвристики заменяющие множество lookup'ов на один sequential read нужны были только тогда когда мы не знали какой запрос прийдет от клиента. Теперь мы знаем — это lookup по id. А раз мы знаем, то мы можем написать хранилище не делающее ничего лишнего — KV-storage.

Вот так они и появились. KV-хранилища — это не "серебрянная пуля" и не "RDBMS killer". Это следствие эволюции взглядов на паттерны доступа к данным. KV-хранилища быстрые лишь потому, что они предоставляют только один способ доступа к данным — lookup по id. Они быстрые потому, что не обременены, как РСУБД, необходимостью тратить лишнее время на определение оптимального плана выполнения запроса, чтобы сэкономить гораздо больше времени во время выполнения этого запроса.

Фронт NoSQL

Но современные постреляционные базы данных (если позволите так их назвать) ушли гораздо дальше чем просто lookup по id. Сейчас начинают набирать популярность документо-ориентированные базы данных (CouchDB, MongoDB), которые предоставляют более сложные способы извлечения и модификации данных. Некоторые KV-хранилища умеют нативно работать с коллекциями. Но эти возможности все равно меньше чем у реляционных баз данных.

Весь фронт NoSQL держится на том, что большинству web-приложений не нужны все возможности реляционной БД. Ну или скажем так, они могут обойтись без всех возможностей реляционной БД в угоду производительности.

Масштабируемость KV-решений

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

Задумайтесь, что дает вам, например, memcachedb для того чтобы легко масштабироваться? Кто-то из вас может сказать: "Легко. Берем остаток от деления хеша первичного ключа на количество серверов и...". Ну ладно ладно, я понял. Кто-то может вспомнить про consistent hashing. Отлично. Но дело в том, что это не хранилище дает вам эту возможность partitioning'а, а тот паттерн доступа к данным которым вы пользуетесь. С таким же успехом, я могу легко вместо memcachedb использовать MySQL и утверждать что "MySQL легко масштабируется".

Справедливости ради, надо сказать что некоторые решения (Cassandra, Project Voldemort, Scalaris) сами по себе предоставляют решения для автоматического partitioning'а ключей по нодам кластера. В отношении этих решений утверждение о масштабируемости все же верно.

Выводы

Суровая реальность научила нас тому, что для того чтобы быстро получать доступ к данным их необходимо хранить в удобном для оперирования над ними виде. Данные и их структура первичны в случае если вы хотите получить максимум производительности. Этому существует немало подтверждений (впрочем, пока эти подтверждения находятся в областях не связанных напрямую с web-программированием, поэтому они могут показаться вам безосновательными).

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

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