直接启动大量goroutine发HTTP请求会崩,因未限制并发导致文件描述符耗尽、DNS超时、目标封IP等;应使用带缓冲channel(如sem := make(chan struct{}, 10))作信号量精确控制同时运行请求数。

为什么直接用 go 启动协程会崩掉
不是协程本身有问题,是没做任何限制时,几万个 go fetch(url) 会在瞬间发起连接请求,触发系统级限制:文件描述符耗尽、DNS 查询超时、目标站封 IP、甚至本地 net.Dial 返回 dial tcp: lookup xxx: no such host。Go 的 http.Client 默认不带并发节流,它只管发,不管排队。
- 默认
http.DefaultClient底层复用连接,但连接池(Transport.MaxIdleConns等)不等于“并发请求数控制” -
runtime.GOMAXPROCS控制的是 OS 线程数,和 HTTP 并发量无关 - 真正起作用的是你主动调度的协程数量 + 它们实际发出的网络请求节奏
用 semaphore 控制并发数最直接
别绕弯子,用一个带缓冲的 channel 当信号量,是最轻量、最可控的方式。它不依赖第三方库,语义清晰,且能精确卡住“同时最多 N 个请求在跑”。
// 并发上限设为 10
sem := make(chan struct{}, 10)
<p>for _, url := range urls {
sem <- struct{}{} // 阻塞直到有空位
go func(u string) {
defer func() { <-sem }() // 退出前释放
resp, err := http.Get(u)
// ... 处理 resp / err
}(url)
}- 缓冲大小就是最大并发数,改一个数字就能调压
- 必须用
defer func() { ,否则 panic 时漏释放,后续协程全卡死 - 不要把
sem声明在循环内,否则每个 goroutine 拿到的是不同 channel
http.Client 连接池配置要跟上
光控协程数不够,如果底层 TCP 连接反复建连,照样慢、照样崩。得让 http.Client 复用连接,并容忍一定数量的闲置连接。
初始化并管理阿里云SDK Go 客户端,涵盖 sync.Once 单例、goroutine 安全、endpoint 与 region 配置、VPC 端点及调试。
<pre class="brush:php;toolbar:false;">client := &http.Client{
Transport: &http.Transport{
MaxIdleConns: 100,
MaxIdleConnsPerHost: 100,
IdleConnTimeout: 30 * time.Second,
},
}MaxIdleConns 是全局最大空闲连接数;<code>MaxIdleConnsPerHost才是关键——它限制单域名能保持多少空闲连接,避免被某个大站吃光池子- 若爬多个域名,
MaxIdleConnsPerHost设太小(如默认 2)会导致频繁建连,拖慢整体速度 - 超时类参数(
Timeout,KeepAlive)也建议显式设,避免卡死在 DNS 或 TLS 握手阶段
错误处理不当会让整个爬虫“静默失联”
HTTP 错误(4xx/5xx)、连接失败、重定向循环、空响应体……这些不显式判断,就会让协程悄无声息退出,你以为还在跑,其实早停了。
- 永远检查
err != nil,而不是只看resp.StatusCode -
resp.Body必须Close(),否则连接无法归还给池子,MaxIdleConns很快被占满 - 对
io.EOF、net/http: request canceled这类错误要区分对待:前者可能是正常空响应,后者大概率是上下文取消或超时 - 建议用
context.WithTimeout包一层请求,避免单个请求卡住整个协程
真正难的不是并发,是让每个协程在出错、超时、限流、重试之间保持状态一致。信号量管数量,连接池管资源,context 管生命周期,三者少一个,爬虫就不可靠。

















