☰
Supabase 自动化定时任务与边缘调度:基于 pg_cron 的免运维数据归档
2026/10/8 7:28:12 网站建设 项目流程

在搭建独立微型 SaaS 或初创产品时,我们总免不了要处理各种各样的周期性定时任务:每天凌晨清理 30 天前的临时上传缓存、每月 1 号归档已完结的账单流水、每周汇总活跃用户的用量指标,或者定时将冷数据搬迁至廉价的对象存储中。

我见过不少技术团队为了跑这几个简单的定时任务,特意买了一台 2 核 4G 的云服务器,上面挂着 PM2、Node-cron、Redis 甚至重型的 Celery 调度集群。这种做法不仅每月白白烧掉几十美元的云账单,更要命的是你多了一个需要打安全补丁、监控内存泄露和处理进程挂起的物理单点。

作为极致追求“免运维(NoOps)”与高成本效益比的独立开发者,能让数据库和 Serverless 解决的事情,我绝不多买一台服务器。今天我将详解如何利用Supabase 内置的pg_cron扩展与边缘调度(Edge Functions),把定时任务直接植入 PostgreSQL 引擎内核,打造一套零常驻服务器、零外部网络跳数的全自动免运维数据归档系统。


一、为什么pg_cron是独立开发者的神器?

pg_cron是 Citus Data 为 PostgreSQL 开发的一个极其实用的内核级调度扩展。它直接作为后台工作线程(Background Worker)常驻在 PostgreSQL 进程池内。

相比于在外部跑常驻脚本,pg_cron拥有三大碾压级优势:

  1. 零外部网络开销与鉴权延迟:任务直接在数据库内部通过 SQL 执行,不需要经过公网 HTTP 握手或 VPC 转发,原本通过 Node.js 处理上百万条历史日志归档需要拉取回本地,而内联 SQL 批处理耗时往往只有几百毫秒。
  2. 零常驻基建成本:直接复用 Supabase 已有的数据库计算资源,不需要额外部署任何独立的调度虚拟机(Worker Droplet)。
  3. 支持标准 Cron 表达式与灵活事务管理:分秒级定义触发周期,天然支持 PostgreSQL 事务回滚与隔离机制。

基于这一套哲学,整个定时归档管线完全在数据库内核与边缘函数之间静默自驱流转:

  • 内核调度触发:PostgreSQL 内部常驻的pg_cron工作线程在每天凌晨 02:00 定时唤醒,直接在数据库内存空间调度预设存储过程;
  • 小步快跑分批归档:调用专用归档 SQL 函数,利用游标分批将 30 天前的存量冷日志以微批事务迁移至audit_logs_archive表,全程零长事务、零锁表隐患;
  • 边缘异步拓展(pg_net):归档完成后,通过pg_net扩展向外部发出非阻塞异步 HTTP POST 请求,唤醒轻量的 Supabase Edge Function;
  • 冷备与监控闭环:Edge Function 打包冷数据 JSON 压缩包,直接推送转存至廉价的 Cloudflare R2 对象存储,并在 Telegram 运维群静默播报归档条目与耗时。

二、开启并配置pg_cron扩展环境

在 Supabase 控制台的 SQL Editor 中,或者在你的本地迁移文件中,我们首先需要启用pg_cron以及负责异步网络调用的pg_net扩展:

-- 启用 pg_cron 与 pg_net 扩展 CREATE EXTENSION IF NOT EXISTS pg_cron; CREATE EXTENSION IF NOT EXISTS pg_net; -- 授予 cron schema 权限给 postgres 超级管理员角色 GRANT USAGE ON SCHEMA cron TO postgres;

三、生产级分批归档 SQL 编写(拒绝大事务锁表)

很多开发者写归档脚本时,最容易犯的一个致命低级错误就是直接写:
DELETE FROM audit_logs WHERE created_at < NOW() - INTERVAL '30 days';

如果你的审计日志表存了数百万行数据,这样一条没有批次限制的巨型DELETE会瞬间打满数据库 I/O,锁死整张表,触发长达数分钟的长事务,导致线上所有正常的业务读写被全部阻塞,最终引发数据库连接池雪崩。

高可靠的归档函数必须采用游标分批迭代(Chunked Batch Processing),并在每次清理后主动让出锁资源:

