Юрий Буянов | (Одноклассники)Нюансы разработки мобильного мессенджера
Александр Шарак, "Одноклассники"
description
Transcript of Александр Шарак, "Одноклассники"
Подсчет уникальных комбинаций на примере
статистики групп в Одноклассниках
Александр Шарак
Руководитель
отдела статистики
Одноклассников
Задача
• Необходимо посчитать «уники» для отображения на графиках в разделе «Статистика» для администраторов групп
Задача • Надо посчитать, сколько у нас уникальных комбинаций такого
типа: • группа, пол, возраст
• группа, тип активности
• Каждый тип комбинаций надо посчитать за разные периоды: • час
• 6 часов
• сутки
• текущие 24 часа
• последние 7 дней
• календарный месяц
• последние 365 дней
Типичные ошибки подсчетов • Данные не должны дублироваться:
• меняется возраст
• меняется место жительства
• меняется пол
• Ну и конечно нельзя просто суммировать данные по дням, чтобы получить данные за неделю.
Выбор платформы • Статистика должна быть точной.
• Подсчёты «задним числом» другим алгоритмом.
• Подсчёты должны быть быстрыми, не обязательно в realtime: • Статистика за текущие 24 часа должна быть готова раз в час (для начала).
• Статистика за закрытые периоды, например 1 астрономический час, должна быть готова за 15 минут.
• Статистика за последние 365 дней должна быть готова «утром».
• Должна быть возможность запросить новые данные по всем группам или все данные за любую дату по отдельно взятой группе.
• Подсчёты должны быть экономными.
• Разработка системы должна быть быстрой.
• Данные должны быть доступны для внутреннего анализа.
Наш выбор
• Учитывая: • требования
• доступные человеческие ресурсы и их компетенцию
• Выбрали: • MS SQL 2012 Always On availability group
• Некоторые сомнения у нас были, особенно про подсчет статистики за последние 365 дней.
В SQL всё просто! • Select groupid, country, count(distinct userid)
from table where timestamp between @datefrom and @dateto group by groupid, country
Однако объёмы не маленькие
• Более 400 000 активных групп в день.
• Более 1 000 000 активных групп в год.
• Более 800 000 000 действий в день.
• Для подсчета «уников» за последние 365 дней «тупым подходом» необходимо обработать более 200 000 000 000 записей каждый день.
Как SQL считает «уники» • Select groupid, country, gender, count(distinct userid)
from table where timestamp between @datefrom and @dateto group by groupid, country, gender
• Алгоритм:
• Считываются данные по кластерному индексу.
• Данные делятся на столько частей, сколько доступно ядер.
• Каждый поток создает хеш-таблицу: (hash key, (groupid, country, gender, user), count).
• Каждый поток из предыдущей хеш-таблицы создает новую: (hash key, (groupid, county, gender), count).
• Хеш-таблицы объединяются и выдается результат.
• Хеш-таблица для первоначального агрегирования может получиться огромной и в памяти не уложится.
Как SQL считает «уники» при не достаточной памяти • Подсчеты делятся на более мелкие части и промежуточные результаты (хеш -таблицы
скидываются в temp db на диск).
• SQL-сервер делает следующие итерации: • считываются данные по кластерному индексу;
• данные делятся на столько частей, сколько доступно ядер;
• каждый поток создает хеш-таблицу;
• если памяти не хватает, то хеш-таблица скидывается на диск и берётся следующая пачка данных;
• потом хеш-таблицы считываются с диска и объединяются;
• если опять не хватает памяти, то опять всё скидывается на диск;
• и так до тех пор, пока не получается результат.
• «Со стороны» это выглядит так: • сначала наблюдаются бешеные процессы чтения и высокая нагрузка на CPU;
• процессы чтения прекращаются, но нагрузка на CPU все еще высокая;
• нагрузка на CPU падает и начинается интенсивная запись на диск;
• эти три шага повторяются много раз.
• В результате: • подсчеты очень медленные;
• IO-система так нагружена, что параллельные процессы «проседают».
Упрощаем задачу для SQL • Необходимо, чтобы селекты укладывались в память
• Самое простое - делим все группы на несколько частей и считаем «уники» для каждой части отдельно.
• Данные надо хранить с кластеризацией по ID групп, что вызовет проблемы при загрузке – сплошные вставки и фрагментация. Исторические данные нельзя было бы удалить.
• Если данные кластеризовать по дате, то для каждой части придется делать full scan за весь период
• Делается много лишних операций, описанных ниже
• Для «уников» за большие периоды надо использовать результаты подсчетов за меньшие периоды, посчитанные ранее, например:
• для подсчета часа использовать минутные «уники»;
• для подсчета месяца использовать суточные «уники».
Доставка логов
• На сервере, куда параллельными потоками пишется много данных, делать серьезные подсчеты невозможно.
• Поставили «буферную» базу для принятия логов и передачи дальше системе статистики
Буфер Система
групп Система
статистики
Выкачка логов из «буфера» • С большой периодичностью в одном потоке выкачиваем данные
из «буфера».
• Обработку строковых типов не делаем, так как это крайне неэффективно.
• Преобразование строк в цифровые значение (нормализация данных) тоже, соответственно, не делаем.
• Получаем только целые числа (ID сущностей) и даты.
COLUMN_NAME DATA_TYPE
Registered smalldatetime
ID_Group bigint
ID_User bigint
ActionType tinyint
VisitType tinyint
MemberType tinyint
Выкачка логов из «буфера» • Сделали кластерный индекс по времени события с партициями
по дням.
• Такой индекс нам позволяет быстро записать и прочитать данные с постепенно возрастающим временем события.
• Время сброса данных растет пропорционально росту активности в группах, но не пропорционально росту размера всей таблицы.
• Однако данные за один квартал мы храним в отдельной базе: • чтобы бэкапы происходили «быстро»;
• чтобы «старые» данные можно было эффективно убрать в архив.
Как передать результаты обратно на сайт • Full dump:
• легко реализовать;
• годится только для небольшого объёма данных.
• По changetime:
• трудно отследить удаления (например, когда в группе всего один пользователь и он стал старше);
• если изменилась одна запись в группе, то надо пометить все связанные;
• постоянный апдейт колонки changetime и деградация индекса.
• Лента изменений:
• в ленту идут только инсерты;
• удаления отслеживать не надо;
• сайт получает полный комплект измененных статистик, которым можно заменить старый комплект без дополнительно обработки.
Топология
• MS SQL Server
• 80 GB RAM
• 2 CPU (6 ядер) (мощные)
• Temp db – SAS-диски
• Данные – массив из SATA-дисков
Буфер Система
групп MS SQL Primary
MS SQL Secondary
(mirror)
Буфер резерв
Big Fail • Сначала всё, как обычно, работало достаточно быстро.
• Но со временем система начала все больше тормозить:
• группы стали популярнее;
• расчеты стали немного сложнее;
• данных за большие периоды накапливалось все больше (особенно для подсчета последних 365 дней);
• подсчеты «уников» за большие периоды так начали нагружать IO, что начали проседать параллельные процессы загрузки данных и подсчеты маленьких периодов.
• Поэтому мы решили, что MS SQL secondary (mirror), который доступен в режиме read only, должен заняться полезным делом:
• Сделали так, чтобы сервис забирал результаты не с primary, а с secondary сервера.
• В результате:
• нагрузка на IO систему сильно снизилась.
Big Fail
• И всё равно надо было предпринимать шаги по масштабированию, так как мы понимали, что долго так не протянем.
О масштабируемости • Масштабируется MS SQL AlwaysOn HA group легко:
• можно добавить в кластер secondary сервера для подсчетов.
• Но это дорого стоит.
• Мы начали думать: можем ли мы посчитать «уники» за большие периоды эффективней, чем MS SQL?
• Тогда мы бы могли в MS SQL оставить только то, что хорошо и быстро работает:
• хранилище для исходных данных;
• хранилище для результатов;
• подсчеты «уников» за маленькие периоды – до часа.
Эффективный алгоритм • Мы придумали быстрый алгоритм, как при помощи Merge Sorta файлов
посчитать «уники», нагружая IO и CPU по максимуму, при этом делая минимум лишних операций и используя минимум памяти.
• Далее рассмотрим частный случай, когда мы из семи дневных результатов высчитываем недельные «уники».
Выгрузка данных в файлы • Из базы данных в память выкачиваем данные за последний закрытый период
(например, день), отсортированные по (groupid, userid).
• Записываем эти данные в 100 файлов, распределяя по groupid mod 100, чтобы все записи одной группы попали только в один файл. Это необходимо для распараллеливания.
• Формат файлов – бинарный, чтобы мы могли четко знать. где именно начинается и заканчивать одна запись.
• Мы пробовали использовать json и csv форматы но сразу от них отказались, потому что на парсинг этих файлов уходила львиная доля мощностей CPU.
• Исходные данные в таком формате:
• За один день – 3 Gb
• За 7 дней – 21 Gb
• За 365 дней – 1.1 Tb
день#1
день#2
день#3
день#4
день#5
день#6
день#7
каждый файл отдельно уже отсортирован по ID_Group, ID_User
(1x вначале) сортируем список открытых файлов по ID_Group, ID_User первой считанной записи (дальше в цикле) отдаем дальше запись с наименьшим ID_Group, ID_User
берем следующую запись из того файла, чья запись «ушла» перемещаем файл в списке файлов на место, соответствующее следующей считанной записи
Отсортированный список всех данных по ID_Group, ID_User
ID_Group ID_User ActionType
G1 U1 7
G1 U1 7
G1 U2 5
G2 U2
G2 U3
Накопленные данные за предыдущую группу можно считать готовыми, когда начинаются данные про следующую группу
данные про поведение конкретного пользователя в рамках конкретной групы
Распараллеливаем • Данный алгоритм работает только в одном потоке, то есть, на одном ядре.
• Поэтому мы и создавали не 7 файлов, а 100 комплектов по 7 файлов, распределяя по groupid mod 100.
• И запустили подсчеты в столько потоков, сколько было ядер на сервере – 24.
• Каждый поток обрабатывал отдельный комплект файлов.
• После завершения подсчетов все результаты записываем (bulk insert) в MS SQL для передачи далее в «прод».
• А выгруженные файлы не удаляем, а оставляем для подсчетов следующих периодов, но не дольше 365 дней.
• «Уники» за 7 дней высчитываем за 3 минуты.
Топология
• Два .NET сервера для «уников» - для HA. Оба делают одно и то же.
Буфер Система
групп MS SQL Primary
MS SQL Secondary
(mirror)
Буфер резерв .NET
«уники» .NET
«уники»
Нагрузка на CPU
• CPU не нагружен на 100% потому, что есть дисковая очередь.
Ускоряем ещё больше • Днем, когда сервер отдыхает, можем высчитывать агрегат за 364 дня. Потом
после полуночи надо будет сделать Merge Sort всего двум файлам. Ускорим в 5 раз.
• Для подсчетов «уников» за последние 364 дня можно использовать комбинацию дневных и месячных агрегатов. Ускорим на 25%.
• Для подсчетов можем использовать оба .NET сервера (первый сначала подсчитывает чётные группы, второй – нечётные). Ускорим в два раза.
Tips and Tricks • Для подсчета «уников» за 365 дней нам надо открыть много файлов – 365x24
(число ядер). Необходимо под каждый FileHandle выделять такой размер буфера, чтобы он не превышал некую MagicConstant (~1Gb). Иначе Windows Server начинает делать swap памяти.
• Файлы, в которых храним числовую (IDs) информацию в бинарном виде, не стоит пытаться зиповать.
• Если шедулируете подсчеты, используя Windows Task Scheduler, то по умолчанию у тасков стоит низкий приоритет на ресурсы. Через UI его повысить нельзя – надо экспортировать XML дефиницию таска, повысить в нем соответствующую настройку и импортировать обратно.
Итоги • Мы вынесли подсчеты из MS SQL и используем его только как хранилище. К тому
же используем не оптимально, так как нам надо, чтобы данные можно было бы хранить с кластеризацией по (groupid, userid).
• То есть использование MS SQL не обосновано. Вместо него мы будем использовать или Open Source базы, или переделаем всё на обработку файлов в .NET.
• .NET и Windows Server 2008 R2 показал себя как хорошая платформа для обработки данных и файлов
• быстрая и стабильная
• с хорошим файл-кешем
• удобный язык программирования для аппликации параллельной обработки информации.
Итоги • «Уники» за 365 дней мы можем посчитать за 4 часа на одном сервере, а
потенциально можем посчитать менее чем за час.
• Это в десятки раз быстрее, чем может MS SQL.
• Подсчеты за меньшие периоды занимают несущественное время.
• Количество серверов, необходимых для подсчетов – 2 (+2 для HA).
• Количество трудозатрат – 3 человеко-месяца.
• А если делать сразу всё правильно – 1 человеко-месяц.