Skip to content

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 是否真跑了

推荐增强:

  1. 接 healthchecks.io 监控(免费) 每次执行后 curl 一个 ping URL:

    // 脚本末尾
    await fetch(`https://hc-ping.com/YOUR-UUID`);
    
    30 分钟没 ping 就收到告警邮件/Telegram。

  2. 用 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 倍。


七、陷阱清单

  1. 印尼不实行夏令时Asia/Jakarta 全年安稳,没有 DST 坑。

  2. cron 时区永远写 IANA 名 Asia/Jakarta > +07:00 偏移量,跨工具兼容性最好。

  3. GitHub Actions cron 不可靠 官方明文 "cron events may be delayed during periods of high loads"。不要用于分钟级精度任务

  4. systemd timer 必须 Persistent=true 否则开机时错过的执行不会补跑,会丢消息。

  5. VPS 时区 vs cron 时区 crontab 用的是 VPS 系统时区强烈建议 VPS 时区直接设为 Asia/Jakarta,cron 表达式直接写印尼时间,不用转换。

  6. 监控必接 单一 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