NewAPI + MySQL 更优雅的日志清理方式

NewAPI + MySQL 更优雅的日志清理方式
NewAPI + MySQL 更优雅的日志清理方式

NewAPI 的日志目前是单表存储,已经看到太多佬友因为日志撑爆磁盘了,下面给出一种解决方式,可能不完全兼容,但是个人测试下来没有什么问题

与 NewAPI 版本无关,基本所有版本都可以用,我用的版本是 1.0.0-rc.10

自带的日志删除

最常用的是使用 NewAPI 自带的 “清理历史日志” 来进行删除

// 代码逻辑如下
// 直接按照时间戳删除
LOG_DB.Where("created_at < ?", targetTimestamp).Limit(limit).Delete(&Log{})

这里有两个问题:

  1. Delete 的效率不高,或者说在数据量过大的情况下使用 Delete 带来的 IO 可能成为灾难
  2. Delete 之后磁盘空间并不会回收,需要手动回收

image

通过数据库分片解决日志删除

适用于 MySQL,如果您是其他的 DB 如 Postgres / SQLite,将下面的内容提供给 AI 相信也会有对应的解决方案

分片的核心逻辑是,将日志拆分到对应的日期上,比如分片 logs_20260601 只存储当天的数据,在超出需要删除的日期后,比如 1 个月后的 2026/07/01,直接删除 logs_20260601 分片,分片占用的空间会立刻释放,对应的日志数据也会直接删除

不过这里要提示您,所有的 DB 变更都可能引入风险,请您在执行前通过 mysqldump 或者类似工具,导出完整的数据结构和数据内容,确保执行出现异常可以随时回滚

执行下面的步骤前,建议停机,否则可能导致数据不完整,如果数据完整性不在考虑范围内,也可以在线更新

Step 1 检查数据符合预期 (Read)

执行下面的查询语句,确保 count 为 0,即所有的日志的创建时间不为空

SELECT COUNT(*) AS null_created_at_count
FROM logs
WHERE created_at IS NULL;

image

Step 2 创建临时表 (Write)

CREATE TABLE logs_new LIKE logs;

Step 3 添加准备分区语句 (Read)

通过下面的 SQL,可以获得一个建表 SQL,对 DB 无任何副作用,可以放心执行

下面的 @start_date@end_date 两行,可以按照您的实际数据存储情况调整,下面的配置为创建 180 天前到 7 天后的分区,如果您的日志已经回收或删除过了,可以考虑缩减分区数量

SET time_zone = '+08:00'; 这里可以按照您的需求调整为具体的时区

分区必须提前创建,否则插入数据会有问题,所以您至少需要创建 7 天后的备用

SET SESSION group_concat_max_len = 1024 * 1024;
SET time_zone = '+08:00';
SET @start_date = DATE_SUB(CURDATE(), INTERVAL 180 DAY);
SET @end_date   = DATE_ADD(CURDATE(), INTERVAL 7 DAY);

WITH RECURSIVE dates AS (
    SELECT @start_date AS d
    UNION ALL
    SELECT DATE_ADD(d, INTERVAL 1 DAY)
    FROM dates
    WHERE d < @end_date
)
SELECT GROUP_CONCAT(
    CONCAT(
        '  PARTITION p', DATE_FORMAT(d, '%Y%m%d'),
        ' VALUES LESS THAN (',
        UNIX_TIMESTAMP(DATE_ADD(d, INTERVAL 1 DAY)),
        ')'
    )
    ORDER BY d
    SEPARATOR ',\n'
) INTO @parts
FROM dates;

SET @sql = CONCAT(
'ALTER TABLE logs_new
  MODIFY `created_at` bigint NOT NULL,
  DROP PRIMARY KEY,
  DROP INDEX `idx_created_at_id`,
  ADD PRIMARY KEY (`id`, `created_at`)
PARTITION BY RANGE (`created_at`) (
', @parts, ',
  PARTITION pmax VALUES LESS THAN MAXVALUE
)'
);

