惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
T
Troy Hunt's Blog
Scott Helme
Scott Helme
T
Threat Research - Cisco Blogs
T
Tenable Blog
L
LINUX DO - 热门话题
V
Visual Studio Blog
I
Intezer
Blog — PlanetScale
Blog — PlanetScale
Cisco Talos Blog
Cisco Talos Blog
A
Arctic Wolf
C
Cyber Attacks, Cyber Crime and Cyber Security
F
Fortinet All Blogs
aimingoo的专栏
aimingoo的专栏
Know Your Adversary
Know Your Adversary
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
N
Netflix TechBlog - Medium
SecWiki News
SecWiki News
I
InfoQ
Microsoft Security Blog
Microsoft Security Blog
Project Zero
Project Zero
W
WeLiveSecurity
Microsoft Azure Blog
Microsoft Azure Blog
A
About on SuperTechFans
Recorded Future
Recorded Future
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
Vercel News
Vercel News
S
Securelist
Spread Privacy
Spread Privacy
L
LangChain Blog
云风的 BLOG
云风的 BLOG
G
Google Developers Blog
MongoDB | Blog
MongoDB | Blog
Google DeepMind News
Google DeepMind News
Recent Commits to openclaw:main
Recent Commits to openclaw:main
D
Darknet – Hacking Tools, Hacker News & Cyber Security
C
CERT Recently Published Vulnerability Notes
罗磊的独立博客
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO
The Last Watchdog
The Last Watchdog
Attack and Defense Labs
Attack and Defense Labs
博客园 - 司徒正美
Help Net Security
Help Net Security
L
Lohrmann on Cybersecurity
人人都是产品经理
人人都是产品经理
Forbes - Security
Forbes - Security
Hacker News - Newest:
Hacker News - Newest: "LLM"
PCI Perspectives
PCI Perspectives
博客园 - 【当耐特】
T
Tor Project blog

Все публикации подряд на Хабре

