3. Задача
• Надо посчитать, сколько у нас уникальных комбинаций такого
типа:
• группа, пол, возраст
• группа, тип активности
• Каждый тип комбинаций надо посчитать за разные периоды:
•
•
•
•
•
•
•
час
6 часов
сутки
текущие 24 часа
последние 7 дней
календарный месяц
последние 365 дней
4. Типичные ошибки подсчетов
• Данные не должны дублироваться:
• меняется возраст
• меняется место жительства
• меняется пол
• Ну и конечно нельзя просто суммировать данные по дням, чтобы
получить данные за неделю.
5. Выбор платформы
• Статистика должна быть точной.
• Подсчёты «задним числом» другим алгоритмом.
• Подсчёты должны быть быстрыми, не обязательно в realtime:
• Статистика за текущие 24 часа должна быть готова раз в час (для начала).
• Статистика за закрытые периоды, например 1 астрономический час, должна быть
готова за 15 минут.
• Статистика за последние 365 дней должна быть готова «утром».
• Должна быть возможность запросить новые данные по всем группам
или все данные за любую дату по отдельно взятой группе.
• Подсчёты должны быть экономными.
• Разработка системы должна быть быстрой.
• Данные должны быть доступны для внутреннего анализа.
6. Наш выбор
• Учитывая:
• требования
• доступные человеческие ресурсы и их компетенцию
• Выбрали:
• MS SQL 2012 Always On availability group
• Некоторые сомнения у нас были, особенно про подсчет
статистики за последние 365 дней.
7. В SQL всё просто!
• Select groupid, country, count(distinct userid)
from table
where timestamp between @datefrom and @dateto
group by groupid, country
8. Однако объёмы не маленькие
• Более 400 000 активных групп в день.
• Более 1 000 000 активных групп в год.
• Более 800 000 000 действий в день.
• Для подсчета «уников» за последние 365 дней «тупым
подходом» необходимо обработать более 200 000 000 000
записей каждый день.
9. Как 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).
Хеш-таблицы объединяются и выдается результат.
•
•
• Хеш-таблица для первоначального агрегирования может получиться огромной и
в памяти не уложится.
10. Как SQL считает «уники» при не
достаточной памяти
• Подсчеты делятся на более мелкие части и промежуточные результаты (хеш -таблицы
скидываются в temp db на диск).
• SQL-сервер делает следующие итерации:
•
•
•
•
•
•
•
считываются данные по кластерному индексу;
данные делятся на столько частей, сколько доступно ядер;
каждый поток создает хеш-таблицу;
если памяти не хватает, то хеш-таблица скидывается на диск и берётся следующая пачка данных;
потом хеш-таблицы считываются с диска и объединяются;
если опять не хватает памяти, то опять всё скидывается на диск;
и так до тех пор, пока не получается результат.
• «Со стороны» это выглядит так:
•
•
•
•
сначала наблюдаются бешеные процессы чтения и высокая нагрузка на CPU;
процессы чтения прекращаются, но нагрузка на CPU все еще высокая;
нагрузка на CPU падает и начинается интенсивная запись на диск;
эти три шага повторяются много раз.
• В результате:
•
•
подсчеты очень медленные;
IO-система так нагружена, что параллельные процессы «проседают».
11. Упрощаем задачу для SQL
• Необходимо, чтобы селекты укладывались в память
• Самое простое - делим все группы на несколько частей и считаем «уники» для
каждой части отдельно.
•
•
Данные надо хранить с кластеризацией по ID групп, что вызовет проблемы при загрузке – сплошные
вставки и фрагментация. Исторические данные нельзя было бы удалить.
Если данные кластеризовать по дате, то для каждой части придется делать full scan за весь период
•
Делается много лишних операций, описанных ниже
• Для «уников» за большие периоды надо использовать результаты подсчетов за
меньшие периоды, посчитанные ранее, например:
•
•
для подсчета часа использовать минутные «уники»;
для подсчета месяца использовать суточные «уники».
12. Доставка логов
• На сервере, куда параллельными потоками пишется много
данных, делать серьезные подсчеты невозможно.
• Поставили «буферную» базу для принятия логов и передачи
дальше системе статистики
Система
групп
Буфер
Система
статистики
13. Выкачка логов из «буфера»
• С большой периодичностью в одном потоке выкачиваем данные
из «буфера».
• Обработку строковых типов не делаем, так как это крайне
неэффективно.
• Преобразование строк в цифровые значение (нормализация
данных) тоже, соответственно, не делаем.
• Получаем только целые числа (ID сущностей) и даты.
COLUMN_NAME
DATA_TYPE
Registered
smalldatetime
ID_Group
bigint
ID_User
bigint
ActionType
tinyint
VisitType
tinyint
MemberType
tinyint
14. Выкачка логов из «буфера»
• Сделали кластерный индекс по времени события с партициями
по дням.
• Такой индекс нам позволяет быстро записать и прочитать
данные с постепенно возрастающим временем события.
• Время сброса данных растет пропорционально росту активности
в группах, но не пропорционально росту размера всей таблицы.
• Однако данные за один квартал мы храним в отдельной базе:
• чтобы бэкапы происходили «быстро»;
• чтобы «старые» данные можно было эффективно убрать в архив.
15. Как передать результаты обратно
на сайт
• Full dump:
•
легко реализовать;
•
годится только для небольшого объёма данных.
• По changetime:
•
•
•
трудно отследить удаления (например, когда в группе всего один пользователь и он стал старше);
если изменилась одна запись в группе, то надо пометить все связанные;
постоянный апдейт колонки changetime и деградация индекса.
• Лента изменений:
•
•
•
в ленту идут только инсерты;
удаления отслеживать не надо;
сайт получает полный комплект измененных статистик, которым можно заменить старый комплект без
дополнительно обработки.
17. Big Fail
• Сначала всё, как обычно, работало достаточно быстро.
• Но со временем система начала все больше тормозить:
•
•
•
•
группы стали популярнее;
расчеты стали немного сложнее;
данных за большие периоды накапливалось все больше (особенно для подсчета последних 365 дней);
подсчеты «уников» за большие периоды так начали нагружать IO, что начали проседать
параллельные процессы загрузки данных и подсчеты маленьких периодов.
• Поэтому мы решили, что MS SQL secondary (mirror), который доступен в режиме
read only, должен заняться полезным делом:
•
Сделали так, чтобы сервис забирал результаты не с primary, а с secondary сервера.
• В результате:
•
нагрузка на IO систему сильно снизилась.
18. Big Fail
•
И всё равно надо было предпринимать шаги по масштабированию, так как мы понимали, что долго так не
протянем.
19. О масштабируемости
• Масштабируется MS SQL AlwaysOn HA group легко:
•
можно добавить в кластер secondary сервера для подсчетов.
• Но это дорого стоит.
• Мы начали думать: можем ли мы посчитать «уники» за большие периоды
эффективней, чем MS SQL?
• Тогда мы бы могли в MS SQL оставить только то, что хорошо и быстро работает:
•
•
•
хранилище для исходных данных;
хранилище для результатов;
подсчеты «уников» за маленькие периоды – до часа.
20. Эффективный алгоритм
• Мы придумали быстрый алгоритм, как при помощи Merge Sorta файлов
посчитать «уники», нагружая IO и CPU по максимуму, при этом делая минимум
лишних операций и используя минимум памяти.
• Далее рассмотрим частный случай, когда мы из семи дневных результатов
высчитываем недельные «уники».
21. Выгрузка данных в файлы
• Из базы данных в память выкачиваем данные за последний закрытый период
(например, день), отсортированные по (groupid, userid).
• Записываем эти данные в 100 файлов, распределяя по groupid mod 100, чтобы
все записи одной группы попали только в один файл. Это необходимо для
распараллеливания.
• Формат файлов – бинарный, чтобы мы могли четко знать. где именно начинается
и заканчивать одна запись.
•
Мы пробовали использовать json и csv форматы но сразу от них отказались, потому что на парсинг
этих файлов уходила львиная доля мощностей CPU.
• Исходные данные в таком формате:
•
•
•
За один день – 3 Gb
За 7 дней – 21 Gb
За 365 дней – 1.1 Tb
22. день
#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
данные про поведение
конкретного пользователя в
рамках конкретной групы
23. Распараллеливаем
• Данный алгоритм работает только в одном потоке, то есть, на одном ядре.
• Поэтому мы и создавали не 7 файлов, а 100 комплектов по 7 файлов,
распределяя по groupid mod 100.
• И запустили подсчеты в столько потоков, сколько было ядер на сервере – 24.
• Каждый поток обрабатывал отдельный комплект файлов.
• После завершения подсчетов все результаты записываем (bulk insert) в MS SQL
для передачи далее в «прод».
• А выгруженные файлы не удаляем, а оставляем для подсчетов следующих
периодов, но не дольше 365 дней.
• «Уники» за 7 дней высчитываем за 3 минуты.
26. Ускоряем ещё больше
• Днем, когда сервер отдыхает, можем высчитывать агрегат за 364 дня. Потом
после полуночи надо будет сделать Merge Sort всего двум файлам. Ускорим в 5
раз.
• Для подсчетов «уников» за последние 364 дня можно использовать комбинацию
дневных и месячных агрегатов. Ускорим на 25%.
• Для подсчетов можем использовать оба .NET сервера (первый сначала
подсчитывает чётные группы, второй – нечётные). Ускорим в два раза.
27. Tips and Tricks
• Для подсчета «уников» за 365 дней нам надо открыть много файлов – 365x24
(число ядер). Необходимо под каждый FileHandle выделять такой размер буфера,
чтобы он не превышал некую MagicConstant (~1Gb). Иначе Windows Server
начинает делать swap памяти.
• Файлы, в которых храним числовую (IDs) информацию в бинарном виде, не стоит
пытаться зиповать.
• Если шедулируете подсчеты, используя Windows Task Scheduler, то по умолчанию
у тасков стоит низкий приоритет на ресурсы. Через UI его повысить нельзя –
надо экспортировать XML дефиницию таска, повысить в нем соответствующую
настройку и импортировать обратно.
28. Итоги
• Мы вынесли подсчеты из MS SQL и используем его только как хранилище. К тому
же используем не оптимально, так как нам надо, чтобы данные можно было бы
хранить с кластеризацией по (groupid, userid).
• То есть использование MS SQL не обосновано. Вместо него мы будем
использовать или Open Source базы, или переделаем всё на обработку файлов в
.NET.
• .NET и Windows Server 2008 R2 показал себя как хорошая платформа для
обработки данных и файлов
•
•
•
быстрая и стабильная
с хорошим файл-кешем
удобный язык программирования для аппликации параллельной обработки информации.
29. Итоги
• «Уники» за 365 дней мы можем посчитать за 4 часа на одном сервере, а
потенциально можем посчитать менее чем за час.
• Это в десятки раз быстрее, чем может MS SQL.
• Подсчеты за меньшие периоды занимают несущественное время.
• Количество серверов, необходимых для подсчетов – 2 (+2 для HA).
• Количество трудозатрат – 3 человеко-месяца.
• А если делать сразу всё правильно – 1 человеко-месяц.