14 KiB
Cron Service Architecture And Task Inventory
本文定义 cron-service 的服务边界、运行模型、当前代码中可迁移的后台任务,以及哪些任务不能放进 cron-service。
核心结论:
cron-service是调度和批处理触发进程,不是新的业务状态 owner。- 它不能直接 import 其他服务的
internal包,也不能绕过 owner service 直接改业务表。 - 房间状态、钱包账务、用户身份、活动消息等业务状态仍由各自服务处理;cron-service 只调用 owner service 暴露的内部 gRPC 批处理接口。
- 不要把所有
time.NewTicker都迁到 cron-service。事件投递、Room Cell 清理和高频 outbox 消费不是通用 cron 问题。
Current Worker Inventory
当前仓库已经有多类后台 worker:
| Service | Current Worker | Current Location | Decision |
|---|---|---|---|
room-service |
stale presence cleanup | services/room-service/internal/room/service/presence.go |
留在 room-service |
room-service |
mic publish timeout cleanup | services/room-service/internal/room/service/mic_publish_timeout.go |
留在 room-service |
room-service |
room outbox relay to direct publishers / RocketMQ | services/room-service/internal/room/service/outbox_worker.go |
留在 room-service 或后续拆 room-worker,不进通用 cron |
room-service |
room treasure delayed open wakeup | services/room-service/internal/room/service/room_treasure.go、services/room-service/internal/room/service/room_treasure_mq.go |
留在 room-service;RocketMQ 只唤醒,到点后仍走 Room Cell 命令链路 |
user-service |
region rebuild task | UserCronService.ProcessRegionRebuildBatch |
已迁移到 cron-service 调度,旧内置 ticker 已删除 |
user-service |
login IP risk job | UserCronService.ProcessLoginIPRiskBatch |
已迁移到 cron-service 调度,旧内置 ticker 已删除 |
user-service |
invite recharge wallet outbox consumer | services/user-service/internal/service/invite/service.go |
先不迁,需补 durable cursor 或事件总线 |
user-service |
room mic outbox consumer | services/user-service/internal/service/mictime/service.go |
先不迁,属于事件消费,不是普通 cron |
user-service |
mic open session compensation | UserCronService.CompensateMicOpenSessions |
已迁移到 cron-service 调度,旧内置 ticker 已删除 |
activity-service |
message fanout job | ActivityCronService.ProcessMessageFanoutBatch |
已迁移到 cron-service 调度,旧内置 ticker 已删除 |
server/admin |
admin export jobs | server/admin/internal/job/runner.go |
留在 admin module |
wallet-service |
wallet outbox table exists, no worker | services/wallet-service/deploy/mysql/initdb/001_wallet_service.sql |
后续做 wallet outbox worker,不进通用 cron |
Service Boundary
cron-service 的职责是“按计划触发 owner service 处理一批任务”,不是“拿到所有数据库连接后自己处理业务”。
graph LR
Cron["cron-service\nscheduler + run history"] --> CronDB[("hyapp_cron")]
Cron -->|"ProcessLoginIPRiskBatch"| User["user-service"]
Cron -->|"ProcessRegionRebuildBatch"| User
Cron -->|"CompensateMicOpenSessions"| User
Cron -->|"ProcessMessageFanoutBatch"| Activity["activity-service"]
Cron -->|"ExpireVipBatch / SettlementBatch"| Wallet["wallet-service"]
User --> UserDB[("hyapp_user")]
Activity --> ActivityDB[("hyapp_activity")]
Wallet --> WalletDB[("hyapp_wallet")]
Room["room-service"] --> RoomDB[("hyapp_room")]
边界规则:
| Rule | Explanation |
|---|---|
| 只调 gRPC | cron-service 通过 hyapp.local/api 生成 client 调用内部 RPC,不 import 其他服务实现 |
| 不直接写业务库 | cron-service 只写自己的 run history 和 schedule lock;业务表由 owner service 写 |
| owner service 自己 claim | 每个批处理 RPC 在 owner service 内完成任务 claim、幂等、事务和状态推进 |
| request meta 仍要透传 | cron 调内部 RPC 也必须带 RequestMeta.request_id/app_code/caller |
| 多 App 按 app_code 执行 | cron 配置任务的 app_codes,每个 app_code 独立触发,避免跨租户混跑 |
| 失败不影响主链路 | cron 挂了只造成后台积压,不能阻塞登录、送礼、进房等在线路径 |
What Goes Into Cron-Service
适合放进 cron-service 的任务必须同时满足:
- 不是用户请求主链路。
- 有持久任务表、状态机或可重试条件。
- 单批处理可幂等,重复调用不会重复入账、重复发奖励或重复清状态。
- owner service 已经有或可以新增
Process...Batch这类内部 RPC。 - 任务失败后可以下一轮重试,且需要运行记录、告警和统一调度。
当前已放入 cron-service 调度的任务:
| Priority | Task | Owner | Why |
|---|---|---|---|
| P0 | login IP risk job | user-service |
登录后异步 IP 查询,已有 login_ip_risk_jobs、claim TTL、session revoke 逻辑;cron 只触发 ProcessLoginIPRiskBatch |
| P1 | user region rebuild | user-service |
已有 user_region_rebuild_tasks 和 ProcessNextRegionRebuildTask;cron 按 app_code 触发单批 |
| P1 | message fanout job | activity-service |
已有 message_fanout_jobs、cursor、lock TTL、retry;cron 触发后 owner service 自己 claim |
| P2 | mic open session compensation | user-service |
低频补偿任务,修正 user mic facts,不反写 Room Cell;窗口参数放在 cron task 配置 |
后续推荐接入 cron-service 的任务:
| Priority | Task | Owner | Why |
|---|---|---|---|
| P2 | VIP expiration scan | wallet-service |
VIP 文档已定义过期扫描,未来由 wallet-service 批量过期并写 outbox |
| P2 | daily task claim retry / expiry | activity-service |
活动任务中的 pending claim、过期状态适合后台补偿 |
| P3 | host salary settlement trigger | user-service + wallet-service |
日结、周结、半月结、月结是明确的定时结算入口,但实际工资单和入账仍由 owner service 完成 |
What Must Not Go Into Cron-Service
以下任务不能放到通用 cron-service:
| Task | Reason |
|---|---|
| Room Cell stale presence cleanup | 依赖本节点已装载 Room Cell 和 Redis lease,必须由 room-service owner 执行命令链路 |
| Mic publish timeout cleanup | 必须对当前 Room Cell 状态做二次校验并执行 MicDown,不能让 cron 直接清麦 |
| room outbox relay | 这是 room-service 的事件投递补偿,不是通用定时任务;需要和 direct publisher、RocketMQ 发布、outbox 状态语义绑定 |
| room treasure open timer | 宝箱状态属于 Room Cell;延迟消息只负责唤醒,开箱必须由 room-service 重新校验 box_id/open_at_ms/UTC reset 后执行命令 |
| wallet ledger transaction | 金币、钻石、积分、余额的加减和冻结必须在 wallet-service 事务内完成 |
| wallet outbox publish | 属于 wallet-service outbox relay 或 MQ 投递,不应该由通用 cron 直接扫表改状态 |
| invite recharge outbox consumer | 当前使用 in-memory cursor 扫 wallet outbox,迁移前必须先做 durable offset 或正式事件总线 |
| room mic outbox consumer | 属于高频事件消费,当前靠消费幂等表兜底;迁移前必须先做 durable offset,不能用 cron 从头扫 |
| admin export jobs | 属于 server/admin 独立 module,涉及后台权限、导出文件和 artifact 下载,继续留在 admin runner |
Runtime Model
cron-service 是长运行进程,不是一次性脚本。它本身只需要自己的库和内部服务地址。
建议本地端口:
| Port | Usage |
|---|---|
13007 |
cron-service gRPC health 和内部管理接口 |
建议目录:
services/cron-service/
cmd/server/main.go
configs/
config.yaml
config.docker.yaml
config.tencent.example.yaml
deploy/mysql/initdb/
001_cron_service.sql
internal/
app/
config/
scheduler/
storage/mysql/
integration/
transport/grpc/
建议配置:
service_name: cron-service
node_id: cron-local
environment: local
grpc_addr: ":13007"
mysql_dsn: "hyapp:hyapp@tcp(127.0.0.1:23306)/hyapp_cron?parseTime=true&charset=utf8mb4&loc=UTC"
user_service_addr: "127.0.0.1:13005"
activity_service_addr: "127.0.0.1:13006"
wallet_service_addr: "127.0.0.1:13004"
tasks:
login_ip_risk:
enabled: true
interval: "5s"
timeout: "3s"
lock_ttl: "30s"
batch_size: 100
app_codes: ["lalu"]
user_region_rebuild:
enabled: true
interval: "5s"
timeout: "5s"
lock_ttl: "30s"
batch_size: 500
app_codes: ["lalu"]
message_fanout:
enabled: true
interval: "1s"
timeout: "10s"
lock_ttl: "30s"
batch_size: 500
app_codes: ["lalu"]
mic_open_session_compensation:
enabled: true
interval: "60s"
timeout: "10s"
lock_ttl: "60s"
batch_size: 100
pending_publish_max_age: "2m"
publishing_session_max_age: "12h"
app_codes: ["lalu"]
Cron Database
cron-service 需要自己的运行记录和调度锁,避免多个 cron 实例同时触发同一个计划。
CREATE TABLE cron_task_leases (
task_name VARCHAR(96) NOT NULL,
app_code VARCHAR(32) NOT NULL,
owner_node_id VARCHAR(128) NOT NULL,
lease_until_ms BIGINT NOT NULL,
updated_at_ms BIGINT NOT NULL,
PRIMARY KEY (task_name, app_code),
KEY idx_cron_task_leases_expire (lease_until_ms)
);
CREATE TABLE cron_task_runs (
run_id VARCHAR(96) NOT NULL,
task_name VARCHAR(96) NOT NULL,
app_code VARCHAR(32) NOT NULL,
owner_node_id VARCHAR(128) NOT NULL,
status VARCHAR(32) NOT NULL,
claimed_count INT NOT NULL DEFAULT 0,
processed_count INT NOT NULL DEFAULT 0,
success_count INT NOT NULL DEFAULT 0,
failure_count INT NOT NULL DEFAULT 0,
started_at_ms BIGINT NOT NULL,
finished_at_ms BIGINT NULL,
duration_ms BIGINT NOT NULL DEFAULT 0,
error_message VARCHAR(512) NOT NULL DEFAULT '',
PRIMARY KEY (run_id),
KEY idx_cron_task_runs_task_time (task_name, app_code, started_at_ms),
KEY idx_cron_task_runs_status (status, started_at_ms)
);
cron_task_leases 只防止调度层重复触发;真正的业务并发安全仍必须由 owner service 的任务表锁和幂等保证。
Internal RPC Contract
不要设计一个可以执行任意任务名的通用 Execute(task_name, payload)。这种接口最后会变成绕过领域边界的后门。
每个 owner service 暴露明确的内部批处理 RPC,例如:
message CronBatchRequest {
RequestMeta meta = 1;
string run_id = 2;
string worker_id = 3;
int32 batch_size = 4;
int64 lock_ttl_ms = 5;
int64 pending_publish_max_age_ms = 6;
int64 publishing_session_max_age_ms = 7;
}
message CronBatchResponse {
int32 claimed_count = 1;
int32 processed_count = 2;
int32 success_count = 3;
int32 failure_count = 4;
bool has_more = 5;
}
owner service 侧实现要求:
- RPC 方法只处理一个明确任务,例如
ProcessLoginIPRiskBatch。 - 任务 claim、事务、状态推进、幂等和业务错误都留在 owner service。
- 返回
has_more=true时,cron-service 可以在同一轮继续拉下一批,直到空批次或达到单轮最大耗时。 - RPC 必须能安全重复调用。cron 超时后无法确认结果时,下一轮重试不能造成重复副作用。
Current Implementation
当前实现已经完成以下迁移:
login_ip_risk_jobs:cron-service 调user-service的ProcessLoginIPRiskBatch,旧RunLoginIPRiskWorker已删除。user_region_rebuild_tasks:cron-service 调user-service的ProcessRegionRebuildBatch,旧RunRegionRebuildWorker已删除。message_fanout_jobs:cron-service 调activity-service的ProcessMessageFanoutBatch,旧RunFanoutWorker已删除。user_mic_sessionsopen session compensation:cron-service 调user-service的CompensateMicOpenSessions,旧RunOpenSessionCompensationWorker已删除。
cron-service 可以直接开启多个实例;调度层通过 cron_task_leases 防重复触发,业务层仍通过 owner service 内部的任务 claim、状态机和幂等保证最终正确。
Event Consumers Stay Separate
以下不是 cron-service 的首要工作:
- invite recharge wallet outbox consumer。
- room mic outbox consumer。
- wallet outbox relay。
- activity room outbox consumer。
- notice room outbox consumer。
- room IM bridge MQ consumer。
- room treasure delayed open consumer。
这些需要的是事件消费基础设施:RocketMQ、Redis Stream、或 MySQL durable offset。cron-service 不能用“每隔几秒从头扫 outbox”的方式替代事件消费者。即使某些消费者内部有低频扫描补偿,也必须留在业务 owner service 中,因为它们需要理解 outbox 状态、消费幂等和领域状态校验。
Future Scheduled Business Jobs
在对应业务落地后再接入:
- VIP 过期扫描。
- 主播工资周期结算触发。
- 钻石兑换过期订单补偿。
- 提现人工打款超时提醒。
- daily task 过期和 pending reward claim 重试。
Operational Notes
| Area | Requirement |
|---|---|
| Shutdown | 停止调度新 run,等待当前 RPC 在 timeout 内结束,再关闭连接 |
| Logs | 固定输出 task_name/run_id/app_code/worker_id/duration_ms/claimed_count/processed_count/status |
| Metrics | 每个任务记录积压、成功数、失败数、最老 pending age、运行耗时 |
| Alerts | 连续失败、积压持续增长、最老 pending 超阈值、owner service gRPC unavailable |
| Backoff | 单任务连续失败时指数退避,但不能影响其他任务 |
| Idempotency | cron-service 只负责少触发,业务不重复必须由 owner service 保证 |
Final Placement Decision
当前 cron-service 已经承接四个成熟批处理任务;后续只继续接明确的定时批处理,不接在线主链路和事件消费者。
最终放置原则:
login_ip_risk_jobs、user_region_rebuild_tasks、message_fanout_jobs、mic open session compensation 由 cron-service 调度。- owner service 暴露明确 RPC,cron-service 不直接访问业务库。
- outbox/event consumer 单独做 durable offset 或 MQ,不混进 cron-service。
- 未来 VIP、主播工资、daily task 这类周期任务统一接入 cron-service。