SELECT @sql;

Step 4 执行分区语句 (Write)

执行上一步输出的 SQL,会将新的 logs_new 表调整为分区表

Step 5 导入旧的数据 (Write)

取决于您的数据量,这一步可能会花费一些时间

INSERT INTO logs_new
SELECT *
FROM logs;

Step 6 校验数据已经导入完成 (Read)

如果您是停机更新,确保两条 SQL 输出的内容是一致的
如果您是在线更新,确保数据接近或一致

SELECT "logs" as `table`, COUNT(*), MIN(created_at), MAX(created_at) FROM logs
UNION
SELECT "logs_new" as `table`, COUNT(*), MIN(created_at), MAX(created_at) FROM logs_new;

image

Step 7 确认分区 (Read)

下面的 SQL 会打印新表的所有分区,以及每个分区的数据量

SELECT
  PARTITION_NAME,
  PARTITION_DESCRIPTION,
  TABLE_ROWS
FROM information_schema.PARTITIONS
WHERE TABLE_SCHEMA = DATABASE()
  AND TABLE_NAME = 'logs_new'
ORDER BY PARTITION_ORDINAL_POSITION;

image

Step 8 切表 (Write)

切换流量到新表,执行完成后请检查 NewAPI 各组件各页面是否能正常工作,特别是依赖日志的内容

RENAME TABLE logs TO logs_old, logs_new TO logs;

Step 9 创建自动回收创建/分区任务 (Write)

MySQL 不会自动创建或者删除分区,需要您创建定时任务来实现,下面会给出创建/删除分区的任务,您可以按需添加

Step 9.1 开启 MySQL Event Scheduler

在数据库中执行下面的命令,通常需要 root 用户,无需重启

SET GLOBAL event_scheduler = ON;

SHOW VARIABLES LIKE 'event_scheduler';

在配置文件的 mysqld 章节,增加下面的配置,无需重启
这一步是为了保证,即使后面重启数据库,Event Scheduler 仍然会是开启状态

[mysqld]
event_scheduler=ON
Step 9.2 创建新建分区任务

下面的两个 SQL 都需要执行

第一个 SQL 中的 SET time_zone = '+08:00'; 可按需修改为您的时区

第二个 SQL 中的时间您可以修改为适用于您的服务的时间,目前设置的是每天的 02:00,用于控制定时触发的时机

DELIMITER $$

DROP PROCEDURE IF EXISTS sp_logs_create_future_partitions$$

CREATE PROCEDURE sp_logs_create_future_partitions()
BEGIN
    DECLARE v_i INT DEFAULT 0;
    DECLARE v_d DATE;
    DECLARE v_partition_name VARCHAR(32);
    DECLARE v_less_than BIGINT;
    DECLARE v_exists INT DEFAULT 0;
    DECLARE v_sql TEXT;

    SET time_zone = '+08:00';

    WHILE v_i <= 7 DO
        SET v_d = DATE_ADD(CURDATE(), INTERVAL v_i DAY);
        SET v_partition_name = CONCAT('p', DATE_FORMAT(v_d, '%Y%m%d'));
        SET v_less_than = UNIX_TIMESTAMP(DATE_ADD(v_d, INTERVAL 1 DAY));

        SELECT COUNT(*)
        INTO v_exists
        FROM information_schema.PARTITIONS
        WHERE TABLE_SCHEMA = DATABASE()
          AND TABLE_NAME = 'logs'
          AND PARTITION_NAME = v_partition_name;

        IF v_exists = 0 THEN
            SET v_sql = CONCAT(
                'ALTER TABLE logs REORGANIZE PARTITION pmax INTO (',
                'PARTITION ', v_partition_name,
                ' VALUES LESS THAN (', v_less_than, '), ',
                'PARTITION pmax VALUES LESS THAN MAXVALUE',
                ')'
            );

            SET @sql = v_sql;
            PREPARE stmt FROM @sql;
            EXECUTE stmt;
            DEALLOCATE PREPARE stmt;
        END IF;

        SET v_i = v_i + 1;
    END WHILE;