Ловим музу за клавиатуру: как айтишнику стать автором Что умеет Midjourney в 2026? Мой немного грустный разбор этого шикарного инструмента Никто не любит писать тесты, но ИИ может исправить это IPv8 выглядит как мечта. Поэтому почти наверняка не взлетит Производители вернули в продажу материнки с DDR3. Что происходит? Управление агентом с телефона через Telegram теперь в KodaCode От координации к лидерству: как меняется роль руководителя разработки Я сделала родителям бизнес вместо пенсии: зарабатываем 70 тысяч, мама не даёт продать В три раза быстрее приемка товара и оптимизация трудозатрат на 73%: как «РСТ-Инвент» помог Gulliver Group ИИ-шечный мир победил? О влиянии искусственного интеллекта на игропром Кремль снижает давление на Телеграмм пока Европа строит интернет по паспорту Как CEO, CTO и CIO за 8 часов собрали ИИ-директора, который умеет держать позицию под давлением Как (не) потерять домен за выходные Вместо 8 разных VPS: как я организовал практику студентам на одном сервере Почему твой Open Source проект не замечают? R&D: искусство управления неопределенностью в разработке AI-дефляция: вакансий для разработчиков больше, а рост зарплат — худший за 15 лет Мы отдали управление роботами OpenClaw. Что из этого вышло Галактический ID: система идентификации для всех форм разумной жизни Шесть основ бизнес-анализа: начинаем с вопроса «Кто в игре?» Код-ревью, в котором дело не в коде Данные переехали. Команда — нет Системной подход к сдаче OSWE в 2025 Почему комната управления реактором покрашена в цвет морской пены 4 YAML-файла вместо PySpark: как аналитикам строить пайплайны без разработчиков LLM-агент для поиска свободных доменов: автоматизируем подбор Когда, зачем и как правильно начинать новую сессию в Claude Code? Как я заставил нейросеть писать макросы для FreeCAD Анатомия ИИ‑агента для подбора персонала. От тысячи резюме к топ‑10 за минуты Опыт разработчика как экономика внимания Автономность как точка невозврата: кто будет субъектом в цифровом будущем Обучение ИИ в «диких» условиях: как рутинные действия превращаются в датасеты Как измерить LLM для задач кибербеза: обзор открытых бенчмарков Где хранить код? Сравнение GitHub, GitLab и Bitbucket Математика объясняет, почему нормальное распределение встречается повсюду Почему ваш FinOps не работает: 12 тезисов от практиков Как подписать проектную документацию УКЭП с использованием бесплатных лицензий Pilot Адаптивное администрирование Sigla Vision Я грузил уран в бочки, а потом 20 лет строил ИТ в атомной отрасли Чем позвонить с Эвереста? История и обзор спутниковой связи. Часть 2 Как языковая модель помогает контролировать качество инструктажей по охране труда в металлургии Как не передать на desktop свой IP в РКН Анатомия SAP Privileges: как устроено управление правами в macOS MoneyDev: Сказка про три главных слова Обновлённый токенизатор видео K-VAE 2.0 от Сбера Как сделать диспетчеризацию дома на 1284 квартиры почти бесплатно Как мы разогнали железную дорогу Мы дали агентам рутину. Теперь надо решить — что делать с освободившимся временем Токсичный контент, промпт-хакинг и защита ИИ — всё о Guardrails для LLM Умный город начинается с точного взгляда: как «Фалькон Тех» меняет пространство к лучшему Навайбкодил приложение для анализа графов Почему Дюну так интересно читать? Упрощаем работу с рутиной или как стать Гендальфом Белым Деконструкция Go: CPU, RAM и что там происходит. Go Assembler база. Часть 1.1 Какие профессии исчезнут из-за ИИ, а какие появятся? И что с этим делать Как мы построили IT-отдел, где хочется расти: архитектурные встречи, прозрачные метрики и книжные подарки Rufler: Делаем из Claude Code автономный рой через один YAML-конфиг Sing-box и белый список приложений Как построить надёжный обмен сообщениями в микросервисах: лучшие практики для enterprise OpenAI строит MLM-пирамиду, а McKinsey и Accenture помогают ей в этом Дом, который не построил Фишер (Часть 2) «Сверхзвуковой математик» против «Вдумчивого логиста»: битва алгоритмов 3D-упаковки Мультимодальные модели – грубый и дорогой инструмент Разговоры ничего не стоят. Код тоже Проверки физических лиц: с кого начнет ФНС Топ-10 бесплатных нейросетей для создания видео в 2026 году Первые слои кода: как наши решения сегодня определяют архитектуру ИИ на десятилетия Разработка нового статического анализатора: PVS-Studio JavaScript Поиск уязвимостей ПО: базовый минимум или роскошный максимум Почему оценка персонала не работает как инструмент управления Как мы разработали ИИ-ассистента и сократили рутину продуктовой команды на 50% Как я ушел из найма, нажарил косточек и продал на маркетплейсах на 168 млн в год Когда 1С:ERP уже внедрена, а нормального производственного плана всё ещё нет Как я сделал Claude мультимодальным, подключив к нему Qwen Omni Как приглашение на вакансию мечты превращается в атаку Infrastructure as Code: философия и лучшие практики IaC Тестируем Yandex Code Assistant на задаче, в которой нужно хранить секреты nxs-universal-chart v3.0: новое поколение универсального Helm-чарта Callback Injection: Техника, которая отправила Microsoft Defender в глухой нокаут «Все идеи на стол»: митап как способ вывести проект из тупика Сегодня я узнал нечто новое о GPU благодаря багу в своей игре Как заставить LLM ̶ ̶г̶а̶л̶л̶ю̶ ̶ эволюционировать Карта событий как фундамент аналитики: практический кейс для E-commerce Что выбрать для AI: x86, ARM или RISC-V? Дайджест железа за март Роль соматических мутаций в развитии аутоиммунных заболеваний: путь к избирательной терапии Mythos от Anthropic — тревожный сигнал для всех, а не только для банков Guardrails для LLM на Java: как приручить промпт‑инъекции и токсичные ответы Green-VLA: как мы собрали VLA-модель для реального антропоморфного робота и не потеряли обобщение Финансовая гонка вооружений: почему умные люди добровольно в ней участвуют Эра ИИ-агентов наступила: выбираем лучшего цифрового сотрудника # Практический опыт внедрения WinCC Redundancy на производственном предприятии Сделал MVP за 3 дня, а потом неделю прикручивал оплату. Оно того стоило? Физика против Маска: почему Starship V3 может оказаться ещё одной катастрофой Нефть Венесуэлы: крупнейшие запасы в мире, но не крупнейшая нефтяная держава JPA 4. Переосмысление Hibernate Почему зеркальная фотокамера Nikon D5 десятилетней давности идеально подошла для миссии «Артемида-2» Проект «Уровень-Спутник» или как мы сделали платформу для гидрологов «Замедлиться, чтобы ускориться»: почему ИИ повышает цену ошибок в требованиях и архитектуре Как с нуля поднять трафик IT-компании на 1657% при бюджете 55 тыс. и выжить Pixel-perfect Downsampling — идеальная отрисовка 50 миллионов точек без потерь
OceanBase как БД для Zabbix + онлайн-миграция с PostgreSQL через Flink CDC. Экономия места в 15–18 раз
Константин · 2026-06-27 · via Все публикации подряд на Хабре

