修复一个卡死问题

This commit is contained in:
2026-04-08 18:44:51 +08:00
parent c154abf410
commit 1d3570a505
3 changed files with 231 additions and 7 deletions
+72 -7
View File
@@ -4,6 +4,7 @@ package crawler
import (
"bytes" // 字节缓冲(构造 HTTP POST 请求体)
"context" // context 超时控制
"encoding/json" // JSON 序列化(发送关键词数据到 harvester)
"log" // 日志输出
"math" // 数学运算(指数衰减、质量评分)
@@ -28,13 +29,30 @@ type Stats struct {
KeywordsFetched int64 // 累计提取的关键词总数
}
// 熔断器状态(用 atomic int32 代替 mutex,避免持有锁时的慢 I/O)。
const (
circuitClosed int32 = iota // 正常:所有请求都发往 harvester
circuitOpen // 断开:连续失败 N 次后,冷却时间内跳过所有请求
circuitHalfOpen // 半开:冷却结束,尝试放行一次请求试探
)
const (
circuitFailureThreshold = 5 // 连续失败多少次后触发熔断
circuitCooldownSeconds = 30 // 熔断持续时间(秒)
)
// Crawler 编排整个 BFS 爬取流程。
type Crawler struct {
fetcher *Fetcher // HTTP 抓取器(含 robots.txt 和限流)
db *storage.DB // 持久化数据库
analyzer *analyzer.Analyzer // 分词和关键词分析
prosperMap map[string]float64 // 域名 → 反向链接繁荣值(来自 info 模块,越大越"有价值")
stats Stats // 原子计数器
fetcher *Fetcher // HTTP 抓取器(含 robots.txt 和限流)
db *storage.DB // 持久化数据库
analyzer *analyzer.Analyzer // 分词和关键词分析
prosperMap map[string]float64 // 域名 → 反向链接繁荣值(来自 info 模块,越大越"有价值")
stats Stats // 原子计数器
// 熔断器(全用 atomic,无 mutex,无慢 I/O 时持有锁的风险)
circuitState int32 // circuitClosed | circuitOpen | circuitHalfOpen
circuitFailures int32 // 连续失败计数(atomic
circuitExpiry int64 // 熔断/半开截止 Unix 时间戳(秒)
}
// New 创建一个 Crawler 实例。
@@ -269,7 +287,32 @@ func (c *Crawler) updateSiteSuccess(host string, res *FetchResult, title, desc,
}
// sendToHarvester 将关键词索引数据通过 HTTP POST 发送到收获服务器(:5000/l 端点)。
// 熔断器基于 atomic 实现(无 mutex,不在持有锁时做慢 I/O),确保 goroutine 不会因 harvester 故障而堆积。
func (c *Crawler) sendToHarvester(finalURL string, kws []analyzer.Keyword) {
now := time.Now().Unix()
// ---- 熔断检查(atomic,无锁) ----
state := atomic.LoadInt32(&c.circuitState)
expiry := atomic.LoadInt64(&c.circuitExpiry)
switch state {
case circuitOpen:
if now < expiry {
return // 熔断中,直接跳过
}
// 冷却结束,切换到半开,放行一个试探请求
atomic.StoreInt32(&c.circuitState, circuitHalfOpen)
atomic.StoreInt64(&c.circuitExpiry, now+int64(circuitCooldownSeconds))
log.Println("[crawler] circuit: half-open, probing harvester")
case circuitHalfOpen:
if now < expiry {
return // 半开冷却中,只放行第一个,其余跳过
}
// 半开超时,重新进入半开状态
atomic.StoreInt32(&c.circuitState, circuitHalfOpen)
atomic.StoreInt64(&c.circuitExpiry, now+int64(circuitCooldownSeconds))
}
type payload struct {
URL string `json:"url"`
Keywords []analyzer.Keyword `json:"keywords"`
@@ -279,12 +322,34 @@ func (c *Crawler) sendToHarvester(finalURL string, kws []analyzer.Keyword) {
if err != nil {
return
}
resp, err := http.Post(config.HarvesterAddr+"/l", "application/json", bytes.NewReader(data))
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
req, err := http.NewRequestWithContext(ctx, "POST", config.HarvesterAddr+"/l", bytes.NewReader(data))
if err != nil {
log.Printf("[crawler] harvester post failed: %v", err)
return
}
req.Header.Set("Content-Type", "application/json")
// ---- HTTP 请求(此时没有任何锁) ----
resp, err := http.DefaultClient.Do(req)
// ---- 结果处理(atomic,无锁) ----
if err != nil {
failures := atomic.AddInt32(&c.circuitFailures, 1)
if failures >= circuitFailureThreshold {
atomic.StoreInt32(&c.circuitState, circuitOpen)
atomic.StoreInt64(&c.circuitExpiry, now+int64(circuitCooldownSeconds))
log.Printf("[crawler] circuit OPEN: harvester unreachable (%d failures), cooling for %ds",
failures, circuitCooldownSeconds)
}
return
}
resp.Body.Close()
// ---- 成功:重置熔断器 ----
atomic.StoreInt32(&c.circuitFailures, 0)
atomic.StoreInt32(&c.circuitState, circuitClosed)
}
// schedule 从候选 URL 集合中选出下一轮 BFS 队列。