Один коміт, а не десяток тематичних, свідомо: теми переплетені в
спільних файлах (store.go, docker-compose.yml, deploy/README.md), і
розділити їх можна було б лише індексуванням шматків. Коміти, які не
збираються, гірші за один великий — тим паче що це рівно той стан, який
перевірявся разом.
ЩО ПРАЦЮЄ НА СТЕНДІ Й ПЕРЕВІРЕНО ТАМ
0058 подієві алерти: syslog, ncm, compliance спрацьовують у мить
події; правило з нереалізованим джерелом більше не зберігається
мовчки
0059 snmp.walk і прототипи шаблонів — таблиці з динамічним індексом
описуються шаблоном, а не Go
0060 відкат конфігу: план як різниця, маскування паролів із підписом
плану, обов'язковий контрольний збір, verifying при обриві
0061 кнопки Telegram: довге опитування, авторизація не з callback_data
0062 аудит і архів хостів; тест на AST, що падає на ключі без назви
0063 RLS: три ролі, окремий пул для фонових тактів
0064 строки зберігання даних і сторінка сховища
0065 приймач SNMP-трапів; перевірено справжніми пакетами по дроту,
переклад v1→v2 за RFC 3584 дає правильний OID
0066 ескалації сповіщень
0067 алерт про вичерпання диска
0068 поля заливки конфігу переїхали в каталог профілів
Плюс: 137 тестів вебу з нуля (їх не було взагалі), одинадцять справжніх
вад, знайдених ними й виправлених, і виправлення двох інтеграційних
тестів grpcapi, які мовчки пропускались півтора року.
ЩО ЩЕ НЕ ЗАПУСКАЛОСЬ
netpulse установник: одна команда замість 18 змінних і
593 рядків інструкції
RLS з першого запуску нова інсталяція під політиками одразу;
RLS-EXISTING-INSTALL.md лишається тільки для
старих інсталяцій
.forgejo + CI раннер не зареєстрований
Ці три перевірені компіляцією й міркуванням, але не виконанням.
ГОЛОВНИЙ ВИСНОВОК ДВОХ СЕСІЙ
Зелена перевірка доводить рівно те, що вона перевіряє. Тест ізоляції RLS
був правильний і зелений — і пропустив зламаний вхід, бо перевіряв «чи
не видно чужого», коли зламалось «чи видно своє». Інтеграційні тести
grpcapi були зелені, бо не виконувались. Схема, довідник і протокол
описували те, чого в коді не існувало, і виглядало це як готове.
Тому в кожному завданні цих сесій стояла вимога назвати НЕПОКРИТЕ, а
чотири задачі закінчились не можливістю, а відмовою: правило з
нереалізованим джерелом не зберігається, профіль без команд заливки
каже про це замість мовчазної кнопки, міграція RLS валить сама себе на
таблиці без політики, тест словника аудиту падає на ключі без назви.
Подробиці — HISTORY.md, розділи за 26 і 27 серпня.
517 lines
35 KiB
PL/PgSQL
517 lines
35 KiB
PL/PgSQL
-- =====================================================================
|
||
-- NetPulse :: 0064_retention.sql
|
||
-- Строки зберігання даних: одне місце, де видно, ЩО і СКІЛЬКИ живе.
|
||
--
|
||
-- ЧОМУ ЦЕ З'ЯВЛЯЄТЬСЯ ЗАРАЗ
|
||
--
|
||
-- На стенді з шістьма хостами база важить 120 МБ, і це не проблема.
|
||
-- Проблема в тому, що жодна з цифр не має стелі. Порт хоста — це рядок
|
||
-- у ts.if_counters на кожному такті опитування; 500 хостів по 24 порти
|
||
-- дають 12 000 рядів, тобто сотні тисяч рядків на добу лише з
|
||
-- лічильників. Сеанс збору конфігу лишає транскрипт у ncm.jobs, прогін
|
||
-- команд — транскрипт на КОЖЕН хост у ncm.command_targets, syslog з
|
||
-- дільниці пише стільки, скільки надішле залізо. Усе це росте лінійно
|
||
-- й мовчки, а від переповненого тому першим падає Postgres — тобто
|
||
-- весь продукт одночасно й без попередження.
|
||
--
|
||
-- ЩО ВЖЕ БУЛО, І ЧОМУ ЦЬОГО ЗАМАЛО
|
||
--
|
||
-- Неправда, що не прибирається нічого. 0005 завела стиснення на восьми
|
||
-- гіпертаблицях і видалення на дев'ятьох, 0007 — на alr.notifications,
|
||
-- 0009 — на bill.license_checkins, 0012 — на core.login_attempts,
|
||
-- 0037 — очистку версій конфігів. Механізм є, і ламати його не можна.
|
||
--
|
||
-- Бракує двох речей, і обидві коштують дорого.
|
||
--
|
||
-- Перша — діри. Політики видалення НЕМАЄ в ts.link_status,
|
||
-- ts.device_status_history і alr.alerts_history. Це не дрібниці:
|
||
-- історія алертів — рядок на кожну аварію кожного хоста назавжди, а
|
||
-- історія станів пише рядок на кожну зміну «вгору/вниз», тобто на
|
||
-- кожен блимок каналу. Саме такі таблиці й переповнюють диск: про них
|
||
-- не думають, бо вони «маленькі», і вони маленькі рівно доти, доки
|
||
-- хостів шість.
|
||
--
|
||
-- Друга, і головна: строки прописані в міграціях, тобто в коді.
|
||
-- Змінити «тримати метрики 35 діб» означає накотити нову міграцію.
|
||
-- Через це строк ніхто не міняє, ніхто не знає, який він, і ніхто не
|
||
-- бачить, що станеться, якщо змінити. А «скільки тримати» — рішення
|
||
-- організації («ми маємо бачити півроку»), а не властивість збірки.
|
||
--
|
||
-- ЩО РОБИТЬ ЦЯ МІГРАЦІЯ
|
||
--
|
||
-- Заводить таблицю строків, переносить у неї ФАКТИЧНИЙ стан бази,
|
||
-- додає індекси під пакетне видалення звичайних таблиць, місце під
|
||
-- спостереження за розміром і функцію, якою застосунок накладає
|
||
-- політики TimescaleDB, не будучи власником таблиць.
|
||
--
|
||
-- Найважливіше — те, чого вона НЕ робить: не видаляє жодного рядка й
|
||
-- не змінює жодного наявного строку. Інсталяція, що оновиться, вранці
|
||
-- має рівно ті самі дані, що й учора. Строки, яких не було, лишаються
|
||
-- порожніми — «не видаляти», а не «видаляти за нашим уявленням про
|
||
-- розумне». Перше, що зробить розумне значення за замовчуванням на
|
||
-- чужій базі, — знищить те, заради чого її й ставили.
|
||
-- =====================================================================
|
||
|
||
-- ---------------------------------------------------------------------
|
||
-- 1. Таблиця строків
|
||
-- ---------------------------------------------------------------------
|
||
|
||
-- Строк — НЕ на кабінет, і це не спрощення, а властивість TimescaleDB.
|
||
--
|
||
-- Видалення старого в гіпертаблиці — це drop_chunks: чанк зноситься
|
||
-- цілою таблицею, разом із рядками всіх кабінетів, що в нього
|
||
-- потрапили. Чанк ріжеться за часом і лише за часом; про кабінет він не
|
||
-- знає нічого. Тобто «тенант А тримає метрики рік, тенант Б — тиждень»
|
||
-- реалізується тільки власним DELETE по рядках — тобто відмовою від
|
||
-- єдиного механізму, заради якого TimescaleDB і взято, і платою у
|
||
-- вигляді розпухлих таблиць, з яких місце вже не повертається на диск.
|
||
--
|
||
-- Тому строк — рішення рівня інсталяції, як розмір диска. Для
|
||
-- коробкового продукту, який ставлять клієнту на його сервер, це і є
|
||
-- правда: кабінет там один. Для спільного хостингу кількох клієнтів це
|
||
-- обмеження, і його треба знати заздалегідь.
|
||
CREATE TABLE core.retention_settings (
|
||
-- Ключ виду даних: metrics_raw, syslog, alerts_history…
|
||
--
|
||
-- Стабільний рядок, а не посилання на таблицю: ім'я таблиці може
|
||
-- змінитись міграцією, а «сирі метрики» лишаються тим самим поняттям
|
||
-- і до, і після перейменування.
|
||
kind text PRIMARY KEY,
|
||
|
||
-- Що саме чистимо, повним іменем для regclass: 'ts.samples'.
|
||
--
|
||
-- Лежить у базі, а не лише в Go: політики накладає функція нижче, і
|
||
-- вона має знати, до чого їх прикладати, без участі застосунку.
|
||
-- Людські назви й пояснення лишаються в Go — там їх видно поруч зі
|
||
-- словником дій аудиту, і правити їх треба в одному місці.
|
||
relation text NOT NULL,
|
||
|
||
-- Чим прибираємо. Різниця не технічна, її видно на диску:
|
||
--
|
||
-- timescale — рідна політика, drop_chunks. Чанк зникає як таблиця,
|
||
-- місце ПОВЕРТАЄТЬСЯ файловій системі.
|
||
-- batch — пакетний DELETE звичайної таблиці. Місце
|
||
-- звільняється ВСЕРЕДИНІ таблиці й перевикористається
|
||
-- наступними рядками, але на диск не повернеться без
|
||
-- VACUUM FULL, який блокує таблицю цілком.
|
||
--
|
||
-- Саме тому телеметрію не прибирають своїм циклом DELETE: на
|
||
-- переповненому томі «видалили 200 ГБ» не звільнило б жодного байта.
|
||
mechanism text NOT NULL CHECK (mechanism IN ('timescale','batch')),
|
||
|
||
-- Колонка часу, за якою рахують вік рядка. У гіпертаблиць це ts, у
|
||
-- безперервних агрегатів — bucket, у звичайних таблиць — created_at.
|
||
--
|
||
-- Теж у базі, а не в Go: це факт схеми, і живе він поруч із іменем
|
||
-- таблиці. Тримати їх у різних місцях означало б, що перейменування
|
||
-- колонки правиться в одному з двох — і невідомо, у якому.
|
||
time_column text NOT NULL DEFAULT 'ts',
|
||
|
||
-- Скільки діб тримати. NULL — не видаляти нічого.
|
||
--
|
||
-- Саме NULL, а не 0 і не «дуже велике число». Нуль читався б як
|
||
-- «тримати нуль днів», тобто як наказ знести все, і одна помилка в
|
||
-- перетворенні типів між формою й API коштувала б усієї телеметрії.
|
||
-- У порожнього поля руйнівного прочитання немає.
|
||
keep_days int CHECK (keep_days IS NULL OR keep_days BETWEEN 1 AND 36500),
|
||
|
||
-- Хто й коли міняв востаннє. Повний слід — у core.audit_log; тут
|
||
-- лише те, що показують поруч зі строком, не ходячи в журнал.
|
||
updated_at timestamptz NOT NULL DEFAULT now(),
|
||
updated_by uuid REFERENCES core.users(id) ON DELETE SET NULL
|
||
);
|
||
|
||
COMMENT ON TABLE core.retention_settings IS
|
||
'Строки зберігання за видами даних. Рівень інсталяції: чанк TimescaleDB не знає кабінету';
|
||
COMMENT ON COLUMN core.retention_settings.keep_days IS
|
||
'Скільки діб тримати; NULL — не видаляти';
|
||
COMMENT ON COLUMN core.retention_settings.mechanism IS
|
||
'timescale — drop_chunks, місце повертається на диск; batch — DELETE, місце лишається в таблиці';
|
||
|
||
-- RLS тут немає, і це не пропуск: політика з 0011 накладається на
|
||
-- таблиці з колонкою tenant_id, а в цієї її немає за побудовою.
|
||
-- Читання дозволене всім ролям — ховати «метрики живуть 35 діб» немає
|
||
-- від кого, а от НЕ знати цього означає дивитись на порожній графік за
|
||
-- півроку й не розуміти чому. Право на зміну перевіряє застосунок
|
||
-- (settings:write) — воно є лише у власника й адміна.
|
||
GRANT SELECT, INSERT, UPDATE, DELETE ON core.retention_settings
|
||
TO netpulse_app, netpulse_worker;
|
||
|
||
-- ---------------------------------------------------------------------
|
||
-- 2. Читання чинної політики
|
||
-- ---------------------------------------------------------------------
|
||
|
||
-- Скільки TimescaleDB тримає це відношення ЗАРАЗ. NULL — політики немає.
|
||
--
|
||
-- Окрема функція, бо запит неочевидний рівно в одному місці: для
|
||
-- безперервного агрегату (ts.samples_5m і решта) політика висить не на
|
||
-- вигляді, а на матеріалізованій гіпертаблиці під ним
|
||
-- (_timescaledb_internal._materialized_hypertable_N). Шукати політику
|
||
-- за іменем вигляду означає не знайти її ніколи й доповісти людині, що
|
||
-- роллапи не прибираються, — при тому що 0005 їм строк задала.
|
||
CREATE FUNCTION core.retention_current(rel text) RETURNS interval
|
||
LANGUAGE sql STABLE AS $$
|
||
SELECT (j.config->>'drop_after')::interval
|
||
FROM timescaledb_information.jobs j
|
||
WHERE j.proc_name = 'policy_retention'
|
||
AND (
|
||
(j.hypertable_schema = split_part(rel, '.', 1)
|
||
AND j.hypertable_name = split_part(rel, '.', 2))
|
||
OR EXISTS (
|
||
SELECT 1 FROM timescaledb_information.continuous_aggregates ca
|
||
WHERE ca.view_schema = split_part(rel, '.', 1)
|
||
AND ca.view_name = split_part(rel, '.', 2)
|
||
AND ca.materialization_hypertable_schema = j.hypertable_schema
|
||
AND ca.materialization_hypertable_name = j.hypertable_name
|
||
)
|
||
)
|
||
LIMIT 1
|
||
$$;
|
||
|
||
COMMENT ON FUNCTION core.retention_current(text) IS
|
||
'Чинний строк політики видалення TimescaleDB; NULL — політики немає';
|
||
|
||
-- Під яким іменем це відношення ЛЕЖИТЬ НА ДИСКУ.
|
||
--
|
||
-- Та сама причина, що й вище, лише з іншого боку. ts.samples_5m — це
|
||
-- вигляд; чанки, розміри й стиснення обліковуються на матеріалізованій
|
||
-- гіпертаблиці під ним. Запитати розмір у вигляду означає отримати нуль
|
||
-- і показати людині, що роллапи нічого не важать, — при тому що саме
|
||
-- вони й лишаються після видалення сирих даних.
|
||
--
|
||
-- Для звичайної гіпертаблиці повертає її саму, тож викликач не має
|
||
-- розрізняти два випадки.
|
||
CREATE FUNCTION core.retention_hypertable(rel text) RETURNS text
|
||
LANGUAGE sql STABLE AS $$
|
||
SELECT COALESCE(
|
||
(SELECT ca.materialization_hypertable_schema || '.' ||
|
||
ca.materialization_hypertable_name
|
||
FROM timescaledb_information.continuous_aggregates ca
|
||
WHERE ca.view_schema = split_part(rel, '.', 1)
|
||
AND ca.view_name = split_part(rel, '.', 2)),
|
||
rel)
|
||
$$;
|
||
|
||
COMMENT ON FUNCTION core.retention_hypertable(text) IS
|
||
'Фізична гіпертаблиця відношення: для агрегату — матеріалізована, для решти — воно саме';
|
||
|
||
-- ---------------------------------------------------------------------
|
||
-- 3. Перенесення ФАКТИЧНОГО стану
|
||
-- ---------------------------------------------------------------------
|
||
|
||
-- Рядки заводяться з тим строком, який стоїть у базі ЗАРАЗ, а не з
|
||
-- бажаним. Це головне рішення міграції.
|
||
--
|
||
-- Спокуса зробити навпаки велика: перелік «правильних» строків у нас є,
|
||
-- і його хочеться накотити одним рухом. Але на чужій інсталяції наш
|
||
-- правильний строк — це чиясь втрачена історія. Тому міграція лише
|
||
-- записує те, що вже діє, у місце, де це видно людині. Екран після
|
||
-- оновлення показує правду про базу, а не наш намір.
|
||
--
|
||
-- Наслідок, який варто розуміти заздалегідь: одразу після накату екран
|
||
-- виглядає нерівно — десь 35 діб, десь порожньо. Так і має бути. Це
|
||
-- знімок реальності, і саме він змушує запитати «а чому історія алертів
|
||
-- не прибирається взагалі».
|
||
DO $$
|
||
DECLARE
|
||
r record;
|
||
cur interval;
|
||
BEGIN
|
||
FOR r IN
|
||
SELECT * FROM (VALUES
|
||
-- Метрики плагінів: cpu, пам'ять, температура, будь-що з SNMP.
|
||
-- Найгустіші дані в системі: один ряд на (хост, метрика, мітки).
|
||
('metrics_raw', 'ts.samples', 'timescale', 'ts'),
|
||
('metrics_5m', 'ts.samples_5m', 'timescale', 'bucket'),
|
||
('metrics_1h', 'ts.samples_1h', 'timescale', 'bucket'),
|
||
|
||
-- Доступність. Пишеться на кожен хост на кожному такті ICMP —
|
||
-- тобто найчастіше з усього, що є.
|
||
('icmp_raw', 'ts.icmp_samples', 'timescale', 'ts'),
|
||
('icmp_5m', 'ts.icmp_5m', 'timescale', 'bucket'),
|
||
('icmp_1h', 'ts.icmp_1h', 'timescale', 'bucket'),
|
||
|
||
-- Лічильники портів. Найбільший обсяг у перерахунку на хост:
|
||
-- множник — кількість портів, а не одиниця.
|
||
('ifc_raw', 'ts.if_counters', 'timescale', 'ts'),
|
||
('ifc_5m', 'ts.if_counters_5m', 'timescale', 'bucket'),
|
||
('ifc_1h', 'ts.if_counters_1h', 'timescale', 'bucket'),
|
||
|
||
-- Стан лінків і історія станів хостів. Рядок на кожну ЗМІНУ, а не
|
||
-- на такт, — тому мале в спокійній мережі й велике в тій, що
|
||
-- блимає. Саме там воно й потрібне, і саме там росте найшвидше.
|
||
('link_status', 'ts.link_status', 'timescale', 'ts'),
|
||
('device_status', 'ts.device_status_history','timescale', 'ts'),
|
||
|
||
-- Те, що надсилає саме залізо. Обсяг не залежить від наших
|
||
-- налаштувань узагалі: скільки надішле, стільки й ляже.
|
||
('syslog', 'ts.syslog', 'timescale', 'ts'),
|
||
('traps', 'ts.snmp_traps', 'timescale', 'ts'),
|
||
|
||
-- Самометрики зондів. Службові дані, які цінні кілька днів.
|
||
('agent_health', 'ts.agent_health', 'timescale', 'ts'),
|
||
|
||
-- Алерти та доставка сповіщень.
|
||
('alerts_history', 'alr.alerts_history', 'timescale', 'ts'),
|
||
('notifications', 'alr.notifications', 'timescale', 'ts'),
|
||
|
||
-- Спроби входу і журнал аудиту. Обидва — доказова база, і строк
|
||
-- тут визначає не місце на диску, а вимоги до організації.
|
||
('login_attempts', 'core.login_attempts', 'timescale', 'ts'),
|
||
('audit_log', 'core.audit_log', 'timescale', 'ts'),
|
||
|
||
-- Звичайні таблиці. Ростуть повільніше за телеметрію, але рядок
|
||
-- у них у тисячі разів товщий: транскрипт сесії — це кілобайти
|
||
-- тексту, а не число з плаваючою комою.
|
||
('command_runs', 'ncm.command_runs', 'batch', 'created_at'),
|
||
('ncm_jobs', 'ncm.jobs', 'batch', 'created_at'),
|
||
('discovery_runs', 'topo.discovery_runs', 'batch', 'created_at')
|
||
) AS v(kind, relation, mechanism, time_column)
|
||
LOOP
|
||
cur := NULL;
|
||
IF r.mechanism = 'timescale' THEN
|
||
cur := core.retention_current(r.relation);
|
||
END IF;
|
||
|
||
INSERT INTO core.retention_settings (kind, relation, mechanism, time_column, keep_days)
|
||
VALUES (
|
||
r.kind, r.relation, r.mechanism, r.time_column,
|
||
-- Політика TimescaleDB задається інтервалом будь-якої точності
|
||
-- («35 days», «12:00:00»). Форма оперує добами, тож інтервал
|
||
-- округлюється ВНИЗ і не менше однієї доби: округлення вгору
|
||
-- показало б людині строк, довший за справжній, а нуль означав
|
||
-- би «видалити все» — обидва прочитання гірші за втрату кількох
|
||
-- годин точності в підписі.
|
||
CASE WHEN cur IS NULL THEN NULL
|
||
ELSE GREATEST(1, floor(EXTRACT(EPOCH FROM cur) / 86400)::int)
|
||
END
|
||
)
|
||
ON CONFLICT (kind) DO NOTHING;
|
||
END LOOP;
|
||
END $$;
|
||
|
||
-- bill.license_checkins свідомо не заведено видом даних. Строк у неї є
|
||
-- (0009, 400 діб), росте вона на рядок за такт перевірки ліцензії, а
|
||
-- розділу «Тариф» у продукті ще немає — тобто рядок у формі означав би
|
||
-- запрошення покрутити те, наслідків чого людині ніде не видно.
|
||
|
||
-- ---------------------------------------------------------------------
|
||
-- 4. Накладання політик TimescaleDB
|
||
-- ---------------------------------------------------------------------
|
||
|
||
-- SECURITY DEFINER, і це не зручність, а необхідність.
|
||
--
|
||
-- add_retention_policy вимагає прав ВЛАСНИКА гіпертаблиці. Після 0063
|
||
-- застосунок ходить у базу роллю netpulse_app, яка власником не є й
|
||
-- ніколи не стане: передавати їй ownership ста таблиць означало б
|
||
-- віддати їй же право знести схему. Тобто без цієї функції екран
|
||
-- строків міг би зберегти число й не змогти його застосувати — рівно
|
||
-- той стан, у якому інтерфейс бреше.
|
||
--
|
||
-- Функція навмисно вузька: вона не приймає ані імені таблиці, ані
|
||
-- строку. Усе, що вона робить, — приводить політики у відповідність до
|
||
-- рядків core.retention_settings, права на які перевіряє застосунок.
|
||
-- Ширший інтерфейс (виконай drop_chunks на цій таблиці) означав би, що
|
||
-- будь-хто з EXECUTE отримав право знищити будь-що правами власника.
|
||
--
|
||
-- search_path прибитий цвяхами: без цього таблицю core.retention_settings
|
||
-- можна було б підмінити своєю схемою в search_path того, хто викликає,
|
||
-- і функція правами власника прочитала б чужі накази.
|
||
CREATE FUNCTION core.apply_retention_policies() RETURNS int
|
||
LANGUAGE plpgsql SECURITY DEFINER
|
||
SET search_path = pg_catalog, core, public AS $$
|
||
DECLARE
|
||
r record;
|
||
cur interval;
|
||
want interval;
|
||
changed int := 0;
|
||
BEGIN
|
||
FOR r IN
|
||
SELECT kind, relation, keep_days
|
||
FROM core.retention_settings
|
||
WHERE mechanism = 'timescale'
|
||
ORDER BY kind
|
||
LOOP
|
||
cur := core.retention_current(r.relation);
|
||
want := CASE WHEN r.keep_days IS NULL THEN NULL
|
||
ELSE make_interval(days => r.keep_days) END;
|
||
|
||
-- Нічого не змінилось — не чіпаємо. Не з ощадливості: кожен
|
||
-- remove/add перезаводить фонову задачу з новим розкладом, і
|
||
-- такт, який робив би це щогодини, відсував би саме видалення
|
||
-- нескінченно.
|
||
CONTINUE WHEN cur IS NOT DISTINCT FROM want;
|
||
|
||
PERFORM remove_retention_policy(r.relation::regclass, if_exists => true);
|
||
IF want IS NOT NULL THEN
|
||
PERFORM add_retention_policy(r.relation::regclass, want, if_not_exists => true);
|
||
END IF;
|
||
changed := changed + 1;
|
||
END LOOP;
|
||
|
||
RETURN changed;
|
||
END $$;
|
||
|
||
COMMENT ON FUNCTION core.apply_retention_policies() IS
|
||
'Приводить політики видалення TimescaleDB у відповідність до core.retention_settings; повертає кількість змінених';
|
||
|
||
REVOKE ALL ON FUNCTION core.apply_retention_policies() FROM PUBLIC;
|
||
GRANT EXECUTE ON FUNCTION core.apply_retention_policies() TO netpulse_app, netpulse_worker;
|
||
|
||
-- Стиснення тут НЕ додається, і про це варто сказати вголос, бо трьом
|
||
-- гіпертаблицям його справді бракує: alr.alerts_history,
|
||
-- alr.notifications і ts.device_status_history.
|
||
--
|
||
-- Причина — урок 0050. Стиснення робить недоступними індекси за всіма
|
||
-- колонками, крім segmentby: замість пошуку читається й розпаковується
|
||
-- пакет на тисячу рядків. Для метрик це вигідно (їх питають діапазоном
|
||
-- часу), для цих трьох — ні:
|
||
--
|
||
-- alr.notifications питають за alert_id, і segmentby alert_id дав би
|
||
-- сегменти по одному-два рядки, тобто стиснення без стиснення.
|
||
-- alr.alerts_history питають і за tenant_id (список історії), і за
|
||
-- device_id (видалення хоста, 0057). Будь-який вибір segmentby
|
||
-- лишає другий запит без індексу — тобто прискорює місце ціною
|
||
-- тихо померлої сторінки.
|
||
-- ts.device_status_history мала за обсягом, і рахувати на ній
|
||
-- компроміс немає сенсу, доки не видно, що вона заважає.
|
||
--
|
||
-- Правильна відповідь для всіх трьох — строк зберігання, а не
|
||
-- стиснення: рядок, якого немає, займає нуль і шукається миттєво.
|
||
-- Стиснення сюди можна додати пізніше, але вже з виміром на живих
|
||
-- даних, як це зроблено в 0050, а не з міркувань симетрії.
|
||
|
||
-- ---------------------------------------------------------------------
|
||
-- 5. Індекси під пакетне видалення
|
||
-- ---------------------------------------------------------------------
|
||
|
||
-- Звичайні таблиці прибирає код, і прибирає ПАРТІЯМИ:
|
||
--
|
||
-- DELETE FROM ncm.jobs WHERE id IN (
|
||
-- SELECT id FROM ncm.jobs WHERE created_at < $1 ORDER BY created_at LIMIT $2)
|
||
--
|
||
-- Партія — не про швидкість, а про блокування: один DELETE на річну
|
||
-- історію тримав би рядки заблокованими стільки, скільки триває
|
||
-- видалення, і ріс би в WAL так само довго. Те саме міркування, що й у
|
||
-- telemetryBatch при повному видаленні хоста (0057).
|
||
--
|
||
-- Наявні індекси для такої вибірки не годяться: у ncm.command_runs і
|
||
-- topo.discovery_runs вони йдуть по (tenant_id, created_at), у
|
||
-- ncm.jobs — по (device_id, created_at). Прибирання ж іде поверх
|
||
-- кабінетів і поверх хостів, тобто веде за собою читання всієї таблиці
|
||
-- на кожну партію — а партій сотні.
|
||
CREATE INDEX command_runs_age_idx ON ncm.command_runs (created_at);
|
||
CREATE INDEX ncm_jobs_age_idx ON ncm.jobs (created_at);
|
||
CREATE INDEX discovery_runs_age_idx ON topo.discovery_runs (created_at);
|
||
|
||
-- ncm.command_targets власного індексу не отримує навмисно: цілі
|
||
-- зникають каскадом від ncm.command_runs (ON DELETE CASCADE), а
|
||
-- command_targets_run_idx по (run_id, created_at) каскад уже
|
||
-- обслуговує. Саме в цілях лежать транскрипти, тобто основна вага
|
||
-- прогону, — і прибираються вони разом із прогоном, до якого належать.
|
||
|
||
-- ---------------------------------------------------------------------
|
||
-- 6. Спостереження за розміром
|
||
-- ---------------------------------------------------------------------
|
||
|
||
-- Розмір бази сам собою не відповідає на питання, заради якого на нього
|
||
-- дивляться. «120 МБ» не означає нічого; «плюс 40 МБ за добу, вільного
|
||
-- на 12 діб» означає все. Різницю дає лише ряд спостережень, тож його
|
||
-- треба вести — інакше перша ж відповідь про приріст буде вигаданою.
|
||
CREATE TABLE core.storage_samples (
|
||
captured_at timestamptz NOT NULL,
|
||
-- Вид даних із core.retention_settings або службові ключі
|
||
-- '@database' (уся база) і '@other' (усе, що не розписано видами).
|
||
-- Позначка @ на початку не косметична: без неї службовий ключ
|
||
-- невідрізнимий від виду даних, який хтось додасть наступною
|
||
-- міграцією.
|
||
kind text NOT NULL,
|
||
bytes bigint NOT NULL,
|
||
PRIMARY KEY (captured_at, kind)
|
||
);
|
||
|
||
CREATE INDEX storage_samples_kind_idx ON core.storage_samples (kind, captured_at DESC);
|
||
|
||
COMMENT ON TABLE core.storage_samples IS
|
||
'Знімки розмірів за видами даних: без ряду спостережень приріст за добу нема з чого рахувати';
|
||
|
||
GRANT SELECT, INSERT, UPDATE, DELETE ON core.storage_samples
|
||
TO netpulse_app, netpulse_worker;
|
||
|
||
-- Скільки взагалі є місця.
|
||
--
|
||
-- Postgres не вміє відповісти на це питання: функції «скільки вільного
|
||
-- на томі» в ньому немає, а сервер застосунку живе в іншому контейнері
|
||
-- й може стояти взагалі на іншій машині. Тобто єдиний спосіб не збрехати
|
||
-- — спитати людину, яка ставила систему.
|
||
--
|
||
-- Порожнє значення (0) — робочий стан, а не недоналаштування: сторінка
|
||
-- тоді чесно показує розмір і приріст, але не показує «вистачить на N
|
||
-- діб». Вигадати ємність замість людини означало б показати дату
|
||
-- переповнення, яка ні на чому не ґрунтується, — а на такі дати
|
||
-- дивляться саме тоді, коли вже пізно перевіряти.
|
||
CREATE TABLE core.storage_config (
|
||
-- Один рядок на інсталяцію. CHECK замість домовленості: без нього
|
||
-- другий рядок з'явиться першим же INSERT без WHERE, і два різні
|
||
-- уявлення про розмір диска житимуть поруч.
|
||
id boolean PRIMARY KEY DEFAULT true CHECK (id),
|
||
disk_bytes bigint NOT NULL DEFAULT 0 CHECK (disk_bytes >= 0),
|
||
-- Від якого відсотка зайнятого вважати, що вже пора. Не поріг
|
||
-- алерту (його вирішує движок правил), а те, з якого моменту
|
||
-- сторінка перестає бути довідкою й починає бути попередженням.
|
||
warn_pct int NOT NULL DEFAULT 80 CHECK (warn_pct BETWEEN 50 AND 99),
|
||
updated_at timestamptz NOT NULL DEFAULT now(),
|
||
updated_by uuid REFERENCES core.users(id) ON DELETE SET NULL
|
||
);
|
||
|
||
INSERT INTO core.storage_config (id) VALUES (true) ON CONFLICT DO NOTHING;
|
||
|
||
COMMENT ON TABLE core.storage_config IS
|
||
'Ємність тому під базу — питання, на яке Postgres не відповідає; заповнює людина';
|
||
|
||
GRANT SELECT, INSERT, UPDATE, DELETE ON core.storage_config
|
||
TO netpulse_app, netpulse_worker;
|
||
|
||
-- ---------------------------------------------------------------------
|
||
-- 7. Право
|
||
-- ---------------------------------------------------------------------
|
||
|
||
-- Окремого права не заводимо: settings:write уже описане як «Змінювати
|
||
-- налаштування організації та її оформлення» (0053) і вже позначене
|
||
-- чутливим у store/roles.go. Строк зберігання — саме таке налаштування:
|
||
-- рівень інсталяції, наслідок незворотний, коло людей те саме.
|
||
--
|
||
-- Досі це право не питав жоден обробник — воно значилось у dormantPerms
|
||
-- як «налаштування організації ще немає». Тепер вони є, і рядок звідти
|
||
-- прибирається тією ж правкою.
|
||
UPDATE core.permissions
|
||
SET description = 'Змінювати налаштування організації: оформлення, строки зберігання даних'
|
||
WHERE key = 'settings:write';
|
||
|
||
-- ---------------------------------------------------------------------
|
||
-- 8. Що лишається поза цією міграцією й чому
|
||
-- ---------------------------------------------------------------------
|
||
|
||
-- Версії конфігів (ncm.configs) сюди НЕ переїжджають.
|
||
--
|
||
-- Їхня очистка вже є (0037), і вона влаштована принципово інакше: не
|
||
-- «старше за N діб», а «останні N версій АБО молодші за M днів», плюс
|
||
-- захист останньої версії хоста, версій під відкатом і версій, на які
|
||
-- посилаються результати перевірок. Звести це до однієї цифри в добах
|
||
-- означало б утратити рівно ті гарантії, заради яких воно й написане:
|
||
-- конфіг, який не міняли три роки, — не сміття, а єдина копія.
|
||
--
|
||
-- Друга ручка для тих самих даних була б гіршою за відсутність
|
||
-- ручки — той самий висновок, що й у коментарі 0037 до
|
||
-- ncm.device_policies.retention_versions. Тому сторінка строків
|
||
-- показує політику конфігів як є, поруч із рештою, і відправляє міняти
|
||
-- її туди, де вона живе.
|
||
--
|
||
-- ncm.compliance_results теж не заводиться: UNIQUE (rule_id, device_id)
|
||
-- робить її обмеженою добутком «правил × хостів», а не часом. Вона не
|
||
-- росте — вона переписується.
|
||
--
|
||
-- core.event_outbox, core.download_tickets і topo.map_revisions уже
|
||
-- прибираються кодом (PruneEvents, квитки за строком, стеля ревізій
|
||
-- мапи), і кожне з трьох обмежене за побудовою. Заводити їм ще й
|
||
-- налаштовуваний строк означало б дати другу ручку до вже вирішеного.
|