Средний

12 мин

161

Немного предыстории

Ещё в 2022 году внедрялся Zabbix у заказчика, нужен он был для мониторинга нескольких MSSQL-серверов и был установлен на MySQL; с тех пор подключали к нему новые хосты, и начал он потихоньку умирать — всё-таки MySQL не для больших объёмов. Проблема миграции стала острой, а я как раз OceanBase последнее время занимаюсь очень активно.

Мне не удалось сразу уговорить заказчика на миграцию основного Zabbix на OceanBase, но была предложена миграция девелоперского Zabbix, который всё равно надо было переносить в целевой сегмент сети, и по результатам этой миграции уже будет принято решение о возможности миграции основного Zabbix, который на MySQL сейчас. Естественно, я согласился, хотя девелоперский Zabbix был на PG, но этот кейс даже более интересный. Мне будет очень интересно услышать отзывы или советы перед миграцией прома.

Итак, поехали.

Не расплываясь в этой статье про преимущества OceanBase, выделю только главное для Zabbix:
— Встроенное сжатие, дающее даже без колоночного формата 3–5-кратную экономию места по сравнению с PostgreSQL.
— Колоночное хранение с LSM-деревьями, благодаря которым не снижается скорость записи.
— Поддержка MySQL-синтаксиса.
— Настоящий enterprise HA без дополнительных компонентов.
— Multitenant-архитектура полезна на будущее, когда на одном кластере надо разграничить ресурсы на несколько проектов.
— И всё это уже в опенсорс CE-версии.

Вот главный плюс — размер топ-5 таблиц в PostgreSQL и OceanBase:

SQL-запросы и вывод: размер топ-5 таблиц (PostgreSQL и OceanBase)

PostgreSQL

zabbix=# SELECT
    table_name,
    (xpath('/row/count/text()', xml_count))[1]::text::bigint AS row_count,
    pg_size_pretty(pg_total_relation_size(quote_ident(table_name))) AS total_size
FROM (
    SELECT
        table_name,
        query_to_xml(format('SELECT count(*) AS count FROM %I', table_name), false, true, '') AS xml_count
    FROM information_schema.tables
    WHERE table_schema = 'public'
) AS counts
ORDER BY row_count DESC LIMIT 5;
  table_name   │ row_count │ total_size
--------------+-----------+------------
 history       │  14093149 │ 2102 MB
 history_uint  │  10940927 │ 1373 MB
 trends        │   2730276 │ 355 MB
 trends_uint   │   2614163 │ 331 MB
 event_tag     │     68147 │ 6872 kB
(5 rows)

OceanBase

[zabbix│zabbix] 07:44:48> SELECT
    t.table_name,
    COALESCE(s.row_count, 0) AS row_count,
    CONCAT(ROUND(s.data_size / 1024 / 1024, 2), ' MB') AS total_size
