Баннерокрутилка на Erlang

Preview:

Citation preview

Баннерокрутилка:как это было на Erlang

Задача

● Выдача ссылок на новости● База из тысяч новостей ● Ссылки выбираются произвольно● Распределение – неравномерное

● Форматирование ссылок перед выдачей● Ссылки должны иметь формат блока● Дополнительные поля: идентификатор блока, …

Ta-da!

● Требования: >= 5000 запросов в секунду● Не справляемся – должны быстро выдавать

пустую страницу, чтобы не «ломать» вёрстку● Никакого кэширования

● Срок выполнения: полтора месяца.● При этом у разработчика баннерокрутилки

есть и другие задачи

Структура решения

● Структуры в памяти, хранящие новостии статистику показов

● Множество потоков, обрабатывающихHTTP-запросы

● Контроллер● добавляющий и удаляющий новости● собирающий статистику● на основе TCP-сокетов

Структура решения

Множество потоков

Erlang

● DSL для многопоточных приложений● Встроенные в язык примитивы для посылки

и приёма сообщений● Встроенные в язык шаблоны поведения● Встроенная в язык in-memory БД

Структура решения

● Структуры в памяти, хранящие новости и статистику показов: mnesia/ets/dict!

● Множество потоков, обрабатывающих HTTP-запросы: gen_server!

● Контроллер: gen_tcp!

Структура решения

● Структуры в памяти, хранящие новости и статистику показов: mnesia/ets/dict!

● Множество потоков, обрабатывающих HTTP-запросы: gen_server!● который уже написан за нас! mochiweb/misultin

● Контроллер: gen_tcp!

Batteries included

Batteries included, though not all

Основная часть решения

● HTTP-сервер Mochiweb получает запрос, создаёт поток

● Поток забирает из Mnesia данные● Поток производит выборку, сортирует

данные, форматирует строки, выдаёт, умирает.Все счастливы.

«In theory, there's no difference betweentheory and practice. In practice, there is».

L. A. van der Snepscheut.

Факап #1

У каждой новости есть коэффициент важности. В соответствии с этим коэффициентом необходимо выдавать новость чаще или реже остальных.

● Перед выдачей нужно назначать взвешенные произвольные числа каждой новости и делать по ним выборку.

● Новостей много.

Факап #1

● Mnesia построена на основе ETS● http://www.erlang.org/doc/man/ets.html:

«In the current implementation, every object insert and look-up operation results in a copy of the object.»

● Т. е. в копировании сотен новостей из потока в поток. Slow as hell.

Эврика!

● Вместо ETS напишем собственную структуру данных на основе gen_server, dict, queue, blackjack и hookers.

● Повесим её в виде отдельного потока● Будем делать там грубую предвыборку

новостей, которые потом быстро скопируются в рабочий поток

● Результат:

рост производительности в 3 раза

● Вывод:● всегда думай, какие объёмы данных копируешь!● профилируй!

Основная часть решения v0.2

● HTTP-сервер Mochiweb получает запрос, создаёт поток

● Поток отправляет запрос в gen_server● gen_server производит предвыборку

новостей и присылает результат● Поток производит выборку, сортирует

данные, форматирует строки, выдаёт, умирает.Все счастливы.

Факап #2

Новости – это текст.

Текст – это строки.

● Строки в Erlang – это связные списки символов

● IO в Erlang – это очень медленно

Эврика!

● Если вы пишете на Erlang,то строка символов записывается так:

"Hello world!~n"

● Конкатенация записывается так:

"Hello " ++ Username ++ "!~n"

Эврика!

● Если вы пишете на Erlang веб-приложения,то строка символов записывается так:

<<"Hello world!~n">>

● Конкатенация записывается так:

[<<"Hello ">>, Username, <<"!~n">>]

Почему:

● <<>> – встроенный бинарный тип● <<"">> – бинарная строка● Списки символов нужно обрабатывать

