監控與 pg_cron/pgmq:讓自架 Supabase 看得住、跑得動 | Supabase 完整教學
上一篇我們補上了自架 Supabase 的備份、升級與上線檢查——讓系統「架得起來、養得好」。這一篇要補上最後一塊:看得住、跑得動。你會學到怎麼用 Studio Reports/Logs 與
pg_stat_statements抓出拖垮效能的慢查詢、怎麼接 Prometheus/Grafana 的 metrics endpoint 監控連線數/CPU/磁碟並設告警;還有怎麼用pg_cron在資料庫內排程定時 SQL 任務、用pgmq把 PostgreSQL 當成訊息佇列,在資料庫層就把非同步背景工作處理掉。這是自架段(SB-5)的第三篇。
前言
先用一句話定義本篇的兩個主題:監控,是讓你在使用者發現問題之前就先發現問題;而 pg_cron 與 pgmq,則是讓你不必額外架 Redis 或 cron 伺服器,就能在資料庫內部把定時任務與非同步佇列處理掉。 前者關乎「看得住」,後者關乎「跑得動」。
用一個類比理解「監控」是什麼。如果說前一篇的備份是幫房子保火險,那監控就是裝煙霧偵測器加保全系統。 火險是出事之後把損失補回來,煙霧偵測器則是在火苗剛冒出來、還沒燒起來時就叫醒你。自架 Supabase 沒有雲端幫你盯著的儀表板與告警,磁碟悄悄滿了、某個容器半夜被 OOM Kill、慢查詢把連線池吃光——這些若沒有監控,你只會在使用者哀嚎「網站掛了」時才知道,而那通常已是數小時後。
再用一個類比理解 pg_cron 與 pgmq。它們讓 PostgreSQL 從「只會被動回答查詢的資料庫」,變成「會自己定時做家事、還能排隊處理待辦事項的員工」。 pg_cron 是那個會準時打卡、每天凌晨三點自動清垃圾的員工;pgmq 則是門口那個待辦事項的收件匣,訊息一筆筆排隊等 Worker 來領走處理。兩者搭起來,你就能在資料庫層組出一整套背景任務系統——而且少養一個要維運的元件。
本篇你會學到:
- 監控三層:Studio Reports/Logs 日常巡檢、
pg_stat_statements抓慢查詢、Prometheus/Grafana 長期趨勢與告警 - 關鍵告警:連線數、CPU、磁碟、慢查詢、容器重啟該在哪條線上設警戒
pg_cron排程:用cron.schedule在資料庫內定義定時 SQL 任務,並管理與查看執行歷史pgmq佇列:用send/read/archive在 PostgreSQL 裡做非同步訊息佇列,搭配pg_cron組出完整 Worker 流程
本篇聚焦「監控」與「排程/佇列 extension」。Supabase 的 metrics endpoint 路徑、認證方式、Studio 各頁面的欄位,以及擴充功能的可用版本,都可能隨版本演進而變動;正式導入前一律以官方文件與你實際部署的版本為準,文中出現的具體路徑與 API 細節請當作方向而非逐字照抄。
核心概念
在動手之前,先建立三張心智圖:一是「監控該分成哪幾層、各層看什麼」,二是「pg_cron 在資料庫層扮演什麼角色」,三是「pgmq 為什麼能取代外部訊息佇列」。
一、監控的三個層次
自架 Supabase 的監控,務實地可以分成由淺到深的三層,對應不同的使用時機:
| 層次 | 工具 | 看什麼 | 適用時機 |
|---|---|---|---|
| 內建儀表 | Studio Reports / Logs | CPU、記憶體、連線數、請求量圖表;各服務日誌 | 日常巡檢、臨時排錯 |
| 資料庫內部 | pg_stat_statements | 每種查詢的執行次數與累積耗時 | 抓慢查詢、找效能瓶頸 |
| 外接可觀測性 | Prometheus + Grafana | metrics endpoint 的約 200 個指標 | 長期趨勢、自動告警 |
理解這三層的關鍵,是搞懂它們回答的問題不同:
- Studio Reports/Logs 回答「現在系統健康嗎?剛剛那個錯誤是哪個服務噴的?」。Reports 頁提供資料庫層級的圖表(CPU、記憶體、磁碟 I/O、連線數等),Logs 頁能分別查 Postgres、Auth(GoTrue)、PostgREST、Realtime、Storage 各服務的日誌。這是最省事的第一站,適合每天掃一眼、出錯時快速定位。
pg_stat_statements回答「到底是哪條查詢在拖慢我的資料庫?」。它是 PostgreSQL 的擴充功能,會累積記錄每一種查詢範式(query pattern)的呼叫次數、總耗時、平均耗時。當你的 API 開始變慢,答案幾乎都藏在這張表裡——通常是某條漏建索引、或 RLS 政策沒優化的查詢在做全表掃描。- Prometheus + Grafana 回答「過去一週的連線數趨勢如何?磁碟什麼時候會滿?」。Supabase 每個實例暴露一個 metrics endpoint(路徑約為
/customer/v1/privileged/metrics,實際路徑與認證方式以官方文件為準),裡面有約 200 個 PostgreSQL 效能指標。用 Prometheus 定期抓、Grafana 畫成儀表板,再對關鍵指標設告警規則,才能做到「在出事前主動通知你」。
一句話選型:先用 Studio Reports/Logs 建立日常感覺,把 pg_stat_statements 打開抓慢查詢,等上了正式流量再接 Prometheus/Grafana 做趨勢與告警。
二、pg_cron:把排程搬進資料庫
pg_cron 是 PostgreSQL 的排程擴充功能,讓你直接在資料庫內部定義 Cron 排程,不必在作業系統層另外設 crontab、也不必架一台專門跑排程的伺服器。Supabase 的 PostgreSQL 映像通常已預先包含此擴充功能。
它的語法核心就一個函式:
cron.schedule('任務名稱', '* * * * *', $$ 要執行的 SQL $$);
第二個參數是標準的 5 欄位 Cron 表達式(分、時、日、月、星期),第三個參數是任務名稱到期時要跑的 SQL。常見用途包括:定時清理過期資料、定期刷新 Materialized View、每分鐘觸發一次 Worker 去消費佇列。在資料庫層做排程的最大價值,是「少一個要維運的元件」——自架本來就有十幾個容器要顧,能用資料庫內建功能解決,就不必再多養一套排程系統。
三、pgmq:用 PostgreSQL 當訊息佇列
pgmq(PostgreSQL Message Queue)是以 PostgreSQL 原生資料表和函式實作的訊息佇列。過去你要做非同步任務(寄信、產報表、呼叫外部 API),常會架一套 Redis 或 RabbitMQ 當 message broker;pgmq 讓你不必多養這套基礎設施,直接在資料庫裡就有一個佇列。
它的核心是一組函式,對應佇列的生命週期:
| 動作 | 函式 | 說明 |
|---|---|---|
| 建立佇列 | pgmq.create('佇列名') | 建立一個持久佇列 |
| 送訊息 | pgmq.send('佇列名', jsonb) | 把一筆 JSON 訊息放進佇列 |
| 讀訊息 | pgmq.read('佇列名', vt, n) | 讀 n 筆,並鎖定 vt 秒(可見性超時) |
| 確認完成 | pgmq.archive(...) / pgmq.delete(...) | 歸檔(保留審計)或刪除 |
其中最關鍵的概念是可見性超時(visibility timeout,vt):當 Worker read 出一筆訊息,這筆訊息會被「鎖定」vt 秒,期間其他 Worker 看不到它。如果 Worker 在 vt 秒內成功處理完並 archive/delete,訊息就消失;如果 Worker 崩潰或處理失敗、沒去確認,vt 到期後訊息會自動重新可見,被下一輪讀取——這就是佇列的「至少一次(at-least-once)」重試機制。
把 pgmq 與 pg_cron 組起來,就是一套完整的背景任務系統:pgmq 當收件匣存待辦訊息,pg_cron 每分鐘觸發一個 Worker 函式去 read 佇列、處理、archive。 下面實作會把這套流程完整攤開。
實作範例
這一節把監控與排程/佇列拆成可執行的步驟:① pg_stat_statements 抓慢查詢 → ② 連線/資源即時監控 SQL → ③ Prometheus/Grafana 與告警 → ④ pg_cron 定時任務 → ⑤ pgmq 佇列 + Worker。以下 SQL 可在 Studio 的 SQL Editor 或連進資料庫後執行。
一、用 pg_stat_statements 抓慢查詢
第一步先把擴充功能打開(Supabase 通常已在 shared_preload_libraries 預載,若沒有需在 postgresql.conf 加入後重啟 db 容器):
-- 啟用查詢統計擴充功能
CREATE EXTENSION IF NOT EXISTS pg_stat_statements;
接著就能找出「最花時間」的查詢。下面這條是效能調校最常用的一句 SQL——按總累積執行時間排序,揪出真正拖垮資料庫的查詢:
-- 找出最慢的前 20 條查詢(按總執行時間排序)
SELECT
round(total_exec_time::numeric, 2) AS total_time_ms, -- 累積總耗時
calls, -- 被呼叫幾次
round(mean_exec_time::numeric, 2) AS mean_time_ms, -- 平均每次耗時
round((100 * total_exec_time
/ sum(total_exec_time) OVER ())::numeric, 2) AS pct, -- 佔總耗時百分比
query
FROM pg_stat_statements
ORDER BY total_exec_time DESC
LIMIT 20;
判讀的訣竅:不要只看「平均耗時最高」的查詢,更要看「總耗時最高」的。 一條平均只要 5ms、但一天被呼叫一百萬次的查詢,對系統的壓力遠大於一條平均 500ms、但一天只跑三次的報表查詢。pct 欄位直接告訴你「這條查詢吃掉了資料庫多少比例的時間」,最上面幾條就是你該優先優化的對象。
想進一步確認某張表是不是在做全表掃描(通常代表漏建索引),可以查 pg_stat_user_tables:
-- 找出 seq scan(全表掃描)次數偏高的資料表
SELECT
relname AS table_name,
seq_scan, -- 全表掃描次數
seq_tup_read, -- 全表掃描讀了幾列
idx_scan -- 用索引掃描的次數
FROM pg_stat_user_tables
ORDER BY seq_scan DESC
LIMIT 20;
如果某張大表的 seq_scan 遠高於 idx_scan,通常就是該建索引了——特別是 RLS 政策裡拿來比對的 user_id 這種欄位。
二、即時監控連線與資源
慢查詢之外,「現在系統的即時狀態」也要能隨手查。下面幾條 SQL 是自架維運的常備工具。
連線數是自架最容易爆的資源之一(Supavisor + 各服務會吃掉不少連線),先看目前連線分布:
-- 依連線狀態統計目前連線數
SELECT
state,
count(*) AS conn_count,
max(now() - state_change) AS max_duration -- 該狀態最久的一條停多久
FROM pg_stat_activity
WHERE pid <> pg_backend_pid() -- 排除這條查詢自己
GROUP BY state
ORDER BY conn_count DESC;
如果 idle in transaction 的連線很多、又停很久,那是應用程式開了交易卻忘了收尾,會白白佔住連線,是自架常見的隱形殺手:
-- 揪出停在 idle in transaction 超過 5 分鐘的連線
SELECT pid, usename, now() - state_change AS idle_duration, query
FROM pg_stat_activity
WHERE state = 'idle in transaction'
AND now() - state_change > interval '5 minutes'
ORDER BY idle_duration DESC;
磁碟空間則是另一條該盯的線。查各資料表(含索引)佔用大小,提早發現異常膨脹:
-- 列出佔空間最大的前 10 張表(含索引)
SELECT
relname AS table_name,
pg_size_pretty(pg_total_relation_size(relid)) AS total_size
FROM pg_catalog.pg_statio_user_tables
ORDER BY pg_total_relation_size(relid) DESC
LIMIT 10;
容器層的即時資源(CPU/記憶體/OOM),則用 Docker 指令看——這在前一篇也提過,是自架監控的基本功:
# 即時監控所有容器資源使用
docker stats
# 過濾最近一小時的錯誤日誌
docker compose logs --since 1h 2>&1 | grep -E "(ERROR|FATAL|PANIC)"
三、Prometheus/Grafana 與告警
當你要做「長期趨勢」與「自動告警」,就得把指標吐給 Prometheus。Supabase 每個實例會暴露一個 metrics endpoint,路徑約為 /customer/v1/privileged/metrics(實際路徑與認證方式請以你部署的版本與官方文件為準),回傳 Prometheus 格式的約 200 個 PostgreSQL 指標。
Prometheus 的抓取設定大致長這樣(示意,認證方式依你的部署調整):
# prometheus.yml(示意)
scrape_configs:
- job_name: 'supabase'
metrics_path: '/customer/v1/privileged/metrics'
scheme: https
basic_auth:
username: 'service_role'
password: 'your-service-role-key' # 依官方文件的認證方式設定
static_configs:
- targets: ['supabase.yourdomain.com']
抓到指標後,重點是設對告警線。以下是自架 Supabase 最該設的幾條關鍵告警,把它們當成你的「煙霧偵測器」:
| 告警項目 | 建議警戒線 | 為什麼重要 |
|---|---|---|
| 連線數 | 超過 max_connections 的 90%(如 200 的 180) | 連線耗盡後新請求全部被拒 |
| 平均查詢時間 | 持續超過 1000ms | 效能劣化,通常是慢查詢或缺索引 |
| 磁碟使用率 | 超過 80% | 磁碟滿了 PostgreSQL 會直接停擺 |
| CPU 使用率 | 持續超過 85% | 資源不足,需優化查詢或升級 |
| 容器重啟 | 15 分鐘內重啟超過 2 次 | 通常是 OOM Kill 或崩潰迴圈 |
Grafana 端則是把 Prometheus 當資料源,匯入或自建 PostgreSQL 儀表板,把上面這些指標畫成時序圖。若你不想一開始就上 Prometheus/Grafana 這套,至少也要接一個外部 uptime 監控(如 UptimeRobot、Better Uptime)定時打你的健康檢查端點,加上磁碟與憑證到期的告警——這是「出事有人通知」的最低門檻。
四、pg_cron:資料庫內的定時任務
接著換排程。先啟用擴充功能(Supabase 映像通常已含):
CREATE EXTENSION IF NOT EXISTS pg_cron;
建立排程任務就靠 cron.schedule。下面示範三個典型任務:
-- 每天凌晨 3 點清理過期 session
SELECT cron.schedule(
'clean-expired-sessions',
'0 3 * * *',
$$DELETE FROM auth.sessions WHERE expires_at < NOW()$$
);
-- 每 15 分鐘刷新 Materialized View(把重運算結果快取起來)
SELECT cron.schedule(
'refresh-user-stats',
'*/15 * * * *',
'REFRESH MATERIALIZED VIEW CONCURRENTLY public.user_stats'
);
-- 每分鐘觸發一次佇列 Worker(下一節會用到)
SELECT cron.schedule(
'process-email-queue',
'* * * * *',
'SELECT public.process_email_queue()'
);
$$ ... $$ 是 PostgreSQL 的 dollar-quoting,讓你在 SQL 字串裡放單引號不必跳脫,寫 DELETE/UPDATE 這種帶條件的任務時特別方便。
排程建好後,一定要會管理與查看執行狀況——這是「cron 任務失敗有沒有告警」的關鍵:
-- 列出所有已排程任務
SELECT * FROM cron.job;
-- 查看最近的執行歷史(status 會顯示 succeeded / failed)
SELECT jobid, status, return_message, start_time, end_time
FROM cron.job_run_details
ORDER BY start_time DESC
LIMIT 20;
-- 暫停 / 刪除任務
SELECT cron.alter_job((SELECT jobid FROM cron.job WHERE jobname = 'refresh-user-stats'), active := false);
SELECT cron.unschedule('clean-expired-sessions');
一個 Docker 自架必踩的坑:如果 pg_cron 任務要透過 pg_net 呼叫 Edge Function,URL 不能用 localhost——因為在 db 容器內 localhost 指的是 db 容器自己,到不了 Kong。要用 Docker 服務名稱:
CREATE EXTENSION IF NOT EXISTS pg_net;
-- ✅ 正確:用容器服務名 kong,不是 localhost
SELECT cron.schedule(
'trigger-edge-function',
'*/5 * * * *',
$$
SELECT net.http_post(
url := 'http://kong:8000/functions/v1/process',
headers := jsonb_build_object(
'Content-Type', 'application/json',
'Authorization', 'Bearer ' || current_setting('app.service_role_key')
),
body := '{"trigger": "cron"}'::jsonb
);
$$
);
pg_cron有兩個限制要記住:最多同時執行 32 個任務;且不支援跨 Replica 的分散式鎖(所以任務要設計成冪等,避免重疊執行出問題)。
五、pgmq:資料庫層的訊息佇列 + Worker
最後把非同步佇列組起來。啟用擴充功能後,建佇列、送訊息:
CREATE EXTENSION IF NOT EXISTS pgmq;
-- 建立一個持久佇列
SELECT pgmq.create('email_notifications');
-- 送一筆訊息(JSON 內容)
SELECT pgmq.send(
'email_notifications',
'{"to": "user@example.com", "template": "welcome"}'::jsonb
);
消費端的關鍵是 read 的可見性超時與消費後的確認:
-- 讀 1 筆,鎖定 30 秒(30 秒內其他 Worker 看不到這筆)
SELECT * FROM pgmq.read('email_notifications', 30, 1);
-- 處理成功後二選一:
SELECT pgmq.archive('email_notifications', 123); -- 歸檔(移到 archive 表,可審計)
SELECT pgmq.delete('email_notifications', 123); -- 直接刪除(不需保留)
把讀取、處理、確認包成一個 Worker 函式,再用前面 pg_cron 的 process-email-queue 每分鐘觸發它,就是完整的背景任務流程:
CREATE OR REPLACE FUNCTION public.process_email_queue()
RETURNS void AS $$
DECLARE
msg RECORD;
BEGIN
-- 一次最多處理 10 筆,每筆鎖定 30 秒
FOR msg IN SELECT * FROM pgmq.read('email_notifications', 30, 10)
LOOP
BEGIN
-- 呼叫 Edge Function 實際寄信(注意用容器名 kong,不是 localhost)
PERFORM net.http_post(
url := 'http://kong:8000/functions/v1/send-email',
headers := jsonb_build_object(
'Content-Type', 'application/json',
'Authorization', 'Bearer ' || current_setting('app.service_role_key')
),
body := msg.message
);
-- 成功 → 歸檔
PERFORM pgmq.archive('email_notifications', msg.msg_id);
EXCEPTION WHEN OTHERS THEN
-- 失敗 → 不確認,讓可見性超時到期後自動重試
RAISE WARNING '處理訊息 % 失敗:%', msg.msg_id, SQLERRM;
END;
END LOOP;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
這段的設計精髓在於 EXCEPTION 區塊:成功才 archive,失敗就什麼都不做,讓那筆訊息在 vt(30 秒)到期後重新可見、下一輪再試。 這就是佇列「失敗自動重試」的本質。
最後,佇列本身也要監控。用 pgmq.metrics 看佇列長度與最舊訊息年齡,避免訊息默默積壓:
-- 查看單一佇列健康:queue_length(待處理數)、oldest_msg_age_sec(最舊訊息年齡)
SELECT * FROM pgmq.metrics('email_notifications');
如果 queue_length 一直往上、oldest_msg_age_sec 越來越大,代表 Worker 消費不過來(或一直失敗重試),該去查 Worker 的錯誤日誌了。
常見錯誤與最佳實踐
坑一:上線後從不看慢查詢。
很多人把 pg_stat_statements 打開就忘了它的存在,直到資料量長大、某條查詢卡到 API 逾時,才回頭發現有條 RLS 政策或漏建索引的查詢一直在做全表掃描。正確做法:把「看 pg_stat_statements 前二十條」排進每週例行巡檢;看到 pct 高、又是全表掃描的查詢,優先建索引或用 (SELECT auth.uid()) 優化 RLS 政策。監控的價值,在於你真的定期去看它。
坑二:cron 任務沒做冪等,失敗也沒告警。
排程任務可能因為前一次還沒跑完就重疊執行,或中途失敗。如果任務不是冪等(重跑一次會出錯或產生重複資料)、又沒人監看 cron.job_run_details,問題會默默累積成資料錯亂。正確做法:任務設計成可安全重跑——刪除類用 WHERE 條件過濾、寫入類用 ON CONFLICT ... DO UPDATE;並定期(或用另一個監控任務)檢查 cron.job_run_details 裡 status = 'failed' 的紀錄,接上告警。記住 pg_cron 沒有分散式鎖,別假設任務一定不會重疊。
坑三:pgmq 訊息讀出來卻沒 archive/delete。
這是佇列最典型的錯誤:Worker read 出訊息、也處理完了,卻忘記 archive 或 delete。結果可見性超時一到,那筆訊息又冒出來被重複處理(重複寄信、重複扣款),佇列還會越積越大。正確做法:處理成功一定要 archive(需要審計就歸檔)或 delete(不需保留就刪掉);只有處理失敗時才「不確認」,讓它自然重試。並且用 pgmq.metrics 盯住 queue_length 與 oldest_msg_age_sec,積壓就是警訊。
坑四:只靠 Studio 看當下,沒有長期趨勢與主動告警。 Studio Reports/Logs 很好用,但它是「你去看它才有用」的被動工具。磁碟在你睡覺時滿了、連線在你放假時爆了,Studio 不會叫你。正確做法:至少接一個外部 uptime 監控加資源告警;上了正式流量就接 Prometheus/Grafana,把連線數、CPU、磁碟、慢查詢、容器重啟這五條線設成告警規則,做到「出事前主動通知」。
坑五:Docker 網路內用 localhost 呼叫服務。
在 pg_cron/pgmq 的 Worker 裡用 net.http_post 打 http://localhost:8000 呼叫 Edge Function,會失敗——因為 db 容器的 localhost 是它自己,到不了 Kong。正確做法:一律用 Docker 服務名稱 http://kong:8000,並把 service_role key 用 ALTER DATABASE postgres SET app.service_role_key = '...' 存進資料庫設定,避免硬編碼。
最佳實踐總結:
- 監控分三層:Studio Reports/Logs 巡檢、
pg_stat_statements抓慢查詢、Prometheus/Grafana 做趨勢與告警 - 告警設對線:連線數 90%、查詢 >1s、磁碟 80%、CPU 85%、容器重啟——五條線是自架的煙霧偵測器
- 慢查詢定期看:看總耗時(不只平均),優先優化
pct最高又全表掃描的查詢 - cron 任務要冪等:
ON CONFLICT/WHERE保護,並監看cron.job_run_details的失敗紀錄 - pgmq 一定要確認:成功
archive/delete、失敗才重試,並用pgmq.metrics盯佇列長度 - Docker 內用服務名:呼叫 Edge Function 用
http://kong:8000,不用localhost - 細節以官方為準:metrics endpoint 路徑、擴充功能版本、Studio 欄位都可能隨版本調整
小結
這是 Supabase 系列教學的第 045 篇,也是自架段(SB-5)的第三篇。上一篇《備份、升級與上線檢查》教你把自架 Supabase 的維運三支柱做齊——備份、還原演練、安全升級與上線清單;這一篇補上了最後兩塊拼圖:看得住(監控) 與 跑得動(排程與佇列)。
核心觀念濃縮成一句話:監控要分三層做——Studio Reports/Logs 日常巡檢、pg_stat_statements 抓慢查詢、Prometheus/Grafana 對連線/CPU/磁碟/慢查詢/容器重啟設告警;而 pg_cron 讓你用 cron.schedule 在資料庫內排定時 SQL 任務、pgmq 讓你用 send/read/archive 把 PostgreSQL 當訊息佇列,兩者搭起來就能在資料庫層做完整的非同步背景任務,少養一個要維運的元件。
- 監控三層:內建儀表(Reports/Logs)→ 資料庫內部(
pg_stat_statements)→ 外接可觀測性(Prometheus/Grafana),由淺到深 - 關鍵告警:連線數、查詢時間、磁碟、CPU、容器重啟——出事前主動通知,而不是等使用者回報
pg_cron:cron.schedule排程、cron.job_run_details看歷史;任務要冪等、Docker 內用服務名pgmq:create/send/read/archive/delete的佇列生命週期,靠可見性超時做失敗重試,搭pg_cron組 Worker
到這裡,你的自架 Supabase 已經能「架起來、養得好、看得住、跑得動」了。自架段走到監控與擴充功能,技術面的維運能力已經齊備。下一篇《最佳實踐與反模式》,我們會把整個 Supabase 系列橫跨資料庫、Auth、Storage、Realtime、Edge Functions 到自架的關鍵原則收斂成一份可對照的「該做 vs 別做」總表,幫你把散落各篇的最佳實踐串成一套完整的心智檢查清單。把該做的變成習慣、把反模式變成警訊,才是讓 Supabase 專案長期健康的最後一哩路。