FROM information_schema.tables AS t
LEFT JOIN (
    SELECT
        table_name,
        SUM(data_length + index_length) AS data_size,
        SUM(table_rows) AS row_count
    FROM information_schema.tables
    WHERE table_schema = DATABASE()
    GROUP BY table_name
) AS s ON s.table_name = t.table_name
WHERE t.table_schema = DATABASE()
ORDER BY row_count DESC LIMIT 5;
+--------------+-----------+------------+
│ table_name   │ row_count │ total_size │
+--------------+-----------+------------+
│ history      │  15049414 │ 100.00 MB  │
│ history_uint │  11826499 │ 74.00 MB   │
│ trends       │   2747343 │ 28.00 MB   │
│ trends_uint  │   2631177 │ 28.00 MB   │
│ event_tag    │     65595 │ 4.00 MB    │
+--------------+-----------+------------+
5 rows in set (0.66 sec)

Как планировалась миграция

Мы решили мигрировать онлайн, не спеша, переключая клиентов по одному со старого на новый сервер. На время миграции было решено, что не будет изменений конфигурации, поэтому большинство таблиц переедут одним одноразовым джобом, а таблицы, в которые пишутся метрики и их история, будут мигрироваться онлайн через Flink CDC без потери данных. На время миграции хосты, оставшиеся на старом заббиксе, будут писать данные в старую базу на постгрес, а Flink CDC будет в реальном времени переливать данные из Postgres в OceanBase. Таким образом, клиентов можно будет переключать не спеша без потери метрик.

Анализ схемы данных Zabbix в PostgreSQL перед миграцией

Схема Zabbix делится на два класса таблиц, и это определило стратегию миграции. Первый — конфигурация и операционные данные (хосты, шаблоны, items, триггеры, события — ~178 таблиц): они относительно статичны и переносятся разовым batch-снимком. Второй — метрики (history, history_uint, history_str, trends, trends_uint): это непрерывный высокочастотный поток, который нельзя «заморозить», поэтому именно эти пять таблиц мы переносили онлайн через CDC (initial snapshot + streaming).

Ключевое для миграции — как Zabbix генерирует идентификаторы. Вопреки ожиданиям, он почти не использует AUTO_INCREMENT СУБД: бизнес-ID раздаёт собственный аллокатор — таблица ids (тройки table_name, field_name, nextid, то есть просто числа, инкрементируемые в коде приложения). Единственный настоящий AUTO_INCREMENT — поле changelogid в служебной таблице changelog (механизм отслеживания изменений), которую мы оставили пустой. А пять таблиц метрик вообще не имеют суррогатных ID — у них натуральный первичный ключ (itemid, clock, ns для history; itemid, clock для trends).

Эта особенность оказалась удачной: отсутствие зависимости от автоинкремента СУБД снимает главную проблему мульти-мастер сценария — коллизии ID при параллельной записи на двух узлах. Натуральный ключ метрик вдобавок делает повторную вставку той же строки безопасной, а монотонный clock идеально ложится на партиционирование по времени в OceanBase.

Миграция по шагам

1. Установили OceanBase CE 4.4.2 — кластер + OBProxy, создали тенант zb_tenant, БД zabbix (обязательно utf8mb4).
2. Установили Flink + Flink CDC — postgres-cdc и jdbc-коннекторы, настроили TaskManager-слоты.
3. Подготовили PostgreSQL-источник — wal_level=logical, пользователь репликации, публикация для CDC.
4. Мигрировали config-таблицы (178 шт.) — через Flink JDBC batch (не CDC, т.к. данные статичные), сверили построчно.
5. Перенесли триггеры (65 шт.) — из дампа.
6. Обошли проверку версии БД — Zabbix требует MySQL ≥8.0.30, а OBServer в handshake отдаёт 5.7.25; решили через OBProxy (mysql_version=8.0.30), подключив Zabbix через прокси.
7. Установили новый zabbix-server 7.0 на отдельном хосте, подключили к OceanBase, подняли web-интерфейс, проверили, что все хосты переехали, ошибок нет.
8. Спроектировали history/trends под OceanBase — columnstore + RANGE-партиции по clock, написали хранимую процедуру + EVENT для авто-нарезки будущих партиций.
9. Мигрировали history/trends (33M строк) — через Flink CDC (initial snapshot + streaming).
10. Обеспечили устойчивость CDC — checkpointing + restart-strategy + лимит удержания WAL.
11. Пережили реальный сетевой сбой — джобы восстановились с чекпоинтов без потери данных (стресс-тест «из жизни»).
12. Перенесли MSSQL ODBC (DSN + драйвер) — наш Zabbix напрямую опрашивает MSSQL.
13. Cutover — запустили новый сервер, переводим клиентов по одному, старый PostgreSQL-Zabbix дорабатывает параллельно.
14. За два дня все хосты перенесли.

