Тысяча датчиков присылает по одному значению в минуту. Это ничтожная нагрузка для записи: полторы тысячи вставок в секунду никого не пугают, а здесь их меньше семнадцати. Тревожно другое: таблица не умеет уставать, и каждый день к ней добавляется ещё 1,44 миллиона строк. Через год их будет больше полумиллиарда, а спрашивать про минуту четырнадцатимесячной давности почти никто не будет.

Вопрос не в том, где взять диск побольше. Вопрос в том, какую подробность данных вы готовы хранить на каждом возрасте и чем за это платите. Разберём путь одного показания: как оно записывается, как попадает в часовую сводку, что происходит, если оно приехало с опозданием, и в какой момент его можно удалить, не потеряв ничего нужного.

Три вопроса к одним и тем же данным

Начнём не с настроек, а с того, кто и о чём спрашивает. Оператор смотрит на дашборд: какая температура в камере сейчас и что было последние два часа. Ему нужны минутные значения, причём свежие, и он не простит задержки. Инженер по надёжности через неделю хочет понять, почему вчера в три ночи сработал аварийный порог, и ему тоже нужна подробность, хотя и реже. Аналитик, который придёт через квартал, спросит про среднее, максимум и минимум по часам или по суткам, и точное значение в 03:07:00 его не интересует.

Это три разных профиля чтения. У первого маленькое окно и высокая частота запросов. У второго окно в дни, запросы редкие, но с детализацией. У третьего окно в месяцы, детализация не нужна, зато нужны быстрые агрегаты. Хранить для всех трёх одну и ту же сырую таблицу бесконечно значит оплачивать подробность, которой через полгода уже никто не пользуется.

Поэтому разумная схема состоит из двух слоёв. Сырые показания живут ограниченное время и отвечают на оперативные вопросы и на разбор инцидентов. Агрегаты по часам и суткам живут намного дольше и отвечают на исторические вопросы. Всё остальное в статье уточняет, как этот зазор между слоями сделать безопасным.

Таблица, которая режется по времени

Телеметрия устроена просто: у каждой строки есть момент времени, идентификатор источника и одно или несколько значений. Новые строки почти всегда относятся к «сейчас», старые почти никогда не переписываются. Именно эта особенность и делает временной ряд особым случаем: можно разделить таблицу на части по времени, и тогда работа со свежим и со старым будет идти в разных кусках хранилища.

В TimescaleDB такая таблица называется hypertable, а её части по времени называются chunk. Для читателя этого достаточно, чтобы понять главное: запрос по последним двум часам трогает один-два свежих куска, а не всю историю. А удаление старого превращается из долгого построчного DELETE в отбрасывание целых кусков, которые целиком устарели.

Исходная таблица может выглядеть обычно, как любая таблица PostgreSQL:

CREATE TABLE sensor_readings (
  ts         timestamptz NOT NULL,
  sensor_id  integer     NOT NULL,
  value      double precision NOT NULL
);

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

Размер куска стоит выбирать по потоку данных и по тому, как вы собираетесь удалять. Если в день приходит полтора миллиона строк, а удалять вы планируете сутками, то слишком крупные куски заставят хранить лишнее: кусок уходит целиком, только когда в нём не осталось нужных данных. Слишком мелкие дадут обилие частей, и планировщик запросов будет возиться с ними зря. Золотой середины в цифрах здесь нет: это решение для вашего потока, которое проверяется замером.

Что доступно в вашей редакции

Прежде чем строить план, стоит выяснить, какие возможности вам реально доступны. TimescaleDB развивается, часть функций зависит от версии, а часть связана с редакцией и лицензией. Непрерывные агрегаты, политики удаления и сжатие в разных сочетаниях версий и редакций ведут себя по-разному и могут иметь разные условия доступности.

Практическое правило такое: до проектирования откройте документацию для вашей версии и проверьте три вещи. Есть ли continuous aggregates и как в вашей версии задаётся окно их обновления. Есть ли политика удаления сырых данных по возрасту. Включено ли сжатие и входит ли оно в вашу редакцию. Если на одном из пунктов вы не уверены, не стройте схему на нём: она развалится в первый же день эксплуатации.

