跳转至

ThingsBoard--使用K3s CronJob自动清理PostgreSQL按日分区表

在管理PostgreSQL数据库时,按日分区是处理时间序列数据的常见策略。然而,随着数据量增长,旧分区会占用大量存储空间,需要定期清理。本文将介绍如何在K3s集群中配置一个CronJob,每天自动清理ts_kv表中30天前的按日分区(分区命名格式为ts_kv_YYYY_MM_DD,如ts_kv_2025_06_01),并确保日志输出详细以便监控。

问题背景

本文的前提是你的 ThingsBoard 已经部署在 K3s 集群中,并且已经安装了 PostgreSQL 数据库。

在我们的场景中,ts_kv表存储时间序列数据,按日分区以提高查询效率和数据管理能力。分区名称遵循ts_kv_YYYY_MM_DD格式,例如ts_kv_2025_06_01。为了避免存储空间浪费,我们需要每天删除30天前的分区,同时记录详细的清理日志以便排查问题。

我们选择K3s(轻量级Kubernetes发行版)来运行CronJob,利用psql命令直接与PostgreSQL交互,执行清理逻辑。这种方法轻量、可靠,适合生产环境。

解决方案

我们将创建一个K3s CronJob,每天凌晨1点(上海时间)运行,执行以下任务:

  • 连接到PostgreSQL数据库。
  • 查询ts_kv表的所有分区(ts_kv_%)。
  • 删除早于30天的分区(基于ts_kv_YYYY_MM_DD命名)。
  • 输出详细日志,记录每个分区的检查和删除操作。

技术栈

  • K3s:运行CronJob的轻量级Kubernetes集群。
  • PostgreSQL:存储ts_kv表的数据库,使用官方postgres:12镜像运行psql。
  • Secret:通过K3s Secret管理数据库凭据。
  • SQL:使用PL/pgSQL脚本处理分区清理逻辑。

实现步骤

创建数据库凭据Secret

为了安全存储PostgreSQL的用户名和密码,我们使用K3s Secret。以下是Secret的YAML定义:

apiVersion: v1
kind: Secret
metadata:
  name: postgres-credentials
  namespace: default
type: Opaque
data:
  DB_USER: cG9zdGdyZXM=         # base64编码的用户名,例如 'postgres'
  DB_PASSWORD: eW91cl9wYXNzd29yZA==  # base64编码的密码

生成base64编码:

echo -n 'your_user' | base64
echo -n 'your_password' | base64

应用Secret:

kubectl apply -f postgres-credentials.yaml

创建CronJob

我们使用postgres:12镜像运行CronJob,通过psql执行SQL脚本清理分区。以下是CronJob的YAML定义:

apiVersion: batch/v1
kind: CronJob
metadata:
  name: cleanup-ts-kv-partitions
  namespace: default # 替换为你的命名空间