Каждый пункт расписывать будет очень объёмно, да и от версии к версии Zabbix схемы данных меняются, и скрипты будут бесполезны. Я посоветую тем, кто пойдёт по этому пути, брать в помощь Opus 4.8 и выше — он вполне справится со всеми пунктами, особенно если дать эту статью как вводные данные.

Отметим только те пункты, в которых были проблемы, и как их решили.

П. 6. Проверка версии

Сервер ставили на standalone-узел OceanBase, без кластера — рассчитывали подключить Zabbix напрямую к OBServer (2881), без OBProxy. Не вышло: Zabbix на старте требует MySQL ≥8.0.30, а OBServer в handshake жёстко отдаёт 5.7.25. AllowUnsupportedDBVersions=1 не помогает (слишком старая), строку версии в OBServer не поменять. Решение — OBProxy с mysql_version=8.0.30: подключили Zabbix через прокси (2883), проверка прошла. Итог: прокси пришлось поднять даже на standalone — ради единственной подмены версии в handshake.

П. 8. Проектирование history/trends: columnstore + партиции

Пять таблиц метрик пересоздали под OceanBase: columnstore (колоночное хранение — сжатие + быстрая аналитика по году trends) и RANGE-партиции по clock (месячные, для быстрого DROP PARTITION вместо медленного housekeeper-DELETE).

Синтаксис для OB 4.4.2 делали эмпирически, два нюанса: граница партиции — только целый литерал (не UNIX_TIMESTAMP()), и WITH COLUMN GROUP идёт строго после PARTITION BY (иначе ERROR 1064). Пример history:

history: CREATE TABLE + процедура авто-нарезки партиций + EVENT
CREATE TABLE `history` (
  `itemid` bigint(20) unsigned NOT NULL,
  `clock`  int(11) NOT NULL DEFAULT '0',
  `value`  double  NOT NULL DEFAULT '0',
  `ns`     int(11) NOT NULL DEFAULT '0',
  PRIMARY KEY (`itemid`,`clock`,`ns`)
) DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_bin
  PARTITION BY RANGE (`clock`) (
    PARTITION `p2025_06` VALUES LESS THAN (1751328000)
  ) WITH COLUMN GROUP (each column);

Встроенный DYNAMIC_PARTITION_POLICY не подошёл — он только для DATE/TIMESTAMP, а clock это int (unix-секунды). Поэтому авто-нарезку будущих партиций сделали сами: хранимая процедура добирает месячные партиции вперёд до «сейчас + N месяцев», и EVENT вызывает её ежедневно.

CREATE PROCEDURE sp_manage_partitions(IN p_months_ahead INT)
BEGIN
  ...
  SET time_zone = '+00:00';  -- clock = unix UTC
  SET v_target = UNIX_TIMESTAMP(DATE_ADD(DATE_FORMAT(NOW(),'%Y-%m-01'),
                                         INTERVAL p_months_ahead MONTH));
  -- по каждой таблице: читаем max-границу, добавляем месяцы вперёд
  SET v_bound = UNIX_TIMESTAMP(DATE_ADD(FROM_UNIXTIME(v_bound), INTERVAL 1 MONTH));
  SET v_pname = CONCAT('p', DATE_FORMAT(FROM_UNIXTIME(v_bound - 1), '%Y_%m'));
  SET @sql = CONCAT('ALTER TABLE `', v_tbl, '` ADD PARTITION (PARTITION `',
                    v_pname, '` VALUES LESS THAN (', v_bound, '))');
  PREPARE st FROM @sql; EXECUTE st; DEALLOCATE PREPARE st;
  ...