Ещё одна оговорка касается самой документации. Факты в этой статье о связи удаления и агрегатов взяты из раздела про retention и continuous aggregates. Они не говорят, что именно у вас установлена такая-то версия или что команды выглядят одинаково во всех версиях. Это нужно сверять самому.

Срок сырых данных и то, что потом не пересчитать

Теперь главный вопрос: сколько хранить сырые показания? Обычный ответ «сколько влезет» плох, потому что он не учитывает цену. Хороший ответ звучит иначе: сырьё хранится столько, сколько могут понадобиться вопросы, которые нельзя ответить из агрегатов, плюс запас на запоздавшие данные и на исправление ошибок в самой агрегации.

Полезно составить таблицу «вопрос — минимальный слой, который его обслуживает». Вот условный пример для нашего потока:

Какой слой отвечает на какой вопрос
ВопросНужный слойЧто теряется после удаления сырья
Какая температура сейчасСырые минутные показанияНичего: вопрос не про старое
Что было вокруг аварии три дня назадСырые минутные показанияФорма пика внутри часа
Среднее и максимум по часам за кварталЧасовые агрегатыНичего, если агрегаты посчитаны полностью
Сколько минут за месяц значение было выше порогаСырые или заранее посчитанный агрегат с этим порогомВозможность посчитать иначе при другом пороге
Медиана или 95-й перцентиль по суткамСырые или заранее сохранённые перцентилиСреднее и максимум для этого не подходят, пересчёт невозможен

Последние две строки самые коварные. Среднее и максимум можно посчитать часами и затем собирать из часов сутки. А вот «минут выше порога» при новом пороге из часовых агрегатов уже не восстановить, потому что нужная информация лежала внутри минуты и ушла вместе с сырьём. С перцентилями так же: из часовых средних суточный перцентиль не получить.

Отсюда правило, которое мы будем применять дальше: удалять сырьё можно только после того, как все вопросы, не отвечающие на агрегатах, получили либо отдельные заранее посчитанные значения, либо чёткое решение «такой вопрос через месяц нас уже не интересует». Бессрочной точной истории после удаления сырых данных не будет, и честнее признать это на старте.

Путь одного показания: запись, сводка и опоздание

Возьмём одно показание. Датчик номер 417 измерил температуру в 14:07 и должен был отправить её сразу, но связь пропала, и значение долетело до базы только в 16:40. Посмотрим, что с ним происходит.

В 14:07 ничего не происходит: показания ещё нет. Для базы оно появится в 16:40, но запись получит исходное время измерения 14:07. Здесь важно различать два времени: когда измерено и когда принято. Строку в таблице хранит первое. Куску, в который она попадёт, достаточно свежим он не будет: это уже вчерашний по меркам оперативной работы, но пока в пределах кусков, которые база держит под рукой.

Теперь про часовую сводку. Допустим, у нас есть continuous aggregate по часам: среднее, минимум и максимум на каждый час и датчик. Он материализован заранее, и именно поэтому запросы к нему быстрые. Сводку за 14:00–15:00 система собрала в 15:05, как только час закончился. В неё вошли все показания, которые к тому моменту были в таблице. Показания датчика 417 в тот момент не было.

В 16:40 строка приходит, но готовая сводка за 14:00–15:00 об этом не знает. Пока агрегат не обновлён за этот час, он показывает старый результат: например, среднее по пятидесяти девяти значениям вместо шестидесяти. Если пропавшая минута была пиковой, то и максимум за час окажется неверным.

Закрыть такое расхождение должно очередное обновление агрегата, и тут нужно сразу посмотреть на окно refresh. Если политика обновления смотрит назад, скажем, на сутки, то в 16:40 час 14:00–15:00 в неё попадает, и при следующем запуске агрегат пересчитается с учётом опоздавшей строки. Если же окно смотрит назад только на два часа, то час 14:00–15:00 из него уже ушёл, и опоздавшее показание останется вне сводки, пока кто-то не обновит этот диапазон вручную.

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

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации Россия, США, Франция и Великобритания. Оплата картой РФ и по СБП.

