美团券 ETL:运行锁改用 /tmp(摆脱 data/ 的 root 权限依赖)+ 新增定时任务运维手册

- 锁文件默认改到系统临时目录(任何账号可写),支持 MEITUAN_ETL_LOCK 覆盖;修无 sudo 部署时 data/ 属 root → 每轮 PermissionError、cron 抓不进数据的问题
- 新增 deploy/meituan-etl.md:启动/关闭/终止/代码位置/健康检查/调频率/根因背景
This commit is contained in:
chenshuobo
2026-06-10 16:32:59 +08:00
committed by lowmaster-chen
parent 195fad4511
commit cf1e5908a3
2 changed files with 95 additions and 1 deletions
+90
View File
@@ -0,0 +1,90 @@
# 美团券 ETL 定时任务 — 运维手册
> 对象:维护「美团 CPS 券抓取入库」这套定时任务的同事。
> 🔒 登录信息(服务器 IP / 账号密码 / DB 密码)见**私密交接清单**,**不入库**。
> 运行账号 `cps` **无 sudo**,本手册所有操作都不需要 root。
## 它是什么
每小时全量抓美团 CPS 券(北京)→ upsert 进 PostgreSQL 的 `meituan_coupon` 表,作为 App「销量最高 / 智能推荐」两个 tab 的数据源。一轮约 140 秒、~2700 条;靠 `cps` 用户的 crontab 常驻,关终端也照跑。
## 代码 / 文件在哪(都在生产服务器上)
| 项 | 路径 |
|---|---|
| 代码目录 | `/opt/shaguabijia-app-server` |
| 抓取脚本 | `scripts/pull_meituan_coupons.py` |
| Python(venv) | `/opt/shaguabijia-app-server/.venv/bin/python` |
| 配置(美团凭证 / DB 连接串) | `/opt/shaguabijia-app-server/.env`(`MT_CPS_*` / `DATABASE_URL`)|
| 日志 | `/home/cps/meituan_etl.log` |
| 运行锁 | `/tmp/meituan_etl.lock`(根治版)或 `~/data/.meituan_etl.lock`(过渡版,见文末)|
| 定时配置 | `cps` 用户 crontab(`crontab -l`)|
| 入库表 | PostgreSQL `shaguabijia` 库的 `meituan_coupon` |
## 怎么看在不在跑 / 健康
```bash
crontab -l # 看定时任务(应有每小时第 17 分那条)
tail -n 20 ~/meituan_etl.log # 看最近几轮:本轮完成 / 入库条数 / 有无报错
# 查库存量与新鲜度(走 .env 连接串,不用输 DB 密码):
cd /opt/shaguabijia-app-server && .venv/bin/python -c "from sqlalchemy import text; from app.db.session import engine; print(engine.connect().execute(text('select count(*) total, count(*) filter (where sale_volume_num is not null) sales, max(last_seen) last_run from meituan_coupon')).one())"
```
**健康标准**:`last_run` 在最近 12 小时内、`total` 两三千、日志最近一轮是 `本轮完成`(不是 `Permission denied` / Traceback)。
## 怎么启动
定时任务已在 `cps` 的 crontab 里常驻,**随系统 cron 服务自动运行,无需手动启动**。若 crontab 被清空、要重新装回:`crontab -e`,粘下面这行存盘。
```cron
# 根治版(锁文件已改到 /tmp 后用这条,推荐):
17 * * * * cd /opt/shaguabijia-app-server && .venv/bin/python -m scripts.pull_meituan_coupons --once --prune-hours 24 >> /home/cps/meituan_etl.log 2>&1
```
**手动立即跑一轮**(不等下一个整点,比如刚发现库空):
```bash
cd /opt/shaguabijia-app-server && .venv/bin/python -m scripts.pull_meituan_coupons --once --prune-hours 24
```
前台跑、约 140 秒,出现 `本轮完成` 即成功。
## 怎么关闭(停用定时任务)
```bash
crontab -e
```
在那一行最前面加 `#` 注释掉(**推荐**,保留备查),存盘即停;要彻底删就删那一行。
> ⚠️ 别用 `crontab -r`——它会**清空你所有 cron**,不只这一条。
## 怎么终止(杀掉正在跑的那一轮)
```bash
pgrep -af pull_meituan_coupons # 查在跑的进程和 PID
pkill -f pull_meituan_coupons # 杀掉它(或 kill <PID>)
rm -f /tmp/meituan_etl.lock ~/data/.meituan_etl.lock # 清掉残留锁,否则下一轮可能"跳过本轮"
```
> 脚本有 **30 分钟陈旧锁自动接管**机制:即使没手动清锁,30 分钟后下一轮也会自愈;急着立刻重跑才需手动 `rm` 锁。
## 调整频率
`crontab -e` 改那行最前面的 cron 表达式:
| 频率 | 表达式 |
|---|---|
| 每小时第 17 分(当前) | `17 * * * *` |
| 每 30 分钟 | `*/30 * * * *` |
| 每 2 小时 | `17 */2 * * *` |
> ⚠️ 别调太密:一轮 ~140 秒,且要守美团限流(脚本已配速到 ~2–3 req/s,远低于 ~15 req/s 红线;402 内置退避重试)。**每小时一轮足够**,勿并行、勿调小页间 sleep。
## 脚本参数
- `--once`:跑一轮(定时任务用这个)
- `--loop --interval 600`:常驻循环,每 600 秒一轮(调试用)
- `--prune-hours N`:清理超过 N 小时没再出现的旧券(默认 24;`0`=不清)
- 锁文件位置可用环境变量 `MEITUAN_ETL_LOCK=/path/to.lock` 覆盖
> 详见脚本头部注释:`head -30 scripts/pull_meituan_coupons.py`
## 已知问题与根治(运行锁权限)
- **现象**:cron 报 `PermissionError: [Errno 13] ... 'data/.meituan_etl.lock'`,每轮抓不进数据、表为空。
- **根因**:旧版脚本把运行锁建在代码目录的 `data/` 下,而 `data/` 属 root、`cps` 不可写(交接清单里"已给写权限"实际不成立)。
- **根治(已在代码层修复)**:脚本把锁默认改到 `/tmp/meituan_etl.lock`(任何账号可写),并支持 `MEITUAN_ETL_LOCK` 覆盖。**部署该版本后,用上面「根治版」那条干净 crontab 即可。**
- **过渡写法(根治版部署前的临时方案)**:crontab 里 `cd /home/cps` 把锁落到家目录、再用 `PYTHONPATH` 让代码仍能 import:
```cron
17 * * * * cd /home/cps && PYTHONPATH=/opt/shaguabijia-app-server /opt/shaguabijia-app-server/.venv/bin/python -m scripts.pull_meituan_coupons --once --prune-hours 24 >> /home/cps/meituan_etl.log 2>&1
```
根治版上线后,把这条换回上面的「根治版」标准写法即可。
## 注意事项
- **只抓北京**:脚本里 `city_id` 硬编码,扩城市需改脚本。
- **表行数 > 单轮抓取数**属正常:美团 `product_view_sign` 会轮换,跨轮累积;查询侧有 `DISTINCT ON(dedup_key)` 按「品牌\|名\|价」去重,用户看到的不重复;旧券 24h 后被 prune 清掉。
- **别和 systemd 双跑**:仓库 `deploy/meituan-etl.{service,timer}` 是**未来由 root 纳入 systemd 正规部署**的备选,和本 user cron **二选一、别同时开**(否则重复抓、浪费限流额度)。当前生产用 user cron。
- **改脚本 / 改部署**:走 git + PR,让有 root 的人 `/opt/deploy.sh` 部署(`git reset` + `alembic upgrade` + 重启);`cps` 账号改不了 `/opt` 下的代码。
+5 -1
View File
@@ -22,6 +22,7 @@ import hashlib
import os
import re
import sys
import tempfile
import time
from datetime import datetime, timedelta, timezone
@@ -44,7 +45,10 @@ PAGE_SIZE = 20
MAX_PAGES = 80 # 单路安全上限(搜索 ~52 页、供给 ~70 页)
PAGE_SLEEP = 0.35 # 页间配速,缓解 402
RETRY = 7
LOCK_FILE = "data/.meituan_etl.lock"
# 运行锁默认放系统临时目录(任何账号可写),不依赖代码目录 data/ 的写权限——
# 无 sudo 部署时 data/ 常属 root,cps 写不了会导致每轮 PermissionError、cron 抓不进数据。
# 需要指定位置时用环境变量 MEITUAN_ETL_LOCK 覆盖。
LOCK_FILE = os.environ.get("MEITUAN_ETL_LOCK") or os.path.join(tempfile.gettempdir(), "meituan_etl.lock")
LOCK_STALE_SEC = 30 * 60 # 锁超过 30min 视为陈旧(进程异常退出残留),自动接管
SOURCES = [