END;

CREATE EVENT evt_manage_partitions
  ON SCHEDULE EVERY 1 DAY
  STARTS (CURRENT_TIMESTAMP + INTERVAL 1 HOUR)
  DO CALL sp_manage_partitions(3);

Процедуру нужно создавать/вызывать с collation_connection = utf8mb4_general_ci (иначе ERROR 1267 на сравнении с information_schema), event_scheduler = ON ставится только от root, а границы считаются строго в UTC (time_zone=‘+00:00’), т.к. clock — unix-время.

Сейчас старые метрики удаляет housekeeper Zabbix обычными DELETE — про партиции он не знает. Но партиционирование открывает путь к мгновенной очистке: отключив housekeeper и переложив ретеншн на свою процедуру, можно будет отбрасывать целый месяц одним DROP PARTITION вместо перебора миллионов строк. А на промышленном кластере из трёх узлов OceanBase распределит партиции по серверам, и запросы к истории пойдут параллельно по узлам — то есть быстрее. Авто-создание партиций мы уже сделали; ускоренное удаление через DROP — следующий шаг.

П. 9. Динамическая миграция через Flink CDC

Заливку реализовали декларативно через Flink SQL. На каждую из пяти таблиц метрик — history, history_uint, history_str, trends, trends_uint — по три выражения: CREATE TABLE источника (коннектор postgres-cdc, режим initial — снапшот + последующий streaming), CREATE TABLE приёмника (коннектор jdbc в OceanBase через прокси) и INSERT INTO … SELECT, который запускает джобу.

Возможно, спорный подход, но для этого случая подходящий.

Вот сами коды:

Flink SQL: пять пар таблиц (источник/приёмник) + INSERT
-- ===== history (double) =====
CREATE TABLE src_history (
  itemid BIGINT, clock INT, `value` DOUBLE, ns INT,
  PRIMARY KEY (itemid, clock, ns) NOT ENFORCED
) WITH (
  'connector'='postgres-cdc','hostname'='192.168.88.41','port'='5432',
  'username'='cdc_zbx','password'='***',
  'database-name'='zabbix','schema-name'='public','table-name'='history',
  'slot.name'='flink_zbx_history','decoding.plugin.name'='pgoutput',
  'debezium.publication.name'='zbx_cdc_pub','debezium.publication.autocreate.mode'='disabled',
  'scan.incremental.snapshot.enabled'='true','scan.startup.mode'='initial'
);
CREATE TABLE snk_history (
  itemid BIGINT, clock INT, `value` DOUBLE, ns INT,
  PRIMARY KEY (itemid, clock, ns) NOT ENFORCED
) WITH (
  'connector'='jdbc',
  'url'='jdbc:mysql://10.10.5.205:2883/zabbix?useSSL=false&rewriteBatchedStatements=true&sessionVariables=foreign_key_checks=0',
  'driver'='com.mysql.cj.jdbc.Driver','username'='zabbix@zb_tenant','password'='***',
  'table-name'='history','sink.buffer-flush.max-rows'='200','sink.buffer-flush.interval'='3s'
);
INSERT INTO snk_history SELECT itemid,clock,`value`,ns FROM src_history;

-- ===== history_uint (bigint unsigned) =====
CREATE TABLE src_history_uint (
  itemid BIGINT, clock INT, `value` BIGINT, ns INT,
  PRIMARY KEY (itemid, clock, ns) NOT ENFORCED
) WITH (
  'connector'='postgres-cdc','hostname'='192.168.88.41','port'='5432',
  'username'='cdc_zbx','password'='***',
  'database-name'='zabbix','schema-name'='public','table-name'='history_uint',
  'slot.name'='flink_zbx_history_uint','decoding.plugin.name'='pgoutput',
  'debezium.publication.name'='zbx_cdc_pub','debezium.publication.autocreate.mode'='disabled',
  'scan.incremental.snapshot.enabled'='true','scan.startup.mode'='initial'
);
CREATE TABLE snk_history_uint (
  itemid BIGINT, clock INT, `value` BIGINT, ns INT,
  PRIMARY KEY (itemid, clock, ns) NOT ENFORCED
) WITH (
  'connector'='jdbc',
  'url'='jdbc:mysql://10.10.5.205:2883/zabbix?useSSL=false&rewriteBatchedStatements=true&sessionVariables=foreign_key_checks=0',
  'driver'='com.mysql.cj.jdbc.Driver','username'='zabbix@zb_tenant','password'='***',
  'table-name'='history_uint','sink.buffer-flush.max-rows'='200','sink.buffer-flush.interval'='3s'
);
INSERT INTO snk_history_uint SELECT itemid,clock,`value`,ns FROM src_history_uint;

