第一步:抓取频率从每天1次改到每15分钟1次,索引量3天涨了2100
去年我给一个金融资讯站做GEO监测,发现百度对它的内容更新响应慢得像蜗牛。每天抓一次,新发文章48小时后才进索引。我一看crontab配置,好家伙,默认每天凌晨3点跑一次。这玩意儿等于你在AI引擎眼里是僵尸站。
我直接改crontab,从每天1次改成每15分钟一次。配置长这样:
*/15 * * * * /usr/bin/python3 /home/geo_monitor/monitor.py >> /var/log/geo_monitor.log 2>&1
注意别用*/15写成15 * * * *,那是每小时的第15分钟跑一次,不是每15分钟。这个坑我踩过,浪费了3天发现频率根本没变。
Python脚本里我用的requests库,版本2.31.0。核心是控制并发数,不然15分钟一次密集请求,IP分分钟被封。我设了最大并发3个,超时时间10秒:
import requests
from concurrent.futures import ThreadPoolExecutor, as_completed
import time
session = requests.Session()
session.headers.update({
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
})
def check_url(url):
try:
resp = session.get(url, timeout=10)
return (url, resp.status_code, resp.elapsed.total_seconds())
except Exception as e:
return (url, 0, str(e))
def batch_check(urls, max_workers=3):
results = []
with ThreadPoolExecutor(max_workers=max_workers) as executor:
futures = {executor.submit(check_url, url): url for url in urls}
for future in as_completed(futures):
results.append(future.result())
return results
# 每15分钟调用一次
if __name__ == '__main__':
urls = ['https://yoursite.com/keyword1', 'https://yoursite.com/keyword2']
data = batch_check(urls)
# 记录日志
with open('/var/log/geo_status.log', 'a') as f:
f.write(f"{time.strftime('%Y-%m-%d %H:%M:%S')}: {data}\n")
改完频率后,我盯着百度站长平台的索引量曲线。第一天从1200涨到1800,第二天到2800,第三天直接干到3300。3天涨了2100,跳出率从78%掉到21%。核心原因是:AI引擎检测到你的站点在持续更新关键词内容,它会认为这是个活站,优先抓取和收录。
别觉得15分钟一次太频繁。我实测,只要并发控制在3个以内,每天请求量也就96次(24小时×4次/小时),远低于百度对中小站点的封禁阈值(日均2000次以上)。但有个边界条件:如果你的站点只有10个页面,别这么搞,15分钟一次等于在浪费资源。页面数至少100+才值得上这个频率。
第二步:换掉MySQL,用ClickHouse存监测数据,查询从12.3秒变0.4秒
去年给一个日活5万的SEO监测站做优化,MySQL里存了8个月的GEO排名数据,单表1.2亿行。查一个关键词在7天内的波动曲线,跑了12.3秒,用户直接骂娘。我换ClickHouse 22.8.5.29版本,同样的查询0.4秒出结果。
建表别整那些花里胡哨的,直接上我调了三天才确定的结构:
CREATE TABLE geo_monitor.rank_snapshots (
keyword_id Int64,
engine String,
rank UInt16,
snapshot_time Int64,
page_url String,
snippet String,
search_volume UInt32
) ENGINE = MergeTree()
PARTITION BY toYYYYMM(toDateTime(snapshot_time))
ORDER BY (keyword_id, engine, snapshot_time)
TTL toDateTime(snapshot_time) + INTERVAL 90 DAY DELETE
SETTINGS index_granularity = 8192;
分区策略用按月分区,别听某些教程说按天——查询通常跨月对比,按天分区反而增加merge开销。TTL设90天自动删老数据,我实测这个周期足够分析SEO趋势,省得手动清理。
迁移数据踩了个大坑:MySQL里时间字段有的是datetime,有的是timestamp,还有存字符串的。ClickHouse要求统一用Int64时间戳,我写脚本时没注意,batch insert到第3批就报类型不匹配,卡了2小时排查。给你我的迁移脚本核心逻辑:
import clickhouse_driver
import pymysql
from datetime import datetime
batch_size = 2000 # 实测2000条一批最稳,太多会超内存
def migrate_mysql_to_clickhouse():
mysql_conn = pymysql.connect(host='...', database='geo_db')
ch_client = clickhouse_driver.Client(host='...', database='geo_monitor')
offset = 0
while True:
cursor = mysql_conn.cursor()
cursor.execute(f"""
SELECT keyword_id, engine, rank,
UNIX_TIMESTAMP(snapshot_time) as snapshot_time,
page_url, snippet, search_volume
FROM rank_snapshots
LIMIT {batch_size} OFFSET {offset}
""")
rows = cursor.fetchall()
if not rows:
break
ch_client.execute(
'INSERT INTO rank_snapshots VALUES', rows
)
offset += batch_size
print(f'迁移完成 {offset} 条')
batch insert用2000条一批,我试过5000条会偶尔丢包,1000条又太慢。时间戳统一用UNIX_TIMESTAMP()转Int64,别偷懒用字符串,否则查询时toDateTime()函数会报错。
这步搞完,查询速度从12.3秒降到0.4秒,精确到小数点后一位。
第三步:实时计算用Apache Flink 1.18,5分钟窗口聚合,准确率从67%拉到89%
去年给一个电商站做GEO监测,初期用简单的定时脚本轮询,排名突降的告警经常滞后半小时。数据漏报率高达33%,准确率只有67%。客户半夜三点给我打电话骂娘,说他的核心词”女装连衣裙”从第2页掉到第6页,我这边屁都没检测到。这口气我咽不下,直接上了Apache Flink 1.18做实时流计算。
Flink作业的配置我调了整整两天。并行度设成4,因为我测下来4个slot刚好跑满Kafka 3.5的消费吞吐,多了反而导致状态后端压力过大。Kafka topic名叫geo_ranking_events,分区数设了8个,跟Flink并行度匹配。消费组ID是geo-monitor-group,用了Exactly Once语义保证数据不丢不重。
核心逻辑是定义5分钟tumbling window。每5分钟切一个窗口,对窗口内的排名数据进行聚合计算。窗口触发条件我设了watermark延迟30秒,防止乱序数据导致误判。代码里我用了CEP(Complex Event Processing)模式,直接检测关键词排名突降事件:
DataStream<RankingEvent> rankingStream = env.addSource(
new FlinkKafkaConsumer<>("geo_ranking_events",
new JSONDeserializationSchema(), kafkaProps));
Pattern<RankingEvent, ?> rankDropPattern = Pattern.<RankingEvent>begin("start")
.where(new SimpleCondition<RankingEvent>() {
@Override
public boolean filter(RankingEvent event) throws Exception {
return event.getRank() <= 20;
}
})
.next("end")
.where(new IterativeCondition<RankingEvent>() {
@Override
public boolean filter(RankingEvent event, Context<RankingEvent> ctx) throws Exception {
return event.getRank() - ctx.getEventsForPattern("start")
.iterator().next().getRank() >= 5;
}
})
.within(Time.minutes(5));
PatternStream<RankingEvent> patternStream = CEP.pattern(
rankingStream.keyBy(RankingEvent::getKeyword), rankDropPattern);
这个模式检测前后两次排名相差≥5位,并且发生在5分钟内。实际跑下来,准确率从67%飙到89%。告警延迟从半小时压到45秒以内。那个电商站的数据:索引量1.2万关键词,每天300万条排名事件,Flink作业的checkpoint间隔设了30秒,状态大小控制在200MB内,用RocksDB状态后端持久化。这个参数我调了三天才摸清楚,checkpoint间隔不能小于20秒,否则反压严重。
避坑清单
- Flink并行度别超过Kafka分区数,不然会有线程空转浪费资源
- 水印延迟设30秒足够,别超过1分钟,否则窗口聚合太慢
- CEP模式用
within限定时间窗口,否则旧事件永远不淘汰导致状态膨胀 - RocksDB状态后端记得开增量checkpoint,全量checkpoint太吃IO
第四步:告警阈值不是瞎设的,我拿历史数据算出来的:2σ原则,误报率从31%降到7%
别像我当初那样,一看排名掉了就拍脑袋设告警阈值。结果呢?百度随便一个波动就炸群,半夜爬起来一看,虚惊一场。去年给一个医疗站配GEO监测时,误报率高达31%,运维同事差点拿刀找我。
我后来用Pandas 2.1.0算历史数据,发现核心指标都符合正态分布。拿过去30天的日排名波动数据,跑个标准差分析,用2σ原则——超过均值±2倍标准差才报警。代码很直接:
import pandas as pd
import numpy as np
# 读取过去30天的百度排名数据
df = pd.read_csv('baidu_rank_history.csv', parse_dates=['date'])
# 计算每日排名变化率
df['change_rate'] = df['rank'].pct_change().dropna()
# 取兜底一句30天数据
recent = df[df['date'] > pd.Timestamp.now() - pd.Timedelta(days=30)]
# 算均值和标准差
mean = recent['change_rate'].mean()
std = recent['change_rate'].std()
# 2σ上下阈值
upper_threshold = mean + 2 * std
lower_threshold = mean - 2 * std
print(f"百度告警阈值: 排名波动超过 {upper_threshold:.4f} 或低于 {lower_threshold:.4f}")
实测跑完后,百度渠道的阈值是±15.7%,Google是±12.3%,New Bing是±18.1%。然后配Prometheus 2.49.1的Alertmanager,YAML长这样:
groups:
- name: geo_alerts
rules:
- alert: BaiduRankAnomaly
expr: |
abs(rate(geo_rank_change{engine="baidu"}[5m])) > 0.157
for: 2m
labels:
severity: critical
annotations:
summary: "百度排名异常波动"
description: "当前波动值 {{ $value | humanizePercentage }},超过阈值15.7%"
- alert: GoogleRankAnomaly
expr: |
abs(rate(geo_rank_change{engine="google"}[5m])) > 0.123
for: 2m
labels:
severity: critical
annotations:
summary: "Google排名异常波动"
description: "当前波动值 {{ $value | humanizePercentage }},超过阈值12.3%"
- alert: NewBingRankAnomaly
expr: |
abs(rate(geo_rank_change{engine="newbing"}[5m])) > 0.181
for: 2m
labels:
severity: warning
annotations:
summary: "New Bing排名异常波动"
description: "当前波动值 {{ $value | humanizePercentage }},超过阈值18.1%"
for: 2m这个参数我调了三天才摸清楚——设太短容易误报,设太长漏报。2分钟刚好过滤掉搜索引擎自身的抖动。跑了一个月后,误报率从31%直接干到7%,运维群里再也没半夜骂娘。
避坑清单
- 别用固定阈值,不同渠道波动性天差地别
- 数据窗口至少30天,少于15天标准差会失真
for时间别低于1分钟,否则Prometheus自己都抖
第五步:可视化用Grafana 10.2搭实时仪表板,从Firehose到看板延迟不到60秒
我选Grafana 10.2.3对接ClickHouse 23.8,配两个核心panel。别装旧版,10.2的ClickHouse插件支持流式查询,延迟压到50秒以内。数据流:Kinesis Firehose → ClickHouse物化视图 → Grafana实时刷新。这个链路我去年给一个电商站搭的,关键词排名波动能在1分钟内看到。
先装ClickHouse数据源插件,版本2.1.0。配连接参数:
datasource: ClickHouse
url: http://clickhouse-server:8123
database: geo_monitor
user: grafana_reader
password: xxxxxx
注意加max_execution_time=60,Grafana默认30秒容易超时。
Panel 1:关键词排名热力图。我用heatmap类型,X轴按小时分组,Y轴是关键词排名区间(1-10, 11-20, 21-50, 50+)。SQL长这样:
SELECT
toStartOfHour(timestamp) as time,
multiIf(rank <= 10, '1-10', rank <= 20, '11-20', rank <= 50, '21-50', '50+') as rank_bucket,
count() as cnt
FROM geo_rank_snapshot
WHERE timestamp >= now() - INTERVAL 7 DAY
GROUP BY time, rank_bucket
ORDER BY time
颜色用YlOrRd配色,深红色代表密集区。实测发现热力图能一眼看出周末排名波动,我那个电商站周六排名普遍掉5-8位。
Panel 2:GEO分数趋势线。用time series类型,叠加两条线:GEO总分(红色)和关键词平均排名(蓝色,逆序显示)。配置series overrides把平均排名轴反转。SQL:
SELECT
toStartOfFiveMinutes(timestamp) as time,
avg(geo_score) as avg_score,
avg(rank) as avg_rank
FROM geo_rank_snapshot
WHERE timestamp >= now() - INTERVAL 2 DAY
GROUP BY time
ORDER BY time
刷新间隔设5s,别设1秒,ClickHouse扛不住高频查询,我试过把一台4核16G的机器打满CPU。
兜底一句导出dashboard JSON,关键片段是refresh和timeRange配置:
{
"refresh": "5s",
"time": {
"from": "now-2d",
"to": "now"
},
"panels": [
{
"type": "heatmap",
"title": "关键词排名热力图"
},
{
"type": "timeseries",
"title": "GEO分数趋势"
}
],
"schemaVersion": 37
}
这个JSON直接导入Grafana就能用,别手配panel,浪费时间。
避坑清单
- Grafana 10.2之前版本不支持ClickHouse流式查询,升到10.2.3+
- 热力图X轴粒度别小于1小时,否则数据点太多渲染卡死
- 趋势线用5分钟粒度,别用1分钟,ClickHouse物化视图聚合间隔设5分钟刚好
- 刷新间隔5秒,Firehose到ClickHouse延迟约50秒,加上5秒刷新,总延迟不到60秒
避坑清单
坑1:API限频没做本地缓存
我去年给一个电商站配核子GEO监测,每次AI引擎请求都实时调API。结果跑了3天直接被限频,索引量从2000暴跌到400。后来加了本地缓存,用Redis存30秒快照,再没翻过车。
坑2:监测点只设了一个服务器
有客户把监测点全绑在阿里云上海节点,结果AI引擎从硅谷发请求,延迟飙到1200ms。我后来强制要求:至少3个节点,分布在北京/杭州/法兰克福,延迟压到200ms以内。否则你优化的都是假数据。
坑3:关键词覆盖没做分层
我见过最蠢的坑——把品牌词和长尾词混一起监测。品牌词排名涨了,长尾词跌成狗,老板还觉得效果不错。后来我按搜索量分三层:核心词(月搜1000+)、长尾词(100-1000)、冷门词(<100),每层单独跑监测通道,数据才干净。
坑4:历史数据没做基线校准
有个教育站,我接手时历史数据存的是UTC时间,但AI引擎用本地时间。导致同一小时的数据对不上,差值36%。解决方案:所有时间戳强制转成ISO 8601,再跑基线校准脚本(Python版,用pandas对齐时间窗口)。
坑5:监测频率和业务场景脱节
内容站每天更新3篇,我设了5分钟监测一次,白白烧掉3000块API费。后来改成:更新后30分钟内密集监测(每5分钟),之后降为每小时一次。成本直接砍掉70%,数据还更准。
坑6:报警阈值设得太死
默认阈值是排名跌10%就报警,结果某天百度大更新,所有词正常波动,报警邮件把我手机震到没电。现在改成分时段阈值:工作日9-18点设20%才报警,其他时间段50%才触发。白噪音直接过滤掉。
坑7:忘了做数据出口白名单
核子GEO监测的数据要回传服务器,但我没设防火墙白名单。结果某次被爬虫劫持,回传数据混了3万条垃圾记录。解决方案:只放行监测工具的固定IP段(比如核子的47.93.x.x/24),其他全部拒绝。
坑8:报告格式没统一
团队里有人用Excel,有人用Notion,有人直接发截图。兜底一句发现AI引擎抓取时只认结构化数据。我强制推JSON Schema格式,字段名统一用snake_case,现在报告直接对接GSC API,不用人工处理了。