АРЕНДОВАТЬ СЕРВЕР

Где удаление встречается с обновлением

До сих пор сырьё и агрегаты жили мирно. Проблема начинается, когда подключается политика удаления. Документация про совместную работу retention и continuous aggregates говорит о трёх вещах. Агрегаты можно сохранить после удаления сырых данных, если правильно выбраны окна обновления. Если refresh захватывает уже удалённый диапазон сырья, соответствующие агрегаты тоже удаляются. Поэтому окно обновления не должно пересекаться с удалённым диапазоном, если агрегаты нужно оставить.

Из этого следует практический вывод, который мы уже сформулируем своими словами, а не документацией: срок хранения сырья и окно обновления нужно задавать парой. Окно refresh должно целиком лежать в той части времени, где сырьё ещё есть. Тогда обычное обновление трогает только свежие часы, а агрегаты за давнее время остаются нетронутыми, хотя сырья под ними уже нет.

Покажем это на условных числах. Сырьё хранится тридцать дней. Окно обновления агрегата смотрит назад на три дня. Между границей окна и границей хранения лежит разрыв в двадцать семь дней: это зона, где сырьё ещё есть, но автоматический refresh уже туда не заглядывает. Если вы исправите ошибочные показания в двухнедельной давности и захотите обновить агрегат вручную, сырьё для этого ещё существует, и пересчёт пройдёт безопасно. А вот попытка вручную обновить диапазон сорокадневной давности опасна: сырья там нет, и по описанному выше правилу вместе с ним могут уйти и агрегаты.

Теперь представим ошибочную конфигурацию. Окно обновления смотрит назад на сорок пять дней, а сырьё хранится тридцать. Каждый запуск refresh заходит в диапазон, где данных уже нет. Агрегаты за дни с тридцать первого по сорок пятый пересчитываются на пустом месте и стираются. Историю вы теряете не из-за одного неудачного дня, а из-за того, что две политики, каждая в отдельности разумные, не согласованы между собой.

Вот короткая последовательность для безопасного включения:

  1. Определите, насколько поздно приходят данные: измерьте разницу между временем измерения и временем приёма.
  2. Задайте окно обновления с запасом на это опоздание, но с границей строго внутри срока хранения сырья.
  3. Задайте срок хранения сырья, оставив зазор между границей окна и границей удаления для ручных исправлений.
  4. Проверьте на копии данных или на тестовой таблице, что после удаления старого куска агрегаты за тот же диапазон на месте.
  5. Только после этого включайте политику удаления на рабочих данных.

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

Сколько места на самом деле нужно

Вернёмся к потоку в тысячу датчиков и одну минуту. Это 1,44 миллиона строк в сутки, около 43,2 миллиона в месяц и примерно 525,6 миллиона в год. Размер строки на диске не равен сумме её полей, поэтому для расчёта возьмём условную цифру в 100 байт на строку вместе с индексом и служебными расходами. Это не измерение, а допущение для арифметики: замените его своим числом.

При такой оценке выходит около 144 МБ в сутки, примерно 4,3 ГБ в месяц и около 52 ГБ в год. Если хранить сырьё тридцать дней, оно займёт порядка 4,3 ГБ, а годовой архив сырых минут по тому же расчёту потянул бы на 52 ГБ. Агрегаты рядом с этим почти незаметны: часовых строк по тысяче датчиков в год около 8,76 миллиона, то есть в шестьдесят раз меньше минутных. Даже если в строке агрегата несколько полей, разница остаётся очень заметной.

Теперь об индексах и служебных расходах. Из чего складывается реальный размер:

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

Поэтому оценку по умножению числа строк на размер значения смело считайте нижней границей. Реальный коэффициент придёт только из замера. Измерить его просто: запишите на тестовой таблице сутки или хотя бы несколько часов реального потока и посмотрите размер таблицы вместе с индексами обычной функцией PostgreSQL:

SELECT pg_size_pretty(pg_total_relation_size('sensor_readings'));