-- ===== history_str (varchar 255) =====
CREATE TABLE src_history_str (
  itemid BIGINT, clock INT, `value` STRING, ns INT,
  PRIMARY KEY (itemid, clock, ns) NOT ENFORCED
) WITH (
  'connector'='postgres-cdc','hostname'='192.168.88.41','port'='5432',
  'username'='cdc_zbx','password'='***',
  'database-name'='zabbix','schema-name'='public','table-name'='history_str',
  'slot.name'='flink_zbx_history_str','decoding.plugin.name'='pgoutput',
  'debezium.publication.name'='zbx_cdc_pub','debezium.publication.autocreate.mode'='disabled',
  'scan.incremental.snapshot.enabled'='true','scan.startup.mode'='initial'
);
CREATE TABLE snk_history_str (
  itemid BIGINT, clock INT, `value` STRING, ns INT,
  PRIMARY KEY (itemid, clock, ns) NOT ENFORCED
) WITH (
  'connector'='jdbc',
  'url'='jdbc:mysql://10.10.5.205:2883/zabbix?useSSL=false&rewriteBatchedStatements=true&sessionVariables=foreign_key_checks=0',
  'driver'='com.mysql.cj.jdbc.Driver','username'='zabbix@zb_tenant','password'='***',
  'table-name'='history_str','sink.buffer-flush.max-rows'='200','sink.buffer-flush.interval'='3s'
);
INSERT INTO snk_history_str SELECT itemid,clock,`value`,ns FROM src_history_str;

-- ===== trends (double agg) =====
CREATE TABLE src_trends (
  itemid BIGINT, clock INT, num INT, value_min DOUBLE, value_avg DOUBLE, value_max DOUBLE,
  PRIMARY KEY (itemid, clock) NOT ENFORCED
) WITH (
  'connector'='postgres-cdc','hostname'='192.168.88.41','port'='5432',
  'username'='cdc_zbx','password'='***',
  'database-name'='zabbix','schema-name'='public','table-name'='trends',
  'slot.name'='flink_zbx_trends','decoding.plugin.name'='pgoutput',
  'debezium.publication.name'='zbx_cdc_pub','debezium.publication.autocreate.mode'='disabled',
  'scan.incremental.snapshot.enabled'='true','scan.startup.mode'='initial'
);
CREATE TABLE snk_trends (
  itemid BIGINT, clock INT, num INT, value_min DOUBLE, value_avg DOUBLE, value_max DOUBLE,
  PRIMARY KEY (itemid, clock) NOT ENFORCED
) WITH (
  'connector'='jdbc',
  'url'='jdbc:mysql://10.10.5.205:2883/zabbix?useSSL=false&rewriteBatchedStatements=true&sessionVariables=foreign_key_checks=0',
  'driver'='com.mysql.cj.jdbc.Driver','username'='zabbix@zb_tenant','password'='***',
  'table-name'='trends','sink.buffer-flush.max-rows'='200','sink.buffer-flush.interval'='3s'
);
INSERT INTO snk_trends SELECT itemid,clock,num,value_min,value_avg,value_max FROM src_trends;

