07 · 定时与调度方案¶
目标读者:要落地"每天 5 次自动推送"的工程师 核心问题:选哪种调度方案?cron 表达式怎么写?印尼时区怎么处理? 前置: 03-Admin-SDK实战 后续: 08-iGaming多平台批量推送实战 最后更新:2026-04-11
一、需求还原¶
iGaming 运营场景:每天 5 次定时推送:
| 原计划(印尼时间 WIB) | 调整后(避开高峰) |
|---|---|
| 11:00 | 11:03 |
| 15:00 | 15:03 |
| 19:00 | 19:03 |
| 21:00 | 21:03 |
| 00:00 (次日) | 00:03 |
为什么加 3 分钟:FCM 官方明确 整点 ±2 分钟是全球流量高峰,直接发会触发 429 限流。错开 +3 分钟既避开高峰、运营又感知不到差别。详见 04-批量发送。
二、印尼时区 → UTC 换算表¶
印尼不实行夏令时,时区固定为 WIB = UTC+7,整年都这样,没有夏令时坑。
| 印尼时间 (WIB) | UTC 时间 | cron (UTC) | cron (Asia/Jakarta) |
|---|---|---|---|
| 11:03 | 04:03 | 3 4 * * * |
3 11 * * * |
| 15:03 | 08:03 | 3 8 * * * |
3 15 * * * |
| 19:03 | 12:03 | 3 12 * * * |
3 19 * * * |
| 21:03 | 14:03 | 3 14 * * * |
3 21 * * * |
| 00:03 | 17:03 | 3 17 * * * |
3 0 * * * |
推荐:如果工具支持时区(大部分现代 cron/scheduler 都支持),直接写 Asia/Jakarta 时区,不要转 UTC,维护成本更低。
三、四种调度方案¶
3.1 方案 A · Cloud Scheduler + Cloud Functions(Google 原生)¶
架构: Cloud Scheduler (cron 触发器)
↓ HTTP / Pub-Sub
Cloud Functions
↓ Admin SDK
FCM → 设备
优点: - Firebase 生态内,无服务器 - Google 自维护时区 - cron 表达式标准 - 与 Firestore / BigQuery 无缝集成 - Cloud Logging 自带监控
缺点: - 跨多 Firebase 项目时配置复杂 - 成本不透明(小用量基本免费,但要看流量)
成本: - Cloud Scheduler:前 3 个 job 免费,超出 $0.10 / job / 月 - Cloud Functions:前 200 万次调用/月免费
配置步骤:
1. Cloud Console → Cloud Scheduler → Create Job
2. Schedule: 3 11 * * *
3. Timezone: Asia/Jakarta(强烈推荐用 Jakarta 时区,比 UTC 好维护)
4. Target: HTTP POST 到 Cloud Functions endpoint
适合:已经全面接入 Firebase 的团队,需要免运维。
3.2 方案 B · 自建 cron + Node.js 脚本(VPS) ⭐ 推荐¶
这是 iGaming 运营场景的首选方案。
crontab 配置(VPS 时区设为 Asia/Jakarta):
# sudo timedatectl set-timezone Asia/Jakarta (一次性设置时区)
# crontab -e
3 11 * * * /usr/bin/node /opt/igaming/scripts/push.js 11点 >> /var/log/igaming-push.log 2>&1
3 15 * * * /usr/bin/node /opt/igaming/scripts/push.js 15点 >> /var/log/igaming-push.log 2>&1
3 19 * * * /usr/bin/node /opt/igaming/scripts/push.js 19点 >> /var/log/igaming-push.log 2>&1
3 21 * * * /usr/bin/node /opt/igaming/scripts/push.js 21点 >> /var/log/igaming-push.log 2>&1
3 0 * * * /usr/bin/node /opt/igaming/scripts/push.js 午夜 >> /var/log/igaming-push.log 2>&1
如果 VPS 时区必须是 UTC(如 AWS 默认):
3 4 * * * /usr/bin/node /opt/igaming/scripts/push.js 11点 >> /var/log/igaming-push.log 2>&1
3 8 * * * /usr/bin/node /opt/igaming/scripts/push.js 15点 >> /var/log/igaming-push.log 2>&1
3 12 * * * /usr/bin/node /opt/igaming/scripts/push.js 19点 >> /var/log/igaming-push.log 2>&1
3 14 * * * /usr/bin/node /opt/igaming/scripts/push.js 21点 >> /var/log/igaming-push.log 2>&1
3 17 * * * /usr/bin/node /opt/igaming/scripts/push.js 午夜 >> /var/log/igaming-push.log 2>&1
优点: - 最便宜(只是一个 VPS 进程) - 最灵活 - 配置即代码 - Debug 方便
缺点: - 单点故障(VPS 挂了就没推送) - 需要监控 cron 是否真跑了
推荐增强:
-
接 healthchecks.io 监控(免费) 每次执行后 curl 一个 ping URL:
30 分钟没 ping 就收到告警邮件/Telegram。// 脚本末尾 await fetch(`https://hc-ping.com/YOUR-UUID`); -
用 systemd timer 替代 crontab(更推荐,见下节)
3.3 方案 B+ · systemd timer(VPS 生产级) ⭐⭐¶
systemd timer 比 crontab 更可观测、更可靠,支持原生时区。
Service 单元(参数化版本):
# /etc/systemd/system/fcm-push@.service
[Unit]
Description=FCM Push (%i)
Wants=network-online.target
After=network-online.target
[Service]
Type=oneshot
WorkingDirectory=/opt/igaming
EnvironmentFile=/opt/igaming/.env
ExecStart=/usr/bin/node scripts/multi-platform-push.js %i
User=igaming
Group=igaming
Timer 单元(一个时间一个 timer):
# /etc/systemd/system/fcm-push-1100.timer
[Unit]
Description=FCM Push 11:03 WIB
[Timer]
OnCalendar=*-*-* 11:03:00 Asia/Jakarta
Persistent=true
Unit=fcm-push@11点.service
[Install]
WantedBy=timers.target
启用 5 个 timer:
# 创建 5 个 timer 文件(1100 / 1500 / 1900 / 2100 / 0000)
sudo systemctl daemon-reload
sudo systemctl enable --now fcm-push-1100.timer
sudo systemctl enable --now fcm-push-1500.timer
sudo systemctl enable --now fcm-push-1900.timer
sudo systemctl enable --now fcm-push-2100.timer
sudo systemctl enable --now fcm-push-0000.timer
# 查看 timer 状态
sudo systemctl list-timers | grep fcm-push
# 查看某次执行的日志
sudo journalctl -u 'fcm-push@11点.service' -n 50
systemd timer 的核心优势:
- OnCalendar 原生支持 IANA 时区后缀 Asia/Jakarta
- Persistent=true —— 开机时自动补跑错过的执行
- journalctl -u 一行命令看日志
- systemctl list-timers 直接看上次/下次执行时间
- 失败有 alert 钩子可接
3.4 方案 C · GitHub Actions scheduled workflow¶
# .github/workflows/push.yml
name: FCM Daily Push
on:
schedule:
- cron: '3 4 * * *' # 11:03 WIB
- cron: '3 8 * * *' # 15:03 WIB
- cron: '3 12 * * *' # 19:03 WIB
- cron: '3 14 * * *' # 21:03 WIB
- cron: '3 17 * * *' # 00:03 WIB
workflow_dispatch: # 允许手动触发
jobs:
push:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: '20' }
- run: npm ci
- name: Send FCM
env:
PLATFORM_A_SA: ${{ secrets.PLATFORM_A_SA }}
PLATFORM_B_SA: ${{ secrets.PLATFORM_B_SA }}
PROMO_CODE: LUCKY888
run: node scripts/multi-platform-push.js
优点: - 免费(公开仓库无限 / 私有 2000 分钟/月) - 配置即代码 - 内置秘密管理(GitHub Secrets) - 运行日志自动留存
缺点: - ⚠️ GitHub Actions cron 经常延迟 5~15 分钟 - GitHub 官方明文 "cron events may be delayed during periods of high loads" - 不适合分钟级精度
适合:MVP 阶段、不要求精准准点、想零运维。
3.5 方案 D · 应用内调度(node-cron / APScheduler)¶
Node.js node-cron:
// scheduler.js - 常驻进程
import cron from 'node-cron';
import { sendAllPlatforms } from './push.js';
// node-cron 支持 IANA 时区
const TZ = { timezone: 'Asia/Jakarta' };
cron.schedule('3 11 * * *', () => sendAllPlatforms('11点'), TZ);
cron.schedule('3 15 * * *', () => sendAllPlatforms('15点'), TZ);
cron.schedule('3 19 * * *', () => sendAllPlatforms('19点'), TZ);
cron.schedule('3 21 * * *', () => sendAllPlatforms('21点'), TZ);
cron.schedule('3 0 * * *', () => sendAllPlatforms('午夜'), TZ);
console.log('FCM scheduler started');
Python APScheduler:
# scheduler.py
from apscheduler.schedulers.blocking import BlockingScheduler
from apscheduler.triggers.cron import CronTrigger
from push import send_all_platforms
sched = BlockingScheduler(timezone='Asia/Jakarta')
sched.add_job(lambda: send_all_platforms('11点'), CronTrigger(hour=11, minute=3))
sched.add_job(lambda: send_all_platforms('15点'), CronTrigger(hour=15, minute=3))
sched.add_job(lambda: send_all_platforms('19点'), CronTrigger(hour=19, minute=3))
sched.add_job(lambda: send_all_platforms('21点'), CronTrigger(hour=21, minute=3))
sched.add_job(lambda: send_all_platforms('午夜'), CronTrigger(hour=0, minute=3))
sched.start()
优点: - 所有逻辑在一个进程 - 不依赖系统 cron - 跨平台可移植
缺点: - 进程挂了就没推送 → 必须配 systemd / pm2 / Docker restart=always - 需要自己管进程生命周期
适合:已经有常驻 Node.js / Python 后端服务,顺便加一个 cron 任务。
四、方案对比矩阵¶
| 维度 | A · Cloud Scheduler | B · VPS cron | B+ · systemd timer | C · GitHub Actions | D · 应用内调度 |
|---|---|---|---|---|---|
| 维护成本 | 极低 | 低 | 低 | 极低 | 中 |
| 可靠性 | 极高(SLA) | 中(单点) | 高(系统级) | 中(cron 延迟) | 中(进程存活) |
| 精度 | 秒级 | 秒级 | 秒级 | 分钟级(常延迟) | 秒级 |
| 成本 | 几乎免费 | VPS 月租 | VPS 月租 | 免费 | VPS 月租 |
| 监控 | Cloud Logging | 自己接 | journalctl 自带 | Actions 日志 | 自己接 |
| 多 Firebase 项目 | 复杂 | 简单 | 简单 | 简单 | 简单 |
| 时区支持 | ✅ IANA | ✅(VPS 时区) | ✅ IANA 原生 | ❌(只能 UTC) | ✅ IANA |
| iGaming 适用度 | ★★★ | ★★★★ | ★★★★★ | ★★ | ★★★★ |
五、推荐方案¶
5.1 首选:方案 B+ · systemd timer¶
理由:
- 用户已经有 VPS 在做运维
- 5 次/天的频率对运维要求不高
- 多 Firebase 项目场景下,同一个进程并行处理最简单
- systemctl list-timers 直接看下次执行时间,可观测性天花板
- 加一层 healthchecks.io 监控:每次执行后 curl ping URL,30 分钟没 ping 就报警
5.2 备选:方案 D · node-cron 应用内调度¶
适用场景:如果项目已有常驻 Node 后端服务,省一个进程。
5.3 不推荐¶
- GitHub Actions cron —— 延迟 5-15 分钟,不适合 iGaming 的准点推送
- Cloud Scheduler —— 多 Firebase 项目场景配置复杂,成本累加
六、监控与告警¶
6.1 healthchecks.io(免费,推荐)¶
注册步骤:
1. https://healthchecks.io → 注册
2. 创建一个 check,拿到 UUID URL(https://hc-ping.com/YOUR-UUID)
3. 配置告警渠道:Email / Slack / Telegram / Discord / Webhook
脚本集成:
// 脚本入口
try {
const result = await sendAllPlatforms('11点');
// 成功 ping
await fetch(`https://hc-ping.com/YOUR-UUID`);
} catch (err) {
// 失败 ping(带详情)
await fetch(`https://hc-ping.com/YOUR-UUID/fail`, {
method: 'POST',
body: err.message,
});
throw err;
}
效果: - ✅ 脚本成功跑完 → 静默 - ❌ 脚本失败 或 30 分钟没 ping → Telegram / Email 告警
6.2 Telegram Bot(也推荐)¶
每次执行后发送结果到运营群:
async function notifyTelegram(summary) {
const TG_TOKEN = process.env.TG_BOT_TOKEN;
const TG_CHAT = process.env.TG_CHAT_ID;
if (!TG_TOKEN || !TG_CHAT) return;
const lines = [
`*FCM 推送 ${summary.timeLabel}* (${summary.promoCode})`,
`成功 ${summary.success} / 失败 ${summary.failed}`,
'',
...summary.details.map((d) =>
d.success
? `✓ ${d.platform} (${d.durationMs}ms)`
: `✗ ${d.platform}: ${d.errorCode}`,
),
];
await fetch(`https://api.telegram.org/bot${TG_TOKEN}/sendMessage`, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({
chat_id: TG_CHAT,
text: lines.join('\n'),
parse_mode: 'Markdown',
}),
});
}
运营体验:每次推送完自动在群里看到 ✓ 平台A / ✓ 平台B / ✗ 平台C: mismatched-credential,比翻日志快 100 倍。
七、陷阱清单¶
-
印尼不实行夏令时 用
Asia/Jakarta全年安稳,没有 DST 坑。 -
cron 时区永远写 IANA 名
Asia/Jakarta>+07:00偏移量,跨工具兼容性最好。 -
GitHub Actions cron 不可靠 官方明文 "cron events may be delayed during periods of high loads"。不要用于分钟级精度任务。
-
systemd timer 必须
Persistent=true否则开机时错过的执行不会补跑,会丢消息。 -
VPS 时区 vs cron 时区 crontab 用的是 VPS 系统时区。强烈建议 VPS 时区直接设为 Asia/Jakarta,cron 表达式直接写印尼时间,不用转换。
-
监控必接 单一 cron 没监控 = 出事才发现。healthchecks.io 是免费的,5 分钟接入。
八、权威来源¶
- https://firebase.google.com/docs/cloud-messaging/scale-fcm(on-the-hour 流量高峰说明)
- https://cloud.google.com/scheduler/docs(Cloud Scheduler 价格与配置)
- https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#schedule(GitHub cron 延迟说明)
- https://github.com/node-cron/node-cron
- https://www.freedesktop.org/software/systemd/man/systemd.timer.html(systemd timer)
- https://healthchecks.io