面试题答案
一键面试设计思路
- 任务队列:使用一个
channel
作为任务队列,将要爬取的URL放入其中。这样可以解耦任务的产生和执行,并且channel
天然具有缓冲能力,可以在一定程度上控制并发量。 - Goroutine池:创建固定数量的Goroutine来从任务队列中获取URL并进行爬取。通过控制Goroutine的数量,可以有效利用资源,避免因Goroutine过多导致的资源耗尽问题。
- 资源限制:
- 网络带宽:通过限制Goroutine数量,减少同时发起的网络请求数,从而避免网络带宽被占满。
- CPU和内存:Goroutine本身占用资源较少,但如果爬取的数据量过大,可能会导致内存问题。可以采用分块处理数据、及时释放不再使用的资源等方式优化。
- 优化Goroutine数量:可以通过实验不同的Goroutine数量,结合系统资源监控工具(如
top
、htop
等),观察系统在不同并发数下的性能表现,找到一个最优的Goroutine数量。也可以根据服务器的CPU核心数、内存大小等硬件参数,按照一定的经验公式来估算初始的Goroutine数量,然后在实际运行中进行调整。 - 错误处理:在每个Goroutine中,对爬取过程中的错误进行捕获,并将错误信息返回给调用者。可以通过返回值或者将错误信息发送到一个专门的错误处理
channel
来实现。 - 超时处理:使用
context.Context
来设置爬取任务的超时时间。context.Context
可以在多个Goroutine之间传递,并且可以方便地取消和设置超时。
关键部分代码示例
package main
import (
"context"
"fmt"
"io/ioutil"
"net/http"
"time"
)
// 定义一个结构体来保存爬取任务和结果
type CrawlTask struct {
URL string
Result []byte
Err error
}
func crawler(ctx context.Context, taskQueue chan string, resultQueue chan CrawlTask, maxWorkers int) {
var semaphore = make(chan struct{}, maxWorkers)
for url := range taskQueue {
semaphore <- struct{}{}
go func(u string) {
defer func() { <-semaphore }()
req, err := http.NewRequestWithContext(ctx, "GET", u, nil)
if err != nil {
resultQueue <- CrawlTask{URL: u, Err: err}
return
}
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
resultQueue <- CrawlTask{URL: u, Err: err}
return
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
resultQueue <- CrawlTask{URL: u, Err: err}
return
}
resultQueue <- CrawlTask{URL: u, Result: body}
}(url)
}
close(resultQueue)
}
func main() {
urls := []string{
"http://example.com",
"http://example.org",
// 更多URL
}
taskQueue := make(chan string, len(urls))
resultQueue := make(chan CrawlTask)
maxWorkers := 10
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
go crawler(ctx, taskQueue, resultQueue, maxWorkers)
for _, url := range urls {
taskQueue <- url
}
close(taskQueue)
for result := range resultQueue {
if result.Err != nil {
fmt.Printf("Error crawling %s: %v\n", result.URL, result.Err)
} else {
fmt.Printf("Successfully crawled %s, length: %d\n", result.URL, len(result.Result))
}
}
}
在上述代码中:
CrawlTask
结构体用于保存爬取任务的URL、结果数据以及可能发生的错误。crawler
函数创建了一个Goroutine池,每个Goroutine从taskQueue
中获取URL进行爬取,并将结果发送到resultQueue
。main
函数初始化任务队列、结果队列,设置超时context
,启动爬虫,并向任务队列中添加URL,最后处理爬取结果。