Для таблицы, разбитой на куски расширением, размер по частям и по индексам лучше смотреть через средства самого расширения, названия которых зависят от версии. Но и приведённый запрос даст ориентир. Поделите размер на число записанных строк и умножьте на ожидаемое количество строк за срок хранения. Эта цифра уже не допущение, а ваш прогноз.

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

Что это значит для выбора сервера

На диске такая база держится на двух ресурсах: месте под данные и скорости записи. Телеметрия с минутным шагом по тысяче датчиков не требует экстремальной вычислительной мощности, но любит быстрый диск: вставка, обновление агрегатов и удаление кусков работают с хранилищем постоянно. На VPS MAATRIX используется NVMe, и именно этот тип накопителя полезен для такой нагрузки. Ресурсы тарифов при этом не обещание ёмкости: ёмкость вашей схемы определяется замером на вашем потоке.

Для ориентира: тариф Ba предлагает 1 vCPU, 2 ГБ RAM и 40 ГБ NVMe, Uab даёт 2 vCPU, 4 ГБ и 80 ГБ, Heka даёт 4 vCPU, 8 ГБ и 160 ГБ, а Maat и Djed поднимаются до 240 и 320 ГБ диска. Для арифметики выше годовой сырой архив в 52 ГБ уже не поместился бы на самый малый диск с учётом системы и журналов. С политикой удаления на тридцать дней расклад совсем другой: сырьё займёт малую часть диска, а остальное место уйдёт под агрегаты, журналы и запас. Именно поэтому политика хранения влияет на выбор плана сильнее, чем число датчиков.

Root-доступ на VPS позволяет самому настроить PostgreSQL и расширение, подобрать параметры под ваш поток и поставить мониторинг размера. Обо всём, что касается установки и настройки самой базы, подробнее написано в материале про VPS для TimescaleDB. Резервные копии нужно организовать отдельно: их автоматическое наличие в каждом тарифе в публичном предложении не заявлено.

Как выглядит безопасное включение удаления

Теперь соберём всё в одну картину. Сырые показания пишутся в таблицу, разбитую на куски по времени. Рядом работает часовой агрегат, его окно обновления достаточно широкое, чтобы поймать опоздавшие данные, но не выходит за пределы срока хранения сырья. Сырьё хранится столько, сколько нужно для оперативных задач и разбора инцидентов, а для вопросов, которые нельзя пересчитать из часовых значений, заранее сохранены нужные величины или принято решение их не поддерживать.

Прежде чем включать политику удаления на рабочей базе, пройдите короткую проверку. Откройте документацию именно вашей версии и убедитесь, что команды совпадают с теми, что вы собираетесь запустить. Прогоните сценарий на копии с искусственно сжатыми сроками: например, храните сырьё два дня, окно обновления сделайте в один день, подождите удаления куска и убедитесь, что старые агрегаты остались. Отдельно пошлите на копию запоздавшее показание в пределах окна и за его пределами и посмотрите, в каком случае агрегат поправился, а в каком нет. Это занимает один вечер и избавляет от неприятного открытия через месяц.

Если что-то пошло не так и агрегаты исчезли, не пытайтесь «доехать» их повторным обновлением: сырья под ними уже нет, и пересчёт ничего не вернёт. Восстанавливать придётся из резервной копии, и тут снова видно, что резервное копирование нужно планировать до удаления, а не после.

Вопрос из будущего

Представьте, что прошёл год. К вам приходит аналитик и спрашивает: сколько минут в прошлом марте температура в третьем цехе была выше двадцати восьми градусов? Если вы сохранили часовые максимум и среднее, то ответить можно только приблизительно: максимум выше порога говорит, что порог был превышен, но не говорит, сколько это длилось. Минутного сырья за март давно нет.

Это не ошибка политики хранения. Это следствие решения, которое нужно было принять заранее: хранить ли в агрегате число минут выше порога, а если порог может меняться, то какую сетку порогов заложить. Точность, которая понадобится через год, должна быть сохранена в агрегатах сегодня, потому что завтра её восстанавливать будет не из чего. Так что прежде чем поставить срок на сырые данные, запишите себе вопрос аналитика из будущего и убедитесь, что у вашей схемы есть на него ответ.