监控与告警系统

监控与告警系统

概述

监控和告警是保障分布式爬虫系统稳定运行的关键。本章介绍如何构建完整的监控和告警体系。


监控体系架构

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
┌──────────────────────────────────────┐
│ Scraper Nodes │
│ - Metrics Exporter (Prometheus) │
│ - Logging (Fluentd/Filebeat) │
└──────────┬───────────────────────────┘
┌──────┴──────┐
│ │
▼ ▼
┌─────────┐ ┌──────────┐
│Prometheus│ │ ELK │
│(Metrics) │ │ (Logs) │
└────┬────┘ └────┬─────┘
│ │
└──────┬─────┘
┌─────────────┐
│ Grafana │
│ (可视化) │
└─────────────┘
┌─────────────┐
│ AlertManager│
│ (告警) │
└─────────────┘

Prometheus 监控

安装部署

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# docker-compose.yml
version: "3"
services:
prometheus:
image: prom/prometheus:latest
ports:
- "9090:9090"
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml
- prometheus_data:/prometheus
command:
- "--config.file=/etc/prometheus/prometheus.yml"
- "--storage.tsdb.path=/prometheus"

grafana:
image: grafana/grafana:latest
ports:
- "3000:3000"
volumes:
- grafana_data:/var/lib/grafana
environment:
- GF_SECURITY_ADMIN_PASSWORD=admin

volumes:
prometheus_data:
grafana_data:

Prometheus 配置

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
# prometheus.yml
global:
scrape_interval: 15s
evaluation_interval: 15s

scrape_configs:
- job_name: "scraper"
static_configs:
- targets: ["scraper:8000"]

- job_name: "redis"
static_configs:
- targets: ["redis-exporter:9121"]

- job_name: "mongodb"
static_configs:
- targets: ["mongodb-exporter:9216"]

- job_name: "node"
static_configs:
- targets: ["node-exporter:9100"]

Python 应用集成

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
from prometheus_client import Counter, Histogram, Gauge, start_http_server
import time

# 定义指标
requests_total = Counter(
'scraper_requests_total',
'Total number of requests',
['status', 'domain']
)

request_duration = Histogram(
'scraper_request_duration_seconds',
'Request duration in seconds',
['domain']
)

active_scrapers = Gauge(
'scraper_active_workers',
'Number of active scrapers'
)

queue_size = Gauge(
'scraper_queue_size',
'Size of the URL queue'
)

class MonitoredScraper:
def __init__(self):
# 启动 Prometheus HTTP 服务器
start_http_server(8000)

def scrape(self, url):
"""爬取 URL 并记录指标"""
domain = self._extract_domain(url)

# 记录活跃爬虫数
active_scrapers.inc()

try:
# 记录请求时长
with request_duration.labels(domain=domain).time():
response = requests.get(url, timeout=10)

# 记录请求总数
requests_total.labels(
status=response.status_code,
domain=domain
).inc()

return response

except Exception as e:
requests_total.labels(status='error', domain=domain).inc()
raise
finally:
active_scrapers.dec()

def update_queue_size(self, size):
"""更新队列大小"""
queue_size.set(size)

def _extract_domain(self, url):
from urllib.parse import urlparse
return urlparse(url).netloc

Grafana 可视化

仪表盘配置


日志收集 (ELK Stack)

Docker Compose 配置

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
version: "3"
services:
elasticsearch:
image: elasticsearch:8.11.0
environment:
- discovery.type=single-node
- xpack.security.enabled=false
ports:
- "9200:9200"
volumes:
- es_data:/usr/share/elasticsearch/data

logstash:
image: logstash:8.11.0
ports:
- "5044:5044"
volumes:
- ./logstash.conf:/usr/share/logstash/pipeline/logstash.conf

kibana:
image: kibana:8.11.0
ports:
- "5601:5601"
environment:
- ELASTICSEARCH_HOSTS=http://elasticsearch:9200

filebeat:
image: elastic.co/beats/filebeat:8.11.0
volumes:
- ./filebeat.yml:/usr/share/filebeat/filebeat.yml
- /var/log:/var/log:ro
- /var/lib/docker/containers:/var/lib/docker/containers:ro

volumes:
es_data:

告警配置

AlertManager 配置

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# alertmanager.yml
global:
resolve_timeout: 5m

route:
group_by: ["alertname"]
group_wait: 10s
group_interval: 10s
repeat_interval: 1h
receiver: "email"

receivers:
- name: "email"
email_configs:
- to: "admin@example.com"
from: "alertmanager@example.com"
smarthost: "smtp.example.com:587"
auth_username: "alertmanager@example.com"
auth_password: "password"

- name: "slack"
slack_configs:
- api_url: "https://hooks.slack.com/services/xxx"
channel: "#alerts"
title: "Scraper Alert"

Prometheus 告警规则

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
# alerts.yml
groups:
- name: scraper_alerts
interval: 30s
rules:
# 成功率告警
- alert: LowSuccessRate
expr: scraper_success_rate < 0.8
for: 5m
labels:
severity: warning
annotations:
summary: "Success rate is low"
description: "Success rate is {{ $value }}"

# 队列堆积告警
- alert: QueueBacklog
expr: scraper_queue_size > 10000
for: 10m
labels:
severity: warning
annotations:
summary: "Queue is backing up"
description: "Queue size: {{ $value }}"

# 高错误率告警
- alert: HighErrorRate
expr: rate(scraper_errors_total[5m]) > 10
for: 5m
labels:
severity: critical
annotations:
summary: "High error rate detected"
description: "Error rate: {{ $value }} errors/s"

# Worker 宕机告警
- alert: WorkerDown
expr: scraper_active_workers == 0
for: 1m
labels:
severity: critical
annotations:
summary: "No active workers"

健康检查

HTTP 健康检查端点

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
from flask import Flask, jsonify
import redis

app = Flask(__name__)
r = redis.Redis()

@app.route('/health')
def health():
"""健康检查"""
checks = {
'redis': check_redis(),
'queue': check_queue(),
'workers': check_workers()
}

all_healthy = all(checks.values())
status_code = 200 if all_healthy else 503

return jsonify({
'status': 'healthy' if all_healthy else 'unhealthy',
'checks': checks
}), status_code

def check_redis():
"""检查 Redis 连接"""
try:
r.ping()
return True
except:
return False

def check_queue():
"""检查队列状态"""
try:
size = r.llen('urls')
return size < 50000 # 队列未过载
except:
return False

def check_workers():
"""检查 Worker 状态"""
try:
active = int(r.get('active_workers') or 0)
return active > 0
except:
return False

if __name__ == '__main__':
app.run(host='0.0.0.0', port=8080)

相关章节


相关内容

如果这篇文章对你有帮助,请我喝杯咖啡吧~
+5 支付宝支付宝
+5 微信微信