перед выдачей● Вывод бинарных данных – это просто

вызов writev(2)● Blazingly Fast

Почему:

● "Hello" ++ "!\n" => "Hello!\n" => строка● Конкатенация списков – O(n)● Вывод строки => цикл по списку из 7 символов

● [<<"Hello">>, <<"!\n">>] – тип iolist().● Добавление в начало списка – O(1)● Вывод => цикл по списку из 2 элементов● Встроенным функциям I/O всё равно, что

выводить

Кроме того

● Строковые операции, наподобие обработки регулярных выражений, всё равно дорогие

● Впрочем, они вообще не очень дешевы.

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

● В данном случае кэширование конструируемых URL новостей и т. п. позволило отыграть 15%

● Результат:

рост производительности в 10 (десять) раз

● Вывод:● всегда думай, как обрабатывать строки!● профилируй!

Основная часть решения v0.3

● HTTP-сервер Mochiweb получает запрос, создаёт поток

● Поток отправляет запрос в gen_server● gen_server производит предвыборку

новостей и присылает результат● Поток производит выборку, сортирует

данные, форматирует iolist()'ы, выдаёт, умирает.

профилируй!

Основная часть решения v0.4

● HTTP-сервер Misultin получает запрос, создаёт поток

● Поток отправляет запрос в gen_server● gen_server производит предвыборку

новостей и присылает результат● Поток производит выборку, сортирует

данные, форматирует iolist()'ы, выдаёт, умирает.

Misultin

● Реализация gen_server, как и Mochiweb● Интерфейс, абсолютно аналогичный

Mochiweb● Стабильно на 10-15% быстрее

Результат

● Один человекомесяц● Быстрое веб-приложение

# ab -qc 7200 -n 450000 http://localhost/block/35237| grep Requests\ per\ secRequests per second: 7693.35 [#/sec] (mean)#

Killing feature!

Начиная со второй недели разработки (как только был написан каркас), приложение было готово к работе.

В любой момент не работал только тот функционал, который не был дописан.

● Ни отладки● Ни непредусмотренного поведения● Только профилирование

Killing feature!

● Ни отладки● Ни непредусмотренного поведения

В Erlang есть концепция «Let it crash».Близкий перевод – «Ну и хрен с ним».

Let it crash

● На обычном языке программирования:

res = web_server.start_link(callback = Fun)

if res == web_server.port_in_use: raise Exception("Port in use")elif res == web_server.socket_error: raise Exception("Socket error")elif res == errno.EACCES: raise Exception("Not enough privileges")

Let it crash

● На Erlang:

{ok, Pid} = misultin:start_link([{loop, Fun}]).

Let it crash

● На Erlang:

{ok, Pid} = misultin:start_link([{loop, Fun}]).

● Если что-то шандарахнется, то предположениеmisultin:start_link/1 => {ok, _}

окажется неверным, и поток вылетит самс сообщением, например, таким:{badmatch, {error, eacces}}

Результат

● Один человекомесяц● Быстрое веб-приложение● Стабильное веб-приложение – за счёт eunit

и «Let it crash»● Минимум кода – за счёт множества

встроенных примитивов и «Let it crash»● Минимум требуемого опыта – Erlang

изучается за 2 недели

Уровень программиста

● С одной стороны, Erlang учится за 2 недели● С другой стороны, нужно иметь навыки

программирования. Not all batteries included

Для написания баннерокрутилки в разное время требовался то JSON-декодер, то перекодировка из UTF8 в CP1251, то htmlspecialchars(). Ничего этого в stdlib нет, нужно брать сторонние библиотеки, оценивать их работоспособность и производительность.

Напутствие

● Предобрабатывай данные, пока это дёшево!● Не выполняй одни и те же операции дважды!● Используй «Let it crash» в интерфейсах

собственного кода!● Профилируй!● Переписывай медленные операции на C,

PHP, OCaml, whatever: существуют открытые биндинги