-- ===== trends_uint (bigint unsigned agg) =====
CREATE TABLE src_trends_uint (
  itemid BIGINT, clock INT, num INT, value_min BIGINT, value_avg BIGINT, value_max BIGINT,
  PRIMARY KEY (itemid, clock) NOT ENFORCED
) WITH (
  'connector'='postgres-cdc','hostname'='192.168.88.41','port'='5432',
  'username'='cdc_zbx','password'='***',
  'database-name'='zabbix','schema-name'='public','table-name'='trends_uint',
  'slot.name'='flink_zbx_trends_uint','decoding.plugin.name'='pgoutput',
  'debezium.publication.name'='zbx_cdc_pub','debezium.publication.autocreate.mode'='disabled',
  'debezium.slot.drop.on.stop'='false',
  'scan.incremental.snapshot.enabled'='true','scan.startup.mode'='initial'
);
CREATE TABLE snk_trends_uint (
  itemid BIGINT, clock INT, num INT, value_min BIGINT, value_avg BIGINT, value_max BIGINT,
  PRIMARY KEY (itemid, clock) NOT ENFORCED
) WITH (
  'connector'='jdbc',
  'url'='jdbc:mysql://10.10.5.205:2883/zabbix?useSSL=false&rewriteBatchedStatements=true&sessionVariables=foreign_key_checks=0',
  'driver'='com.mysql.cj.jdbc.Driver','username'='zabbix@zb_tenant','password'='***',
  'table-name'='trends_uint','sink.buffer-flush.max-rows'='200','sink.buffer-flush.interval'='3s'
);
INSERT INTO snk_trends_uint SELECT itemid,clock,num,value_min,value_avg,value_max FROM src_trends_uint;

П. 10 и 11. Обеспечили устойчивость CDC, пережили сбой

Ничего хитрого — несколько настроек, без которых CDC не переживёт сбой:

— Checkpointing (interval=30s, EXACTLY_ONCE) — Flink фиксирует позицию в WAL; после сбоя возобновляется с чекпоинта, а не с нуля.
— Restart-strategy (fixed-delay, бесконечно) — без неё джоба умирает при первой ошибке; с ней сама перезапускается.
— max_slot_wal_keep_size на PostgreSQL — страховка от переполнения диска источника отставшим слотом.
— slot.drop.on.stop=false — чтобы при рестарте слот не пересоздавался и не терялись события.

Связка checkpointing + restart-strategy и даёт самовосстановление: не записал → чекпоинт не зафиксирован → слот не сдвинулся → после рестарта перечитал с той же точки. Дублей нет — натуральный PK делает повторную вставку безопасной.

Во время переезда пару раз пропадала связь (одна из причин переноса сервера), и Flink CDC даже в односерверной конфигурации отлично справился с восстановлением репликации.

Финальные пункты, проверка и переезд хостов

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

Хосты, которые опрашивались с сервера Zabbix, начали опрашиваться с обоих серверов, но на короткое время это не страшно: их на старом сервере задизейбили первыми. Напомню, что таблицы конфигурации мы перенесли один раз, и далее через CDC изменения не реплицировались. Хосты, на которые локальный агент сам отправлял данные на сервер, переводили вручную, не спеша, меняя сервер в локальном конфиге.

В проме планируется оставить тот же сервер, просто изменив настройки базы.

Подводные камни / особенности

Коллации: объекты БД (триггеры, процедуры), обращающиеся к строкам, падали с ERROR 1267 из-за расхождения коллации сессии (utf8mb4_general_ci) и БД (utf8mb4_bin); лечится установкой collation_connection перед выполнением.

Беззнаковые таблицы (history_uint, trends_uint): PostgreSQL хранит их как numeric, OceanBase — как bigint unsigned, а во Flink беззнакового типа нет, и мы маппили в BIGINT — для значений до 2^63 это работает, но на огромных счётчиках (>2^63) возможно переполнение, и там надёжнее DECIMAL(20,0) сквозь весь конвейер.

На тестовой инсталляции суммарный объём данных на диске сократился с 4232 MB до 427 MB — примерно в 10 раз. Этот средний показатель занижен спецификой стенда: значительную долю занимают мелкие конфигурационные таблицы, которые практически не сжимаются. Сами таблицы истории — history (2102 → 114 MB) и history_uint (1373 → 78 MB) — дали 17–18-кратное сокращение. Поскольку в продакшене доля исторических данных в общем объёме существенно выше, реалистично ожидать 12–15-кратного сокращения требуемого дискового пространства на реальной нагрузке.

title

description