как написать масштабируемую баннерокрутилку. денис...
description
Transcript of как написать масштабируемую баннерокрутилку. денис...
Как написать масшабируемую баннерокрутилку
Бирюков Денис, компания Каванга
Задачи перед сервисом
• Производительность– сейчас: 1000 баннеров/сек.– хотим: 10 000 баннеров/сек.– время отклика < 200 миллисекунд.
• 365*24*7, обязательно отдать контент (хотя бы заглушку).
• Много площадок (>1.000), много баннеров (>100.000).
Задачи (прод.)
• Таргетинги– Стандартные (UserAgent, geo …).– Стандартные ограничения, скорость, бюджет.– Стандартные уникальные ограничения.– Аудиторные таргетинги (ретагетинг: бумеранг,
поисковый, соцдем).
• Точный подсчёт показов, кликов и событий, начислений и списаний денег.
Резюмируем свойства
• Горизонтально масштабируемый (до 10000 хитов/сек). Перекрутов, в идеале, быть не должно.
• Многопоточный (медленные запросы не должны держать быстрые). Минимизировать блокировки.
• Синхронизация серверов в режиме “реального времени” (уменьшаем перекруты).
Схема сервиса в целомПлощадка Клиент Пользователь Пользователь
MySQL
hadoopPHP nginx
uuserver
front front
Se
arch
Se
rve r
sphinx uuserver
hadoop
соцдем
UUServer
• Кука всего 4 KB – мало. • Очень близко к хранилищу key/value.• Выдача данных по TCP (свой протокол).• Прием данных по UDP (свой протокол).• Масштабируемый.
• Многопоточный (много блокировок).
UUServer (прод.)
• Внутри 64 независимых дерева - боремся с локами и балансировками дерева при вставке.
• Раз в 5 мин запускается цикл сохранения пользователей на диск.
• Классический ретаргетинг организован плагинами на стороне uuserver.
• Соединения ‘постоянные’.
Схема uuserver
Выбор mapПоиск user
Increment, Update;
UPD пакет (событие)
UPD пакет (событие)
Выбираем очередь
Выбираем очередь
Выбор mapПоиск user
foreach(libs){Retargeting;}
Выбор mapПоиск user
TCP запрос
Анализ
TCP ответMap.insert(…)
Схема frontnginx
fastcgiexp
fastcgiexp
fastcgievent
fastcgievent fastcgidummy
fastcgidummyUUserver
UUserver
SearchServer
receiversender
receiver sender
Syslog
MySQL
Gearman
hadoop
CounterQueue
cache
Cache• Хранит много объектов, если примитивно, то
std::map(u_int64_t, std::vector<...sort...> *). Значение в каждой записи — это табличка из СУБД.
• Объекты — берутся из базы данных (она отвечает за сортировку и за целостность данных).
• Данные в некоторых 'табличках' меняются редко (площадки, рекламные кампании, цены), или очень редко (гео база) — изменения всегда приходят из СУБД.
Cache (прод.)
• Есть 'таблички', данные в которых меняются часто ('счетчики') — изменения приходят как из СУБД так и от CounterQueue.
• Многопоточный (на каждое соединение — свой поток).
• Соединения 'постоянные'.• Блокировки (чтение/запись) накладываются на
всю таблицу.
Cache: логика обновления
TCP запрос Данные из СУБД CacheInOut { u_int32_t size; u_int32_t func; char data[size];}
Анализ ипостроение ‘таблички’
WaitToWrite()
Swap(std::vector<...>*)
Done()Delete old data
Данные из CacheTCP ответ
Cache: выбор баннеров
TCP запрос
TCP ответ
WaitToRead(),…
Done(),…
Find place, geo
Сортировка РК
foreach(PK){Targets;}
result
Cache: инкрементация (2экз)
TCP запрос
TCP ответ
WaitToWrite()
Done()
foreach(…){ Increment; syslog(...);}
Есть 2 экземпляраСчетчиков (Readers-1)Increment(…)
Call Increment(2)
Swap(1,2)
Call Increment(2)
FastCgiExp• Сервер – и диспетчер и обработчик.• Есть пулы нитей (nginx (fastcgi), cache, uuserver).• В каждом процессе хранятся тела баннеров,
заглушек и ссылок (доступ к ним производиться через read/write блокировку).
• Настройка количества listen сокетов, размеры пулов, количество процессов производится редактированием файла конфигурации, с последующим перезапуском.
FastCgiExp: запросFastCgi запрос
FastCgi ответ
Выбрать Нить
CheckReferer
Выделить Нить
Get BannerId
Dummy
PoolThread FastCgi
PoolThread UUServer
PoolThread Cache
BannerUDP to UUserver
mq_send(…)mq_send(…)
UDP to SearchServer
Выделить Нить
Get User Info
FastCgiEvent: запрос
FastCgi запрос
FastCgi ответ
Выбрать Нить
Выделить Нить
Get User Info
Get Location
204
PoolThread FastCgi
PoolThread UUServer
UDP to UUserver
mq_send(…)mq_send(…)
FastCgiDummy: запрос
FastCgi запрос
FastCgi ответ
Выделить Нить
Dummy
PoolThread FastCgi
UDP to SearchServer
Что дальше?
• Постоянные изменения логики движка (поддержка).
• Написать антинакрутчик (aio).• Переписать UUserver (aio) ?• Мониторинг серверов (zabbix).
Что дальше? (прод.)
• Новые интерфейсы рекламодателям и владельцам площадок (и поддержка и юзабилити).
• Соцдем.
• Новые отчеты (hadoop).
• Мониторинг серверов (zabbix).
Оптимизация трафика
Трафика много, денег мало?
Возможно мало уников
Возможно сейчас у нас нет рекламы для вас (новые форматы)
Возможно вы хотите много денег, или другие таргетинги.
Опрашивайте рекламные движки каскадом
Делитесь информацией о своих юзерах
Тестирование. Машина.Процессор Intel(R) Xeon(R) CPU E5630 @ 2.53GHz (2527.30-MHz K8-
class CPU), 2x4 core
Память 96 GB
nload Incoming:
Curr: 13.65 MBit/s
Avg: 3.93 MBit/s
Min: 1.02 kBit/s
Max: 16.60 MBit/s
Ttl: 402.25 MByte
Outgoing:
Curr: 57.67 MBit/s
Avg: 16.72 MBit/s
Min: 5.52 kBit/s
Max: 66.14 MBit/s
Ttl: 783.21 MByte
netstat 27 LISTEN 625 SYN_SENT
42 CLOSE_WAIT 971 LAST_ACK
42 FIN_WAIT_2 1928 ESTABLISHED
73 CLOSED 23593 TIME_WAIT
Тестирование. Бенчмарк.
3700
3750
3800
3850
3900
3950
4000
4050
кол-во запросов
в сек
одновременные соединения / кол-во
запросов всего
100/10000
100/50000
1000/10000
1000/50000
0
50
100
150
200
250
300
350
400
450
500
50%
66%
75%
80%
90%
95%
98%
99%
100%
доля запросов
микросек
100/10000
100/50000
10000/10000
10000/50000
denis:/home/bdn# /usr/sbin/ab -r -c100 -n10000 -b2048 -C kui1v=777 "http://10.5.1.50/exp?sid=5&bt=5&bn=1&bc=3&ct=2"
Тестирование. TOP.[root@mega ~]# toplast pid: 10965; load averages: 13.32, 6.52, 3.64 up 1+23:47:28 16:18:52646 processes: 8 running, 638 sleepingCPU: 55.2% user, 0.0% nice, 14.3% system, 7.2% interrupt, 23.3% idleMem: 18G Active, 1610M Inact, 11G Wired, 136K Cache, 9833M Buf, 63G FreeSwap: 4096M Total, 4096M Free
Тестирование. TOP.PID UN THR PRI NICE SIZE RES STATE C TIME WCPU COMMAND2583 kbe 54 47 0 220M 147M CPU7 7 21:25 353.47% cache706 root 1 73 0 6956K 1520K RUN 7 4:29 23.39% syslogd10890 www 1 65 0 50268K 36736K RUN 1 1:01 18.46% nginx10891 www 1 67 0 42076K 30064K RUN 1 0:56 18.16% nginx2615 kbe 49 44 0 76784K 22176K nanslp 0 1:42 11.52% fcgiexp2616 kbe 49 44 0 78832K 22268K nanslp 1 1:43 11.43% fcgiexp2617 кbe 49 44 0 82672K 20876K nanslp 0 1:40 11.33% fcgiexp2613 kbe 49 44 0 78704K 21924K nanslp 0 1:43 11.23% fcgiexp2614 kbe 49 44 0 80752K 22112K nanslp 1 1:42 11.04% fcgiexp1557 kbe 170 44 0 18291M 18159M sbwait 5 100:46 3.52% uuserver2609 kbe 3 44 0 28944K 4052K nanslp 0 0:06 1.07% sender2605 kbe 3 44 0 47372K 4232K nanslp 7 0:08 0.98% counterqueue2626 kbe 73 44 0 62856K 22464K nanslp 5 0:16 0.00% fcgievent2639 kbe 73 44 0 62856K 22472K nanslp 3 0:16 0.00% fcgievent1013 root 1 44 0 12064K 4152K select 3 0:02 0.00% sendmail10889 root 1 76 0 17500K 6024K pause 2 0:00 0.00% nginx
Масшабируемая баннерокрутилка: как это былона Erlang
Артем Гавриченков, Highload Lab
Задача
• Выдача ссылок на новости– База из тысяч новостей – Ссылки выбираются произвольно– Распределение – неравномерное
• Форматирование ссылок перед выдачей– Ссылки должны иметь формат блока– Дополнительные поля: идентификатор блока, …
Ta-da!
• Требования: >= 5000 запросов в секунду– Не справляемся – должны быстро выдавать
пустую страницу, чтобы не «ломать» вёрстку– Никакого кэширования
• Срок выполнения: полтора месяца.– При этом у разработчика баннерокрутилки
есть и другие задачи
Схема решения• Структуры в памяти, хранящие новости
и статистику показов
• Множество потоков, обрабатывающих HTTP-запросы
• Контроллер– добавляющий и удаляющий новости
– собирающий статистику
– на основе TCP-сокетов
Схема решенияСтруктуры в памяти, хранящие новостии статистику показов
Множество потоков, обрабатывающих запросы
Контроллердобавляющий и удаляющий новости
собирающий статистику
на основе TCP-сокетов
Erlang• DSL для многопоточных приложений• Встроенные в язык примитивы для посылки и
приёма сообщений• Встроенные в язык шаблоны поведения• Встроенная в язык in-memory БД
Схема решения• Структуры в памяти, хранящие новости
и статистику показов: mnesia/ets/dict!
• Множество потоков, обрабатывающих HTTP-запросы:gen_server!
• Контроллер: gen_tcp!добавляющий и удаляющий новости
собирающий статистику
на основе TCP-сокетов
Схема решения• Структуры в памяти, хранящие новости
и статистику показов: mnesia/ets/dict!
• Множество потоков, обрабатывающих HTTP-запросы:gen_server!– который уже написан за нас! mochiweb/misultin
• Контроллер: gen_tcp!добавляющий и удаляющий новости
собирающий статистику
на основе TCP-сокетовBatteries included
Batteries included, though not all
Костяк решения
• HTTP-сервер Mochiweb получает запрос, создаёт поток
• Поток забирает из Mnesia данные
• Поток производит выборку, сортирует данные, форматирует строки, выдаёт, умирает.Все счастливы.
«In theory, there's no difference between
theory 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 элементов– Built-in функциям I/O всё равно, что выводить
Кроме того:
• Строковые операции, наподобие обработки регулярных выражений, всё равно дорогие
• Впрочем, они вообще не очень дешевы.
Надо делать предобработку• Кэширование конструируемых URL новостей
и т. п. позволило отыграть 15%
Результат:рост производительности в 10 (десять) раз
Вывод:– всегда думай, как обрабатывать строки!– профилируй!
Костяк решения v0.3
• HTTP-сервер Mochiweb получает запрос, создаёт поток
• Поток отправляет запрос в gen_server
• gen_server производит предвыборку новостей и присылает результат
• Поток производит выборку, сортирует данные, форматирует iolist()'ы, выдаёт, умирает.
Результат:рост производительности в 10 (десять) раз
Вывод:всегда думай, как обрабатывать строки!профилируй!
Костяк решения 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 = 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")
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")
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")
Результат
• Один человекомесяц
• Быстрое веб-приложение
• Стабильное веб-приложение– eunit и «Let it crash»
• Минимум кода– множество встроенных примитивов и «Let it crash»
• Минимум требуемого опыта
Уровень кодера
• С одной стороны, Erlang учится за 2 недели• С другой стороны, нужно иметь навыки
программирования. Not all batteries included
Напутствие
• Предобрабатывай данные, пока это дёшево!• Не выполняй одни и те же операции дважды!• Используй «Let it crash» в интерфейсах• Профилируй!• Переписывай медленные операции на C,
PHP, OCaml, whatever
Блокировки. (опц.)
pthread_rwlock_t rwlock; // 1pthread_rwlock_rdlock(&rwlock);//pthread_rwlock_wrlock(&rwlock);...do something...pthread_rwlock_unlock(&rwlock);
pthread_mutex_t mtx; // 2pthread_mutex_lock(&mtx);...do something...pthread_mutex_unlock(&mtx);
• Блокировки нужны многопоточным серверам (время отклика клиентам очень отличается от запроса к запросу)
• Возможно вы кроме функции блокировки вызовете еще и планировщик (если поток будет заблокирован)
Блокировки. (прод.) (опц.)
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?
SearchServer (java) (опц.)• Поисковые РК: трафика и пользователей нужно
много (чем быстрее отдадим пользователю поисковый баннер тем лучше).
• Интересных поисковых фраз много (сейчас пару тысяч).
• Запросов пользователей много – мы примерно 100 пользователей в сек добавляем в различные поисковые аудитории.
SearchServer (java) (прод.)• Сервер хочется сделать независимым
(манипуляции с ним не должны влиять на основной движок).
• Перебор Regexp.match() перестал работать уже на паре сотен поисковых фраз.
• Хочется учесть семантику русского языка и не заставлять менеджеров вводить все возможные сочетания слов в фразе (стемминг).
SearchServer: схема (опц.)
Аудитория1
Дом за КАД Дома
Аудитория2
Домашний уют В доме… …
РК2
РК8 РК3
РК5
дом дом… …домашн уютдом кад
{230} {230}… …{389 501}{230 5589}
230 ……
“у дома”дом{230}230UDP: PK2,РК8,РК3,РК5
После стемминга и lowcast
После применения hash
Обратный индекс Пользователь ввел:
Тестирование. Конфиг (опц.)[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, numbercounter_queue_circ_buff_capacity=60000# Name for system queue. path (string) - only small sibols in root foldercounter_queue=/cache_counter_queue# Maximum messages in system queue, numbermax_msg_in_queue=200# Maximum messages in internal queue (if it more, they'll send to cache), numbermax_internal_queuq_len=5# Period time when thread send counters to cache, microsecondstime_for_periodic_counters_send=1000000# Time for limit wait data from system queue, (for reaction on TERM), nanosecondstime_for_max_queue_wait=110000000# Time for limit wait data from read process, (for reaction on TERM), nanosecondstime_for_max_condition_wait=220000000
Тестирование. Конфиг (опц.)# Delay to connect "dead" UUserver (in microseconds)repeat_time_to_uuserver=1000000# Thread count in cache pool, numberthread_count_in_pool_cache=10# Thread count in pool unique user, numberthread_count_in_pool_unique_user_exp=10# Time out for request for wait cache in cache queue, microsecondstime_out_in_pool_cache_queue=200000# Time out for request for wait unique user in unique user queue,in microsecondstime_out_in_pool_unique_user_queue=200000# Number of main fast cgi exposure processfast_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,:9201fast_cgi_2_exp_ports=:9040,:9300,:9301fast_cgi_3_exp_ports=:9010,:9400,:9401fast_cgi_4_exp_ports=:9020,:9500,:9501fast_cgi_5_exp_ports=:9030,:9600,:9601# Number of threads processing the request on one socketfast_cgi_exp_concurency_for_port=5
Тестирование. Конфиг (опц.)# Maximum possible clients connected through Unix socketmax_internal_clients=110# Maximum possible clients connected through TCP socketmax_external_clients=10cache_external_port=1030cache_external_addr=127.0.0.1cache_internal_port=1031cache_internal_addr=/tmp/InternalSocketName
#################################################### Parameters for pool initialised in fastcgidummy ##################################################### Number of main fast cgi exposure processfast_cgi_dummy_process_number=2# Ports for fastcgilight, string - divided by ',' ## For main process (check nginx nginx.conf)fast_cgi_1_dummy_ports=:9010,:9700fast_cgi_2_dummy_ports=:9020,:9800# Number of threads processing the request on one socketfast_cgi_dummy_concurency_for_port=10