-- 创建专用的归档存储表(冷数据表) CREATE TABLE IF NOT EXISTS audit_logs_archive ( id UUID PRIMARY KEY, user_id UUID, action_type VARCHAR(64), payload JSONB, created_at TIMESTAMPTZ, archived_at TIMESTAMPTZ DEFAULT NOW() ); -- 创建分批安全归档清洗函数 CREATE OR REPLACE FUNCTION archive_old_audit_logs( batch_size INT DEFAULT 5000, retention_days INT DEFAULT 30 ) RETURNS JSONB LANGUAGE plpgsql SECURITY DEFINER AS $$ DECLARE deleted_count INT := 0; total_archived INT := 0; cutoff_date TIMESTAMPTZ; BEGIN cutoff_date := NOW() - (retention_days || ' days')::INTERVAL; LOOP -- 1. 使用 CTE 结构:原子地将过期数据搬移到归档表,并从热表中删除 WITH moved_rows AS ( DELETE FROM audit_logs WHERE id IN ( SELECT id FROM audit_logs WHERE created_at < cutoff_date ORDER BY created_at ASC LIMIT batch_size FOR UPDATE SKIP LOCKED -- 避免高并发冲突 ) RETURNING id, user_id, action_type, payload, created_at ) INSERT INTO audit_logs_archive (id, user_id, action_type, payload, created_at) SELECT id, user_id, action_type, payload, created_at FROM moved_rows; -- 获取本次批处理搬迁的行数 GET DIAGNOSTICS deleted_count = ROW_COUNT; total_archived := total_archived + deleted_count; -- 如果处理数量小于预设 batch_size,说明已经没有更多过期数据,跳出循环 EXIT WHEN deleted_count < batch_size; -- 短暂休眠 50ms,释放 CPU 与 WAL 刷盘压力,避免影响在线业务 PERFORM pg_sleep(0.05); END LOOP; RETURN jsonb_build_object( 'status', 'success', 'total_archived', total_archived, 'cutoff_date', cutoff_date ); END; $$;

四、调度任务编排与边缘函数告警联动

编写好函数后,我们通过cron.schedule将任务设定在每天业务低峰期的凌晨 3:00 自动运行。

更强大的是,结合pg_net,我们可以在归档完成后直接通过边缘函数向独立开发者的手机推送执行报告:

-- 编排定时归档任务:每天凌晨 03:00 触发 SELECT cron.schedule( 'nightly-audit-log-archive', -- 任务唯一标识名 '0 3 * * *', -- 标准 Cron 语法 $$ DO $$ DECLARE archive_res JSONB; BEGIN -- 执行分批归档 archive_res := archive_old_audit_logs(5000, 30); -- 如果归档数量大于 0,调用外部 Webhook 发送 Telegram 状态通知 IF (archive_res->>'total_archived')::INT > 0 THEN PERFORM net.http_post( url := 'https://your-project.functions.supabase.co/notify-archive', headers := jsonb_build_object( 'Content-Type', 'application/json', 'Authorization', 'Bearer YOUR_SERVICE_ROLE_KEY' ), body := archive_res ); END IF; END $$; $$ );

我们写一个极简的 Supabase Edge Function(TypeScript)来消费这个通知:

// supabase/functions/notify-archive/index.ts import { serve } from 'https://deno.land/std@0.168.0/http/server.ts'; serve(async (req) => { const { total_archived, cutoff_date } = await req.json(); const botToken = Deno.env.get('TELEGRAM_BOT_TOKEN'); const chatId = Deno.env.get('TELEGRAM_CHAT_ID'); const text = `🧹【数据归档巡检】\n成功清理并归档 ${cutoff_date} 前的冷数据:${total_archived} 行。\n数据库主表无锁等待,性能指标健康。`; // 推送至开发者移动端 await fetch(`https://api.telegram.org/bot${botToken}/sendMessage`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ chat_id: chatId, text }), }); return new Response(JSON.stringify({ ok: true }), { headers: { 'Content-Type': 'application/json' } }); });

五、定时任务巡检与运维自愈排查

把任务放进数据库之后,绝不能变成看不见的“黑盒子”。pg_cron提供了极其透明的执行历史视图cron.job_run_details。

我们可以随时通过以下 SQL 审查所有历史任务的执行耗时、是否成功以及报错信息:

-- 查看最近 20 次定时任务的执行轨迹与状态 SELECT jobid, runid, job_pid, status, return_message, start_time, end_time, round(EXTRACT(EPOCH FROM (end_time - start_time))::numeric, 2) AS duration_seconds FROM cron.job_run_details ORDER BY start_time DESC LIMIT 20;

如果某次任务因为内存不足或锁超时抛出异常,状态会直接显示为failed,并精准记录return_message。我们还可以编写一个简单的健康探针,在检测到最近 24 小时存在failed记录时自动触发邮件预警。


六、极简架构带来的长期收益

这套基于pg_cron的免运维定时归档方案上线运行了整整半年,给我们的技术栈带来了实实在在的解脱:

  1. 服务器账单直接抹平:干掉了过去常驻的一台定时任务 Worker 服务器,每年节省数百美元硬件成本。
  2. 核心热表读写延迟极低:由于audit_logs热表始终被控制在 30 天以内,索引树体积保持紧凑,后台 API 的查询延迟始终稳定在 5ms 左右。
  3. 心智负担归零:不需要担心 Node 进程崩溃,不需要维护复杂的进程守护。数据库本身就是最坚固的守护神。

独立开发者的核心竞争力永远在于用最小的系统复杂度撬动最大的业务稳定性。让数据库干它最擅长的事情,把双手解放出来去打磨更有价值的核心特性。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询