END$$

DELIMITER ;
DROP EVENT IF EXISTS ev_logs_create_future_partitions;

CREATE EVENT ev_logs_create_future_partitions
ON SCHEDULE EVERY 1 DAY
STARTS TIMESTAMP(CURRENT_DATE, '02:00:00')
DO
  CALL sp_logs_create_future_partitions();
Step 9.3 创建删除分区任务 (可选)

这个是用于替代 NewAPI 自带的删除数据任务,如果您有需要可以配置这里的自动删除

第一个 SQL 中的 INTERVAL 180 DAY 表示删除 180 天之前的数据,可以按需修改,SET time_zone = '+08:00'; 也可按需修改为您的时区

第二个 SQL 中的时间您可以修改为适用于您的服务的时间,目前设置的是每天的 03:00,用于控制定时触发的时机

DELIMITER $$

DROP PROCEDURE IF EXISTS sp_logs_drop_old_partitions$$

CREATE PROCEDURE sp_logs_drop_old_partitions()
BEGIN
    DECLARE v_cutoff_date DATE;
    DECLARE v_cutoff_ts BIGINT;
    DECLARE v_drop_partitions TEXT;
    DECLARE v_sql TEXT;

    SET time_zone = '+08:00';

    SET v_cutoff_date = DATE_SUB(CURDATE(), INTERVAL 180 DAY);
    SET v_cutoff_ts = UNIX_TIMESTAMP(v_cutoff_date);

    SELECT GROUP_CONCAT(PARTITION_NAME ORDER BY PARTITION_ORDINAL_POSITION)
    INTO v_drop_partitions
    FROM information_schema.PARTITIONS
    WHERE TABLE_SCHEMA = DATABASE()
      AND TABLE_NAME = 'logs'
      AND PARTITION_NAME REGEXP '^p[0-9]{8}$'
      AND CAST(PARTITION_DESCRIPTION AS UNSIGNED) <= v_cutoff_ts;

    IF v_drop_partitions IS NOT NULL AND v_drop_partitions <> '' THEN
        SET v_sql = CONCAT(
            'ALTER TABLE logs DROP PARTITION ',
            v_drop_partitions
        );

        SET @sql = v_sql;
        PREPARE stmt FROM @sql;
        EXECUTE stmt;
        DEALLOCATE PREPARE stmt;
    END IF;
END$$

DELIMITER ;
DROP EVENT IF EXISTS ev_logs_drop_old_partitions;

CREATE EVENT ev_logs_drop_old_partitions
ON SCHEDULE EVERY 1 DAY
STARTS TIMESTAMP(CURRENT_DATE, '00:10:00')
DO
  CALL sp_logs_drop_old_partitions();
Step 9.4 检查任务
show events;

image

SHOW PROCEDURE STATUS WHERE Db = DATABASE();

image

Step 10 大功告成

一切已准备就绪,请使用吧!数据库将按照您的配置自动创建新的分片,回收旧的分片,后续如果有调整,也可以直接修改 SQL 配置再次执行。

您可以定期通过下面的 SQL 来检查分区任务的运行状态和分区的数据量,请检查 pmax 分区数据量为 0,且已经创建了 7 天后的分区

SELECT
  PARTITION_NAME,
  PARTITION_DESCRIPTION,
  TABLE_ROWS
FROM information_schema.PARTITIONS
WHERE TABLE_SCHEMA = DATABASE()
  AND TABLE_NAME = 'logs'
ORDER BY PARTITION_ORDINAL_POSITION DESC 
LIMIT 15;

image

1 个帖子 - 1 位参与者

阅读完整话题

来源: LinuxDo 最新话题查看原文