как написать масштабируемую баннерокрутилку. денис...

Post on 14-Dec-2014

2.947 Views

Category:

Documents

1 Downloads

Preview:

Click to see full reader

DESCRIPTION

 

TRANSCRIPT

Как написать масшабируемую баннерокрутилку

Бирюков Денис, компания Каванга

Задачи перед сервисом

• Производительность– сейчас: 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

top related