采集监控、日志与告警
采集系统最危险的状态不是「报错」,而是「静默失败」:进程还在跑、日志还在刷,但数据一条没进来——选择器被前端改版改坏了,成功率掉到 0 而没人发现。本章讲清该监控什么、结构化日志怎么写、失败样本怎么留、告警怎么降噪。
采集需要监控什么
| 指标 | 含义 | 异常信号 |
|---|---|---|
| 请求成功率 | 2xx 响应占全部请求的比例 | 低于 95% 说明被限流或站点异常 |
| 抓取速率 | 每分钟成功请求数 | 骤降或暴涨都要看 |
| 队列积压 | 待爬 URL 数量(Redis llen) | 持续增长说明消费能力不足 |
| 解析失败率 | 解析出字段为空的比例 | 突然升高几乎总是选择器失效 |
| 入库条数 | 每批新增与更新的行数 | 与成功率背离即是静默失败 |
| 异常类型分布 | 超时、连接失败、403/429、解析异常 | 定位问题类型的关键维度 |
| 单次任务耗时 | 一批任务从开始到结束的时间 | 逐渐变长预示站点变慢或数据量增长 |
| 重复率 | 去重命中比例 | 异常升高说明断点或 URL 规则有问题 |
监控不是「指标越多越好」,而是每个指标都要能指向一个具体动作:成功率低看限流与代理,解析失败率高看选择器,入库条数掉看管道。
结构化日志
文本日志方便人看,JSON 日志方便机器聚合。自定义一个 Formatter,让每条日志带上站点、阶段、trace 字段:
# log_setup.py —— Python 3.10+
import json, logging, sys
from logging.handlers import RotatingFileHandler
EXTRA_FIELDS = ("site", "trace_id", "url", "stage", "count", "duration_ms")
class JsonFormatter(logging.Formatter):
"""把日志序列化为单行 JSON,便于 ELK/Loki 直接解析"""
def format(self, record: logging.LogRecord) -> str:
payload = {"time": self.formatTime(record, "%Y-%m-%dT%H:%M:%S"),
"level": record.levelname, "logger": record.name,
"msg": record.getMessage()}
for field in EXTRA_FIELDS:
value = getattr(record, field, None)
if value is not None:
payload[field] = value
if record.exc_info:
payload["exc"] = self.formatException(record.exc_info)
return json.dumps(payload, ensure_ascii=False)
def setup_logger(name: str = "crawler", path: str = "logs/crawler.log") -> logging.Logger:
logger = logging.getLogger(name)
logger.setLevel(logging.INFO)
if logger.handlers: # 避免重复 addHandler 导致日志翻倍
return logger
file_handler = RotatingFileHandler(path, maxBytes=50 * 1024 * 1024,
backupCount=7, encoding="utf-8")
file_handler.setFormatter(JsonFormatter())
stream_handler = logging.StreamHandler(sys.stdout)
stream_handler.setFormatter(JsonFormatter())
logger.addHandler(file_handler)
logger.addHandler(stream_handler)
return logger
if __name__ == "__main__":
log = setup_logger()
log.info("解析完成", extra={"site": "example.com", "stage": "parse", "count": 120})
try:
1 / 0
except ZeroDivisionError:
log.exception("解析异常", extra={"site": "example.com", "stage": "parse"})
失败样本比统计值更有价值:把解析失败的 URL 与响应片段落盘(save_failed(url, reason, html[:5000], site) 写成 logs/failed/<site>-<hash>.json),第二天可以直接对着 HTML 改选择器,不必重现整批任务。
告警渠道与降噪
| 渠道 | 适用 | 注意 |
|---|---|---|
| 邮件 | 日报、非紧急通知 | 实时性差,容易被忽略 |
| 钉钉/企业微信群机器人 | 团队实时告警 | 用 Webhook,注意限频与 @ 人规则 |
| Server 酱等推送 | 个人项目 | 有免费额度限制 |
| Prometheus Alertmanager | 与指标阈值联动 | 需部署,适合长期运行的系统 |
| 电话/短信 | 核心业务中断 | 成本高,只留给最严重的级别 |
| 降噪策略 | 做法 | 效果 |
|---|---|---|
| 连续失败才告警 | 连续 3 次任务失败才发通知 | 过滤单次抖动 |
| 聚合告警 | 同站点同类型 5 分钟内只发一条 | 防止消息轰炸 |
| 静默期 | 相邻告警间隔至少 30 分钟 | 给处理留出时间窗口 |
| 分级 | P0 电话、P1 群消息、P2 日报汇总 | 注意力留给真正紧急的事 |
| 阈值带滞回 | 成功率 < 90% 告警,> 95% 才恢复 | 避免阈值附近反复告警 |
用 Prometheus + Grafana 做看板
Grafana 负责可视化,Prometheus 按间隔抓取指标,采集程序只需用 prometheus_client 暴露一个 HTTP 端口:
# metrics.py —— 依赖 prometheus_client
from prometheus_client import Counter, Gauge, Histogram, start_http_server
# Counter 只增不减,适合累计量;Gauge 可增可减,适合当前状态
REQUESTS = Counter("crawler_requests_total", "请求总数", ["site", "status"])
ITEMS = Counter("crawler_items_total", "入库条数", ["site"])
QUEUE_SIZE = Gauge("crawler_queue_size", "待爬队列长度", ["site"])
SUCCESS_RATE = Gauge("crawler_success_rate", "最近一批成功率", ["site"])
LATENCY = Histogram("crawler_request_seconds", "请求耗时分布", ["site"],
buckets=(0.1, 0.3, 1, 2, 5, 10))
start_http_server(9101) # Prometheus 从 /metrics 抓取
# prometheus.yml 片段
scrape_configs:
- job_name: crawler
scrape_interval: 30s
static_configs:
- targets: ["10.0.0.21:9101", "10.0.0.22:9101"]
Grafana 里建议做四块图:成功率与队列长度(同图,两条曲线分离即异常)、解析失败率、请求耗时 P95、入库条数按小时柱状图。入库条数必须与成功率同屏,因为「成功率正常但入库为 0」正是静默失败的典型形态。
静默失败怎么检测
| 检测项 | 规则示例 | 说明 |
|---|---|---|
| 条数突降 | 今日条数 < 近 7 日均值 × 30% | 最强的静默失败信号 |
| 成功率为 0 但进程存活 | 连续 2 个周期无成功请求 | 通常是站点封禁或出口故障 |
| 字段全空 | 某字段空值率 > 90% | 选择器失效 |
| 队列不动 | llen 连续 3 次采样完全不变 | 调度器卡死或 worker 全退出 |
| 时间戳陈旧 | 最新数据的采集时间超过 2 个周期 | 任务未真正执行 |
| 重复率异常 | 去重命中率 > 95% | URL 规则或断点逻辑出错 |
常见坑
| 现象 | 原因 | 处理 |
|---|---|---|
| 日志出现两份 | 重复 addHandler | 加 if logger.handlers: return |
| 日志文件无限增长 | 未用 RotatingFileHandler | 设置 maxBytes 与 backupCount |
| JSON 日志解析失败 | 日志里有换行或未转义内容 | 只输出单行,异常用 formatException |
| 告警风暴 | 每个失败都发一条 | 连续 N 次失败才告警 + 聚合窗口 |
| 指标口径不一致 | 手动统计与日志口径不同 | 指标与日志的统计点放在同一处代码 |
合规提示:监控与日志本身也可能越界——记录 URL 与响应片段时,不要落盘个人隐私字段(手机号、身份证号、简历个人信息等)。只在合法授权范围内采集公开数据,遵守 robots.txt 与站点服务条款,控制频率与并发,不绕过登录、验证码与付费墙。日志与数据的留存、导出需符合《个人信息保护法》《数据安全法》《著作权法》,优先使用官方 API 与开放数据。
小结:采集监控要盯住成功率、队列积压、解析失败率与入库条数四个核心指标,其中「成功率正常而入库为 0」是静默失败的标准形态;日志用 JSON 结构化并配 RotatingFileHandler 切割,失败样本落盘便于次日复现;告警必须做连续失败判定、聚合与静默期,用 Prometheus 暴露指标、Grafana 做同屏看板。