spec:
  schedule: "0 1 * * *"  # 每天凌晨1点
  jobTemplate:
    spec:
      template:
        spec:
          containers:
          - name: cleanup-partitions
            image: hub.yiqisoft.cn/library/postgres:12
            command:
            - /bin/sh
            - -c
            - |
              psql -h $DB_HOST -U $DB_USER -d $DB_NAME -c "
              DO \$\$
              DECLARE
                  r RECORD;
                  cutoff_date DATE := CURRENT_DATE - INTERVAL '30 days'; # 30天前的日期
                  cutoff_str TEXT;
                  partition_count INTEGER := 0;
                  dropped_count INTEGER := 0;
              BEGIN
                  SET TIME ZONE 'Asia/Shanghai';
                  RAISE NOTICE 'Starting partition cleanup at %', current_timestamp;
                  RAISE NOTICE 'Cutoff date: %', cutoff_date;

                  cutoff_str := to_char(cutoff_date, 'YYYY_MM_DD');

                  FOR r IN (
                      SELECT tablename 
                      FROM pg_tables 
                      WHERE schemaname = 'public' 
                      AND tablename LIKE 'ts_kv_%'
                  ) LOOP
                      partition_count := partition_count + 1;
                      RAISE NOTICE 'Checking partition: %', r.tablename;

                      IF r.tablename < 'ts_kv_' || cutoff_str THEN
                          BEGIN
                              EXECUTE 'DROP TABLE IF EXISTS ' || quote_ident(r.tablename);
                              dropped_count := dropped_count + 1;
                              RAISE NOTICE 'Successfully dropped partition: %', r.tablename;
                          EXCEPTION
                              WHEN OTHERS THEN
                                  RAISE WARNING 'Failed to drop partition %: %', r.tablename, SQLERRM;
                          END;
                      ELSE
                          RAISE NOTICE 'Partition % is within retention period, keeping it.', r.tablename;
                      END IF;
                  END LOOP;

                  RAISE NOTICE 'Cleanup completed. Checked % partitions, dropped % partitions.', partition_count, dropped_count;
              EXCEPTION
                  WHEN OTHERS THEN
                      RAISE WARNING 'Cleanup process failed: %', SQLERRM;
                      RAISE EXCEPTION 'Terminating due to error';
              END \$\$;"
            env:
            - name: DB_HOST
              value: "postgres-service"  # 替换为你的PostgreSQL服务名
            - name: DB_NAME
              value: "your_database"     # 替换为你的数据库名
            - name: DB_USER
              valueFrom:
                secretKeyRef:
                  name: postgres-credentials
                  key: DB_USER
            - name: PGPASSWORD
              valueFrom:
                secretKeyRef:
                  name: postgres-credentials
                  key: DB_PASSWORD
          restartPolicy: OnFailure

关键点:

  • 调度:schedule: "0 1 * * *"表示每天凌晨1点(HKT)运行。
  • 时区:SET TIME ZONE 'Asia/Shanghai';确保日期计算和日志使用上海时区。
  • 分区逻辑:使用to_char(cutoff_date, 'YYYY_MM_DD')生成YYYY_MM_DD格式,匹配ts_kv_YYYY_MM_DD。
  • 日志:记录清理开始时间、截止日期、每个分区的状态(删除或保留)、总计检查和删除的分区数。

应用CronJob:

kubectl apply -f cleanup-ts-kv-cronjob.yaml

验证执行结果

检查CronJob:

kubectl get cronjobs

手动触发Job(测试):

kubectl create job --from=cronjob/cleanup-ts-kv-partitions manual-test

查看日志:

kubectl logs <pod-name>

示例日志:

NOTICE: Starting partition cleanup at 2025-06-09 01:00:00.123456+08
NOTICE: Cutoff date: 2025-05-10
NOTICE: Checking partition: ts_kv_2025_05_01
NOTICE: Successfully dropped partition: ts_kv_2025_05_01
NOTICE: Checking partition: ts_kv_2025_06_01
NOTICE: Partition ts_kv_2025_06_01 is within retention period, keeping it.
NOTICE: Cleanup completed. Checked 2 partitions, dropped 1 partitions.

如果发生错误:

NOTICE: Checking partition: ts_kv_2025_05_01
WARNING: Failed to drop partition ts_kv_2025_05_01: permission denied

注意事项

  • 分区命名:脚本假设分区名为ts_kv_YYYY_MM_DD。如果格式不同,需调整to_char和比较逻辑。
  • 权限:确保数据库用户有删除分区的权限,否则会记录permission denied错误。
  • 备份:生产环境中,建议在删除分区前备份数据。
  • 日志监控:通过kubectl logs查看详细日志。考虑集成日志收集工具(如Fluentd或Loki)以便长期监控。
  • 网络:确保CronJob pod能通过DB_HOST访问PostgreSQL服务。
  • 调试:若日志不完整,可在psql命令加-e选项回显SQL语句。

总结

通过K3s CronJob和psql,我们可以高效地自动化清理PostgreSQL按日分区表,释放存储空间,同时通过详细日志监控执行情况。这种方案轻量、可靠,适合在K3s集群中管理时间序列数据。如果你需要进一步优化(例如集成通知、调整保留天数),可以扩展SQL脚本或添加外部工具。

希望这篇文章对你管理PostgreSQL分区表有所帮助!如果有其他问题,欢迎在评论区交流。