errgroup.Group 不适合直接抓千万级网页,因其仅聚合首个错误且无内置限流、重试、去重、错误抑制和内存控制机制;直接启动百万 goroutine 会瞬间耗尽内存与文件描述符,引发 OOM 或 “too many open files” 错误。
为什么 errgroup.Group 不适合直接抓千万级网页
它本身不解决限流、重试、去重、错误抑制或内存爆炸问题,只负责“等待一组 goroutine 并聚合首个非 nil error”。直接用 errgroup.withcontext(ctx) 启动百万 goroutine,会瞬间耗尽内存和文件描述符,触发 too many open files 或 oom kill。
真实场景中,你要的不是“并发启动所有请求”,而是“可控并发 + 持续消费 URL 队列 + 失败可恢复 + 结果可流式聚合”。
- 千万级 URL 不能全 load 到内存;得用 channel 或迭代器分批供给
-
errgroup.Go每调用一次就启一个 goroutine,无内置限速机制 - 单个请求失败默认导致整个
errgroup.Wait()提前退出,而你通常希望跳过失败页继续抓 - 聚合结果需避免全量存内存——应边抓边写入文件、数据库或发送到下游 channel
用 semaphore.Weighted + errgroup.Group 实现安全并发控制
Go 1.21+ 标准库 golang.org/x/sync/semaphore 提供了带权重的信号量,比手写计数器更可靠。配合 errgroup.Group 可做到:固定并发数、允许部分失败、支持 context 取消。
示例核心逻辑:
sem := semaphore.NewWeighted(int64(concurrency))
eg, ctx := errgroup.WithContext(context.Background())
<p>for _, url := range urls[:1000] { // 实际应从 stream 读取
u := url // 防止闭包引用
eg.Go(func() error {
if err := sem.Acquire(ctx, 1); err != nil {
return err
}
defer sem.Release(1)</p><pre class="brush:php;toolbar:false;"> resp, err := http.DefaultClient.Do(http.NewRequestWithContext(ctx, "GET", u, nil))
if err != nil {
return fmt.Errorf("fetch %s: %w", u, err) // 错误带上下文
}
defer resp.Body.Close()
// 解析、存结果(建议写入 chan<result> 而非 []Result)
return nil
})</result>
}
if err := eg.Wait(); err != nil { log.Printf("group done with error: %v", err) }
- 并发数由
semaphore.NewWeighted(N)控制,N 通常设为 50~200,取决于目标站点抗压能力与本地资源 -
sem.Acquire可能被 ctx cancel 中断,返回context.Canceled,此时应快速退出而非重试 - 不要在
eg.Go闭包里直接用循环变量(如url),必须显式拷贝(u := url) - HTTP client 应复用连接:
&http.Client{Transport: &http.Transport{MaxIdleConns: 100, MaxIdleConnsPerHost: 100}}
如何避免结果堆积内存:用 channel 替代 slice 聚合
把千万条抓取结果全 append 到 []Result,极易触发 GC 压力甚至 OOM。正确做法是让每个 worker 抓完后发到一个带缓冲的 chan,由单独 goroutine 消费并落地(写文件、入库、打日志)。
关键改动点:
- 声明
results := make(chan Result, 1000),缓冲区大小按吞吐调节 - 每个
eg.Go函数里,解析成功后执行select { case results - 额外起一个 goroutine:
go func() { for r := range results { save(r) } }() - 主流程结束前关闭 channel:
close(results)(注意要在eg.Wait()后)
这样内存占用恒定,且天然支持背压——如果 save() 变慢,channel 缓冲区满后会阻塞 worker,自动降速。
实际部署时最常被忽略的三个点
不是代码写对就能跑通千万级任务,以下三点漏掉任一,上线必崩:
- DNS 缓存没做:默认
net/http每次都走系统 resolver,高并发下 DNS 查询成瓶颈。加http.DefaultClient.Transport.DialContext = fastDialer,或用github.com/miekg/dns做本地缓存 - 没有 User-Agent 和 robots.txt 检查:大量站点封禁默认 UA,且可能违反爬虫协议。至少设置
req.Header.Set("User-Agent", "MyCrawler/1.0"),并提前解析目标站/robots.txt - 日志没分级:用
log.Printf打所有请求,IO 直接打满。应只对 error 和 warn 级别打完整 URL,info 级别仅记统计(如 “200: 12489”),debug 级别关掉
真正卡住进度的,往往不是并发模型,而是 DNS、TCP 连接池、TLS 握手缓存、磁盘 IO 这些底层细节。先压测单机 1k URL/min,再逐步扩量,比一上来就跑千万更靠谱。











