修复一个卡死问题
This commit is contained in:
+72
-7
@@ -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 队列。
|
||||
|
||||
Reference in New Issue
Block a user