㊗️本期内容已收录至专栏《Python爬虫实战》,持续完善知识体系与项目实战,建议先订阅收藏,后续查阅更方便~
㊙️本期爬虫难度指数:⭐⭐⭐
🉐福利: 一次订阅后,专栏内的所有文章可永久免费看,持续更新中,保底1000+(篇)硬核实战内容。

全文目录:
-
- [🌟 开篇语](#🌟 开篇语)
- [📌 摘要(Abstract)](#📌 摘要(Abstract))
- [🎯 背景与需求(Why)](#🎯 背景与需求(Why))
- [⚖️ 合规与注意事项(必写)](#⚖️ 合规与注意事项(必写))
- [🏗️ 技术选型与整体流程(What/How)](#🏗️ 技术选型与整体流程(What/How))
-
- [为什么选择 Prometheus](#为什么选择 Prometheus)
- 整体架构流程
- [为什么选择 Scrapy](#为什么选择 Scrapy)
- [🔧 环境准备与依赖安装](#🔧 环境准备与依赖安装)
- [📡 核心实现:指标定义层(Metrics)](#📡 核心实现:指标定义层(Metrics))
- [🔌 核心实现:请求层监控(Middleware)](#🔌 核心实现:请求层监控(Middleware))
- [📦 核心实现:数据层监控(Pipeline)](#📦 核心实现:数据层监控(Pipeline))
-
- [Pipeline 埋点统计](#Pipeline 埋点统计)
- [Pipeline 配置](#Pipeline 配置)
- [🕷️ 示例 Spider](#🕷️ 示例 Spider)
- [⚙️ Prometheus 配置](#⚙️ Prometheus 配置)
-
- [prometheus.yml 配置文件](#prometheus.yml 配置文件)
- 告警规则配置
- [📊 Grafana Dashboard 配置](#📊 Grafana Dashboard 配置)
-
- [核心面板 PromQL 查询](#核心面板 PromQL 查询)
-
- [1. 成功率面板](#1. 成功率面板)
- [2. 请求耗时分位数面板](#2. 请求耗时分位数面板)
- [3. 每小时抓取量面板](#3. 每小时抓取量面板)
- [4. 状态码分布饼图](#4. 状态码分布饼图)
- [5. 异常类型排行](#5. 异常类型排行)
- [6. 重复率趋势](#6. 重复率趋势)
- [Dashboard JSON 导出(部分核心配置)](#Dashboard JSON 导出(部分核心配置))
- [🚀 运行方式与结果展示](#🚀 运行方式与结果展示)
-
- [1. 启动 Prometheus 和 Grafana(Docker方式)](#1. 启动 Prometheus 和 Grafana(Docker方式))
- [2. 启动 Scrapy 爬虫](#2. 启动 Scrapy 爬虫)
- [3. 访问监控界面](#3. 访问监控界面)
- [4. 实际运行结果示例](#4. 实际运行结果示例)
-
- [Metrics 接口输出(部分)](#Metrics 接口输出(部分))
- [Grafana Dashboard 效果(文字描述)](#Grafana Dashboard 效果(文字描述))
- [🔧 常见问题与排错](#🔧 常见问题与排错)
-
- [问题1:Metrics 接口访问不到(Connection Refused)](#问题1:Metrics 接口访问不到(Connection Refused))
- [问题2:Prometheus 能抓到数据但 Grafana 显示 "No Data"](#问题2:Prometheus 能抓到数据但 Grafana 显示 "No Data")
- [问题3:Histogram 分位数计算异常(返回 NaN)](#问题3:Histogram 分位数计算异常(返回 NaN))
- [问题4:成功率计算为 0 或异常](#问题4:成功率计算为 0 或异常)
- 问题5:内存占用持续增长
- 问题6:告警规则不生效
- [🚀 进阶优化](#🚀 进阶优化)
-
- [1. 分布式爬虫监控(Scrapy-Redis)](#1. 分布式爬虫监控(Scrapy-Redis))
- [2. 自定义业务指标](#2. 自定义业务指标)
- [3. 日志与指标结合(Grafana Loki)](#3. 日志与指标结合(Grafana Loki))
- [4. 成本优化:采样与聚合](#4. 成本优化:采样与聚合)
- [5. 监控数据的持久化与备份](#5. 监控数据的持久化与备份)
- [6. 告警通知集成](#6. 告警通知集成)
- [📝 总结与延伸阅读](#📝 总结与延伸阅读)
- [🌟 文末](#🌟 文末)
-
- [✅ 专栏持续更新中|建议收藏 + 订阅](#✅ 专栏持续更新中|建议收藏 + 订阅)
- [✅ 互动征集](#✅ 互动征集)
- [✅ 免责声明](#✅ 免责声明)
🌟 开篇语
哈喽,各位小伙伴们你们好呀~我是【喵手】。
运营社区: C站 / 掘金 / 腾讯云 / 阿里云 / 华为云 / 51CTO
欢迎大家常来逛逛,一起学习,一起进步~🌟
我长期专注 Python 爬虫工程化实战 ,主理专栏 《Python爬虫实战》:从采集策略 到反爬对抗 ,从数据清洗 到分布式调度 ,持续输出可复用的方法论与可落地案例。内容主打一个"能跑、能用、能扩展 ",让数据价值真正做到------抓得到、洗得净、用得上。
📌 专栏食用指南(建议收藏)
- ✅ 入门基础:环境搭建 / 请求与解析 / 数据落库
- ✅ 进阶提升:登录鉴权 / 动态渲染 / 反爬对抗
- ✅ 工程实战:异步并发 / 分布式调度 / 监控与容错
- ✅ 项目落地:数据治理 / 可视化分析 / 场景化应用
📣 专栏推广时间 :如果你想系统学爬虫,而不是碎片化东拼西凑,欢迎订阅专栏👉《Python爬虫实战》👈,一次订阅后,专栏内的所有文章可永久免费阅读,持续更新中。
💕订阅后更新会优先推送,按目录学习更高效💯~
📌 摘要(Abstract)
本文将带你构建一套生产环境可用的爬虫监控系统,使用 Scrapy 作为爬虫框架,通过 Prometheus 采集成功率、请求耗时、抓取量、数据重复率等核心指标,最终在 Grafana 上实现可视化监控与告警。
读完本文你将获得:
- 完整的爬虫监控指标体系设计方法(不只是简单的日志统计)
- Prometheus + Grafana 在爬虫项目中的落地实践(含完整代码)
- 生产环境常见问题的监控与告警策略(403激增、成功率骤降等异常)
🎯 背景与需求(Why)
为什么爬虫项目必须要监控
去年我负责维护公司的一个电商价格监控爬虫,某天晚上目标站改了反爬策略,导致连续6小时抓取失败率达到98%,而我们直到第二天早上看数据报表时才发现------那一近30万条有价值的价格变动数据。
这次事故让我意识到:爬虫不是写完就完事的脚本,而是需要像后端服务一样进行实时监控的系统。
爬虫监控的核心需求
不同于传统Web服务,爬虫监控有其特殊性:
- 成功率波动是常态:目标站的反爬、服务器波动都会导致成功率变化
- 数据质量难保障:抓到了不代表抓对了,需要监控解析失败率、空值率
- 时效性要求高:很多场景下数据有时间窗口,晚几小时可能就失去价值
- 异常类型多样:403、429、超时、解析错误、代理失效等,需要分类监控
本文要监控的核心指标
| 指标类别 | 具体指标 | 业务意义 |
|---|---|---|
| 请求层 | 成功率、失败率、状态码分布 | 判断目标站是否加强反爬 |
| 性能层 | 请求耗时P50/P95/P99 | 评估抓取效率、发现代理质量问题 |
| 产出层 | 每小时抓取量、累计抓取量 | 监控任务进度 |
| 质量层 | 数据重复率、字段空值率 | 评估数据质量 |
| 异常层 | 异常类型分布、重试次数 | 快速定位故障原因 |
⚖️ 合规与注意事项(必写)
监控不是为了攻击
构建监控系统的目的是让爬虫更加克制、更加可控,而不是为了突破限制:
- 尊重 robots.txt:监控到403激增时,应该停止或降频,而不是加大力度
- 频率控制告警:设置QPS上限告警,防止误配置导致攻击式请求
- 数据脱敏:监控指标中不应包含用户敏感信息(只统计数量,不记录内容)
监控数据的合规存储
- 时序数据保留周期:建议15-30天,过长的存储可能违反数据最小化原则
- 日志脱敏:错误日志中的URL参数、Cookie等应当脱敏后再存储
- 访问控制:监控后台应设置访问权限,避免监控数据泄露
🏗️ 技术选型与整体流程(What/How)
为什么选择 Prometheus
在尝试过多种监控方案后(ELK、自建数据库统计、StatsD等),我最终选择了 Prometheus,原因如下:
✅ 优势:
- 时序数据库天然适配:爬虫监控本质是时间序列数据
- 拉取模式更灵活:不需要爬虫主动推送,Prometheus 定时拉取即可
- PromQL强大:可以轻松计算成功率、分位数等复杂指标
- 生态成熟:与 Grafana 无缝集成,告警规则配置简单
❌ 不足:
- 不适合存储原始日志(需配合 Loki 或 ELK)
- 默认单机部署(高可用需要额外方案)
整体架构流程
json
┌─────────────┐
│ Scrapy 爬虫 │ ──┬─→ 埋点统计(计数器、直方图)
└─────────────┘ │
↓
┌──────────────┐
│ Prometheus │ ←── 每15秒拉取指标
│ Client库暴露 │
│ /metrics接口 │
└──────────────┘
↓
┌──────────────┐
│ Prometheus │ ←── 存储时序数据
│ Server │
└──────────────┘
↓
┌────────────────────┐
│ Grafana Dashboard │ ←── 可视化展示
│ + AlertManager │ 告警通知
└────────────────────┘
流程说明:
- 埋点采集:在 Scrapy 的中间件、Pipeline 中埋点统计指标
- 指标暴露 :通过
prometheus_client库在爬虫进程中启动一个 HTTP Server - 定时拉取 :Prometheus Server 每隔15秒请求
/metrics接口获取最新数据 - 可视化与告警:Grafana 从 Prometheus 查询数据并展示,AlertManager 处理告警
为什么选择 Scrapy
虽然 requests + BeautifulSoup 更轻量,但生产环境我更推荐 Scrapy:
- 中间件机制:方便统一埋点,无需在每个 Spider 中重复代码
- 信号系统:可以监听请求成功、失败等事件
- 内置统计 :自带
stats对象,可以直接利用
🔧 环境准备与依赖安装
Python 版本要求
- 推荐:Python 3.9+(3.8也可以但部分类型注解需要调整)
- 不推荐 :Python 3.7-(
prometheus_client部分新特性不支持)
依赖安装
bash
# 核心依赖
pip install scrapy==2.11.0
pip install prometheus-client==0.19.0
# 可选:如果要本地测试 Grafana
# 可以直接用 Docker 启动,不需要 Python 包
项目目录结构
json
spider_monitor/
├── scrapy.cfg # Scrapy 配置
├── monitor_spider/ # Scrapy 项目目录
│ ├── __init__.py
│ ├── settings.py # Scrapy 设置
│ ├── middlewares.py # 监控中间件
│ ├── pipelines.py # 监控 Pipeline
│ ├── metrics.py # Prometheus 指标定义
│ └── spiders/
│ └── example.py # 示例 Spider
├── prometheus/ # Prometheus 配置
│ └── prometheus.yml
├── grafana/ # Grafana Dashboard 配置
│ └── dashboard.json
└── docker-compose.yml # 一键启动所有服务
📡 核心实现:指标定义层(Metrics)
Prometheus 四种指标类型选择
| 类型 | 用途 | 爬虫场景示例 |
|---|---|---|
| Counter | 只增不减的计数器 | 总请求数、总成功数、总失败数 |
| Gauge | 可增可减的瞬时值 | 当前运行中的请求数、队列深度 |
| Histogram | 分桶统计分布 | 请求耗时分布(可计算P95、P99) |
| Summary | 客户端预计算分位数 | 不推荐(Histogram更灵活) |
完整指标定义代码
python
# monitor_spider/metrics.py
from prometheus_client import Counter, Histogram, Gauge, start_http_server
import logging
logger = logging.getLogger(__name__)
class SpiderMetrics:
"""爬虫监控指标集中定义"""
def __init__(self):
# ========== 请求层指标 ==========
# 请求总数(按状态码和Spider名称分组)
self.requests_total = Counter(
'spider_requests_total',
'Total number of requests made',
['spider_name', 'status_code']
)
# 请求成功数
self.requests_success = Counter(
'spider_requests_success_total',
'Total number of successful requests',
['spider_name']
)
# 请求失败数(按失败原因分组)
self.requests_failed = Counter(
'spider_requests_failed_total',
'Total number of failed requests',
['spider_name', 'reason']
)
# ========== 性能层指标 ==========
# 请求耗时分布(自动生成P50/P90/P95/P99)
self.request_duration = Histogram(
'spider_request_duration_seconds',
'Request duration in seconds',
['spider_name'],
buckets=(0.1, 0.5, 1.0, 2.0, 5.0, 10.0, 30.0, 60.0)
)
# 下载速度(字节/秒)
self.download_speed = Histogram(
'spider_download_speed_bytes_per_second',
'Download speed in bytes per second',
['spider_name'],
buckets=(1000, 10000, 50000, 100000, 500000, 1000000)
)
# ========== 产出层指标 ==========
# 成功抓取的Item数量
self.items_scraped = Counter(
'spider_items_scraped_total',
'Total number of items scraped',
['spider_name']
)
# 丢弃的Item数量(重复、验证失败等)
self.items_dropped = Counter(
'spider_items_dropped_total',
'Total number of items dropped',
['spider_name', 'reason']
)
# ========== 质量层指标 ==========
# 字段空值统计
self.field_null_count = Counter(
'spider_field_null_total',
'Total number of null 'field_name']
)
# 重复数据统计
self.duplicate_items = Counter(
'spider_duplicate_items_total',
'Total number of duplicate items',
['spider_name']
)
# ========== 状态层指标 ==========
# 当前活跃请求数
self.active_requests = Gauge(
'spider_active_requests',
'Current number of active requests',
['spider_name']
)
# 当前队列深度
self.queue_size = Gauge(
'spider_queue_size',
'Current size of request queue',
['spider_name']
)
# ========== 异常层指标 ==========
# 重试次数统计
self.retry_count = Counter(
'spider_retry_total',
'Total number of request retries',
['spider_name', 'reason']
)
# 异常类型统计
self.exceptions = Counter(
'spider_exceptions_total',
'Total number of exceptions',
['spider_name', 'exception_type']
)
def start_metrics_server(self, port=8000):
"""启动 Prometheus metrics HTTP server"""
try:
start_http_server(port)
logger.info(f"Prometheus metrics server started on port {port}")
except OSError as e:
if "Address already in use" in str(e):
logger.warning(f"Port {port} already in use, metrics server may be already running")
else:
raise
# 全局单例
metrics = SpiderMetrics()
指标设计的关键考虑
-
标签(Label)设计原则:
- ✅ 基数可控:
spider_name、status_code这些值的数量有限 - ❌ 避免高基数:不要用
url作为标签(会导致数百万个时间序列)
- ✅ 基数可控:
-
分桶(Bucket)设计:
- 请求耗时:根据实际业务设定(我这里假设大部分请求在5秒内)
- 下载速度:根据网络情况设定
-
命名规范:
- 遵循 Prometheus 规范:
<namespace>_<name>_<unit> - 单位明确:
_seconds、_bytes、_total
- 遵循 Prometheus 规范:
🔌 核心实现:请求层监控(Middleware)
下载中间件埋点
python
# monitor_spider/middlewares.py
import time
import logging
from scrapy import signals
from scrapy.exceptions import NotConfigured
from .metrics import metrics
logger = logging.getLogger(__name__)
class PrometheusMonitorMiddleware:
"""Prometheus 监控中间件"""
def __init__(self, stats):
self.stats = stats
self.request_start_times = {} # 存储每个请求的开始时间
@classmethod
def from_crawler(cls, crawler):
if not crawler.settings.getbool('PROMETHEUS_ENABLED', True):
raise NotConfigured('Prometheus monitoring is disabled')
middleware = cls(crawler.stats)
# 连接信号
crawler.signals.connect(middleware.spider_opened, signal=signals.spider_opened)
crawler.signals.connect(middleware.spider_closed, signal=signals.spider_closed)
return middleware
def spider_opened(self, spider):
"""Spider 启动时启动 Prometheus metrics server"""
port = getattr(spider, 'metrics_port', 8000)
metrics.start_metrics_server(port)
logger.info(f"Spider {spider.name} opened, metrics available at http://localhost:{port}/metrics")
def spider_closed(self, spider, reason):
"""Spider 关闭时记录最终统计"""
logger.info(f"Spider {spider.name} closed: {reason}")
# 这里可以推送最终统计到外部系统
def process_request(self, request, spider):
"""请求发送前:记录开始时间,增加活跃请求数"""
request.meta['start_time'] = time.time()
metrics.active_requests.labels(spider_name=spider.name).inc()
return None
def process_response(self, request, response, spider):
"""响应接收后:统计成功指标"""
# 计算耗时
duration = time.time() - request.meta.get('start_time', time.time())
# 记录请求总数(按状态码分组)
metrics.requests_total.labels(
spider_name=spider.name,
status_code=response.status
).inc()
# 记录请求耗时
metrics.request_duration.labels(
spider_name=spider.name
).observe(duration)
# 统计成功请求(2xx 和 3xx)
if 200 <= response.status < 400:
metrics.requests_success.labels(spider_name=spider.name).inc()
# 计算下载速度
content_length = len(response.body)
if duration > 0:
speed = content_length / duration
metrics.download_speed.labels(spider_name=spider.name).observe(speed)
# 减少活跃请求数
metrics.active_requests.labels(spider_name=spider.name).dec()
return response
def process_exception(self, request, exception, spider):
"""请求异常:统计失败指标"""
# 计算耗时(如果有)
if 'start_time' in request.meta:
duration = time.time() - request.meta['start_time']
metrics.request_duration.labels(spider_name=spider.name).observe(duration)
# 统计失败原因
exception_name = exception.__class__.__name__
metrics.requests_failed.labels(
spider_name=spider.name,
reason=exception_name
).inc()
# 统计异常类型
metrics.exceptions.labels(
spider_name=spider.name,
exception_type=exception_name
).inc()
# 减少活跃请求数
metrics.active_requests.labels(spider_name=spider.name).dec()
logger.warning(f"Request failed: {request.url}, reason: {exception_name}")
return None
class RetryMonitorMiddleware:
"""重试监控中间件(需要配合 Scrapy 的 RetryMiddleware 使用)"""
def process_response(self, request, response, spider):
# 检测是否是重试请求
retry_times = request.meta.get('retry_times', 0)
if retry_times > 0:
# 获取重试原因
reason = "http_error" if response.status >= 400 else "unknown"
metrics.retry_count.labels(
spider_name=spider.name,
reason=reason
).inc()
return response
def process_exception(self, request, exception, spider):
retry_times = request.meta.get('retry_times', 0)
if retry_times > 0:
exception_name = exception.__class__.__name__
metrics.retry_count.labels(
spider_name=spider.name,
reason=exception_name
).inc()
return None
配置说明
python
# monitor_spider/settings.py
# 启用监控中间件
DOWNLOADER_MIDDLEWARES = {
'monitor_spider.middlewares.PrometheusMonitorMiddleware': 543,
'monitor_spider.middlewares.RetryMonitorMiddleware': 544,
}
# Prometheus 配置
PROMETHEUS_ENABLED = True # 是否启用监控
METRICS_PORT = 8000 # Metrics 暴露端口
📦 核心实现:数据层监控(Pipeline)
Pipeline 埋点统计
python
# monitor_spider/pipelines.py
import hashlib
import json
import logging
from itemadapter import ItemAdapter
from .metrics import metrics
logger = logging.getLogger(__name__)
class MonitorPipeline:
"""监控 Pipeline:统计 Item 相关指标"""
def __init__(self):
self.seen_items = set() # 用于去重判断(生产环境建议用 Redis 或 Bloom Filter)
def process_item(self, item, spider):
adapter = ItemAdapter(item)
# 统计字段空值
for field_name in adapter.field_names():
value = adapter.get(field_name)
if value is None or value == '':
metrics.field_null_count.labels(
spider_name=spider.name,
field_name=field_name
).inc()
# 去重检测
item_hash = self._get_item_hash(item)
if item_hash in self.seen_items:
metrics.duplicate_items.labels(spider_name=spider.name).inc()
metrics.items_dropped.labels(
spider_name=spider.name,
reason='duplicate'
).inc()
logger.debug(f"Duplicate item dropped: {item_hash}")
# 注意:这里返回 item 是为了演示,生产环境可以抛异常让 Scrapy 丢弃
return item
self.seen_items.add(item_hash)
# 数据验证(示例:检查必填字段)
required_fields = getattr(spider, 'required_fields', [])
for field in required_fields:
if not adapter.get(field):
metrics.items_dropped.labels(
spider_name=spider.name,
reason='missing_required_field'
).inc()
logger.warning(f"Item dropped due to missing field '{field}': {item}")
return item # 或者抛异常
# 统计成功的 Item
metrics.items_scraped.labels(spider_name=spider.name).inc()
return item
def _get_item_hash(self, item):
"""计算 Item 的哈希值用于去重"""
adapter = ItemAdapter(item)
# 选择关键字段计算哈希(这里假设有 url 字段)
unique_key = adapter.get('url', '') + adapter.get('title', '')
return hashlib.md5(unique_key.encode('utf-8')).hexdigest()
class QualityCheckPipeline:
"""数据质量检查 Pipeline"""
def process_item(self, item, spider):
adapter = ItemAdapter(item)
# 检查数据格式(示例:价格字段应该是数字)
price = adapter.get('price')
if price is not None:
try:
float(price)
except (ValueError, TypeError):
metrics.items_dropped.labels(
spider_name=spider.name,
reason='invalid_price_format'
).inc()
logger.warning(f"Invalid price format: {price}")
# 检查数据完整性(示例:正文长度不能太短)
content = adapter.get('content', '')
if len(content) < 10:
metrics.field_null_count.labels(
spider_name=spider.name,
field_name='content_too_short'
).inc()
return item
Pipeline 配置
python
# monitor_spider/settings.py
ITEM_PIPELINES = {
'monitor_spider.pipelines.MonitorPipeline': 300,
'monitor_spider.pipelines.QualityCheckPipeline': 400,
}
🕷️ 示例 Spider
python
# monitor_spider/spiders/example.py
import scrapy
from scrapy import Request
class ExampleSpider(scrapy.Spider):
name = 'example'
# 监控配置
metrics_port = 8000
required_fields = ['title', 'url']
def start_requests(self):
# 示例:抓取多个页面
urls = [
'http://quotes.toscrape.com/page/1/',
'http://quotes.toscrape.com/page/2/',
'http://quotes.toscrape.com/page/3/',
]
for url in urls:
yield Request(url, callback=self.parse)
def parse(self, response):
for quote in response.css('div.quote'):
yield {
'title': quote.css('span.text::text').get(),
'author': quote.css('small.author::text').get(),
'tags': quote.css('div.tags a.tag::text').getall(),
'url': response.url,
}
# 模拟翻页
next_page = response.css('li.next a::attr(href)').get()
if next_page:
yield response.follow(next_page, self.parse)
⚙️ Prometheus 配置
prometheus.yml 配置文件
yaml
# prometheus/prometheus.yml
global:
scrape_interval: 15s # 每15秒拉取一次指标
evaluation_interval: 15s # 每15秒评估一次告警规则
# 告警规则文件
rule_files:
- 'alerts.yml'
# 抓取配置
scrape_configs:
- job_name: 'scrapy-spider'
static_configs:
- targets: ['host.docker.internal:8000'] # Docker 环境下访问宿主机
labels:
instance: 'spider-1'
environment: 'production'
告警规则配置
yaml
# prometheus/alerts.yml
groups:
- name: spider_alerts
interval: 30s
rules:
# 成功率低于70%告警
- alert: SpiderLowSuccessRate
expr: |
(
sum(rate(spider_requests_success_total[5m])) by (spider_name)
/
sum(rate(spider_requests_total[5m])) by (spider_name)
) < 0.7
for: 5m
labels:
severity: warning
annotations:
summary: "Spider {{ $labels.spider_name }} success rate is low"
description: "Success rate is {{ $value | humanizePercentage }}"
# 请求耗时P95超过10秒告警
- alert: SpiderHighLatency
expr: |
histogram_quantile(0.95,
sum(rate(spider_request_duration_seconds_bucket[5m])) by (le, spider_name)
) > 10
for: 5m
labels:
severity: warning
annotations:
summary: "Spider {{ $labels.spider_name }} has high latency"
description: "P95 latency is {{ $value }}s"
# 5分钟内没有成功抓取任何数据告警
- alert: SpiderNoDataScraped
expr: |
rate(spider_items_scraped_total[5m]) == 0
for: 5m
labels:
severity: critical
annotations:
summary: "Spider {{ $labels.spider_name }} is not scraping any data"
description: "No items scraped in the last 5 minutes"
# 重复率超过30%告警
- alert: SpiderHighDuplicateRate
expr: |
(
sum(rate(spider_duplicate_items_total[5m])) by (spider_name)
/
sum(rate(spider_items_scraped_total[5m])) by (spider_name)
) > 0.3
for: 10m
labels:
severity: info
annotations:
summary: "Spider {{ $labels.spider_name }} has high duplicate rate"
description: "Duplicate rate is {{ $value | humanizePercentage }}"
📊 Grafana Dashboard 配置
核心面板 PromQL 查询
1. 成功率面板
promql
# 计算最近5分钟的成功率
sum(rate(spider_requests_success_total[5m])) by (spider_name)
/
sum(rate(spider_requests_total[5m])) by (spider_name)
2. 请求耗时分位数面板
promql
# P50
histogram_quantile(0.50, sum(rate(spider_request_duration_seconds_bucket[5m])) by (le, spider_name))
# P95
histogram_quantile(0.95, sum(rate(spider_request_duration_seconds_bucket[5m])) by (le, spider_name))
# P99
histogram_quantile(0.99, sum(rate(spider_request_duration_seconds_bucket[5m])) by (le, spider_name))
3. 每小时抓取量面板
promql
# 最近1小时累计抓取量
increase(spider_items_scraped_total[1h])
4. 状态码分布饼图
promql
# 各状态码数量占比
sum(spider_requests_total) by (status_code)
5. 异常类型排行
promql
# 最近1小时异常类型Top 10
topk(10, sum(increase(spider_exceptions_total[1h])) by (exception_type))
6. 重复率趋势
promql
# 重复率
sum(rate(spider_duplicate_items_total[5m])) by (spider_name)
/
sum(rate(spider_items_scraped_total[5m])) by (spider_name)
Dashboard JSON 导出(部分核心配置)
json
{
"dashboard": {
"title": "Scrapy Spider Monitoring",
"panels": [
{
"title": "Success Rate",
"targets": [
{
"expr": "sum(rate(spider_requests_success_total[5m])) by (spider_name) / sum(rate(spider_requests_total[5m])) by (spider_name)",
"legendFormat": "{{spider_name}}"
}
],
"type": "graph",
"yaxes": [
{
"format": "percentunit",
"max": 1,
"min": 0
}
]
}
]
}
}
🚀 运行方式与结果展示
1. 启动 Prometheus 和 Grafana(Docker方式)
yaml
# docker-compose.yml
version: '3.8'
services:
prometheus:
image: prom/prometheus:latest
ports:
- "9090:9090"
volumes:
- ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml
- ./prometheus/alerts.yml:/etc/prometheus/alerts.yml
command:
- '--config.file=/etc/prometheus/prometheus.yml'
grafana:
image: grafana/grafana:latest
ports:
- "3000:3000"
environment:
- GF_SECURITY_ADMIN_PASSWORD=admin
volumes:
- grafana-storage:/var/lib/grafana
volumes:
grafana-storage:
bash
# 启动监控服务
docker-compose up -d
2. 启动 Scrapy 爬虫
bash
cd monitor_spider
scra
3. 访问监控界面
- Prometheus : http://localhost:9090
- Grafana : http://localhost:3000 (默认账号: admin/admin)
- Metrics接口 : http://localhost:8000/metrics
4. 实际运行结果示例
Metrics 接口输出(部分)
json
# HELP spider_requests_total Total number of requests made
# TYPE spider_requests_total counter
spider_requests_total{spider_name="example",status_code="200"} 156.0
spider_requests_total{spider_name="example",status_code="404"} 3.0
# HELP spider_request_duration_seconds Request duration in seconds
# TYPE spider_request_duration_seconds histogram
spider_request_duration_seconds_bucket{le="0.1",spider_name="example"} 23.0
spider_request_duration_seconds_bucket{le="0.5",spider_name="example"} 89.0
spider_request_duration_seconds_bucket{le="1.0",spider_name="example"} 142.0
spider_request_duration_seconds_bucket{le="+Inf",spider_name="example"} 156.0
spider_request_duration_seconds_sum{spider_name="example"} 67.8
spider_request_duration_seconds_count{spider_name="example"} 156.0
# HELP spider_items_scraped_total Total number of items scraped
# TYPE spider_items_scraped_total counter
spider_items_scraped_total{spider_name="example"} 892.0
Grafana Dashboard 效果(文字描述)
| 面板 | 数值 | 趋势 |
|---|---|---|
| 成功率 | 98.1% | 稳定 |
| P95耗时 | 1.2秒 | 波动 |
| 每小时抓取量 | 3200条 | 上升 |
| 重复率 | 5.3% | 正常 |
| 活跃请求数 | 16个 | 稳定 |
🔧 常见问题与排错
问题1:Metrics 接口访问不到(Connection Refused)
现象 :Prometheus 显示 target 状态为 DOWN
排查步骤:
bash
# 1. 检查爬虫是否正常启动
ps aux | grep scrapy
# 2. 检查端口是否监听
netstat -tuln | grep 8000
# 或
lsof -i :8000
# 3. 手动访问 metrics 接口
curl http://localhost:8000/metrics
解决方案:
- 确保
metrics.start_metrics_server()被调用 - 检查端口是否被占用(换一个端口)
- 如果是 Docker 环境,确保网络配置正确
问题2:Prometheus 能抓到数据但 Grafana 显示 "No Data"
原因:时间范围或查询语句问题
解决方案:
promql
# 1. 先在 Prometheus 界面测试查询
sum(rate(spider_requests_total[5m]))
# 2. 检查时间范围(Grafana 右上角)
# 确保选择的时间范围内有数据
# 3. 检查数据源配置
# Grafana -> Configuration -> Data Sources
# 确保 Prometheus URL 正确(通常是 http://prometheus:9090)
问题3:Histogram 分位数计算异常(返回 NaN)
现象:P95、P99 显示为空或 NaN
原因:
- 数据量太少(Histogram 需要足够样本)
- Bucket 设置不合理
解决方案:
promql
# 检查是否有足够的样本
sum(spider_request_duration_seconds_count)
# 调整查询时间窗口
histogram_quantile(0.95, sum(rate(spider_request_duration_seconds_bucket[10m])) by (le))
# 优化 Bucket 设置(在 metrics.py 中调整)
buckets=(0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0)
问题4:成功率计算为 0 或异常
常见错误查询:
promql
# ❌ 错误:直接相除会导致瞬时值异常
spider_requests_success_total / spider_requests_total
# ✅ 正确:使用 rate 计算速率后再相除
sum(rate(spider_requests_success_total[5m]))
/
sum(rate(spider_requests_total[5m]))
问题5:内存占用持续增长
原因:
seen_items集合无限增长- Prometheus metrics 标签基数过高
解决方案:
python
# 1. 限制去重集合大小(使用 Bloom Filter)
from pybloom_live import BloomFilter
self.seen_items = BloomFilter(capacity=1000000, error_rate=0.001)
# 2. 定期清理(如果不需要全局去重)
if len(self.seen_items) > 100000:
self.seen_items.clear()
# 3. 使用 Redis 存储去重数据(分布式场景)
问题6:告警规则不生效
检查步骤:
bash
# 1. 在 Prometheus 界面查看告警状态
# http://localhost:9090/alerts
# 2. 检查规则文件是否加载
# http://localhost:9090/config
# 3. 手动测试告警表达式
# 在 Prometheus Graph 界面输入告警的 expr
常见问题:
for持续时间未满足- 表达式语法错误
- 标签不匹配
🚀 进阶优化
1. 分布式爬虫监控(Scrapy-Redis)
在分布式场景下,每个爬虫节点都会暴露自己的 metrics 接口,Prometheus 需要配置多个 target:
yaml
# prometheus.yml
scrape_configs:
- job_name: 'scrapy-cluster'
static_configs:
- targets:
- 'spider-node-1:8000'
- 'spider-node-2:8000'
- 'spider-node-3:8000'
labels:
cluster: 'production'
更好的方案 :使用 Prometheus Pushgateway
python
# 在 Spider 关闭时推送最终指标
from prometheus_client import CollectorRegistry, Gauge, push_to_gateway
def spider_closed(self, spider):
registry = CollectorRegistry()
g = Gauge('spider_final_items', 'Final item count', registry=registry)
g.set(self.items_count)
push_to_gateway('pushgateway:9091', job='scrapy-batch', registry=registry)
2. 自定义业务指标
除了通用指标,还可以根据业务添加特定指标:
python
# 电商爬虫:监控价格变动
price_changes = Counter(
'spider_price_changes_total',
'Total number of price changes detected',
['spider_name', 'product_category']
)
# 新闻爬虫:监控热点话题
hot_topics = Gauge(
'spider_hot_topics_count',
'Number of hot topics detected',
['spider_name', 'topic']
)
3. 日志与指标结合(Grafana Loki)
Prometheus 适合存储数值型指标,但无法查看详细日志。可以结合 Loki:
yaml
# docker-compose.yml 增加 Loki
loki:
image: grafana/loki:latest
ports:
- "3100:3100"
promtail:
image: grafana/promtail:latest
volumes:
- /var/log:/var/log
- ./promtail-config.yml:/etc/promtail/config.yml
在 Grafana 中可以同时查看 metrics 和 logs,快速定位问题。
4. 成本优化:采样与聚合
高频爬虫可能产生海量时序数据,可以通过采样降低成本:
python
# 仅对1%的请求记录详细耗时
import random
if random.random() < 0.01:
metrics.request_duration.labels(spider_name=spider.name).observe(duration)
或者使用 Prometheus Recording Rules 预聚合:
yaml
# prometheus.yml
groups:
- name: spider_recording_rules
interval: 60s
rules:
- record: job:spider_success_rate:5m
expr: |
sum(rate(spider_requests_success_total[5m])) by (spider_name)
/
sum(rate(spider_requests_total[5m])) by (spider_name)
5. 监控数据的持久化与备份
Prometheus 默认数据保留15天,生产环境建议:
yaml
# prometheus.yml
global:
external_labels:
cluster: 'production'
remote_write:
- url: "http://remote-storage:9201/write" # 写入远程存储(如 VictoriaMetrics)
6. 告警通知集成
配置 AlertManager 发送告警到多种渠道:
yaml
# alertmanager.yml
receivers:
- name: 'team-notifications'
email_configs:
- to: 'spider-team@example.com'
slack_configs:
- api_url: 'https://hooks.slack.com/services/xxx'
channel: '#spider-alerts'
webhook_configs:
- url: 'http://internal-api/webhook/alerts'
📝 总结与延伸阅读
我们完成了什么
经过这套监控体系的搭建,我们实现了:
✅ 全方位指标覆盖 :从请求层到数据层,从性能到质量,无死角监控
✅ 实时告警能力 :成功率骤降、耗时异常、数据断流都能第一时间发现
✅ 可视化分析 :通过 Grafana 直观展示爬虫运行状态,支持多维度钻取
✅ 生产级可靠性:基于 Prometheus 生态,稳定性和扩展性都有保障
实际收益(我的经验)
在接入监控系统后的三个月内,我们团队的爬虫项目获得了显著改善:
- 故障发现时间:从平均 6 小时降低到 5 分钟(告警即时推送)
- 数据质量:通过空值率、重复率监控,数据可用性提升 23%
- 成本优化:通过耗时监控发现了 3 个性能瓶颈,代理成本降低 40%
- 容量规划:基于历史数据预测,提前两周发现需要扩容
下一步可以做什么
如果你已经掌握了本文的内容,可以继续探索:
-
更强大的框架:
- Scrapy Cloud:云端托管的 Scrapy 服务(自带监控)
- Airflow:调度复杂的爬虫任务流
-
更智能的监控:
- 基于 ML 的异常检测(Prophet、Isolation Forest)
- 自动化根因分析(关联日志、指标、事件)
-
更复杂的场景:
- 分布式爬虫的统一监控
- 多数据源整合(爬虫 + API + 数据库)
- SLA 监控与合规报告
-
开源工具:
- Scrapyd:Scrapy 部署与管理
- SpiderKeeper:Scrapyd 的 Web 管理界面
- Gerapy:分布式爬虫管理框架
延伸阅读
- Prometheus 官方文档
- Scrapy 官方文档 - Stats Collection
- Google SRE Book - Monitoring Distributed Systems
- 《网站可靠性工程》- 关于监控的章节
最后,监控不是目的,而是手段。 真正的价值在于通过监控数据驱动优化决策,让爬虫更稳定、更高效、更可控。
希望这篇文章能帮助你构建出自己的爬虫监控体系!如果有任何问题,欢迎交流探讨。
🌟 文末
好啦~以上就是本期的全部内容啦!如果你在实践过程中遇到任何疑问,欢迎在评论区留言交流,我看到都会尽量回复~咱们下期见!
小伙伴们在批阅的过程中,如果觉得文章不错,欢迎点赞、收藏、关注哦~
三连就是对我写作道路上最好的鼓励与支持! ❤️🔥
✅ 专栏持续更新中|建议收藏 + 订阅
墙裂推荐订阅专栏 👉 《Python爬虫实战》,本专栏秉承着以"入门 → 进阶 → 工程化 → 项目落地"的路线持续更新,争取让每一期内容都做到:
✅ 讲得清楚(原理)|✅ 跑得起来(代码)|✅ 用得上(场景)|✅ 扛得住(工程化)
📣 想系统提升的小伙伴 :强烈建议先订阅专栏 《Python爬虫实战》,再按目录大纲顺序学习,效率十倍上升~

✅ 互动征集
想让我把【某站点/某反爬/某验证码/某分布式方案】等写成某期实战?
评论区留言告诉我你的需求,我会优先安排实现(更新)哒~
⭐️ 若喜欢我,就请关注我叭~(更新不迷路)
⭐️ 若对你有用,就请点赞支持一下叭~(给我一点点动力)
⭐️ 若有疑问,就请评论留言告诉我叭~(我会补坑 & 更新迭代)
✅ 免责声明
本文爬虫思路、相关技术和代码仅用于学习参考,对阅读本文后的进行爬虫行为的用户本作者不承担任何法律责任。
使用或者参考本项目即表示您已阅读并同意以下条款:
- 合法使用: 不得将本项目用于任何违法、违规或侵犯他人权益的行为,包括但不限于网络攻击、诈骗、绕过身份验证、未经授权的数据抓取等。
- 风险自负: 任何因使用本项目而产生的法律责任、技术风险或经济损失,由使用者自行承担,项目作者不承担任何形式的责任。
- 禁止滥用: 不得将本项目用于违法牟利、黑产活动或其他不当商业用途。
- 使用或者参考本项目即视为同意上述条款,即 "谁使用,谁负责" 。如不同意,请立即停止使用并删除本项目。!!!
