2. Задачи перед сервисом
• Производительность
– сейчас: 1000 баннеров/сек.
– хотим: 10 000 баннеров/сек.
– время отклика < 200 миллисекунд.
• 365*24*7, обязательно отдать контент (хотя
бы заглушку).
• Много площадок (>1.000), много баннеров
(>100.000).
4. Резюмируем свойства
• Горизонтально масштабируемый (до 10000
хитов/сек). Перекрутов, в идеале, быть не
должно.
• Многопоточный (медленные запросы не должны
держать быстрые). Минимизировать блокировки.
• Синхронизация серверов в режиме “реального
времени” (уменьшаем перекруты).
5. Схема сервиса в целом
Площадка Клиент Пользователь Пользователь
hadoop
PHP nginx
hadoop
front front
SearchServer
MySQL
uuserver
sphinx соцдем uuserver
6. UUServer
• Кука всего 4 KB – мало.
• Очень близко к хранилищу key/value.
• Выдача данных по TCP (свой протокол).
• Прием данных по UDP (свой протокол).
• Масштабируемый.
• Многопоточный (много блокировок).
7. UUServer (прод.)
• Внутри 64 независимых дерева - боремся с
локами и балансировками дерева при
вставке.
• Раз в 5 мин запускается цикл сохранения
пользователей на диск.
• Классический ретаргетинг организован
плагинами на стороне uuserver.
• Соединения ‘постоянные’.
8. Схема uuserver
TCP запрос UPD пакет (событие)
UPD пакет (событие) foreach(libs)
{Retargeting;}
Выбор map
Поиск user Выбираем очередь
Выбор map
Выбор map
Поиск user
Анализ Поиск user
Map.insert(…)
Выбираем очередь
TCP ответ
Increment,
Update;
10. Cache
• Хранит много объектов, если примитивно, то
std::map(u_int64_t, std::vector<...sort...> *).
Значение в каждой записи — это табличка из
СУБД.
• Объекты — берутся из базы данных (она отвечает
за сортировку и за целостность данных).
• Данные в некоторых 'табличках' меняются редко
(площадки, рекламные кампании, цены), или
очень редко (гео база) — изменения всегда
приходят из СУБД.
11. Cache (прод.)
• Есть 'таблички', данные в которых меняются часто
('счетчики') — изменения приходят как из СУБД
так и от CounterQueue.
• Многопоточный (на каждое соединение — свой
поток).
• Соединения 'постоянные'.
• Блокировки (чтение/запись) накладываются на
всю таблицу.
12. Cache: логика обновления
CacheInOut {
TCP запрос Данные из СУБД
u_int32_t size;
u_int32_t func;
Анализ и char data[size];
построение }
‘таблички’
WaitToWrite()
TCP ответ Данные из Cache Swap(std::vector<...>*)
Delete old data Done()
13. Cache: выбор баннеров
TCP запрос WaitToRead(),…
Find place, geo
Сортировка РК
foreach(PK)
{Targets;}
TCP ответ Done(),… result
15. FastCgiExp
• Сервер – и диспетчер и обработчик.
• Есть пулы нитей (nginx (fastcgi), cache, uuserver).
• В каждом процессе хранятся тела баннеров,
заглушек и ссылок (доступ к ним производиться
через read/write блокировку).
• Настройка количества listen сокетов, размеры
пулов, количество процессов производится
редактированием файла конфигурации, с
последующим перезапуском.
16. FastCgiExp: запрос
FastCgi запрос Выбрать Нить PoolThread FastCgi
Dummy CheckReferer UDP to SearchServer
FastCgi ответ Выделить Нить PoolThread UUServer
Get User Info PoolThread Cache
mq_send(…)
mq_send(…)
Выделить Нить Get BannerId
UDP to UUserver Banner
17. FastCgiEvent: запрос
FastCgi запрос Выбрать Нить PoolThread FastCgi
204 Выделить Нить PoolThread UUServer
FastCgi ответ Get User Info
mq_send(…)
Get Location
mq_send(…)
UDP to UUserver
20. Что дальше? (прод.)
• Новые интерфейсы рекламодателям и
владельцам площадок (и поддержка и
юзабилити).
• Соцдем.
• Новые отчеты (hadoop).
• Мониторинг серверов (zabbix).
21. Оптимизация трафика
Трафика много, денег Возможно мало уников
мало?
Возможно сейчас у нас нет рекламы
для вас (новые форматы)
Возможно вы хотите много денег, или
другие таргетинги.
Опрашивайте рекламные движки каскадом
Делитесь информацией о своих юзерах
27. Задача
• Выдача ссылок на новости
– База из тысяч новостей
– Ссылки выбираются произвольно
– Распределение – неравномерное
• Форматирование ссылок перед выдачей
– Ссылки должны иметь формат блока
– Дополнительные поля: идентификатор блока, …
28. Ta-da!
• Требования: >= 5000 запросов в секунду
– Не справляемся – должны быстро выдавать
пустую страницу, чтобы не «ломать» вёрстку
– Никакого кэширования
• Срок выполнения: полтора месяца.
– При этом у разработчика баннерокрутилки
есть и другие задачи
29. Схема решения
• Структуры в памяти, хранящие новости
и статистику показов
• Множество потоков, обрабатывающих HTTP-запросы
• Контроллер
– добавляющий и удаляющий новости
– собирающий статистику
– на основе TCP-сокетов
30. Схема решения
Структуры в памяти, хранящие новости
и статистику показов
Множество потоков, обрабатывающих запросы
Контроллер
добавляющий и удаляющий новости
собирающий статистику
на основе TCP-сокетов
31. Erlang
• DSL для многопоточных приложений
• Встроенные в язык примитивы для посылки и
приёма сообщений
• Встроенные в язык шаблоны поведения
• Встроенная в язык in-memory БД
32. Схема решения
• Структуры в памяти, хранящие новости
и статистику показов: mnesia/ets/dict!
• Множество потоков, обрабатывающих HTTP-запросы:
gen_server!
• Контроллер: gen_tcp!
добавляющий и удаляющий новости
собирающий статистику
на основе TCP-сокетов
33. Схема решения
• Структуры в памяти, хранящие новости
и статистику показов: mnesia/ets/dict!
• Множество потоков, обрабатывающих HTTP-запросы:
gen_server!
– который уже написан за нас! mochiweb/misultin
• Контроллер: gen_tcp!
добавляющий и удаляющий новости
собирающий статистику
Batteries included
на основе TCP-сокетов
35. Костяк решения
• HTTP-сервер Mochiweb получает запрос, создаёт поток
• Поток забирает из Mnesia данные
• Поток производит выборку, сортирует данные,
форматирует строки, выдаёт, умирает.
Все счастливы.
«In theory, there's no difference between
theory and practice. In practice, there is».
L. A. van der Snepscheut.
36. Факап #1
У каждой новости есть коэффициент важности. В
соответствии с этим коэффициентом необходимо
выдавать новость чаще или реже остальных.
• Перед выдачей нужно назначать взвешенные
произвольные числа каждой новости и делать по
ним выборку.
• Новостей много.
37. Факап #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.
38. Эврика:
• Вместо ETS напишем собственную структуру
данных на основе gen_server, dict, queue,
blackjack и hookers.
• Повесим её в виде отдельного потока
• Будем делать там грубую предвыборку
новостей, которые потом быстро
скопируются в рабочий поток
39. Результат:
рост производительности в 3 раза
Вывод:
– всегда думай, что копируешь!
– профилируй!
40. Костяк решения v0.2
• HTTP-сервер Mochiweb получает запрос, создаёт поток
• Поток отправляет запрос в gen_server
• gen_server производит предвыборку новостей и
присылает результат
• Поток производит выборку, сортирует данные,
форматирует строки, выдаёт, умирает.
Все счастливы.
41. Факап #2
Новости – это текст.
Текст – это строки.
• Строки в Erlang – это связные списки
символов
• IO в Erlang – это очень медленно
42. Эврика:
• Если вы пишете на Erlang,
то строка символов записывается так:
"Hello world!~n"
• Конкатенация записывается так:
"Hello " ++ Username ++ "!~n"
43. Эврика:
• Если вы пишете на Erlang веб-приложения,
то строка символов записывается так:
<<"Hello world!~n">>
• Конкатенация записывается так:
[<<"Hello ">>, Username, <<"!~n">>]
44. Почему:
• <<>> – встроенный бинарный тип
• <<"">> – бинарная строка
• Списки символов нужно обрабатывать
перед выдачей
• Вывод бинарных данных – это просто
вызов writev(2)
– Blazingly Fast
45. Почему:
• "Hello" ++ "!n" => "Hello!n" => строка
– Конкатенация списков – O(n)
– Вывод строки => цикл по списку из 7 символов
• [<<"Hello">>, <<"!n">>] – тип iolist()
– Добавление в начало списка – O(1)
– Вывод => цикл по списку из 2 элементов
– Built-in функциям I/O всё равно, что выводить
46. Кроме того:
• Строковые операции, наподобие обработки
регулярных выражений, всё равно дорогие
• Впрочем, они вообще не очень дешевы.
Надо делать предобработку
• Кэширование конструируемых URL новостей
и т. п. позволило отыграть 15%
47. Результат:
рост производительности в 10 (десять) раз
Вывод:
– всегда думай, как обрабатывать строки!
– профилируй!
48. Костяк решения v0.3
• HTTP-сервер Mochiweb получает запрос, создаёт поток
• Поток отправляет запрос в gen_server
• gen_server производит предвыборку новостей и
присылает результат
• Поток производит выборку, сортирует данные,
форматирует iolist()'ы, выдаёт, умирает.
49. Результат:
рост производительности в 10 (десять) раз
Вывод:
всегда думай, как обрабатывать строки!
профилируй!
50. Костяк решения v0.4
• HTTP-сервер Misultin получает запрос, создаёт поток
• Поток отправляет запрос в gen_server
• gen_server производит предвыборку новостей и
присылает результат
• Поток производит выборку, сортирует данные,
форматирует iolist()'ы, выдаёт, умирает.
52. Результат
• Один человекомесяц
• Быстрое веб-приложение
# ab -qc 7200 -n 450000 http://localhost/block/35237
| grep Requests per sec
Requests per second: 7693.35 [#/sec] (mean)
#
53. Killing feature!
Начиная со второй недели разработки (как
только был написан каркас), приложение
было готово к работе.
• Ни отладки
• Ни непредусмотренного поведения
• Только фичи и профилирование
54. Killing feature!
• Ни отладки
• Ни непредусмотренного поведения
В Erlang есть концепция «Let it crash».
Близкий перевод – «Ну и хрен с ним».
55. Let it crash
• На обычном языке программирования:
res = web_server.start_link(callback = F)
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")
56. Let it crash
• На Erlang
{ok, Pid} = misultin:start_link([{loop, F}]).
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")
57. Let it crash
• На Erlang
{ok, Pid} = misultin:start_link([{loop, F}]).
if res == web_server.port_in_use:
raise Exception("Port in use")
Not ok? {badmatch, {error, eacces}}
raise Exception("Socket error")
elif res == errno.EACCES:
raise Exception("Not enough privileges")
58. Результат
• Один человекомесяц
• Быстрое веб-приложение
• Стабильное веб-приложение
– eunit и «Let it crash»
• Минимум кода
– множество встроенных примитивов и «Let it crash»
• Минимум требуемого опыта
59. Уровень кодера
• С одной стороны, Erlang учится за 2 недели
• С другой стороны, нужно иметь навыки
программирования. Not all batteries included
60. Напутствие
• Предобрабатывай данные, пока это дёшево!
• Не выполняй одни и те же операции дважды!
• Используй «Let it crash» в интерфейсах
• Профилируй!
• Переписывай медленные операции на C,
PHP, OCaml, whatever
61. Блокировки. (опц.)
pthread_rwlock_t rwlock; // 1 • Блокировки нужны
pthread_rwlock_rdlock(&rwlock); многопоточным серверам
(время отклика клиентам
//pthread_rwlock_wrlock(&rwlock); очень отличается от
...do something... запроса к запросу)
pthread_rwlock_unlock(&rwlock); • Возможно вы кроме
функции блокировки
pthread_mutex_t mtx; // 2 вызовете еще и
планировщик (если поток
pthread_mutex_lock(&mtx); будет заблокирован)
...do something...
pthread_mutex_unlock(&mtx);
62. Блокировки. (прод.) (опц.)
volatile int lock = 0; // 3 (0-unlock, 1-lock)
while (__sync_bool_compare_and_swap(&lock, 0, 1))
{usleep(10);}
...do something...
lock = 0;
• Если вы уверенны что блокировки потоков 'ПОЧТИ' не будет —
используйте 3-й тип
• Если обработка запроса ВСЕГДА БЫСТРАЯ — а почему не aio?
63. SearchServer (java) (опц.)
• Поисковые РК: трафика и пользователей нужно
много (чем быстрее отдадим пользователю
поисковый баннер тем лучше).
• Интересных поисковых фраз много (сейчас пару
тысяч).
• Запросов пользователей много – мы примерно
100 пользователей в сек добавляем в различные
поисковые аудитории.
64. SearchServer (java) (прод.)
• Сервер хочется сделать независимым
(манипуляции с ним не должны влиять на
основной движок).
• Перебор Regexp.match() перестал работать уже на
паре сотен поисковых фраз.
• Хочется учесть семантику русского языка и не
заставлять менеджеров вводить все возможные
сочетания слов в фразе (стемминг).
65. SearchServer: схема (опц.)
РК8 РК3
Аудитория1 Аудитория2
РК2 РК5
Дом за КАД Дома … Домашний уют В доме …
После стемминга и lowcast
дом кад дом … домашн уют дом …
После применения hash
{230 5589} {230} … {389 501} {230} …
Обратный индекс … 230 …
Пользователь ввел:
UDP: PK2,РК8,РК3,РК5 230 {230} дом “у дома”
66. Тестирование. Конфиг (опц.)
[root@mega /usr/local/kbe]# cat etc/kbe.conf
##########################################################################
# Counters from different process's come in this queue, and than sended to cache process #
##########################################################################
# Maximum messages in system queue, number
counter_queue_circ_buff_capacity=60000
# Name for system queue. path (string) - only small sibols in root folder
counter_queue=/cache_counter_queue
# Maximum messages in system queue, number
max_msg_in_queue=200
# Maximum messages in internal queue (if it more, they'll send to cache), number
max_internal_queuq_len=5
# Period time when thread send counters to cache, microseconds
time_for_periodic_counters_send=1000000
# Time for limit wait data from system queue, (for reaction on TERM), nanoseconds
time_for_max_queue_wait=110000000
# Time for limit wait data from read process, (for reaction on TERM), nanoseconds
time_for_max_condition_wait=220000000
67. Тестирование. Конфиг (опц.)
# Delay to connect "dead" UUserver (in microseconds)
repeat_time_to_uuserver=1000000
# Thread count in cache pool, number
thread_count_in_pool_cache=10
# Thread count in pool unique user, number
thread_count_in_pool_unique_user_exp=10
# Time out for request for wait cache in cache queue, microseconds
time_out_in_pool_cache_queue=200000
# Time out for request for wait unique user in unique user queue,in microseconds
time_out_in_pool_unique_user_queue=200000
# Number of main fast cgi exposure process
fast_cgi_exp_process_number=5
# Ports for fastcgiexp, string - divided by ',' #
# For main process (check nginx nginx.conf)
fast_cgi_1_exp_ports=:9000,:9200,:9201
fast_cgi_2_exp_ports=:9040,:9300,:9301
fast_cgi_3_exp_ports=:9010,:9400,:9401
fast_cgi_4_exp_ports=:9020,:9500,:9501
fast_cgi_5_exp_ports=:9030,:9600,:9601
# Number of threads processing the request on one socket
fast_cgi_exp_concurency_for_port=5
68. Тестирование. Конфиг (опц.)
# Maximum possible clients connected through Unix socket
max_internal_clients=110
# Maximum possible clients connected through TCP socket
max_external_clients=10
cache_external_port=1030
cache_external_addr=127.0.0.1
cache_internal_port=1031
cache_internal_addr=/tmp/InternalSocketName
###################################################
# Parameters for pool initialised in fastcgidummy #
###################################################
# Number of main fast cgi exposure process
fast_cgi_dummy_process_number=2
# Ports for fastcgilight, string - divided by ',' #
# For main process (check nginx nginx.conf)
fast_cgi_1_dummy_ports=:9010,:9700
fast_cgi_2_dummy_ports=:9020,:9800
# Number of threads processing the request on one socket
fast_cgi_dummy_concurency_for_port=10