Добрый день, Хабр!
Меня зовут Олег, я являюсь Backend-разработчиком в IT-компании «Философт» последние полтора года. Мы занимаемся разработкой платформы для жителей, подключённых к нашей системе, которая призвана помочь взаимодействовать с различными «умными» устройствами, коммуницировать с управляющей компанией, оплачивать счета ЖКХ и т.п.
Изначально разработка велась силами подрядчиков, оставивших после себя крайне посредственного качества монолит, который мы с коллегами приводим в приличный вид, постепенно распиливая на модули, а также занимаемся внедрением новых возможностей и интеграций. Одной из областей взаимодействия с пользователями является отображение показаний приборов учёта (электричество, водоснабжение и т.п.) Не так давно эта область кода была подвергнута достаточно масштабному рефакторингу и переработке, в рамках чего и состоялось наше знакомство с БД Clickhouse.
К сожалению, всё прошло далеко не так гладко, как могло бы быть; шишки были набиты, а ценные уроки вынесены. Именно этой историей и хотелось бы поделиться. Многим полученные нами уроки покажутся совершенно тривиальными; таковыми они и являются, но нам они помогли в какой-то мере переосмыслить процесс разработки. Возможно, помогут и кому-то другому.
Преамбула
База MariaDB – таблица почасовых показаний приборов учёта за несколько лет размером в 1,5 миллиардов записей и 150 ГБ на накопителе, что как бы достаточно много. В таблице хранятся как автоматически переданные данные, так и внесённые жителями вручную. В связи с этим назрела идея подобрать более оптимальное хранилище без ущерба для производительности.
Изначальные требования выглядели так:
пользователи должны иметь возможность просмотреть статистику потребления по часам, дням, месяцам и годам за выбранный период
управляющая компания должна иметь возможность получать последние на выбранный момент времени показания счётчиков
Начало пути
Основной идеей стало использование другой БД, которая обеспечила бы более компактное сжатие. Так как у команды имелась некая экспертиза в области Postgres, первым вариантом было использовать именно её. Выгрузили порядка 100 миллионов записей, загрузили, прикинули объём на полном датасете, прикинули потенциальный рост, нашли данный вариант недостаточным.
Вторым шагом в нашем поиске стал отказ от более привычных БД. Немного поизучав альтернативы, решили было попробовать колоночную Clickhouse. Аналогичные шаги (выгрузили, закинули, оценили) показали, что даже с алгоритмами сжатия мы по умолчанию получаем выгоду примерно в 10 раз относительно объёма в MariaDB, тем более что запас по дальнейшему улучшению был весьма неплохим. Опять же, в пользу этого решения сыграл знакомый синтаксис запросов, так что необходимые данные удавалось получить не только быстро, но и легко.
В результате вся работа с приборами учёта из монолита переехала в отдельный микросервис; была написана необходимая обвязка, и всё вроде было хорошо. Но нет.
Прилет птички
Релиз данного функционала приближается, код переезжает на stage-сервер для окончательного тестирования, в базу заливаются данные со всех счётчиков («приключение» часов на 30-40), размер растёт в рамках ожидаемого. Всё с виду неплохо. Казалось бы, что может пойти не так?
Первый же тест показывает, что не так пошло практически всё – буквально все запросы к базе банально валятся по тайм-ауту. Выдача показаний по одному счётчику за нужное время занимает больше 30 секунд. Соответственно, результата работы каких-то более сложных агрегаций можно было и не ждать. То, что идеально работало на датасете в 100 миллионов записей, наглухо дохло, когда речь шла о миллиарде. Оптимизировать запросы дальше уже некуда. Время искать альтернативы.
Дубли
Напомню: данный функционал и данные не трогала новая команда, и всё это было, по сути, наследством, доставшимся нам от предков. Полезли изучать данные. Оказалось, что в таблице не то что имеются дублирующиеся записи, а их количество составляет чуть не две трети от общего числа. Прореживать старую таблицу и удалять из неё дубли, учитывая объёмы, было занятием неблагодарным; поэтому в данном случае мы сказали большое спасибо разработчикам Clickhouse за движок ReplacingMergeTree.
Для незнакомых с ним – краткая справка: данный движок автоматически удаляет дубликаты записей по ключу сортировки (не путать с PrimaryKey!). Производится, правда, это не при вставке, а впоследствии (приблизительно раз в 4 часа).
Проблема дублей решена, размер ещё меньше; вроде бы, наступило счастье? Нет. Не помогло ни на грош. Единственное, что отсюда вынесено: не стоит верить предшественникам, свои данные следует знать лучше.
Партиции
И вот тут мы долгими окольными путями подошли к ключевому моменту, а именно – грамотному определению партиций. За просвещение хочется сказать отдельное спасибо профильному Telegram-каналу: за отзывчивость коллег, проконсультировавших и подсказавших наилучшее решение.
Итак, в чём же суть: фактически мы полностью пересоздали структуру всех таблиц для оптимального для нас разбиения данных на отдельные партиции (PARTITION BY toYYYYMM(created)). В итоге большинство выборок затрагивали данные только в одной партиции, что значительно ускорило выполнение запросов.
Как результат, мы не только оптимизировали хранение, но при этом ещё и получили немало ценной экспертизы, сделав очевидный вывод: перед выбором инструмента полезно по максимуму изучить инструкцию к нему.
We need to go deeper
На данный момент мы уже были довольны результатом, но появилась мысль: а нельзя ли сделать всё ещё быстрее? В поисках дополнительных оптимизаций было проведено несколько совещаний со стейкхолдерами, в рамках которых уточнены бизнес-требования к этой фиче.
Как результат, нам потребовалось работать не с полными историческими данными, а с достаточно скромным срезом протяжённостью в последние пару месяцев. В таком случае нам на помощь пришли materialized view, в которые собирались записи за необходимый промежуток, а устаревшие записи автоматически удалялись благодаря встроенному механизму TTL спустя указанный срок. Можно и не говорить о том, какой скачок в производительности мы получили, перейдя на работу с materialized view.
Освоив данный инструмент, мы также добавили несколько дополнительных представлений, в которые агрегировались данные по неделям, месяцам, годам и т.п. Тут нам опять же помог движок таблиц ReplacingMergeTree: свежие данные попросту затирают устаревшие, автоматически поддерживая актуальность.
Какой же из всего этого нам удалось вынести урок? Всегда надо уточнять реальные необходимости и требования.
Эпилог
В совокупности данная история тянулась несколько месяцев, отбирая время и силы команды. При этом большинства подводных камней можно было бы избежать, заранее уточнив требования и более тщательно изучив данные и механизм работы Clickhouse. Да, время на это было потрачено; но благодаря пройденному пути – пусть в процессе и было набито немало шишек – удалось вынести немало бесценного опыта и определить новые точки роста команды. Нам же остаётся надеяться, что данный материал поможет кому-то не наступить на те же грабли, по которым прошлись мы.
Комментарии (17)
Akina
28.11.2023 06:44краткая справка: данный движок автоматически удаляет дубликаты записей по ключу сортировки
Вот тут имеются вопросы. Понятно, что в исходных данных, которые переносятся, поле версии отсутствует как класс... но что использовалось как поле сортировки? или исходные данные содержали в том числе и штамп времени создания записи (или штамп времени создания и штамп времени, на который передаются показания, у вас одно и то же)? но если так - то есть ли гарантия, что последние значения - верные? Потому как вы указываете:
В таблице хранятся как автоматически переданные данные, так и внесённые жителями вручную.
что не исключает сценария типа "сегодня снимаем показания автоматом, а завтра абонент вручную вводит показания, снятые им два дня назад"... или вообще случая, когда, зная о грядущем повышении тарифа, потребитель сознательно завышает показания, чтобы оплатить авансом побольше по текущему тарифу - в этом случае автоматический съём, который получит значения меньше предыдущих, вообще неясно как себя должен повести.
Мне лично вообще категорически не понравился подход, когда часть исходного сырого датасета (пусть и имеющего дубликаты и иные погрешности) удаляется безвозвратно, и уж тем более когда это выполняется автоматически.
filo-soft Автор
28.11.2023 06:44Исходные данные содержат как время внесения показаний, так и флаг, вручную они были добавлены или автоматически. Соответственно, при выборке данных для отчета это также учитывается (если ручные показания были внесены Х дней назад, приоритетны они, иначе - автоматические). По поводу безвозвратного удаления — в крайнем случае можно получить эталонные показания, но это весьма трудозатратно.
iwram
28.11.2023 06:44+2Не написали самого важного, про вставку данных. Думаю автор знает, что clickhouse не умеет транзакций, а гарантию доставки хочется (вдруг какие то данные не придут).
savostin
28.11.2023 06:44Clickhouse много чего делает «не так» (как мы привыкли в mysql). И многое он делает «сам» периодически и тихо. Короче нельзя просто так взять и скопировать таблицу из MySQL ;)
titan_pc
28.11.2023 06:44Вроде у них там уже есть транзакции на уровне экспериментальной фичи.
Но а ещё есть чисто философская мысль, если я вставляю в КХ через кафку и в кафке включаю транзакции - это годная вещь? Или надо больше транзакционности?
ilnuribat
28.11.2023 06:44Replicated replacing merge tree умеет в дедубликацию при вставке, видит последние n инсертов (вроде по умолчанию 50) и в случае если один и тот же батч данных прилетело, то второй раз просто будет игнорировать
MentalSky
28.11.2023 06:44для PostgreSQL есть extension:
https://github.com/citusdata/citus
тут и колоночное хранение и распределенные таблицы
ну и справедливости ради - был опыт работы c БД Percona (форк MySQL) размером 1Tb, правда - хорошо спроектированной - было все ок.akakoychenko
28.11.2023 06:44Спасибо, не надо)
Citus, увы, так prod-ready решением и не стал. В одном проекте имели неосторожность, как раз под IoT, взять его. И, когда начали возникать необъснимые проблемы, и вышло связаться с разрабами, те так и сказали, что "простите, но мы не можем гарантировать, что не будет случайных потерь данных при вашем сценарии использования".
Fafhrd
28.11.2023 06:44А что за сценарий использования был, что они так ответили?
Наличие Azure Cosmos DB for PostgreSQL как-то не похоже на "prod-ready решением и не стал"
MentalSky
28.11.2023 06:44простите, но мы не можем гарантировать, что не будет случайных потерь данных при вашем сценарии использования
вау, жестко как, впрочем реальность такова, что любой продукт приходится проверять, подойдет ли он под реальную с запасом нагрузку
freeExec
28.11.2023 06:44Что-то я не понял, раньше была полная история показаний, а потом эффективные менеджеры оптимизировали и вы стали данные через пол года нафиг того, на мусорку.
x67
28.11.2023 06:44Учитывая, что вы явно с превеликим удовольствием открыли для себя партиции, знали ли вы о индексах в mariadb?
И зачем экономить место? Неужто ваше время, потраченное на это все стоит дешевле?
И кажется вы не очень хорошо знакомы с разными бд, от чего сделали совсем не оптимальный выбор. По вашим же словам и вашим проблемам, вам нужна именно OLTP бд, к которым относится постгрес, например. А потом накинуть туда нормальную модель данных. И ничто тогда не остановит этот паровоз. А статистику считать в ваших объемах - пустяк, постгрес прожует легко.
А вот клик мучать запросом показаний конкретного юзера - такое себе)
Vladimirsencov
28.11.2023 06:44Да он и будет работать не очень хорошо с таким запросом. Там структура неподходящая. Для такого запроса как раз нужна обычная реляционная база. Вот группировки клик хорошо будет делать. Я вот не пойму почему просто две базы рядом не поставить?
Tzimie
Хм. 150гб? Это очень мало чтобы создавать проблемы. Для MSSQL это игрушечный объем.
savostin
Гоняю 2Тб в MariaDB на среднем дедике за 80 евро тоже без вопросов. Имхо и правда дело в данных. Если их так же почистить и подправить структуру, то и переходить не надо. Хотя согласен, Clickhouse мощная вещь.