Добрый день, Хабр! 

Меня зовут Олег, я являюсь 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)


  1. Tzimie
    28.11.2023 06:44
    +5

    Хм. 150гб? Это очень мало чтобы создавать проблемы. Для MSSQL это игрушечный объем.


    1. savostin
      28.11.2023 06:44
      +2

      Гоняю 2Тб в MariaDB на среднем дедике за 80 евро тоже без вопросов. Имхо и правда дело в данных. Если их так же почистить и подправить структуру, то и переходить не надо. Хотя согласен, Clickhouse мощная вещь.


  1. Akina
    28.11.2023 06:44

    краткая справка: данный движок автоматически удаляет дубликаты записей по ключу сортировки

    Вот тут имеются вопросы. Понятно, что в исходных данных, которые переносятся, поле версии отсутствует как класс... но что использовалось как поле сортировки? или исходные данные содержали в том числе и штамп времени создания записи (или штамп времени создания и штамп времени, на который передаются показания, у вас одно и то же)? но если так - то есть ли гарантия, что последние значения - верные? Потому как вы указываете:

    В таблице хранятся как автоматически переданные данные, так и внесённые жителями вручную.

    что не исключает сценария типа "сегодня снимаем показания автоматом, а завтра абонент вручную вводит показания, снятые им два дня назад"... или вообще случая, когда, зная о грядущем повышении тарифа, потребитель сознательно завышает показания, чтобы оплатить авансом побольше по текущему тарифу - в этом случае автоматический съём, который получит значения меньше предыдущих, вообще неясно как себя должен повести.

    Мне лично вообще категорически не понравился подход, когда часть исходного сырого датасета (пусть и имеющего дубликаты и иные погрешности) удаляется безвозвратно, и уж тем более когда это выполняется автоматически.


    1. filo-soft Автор
      28.11.2023 06:44

      Исходные данные содержат как время внесения показаний, так и флаг, вручную они были добавлены или автоматически. Соответственно, при выборке данных для отчета это также учитывается (если ручные показания были внесены Х дней назад, приоритетны они, иначе - автоматические). По поводу безвозвратного удаления — в крайнем случае можно получить эталонные показания, но это весьма трудозатратно.


  1. iwram
    28.11.2023 06:44
    +2

    Не написали самого важного, про вставку данных. Думаю автор знает, что clickhouse не умеет транзакций, а гарантию доставки хочется (вдруг какие то данные не придут).


    1. savostin
      28.11.2023 06:44

      Clickhouse много чего делает «не так» (как мы привыкли в mysql). И многое он делает «сам» периодически и тихо. Короче нельзя просто так взять и скопировать таблицу из MySQL ;)


    1. titan_pc
      28.11.2023 06:44

      Вроде у них там уже есть транзакции на уровне экспериментальной фичи.

      Но а ещё есть чисто философская мысль, если я вставляю в КХ через кафку и в кафке включаю транзакции - это годная вещь? Или надо больше транзакционности?


    1. ilnuribat
      28.11.2023 06:44

      Replicated replacing merge tree умеет в дедубликацию при вставке, видит последние n инсертов (вроде по умолчанию 50) и в случае если один и тот же батч данных прилетело, то второй раз просто будет игнорировать


  1. MentalSky
    28.11.2023 06:44

    для PostgreSQL есть extension:
    https://github.com/citusdata/citus
    тут и колоночное хранение и распределенные таблицы

    ну и справедливости ради - был опыт работы c БД Percona (форк MySQL) размером 1Tb, правда - хорошо спроектированной - было все ок.


    1. akakoychenko
      28.11.2023 06:44

      Спасибо, не надо)

      Citus, увы, так prod-ready решением и не стал. В одном проекте имели неосторожность, как раз под IoT, взять его. И, когда начали возникать необъснимые проблемы, и вышло связаться с разрабами, те так и сказали, что "простите, но мы не можем гарантировать, что не будет случайных потерь данных при вашем сценарии использования".


      1. Fafhrd
        28.11.2023 06:44

        А что за сценарий использования был, что они так ответили?
        Наличие Azure Cosmos DB for PostgreSQL как-то не похоже на "prod-ready решением и не стал"


      1. MentalSky
        28.11.2023 06:44

        простите, но мы не можем гарантировать, что не будет случайных потерь данных при вашем сценарии использования

        вау, жестко как, впрочем реальность такова, что любой продукт приходится проверять, подойдет ли он под реальную с запасом нагрузку


  1. freeExec
    28.11.2023 06:44

    Что-то я не понял, раньше была полная история показаний, а потом эффективные менеджеры оптимизировали и вы стали данные через пол года нафиг того, на мусорку.


  1. vasilisc
    28.11.2023 06:44
    +1

    А в MariaDB не использовали партицирование?


  1. x67
    28.11.2023 06:44

    Учитывая, что вы явно с превеликим удовольствием открыли для себя партиции, знали ли вы о индексах в mariadb?

    И зачем экономить место? Неужто ваше время, потраченное на это все стоит дешевле?

    И кажется вы не очень хорошо знакомы с разными бд, от чего сделали совсем не оптимальный выбор. По вашим же словам и вашим проблемам, вам нужна именно OLTP бд, к которым относится постгрес, например. А потом накинуть туда нормальную модель данных. И ничто тогда не остановит этот паровоз. А статистику считать в ваших объемах - пустяк, постгрес прожует легко.

    А вот клик мучать запросом показаний конкретного юзера - такое себе)


    1. Vladimirsencov
      28.11.2023 06:44

      Да он и будет работать не очень хорошо с таким запросом. Там структура неподходящая. Для такого запроса как раз нужна обычная реляционная база. Вот группировки клик хорошо будет делать. Я вот не пойму почему просто две базы рядом не поставить?


  1. Ravius
    28.11.2023 06:44
    +1

    Вы пришли к этому: PARTITION BY toYYYYMM(created)

    А что было изначально?