【问题标题】:Crawling in Golang without loading in memory [closed]在不加载内存的情况下在 Golang 中爬行[关闭]
【发布时间】:2018-03-07 12:52:55
【问题描述】:

我正在尝试用 go 重写一个网络爬虫(最初是用 python 和 gevent 编写的)。但是我碰壁了,无论我做什么,我都会快速消耗大量内存。例如下面的简单代码:

package main

import (
     "bufio"
     "fmt"
     "os"
     "net/http"
     "io"
     "strings"
     "time"
)

func readLine(in *bufio.Reader, domains chan<- string) {
    for conc := 0; conc < 500; conc++ {
        input, err := in.ReadString('\n')
        if err == io.EOF {
            break
        }
        if err != nil {
            fmt.Fprintf(os.Stderr, "read(stdin): %s\n", err)
            os.Exit(1)
        }

        input = strings.TrimSpace(input)
        if input == "" {
            continue
        }

        domain := input
        domains <- domain
    }
}

func get(domains <-chan string) {
   url := <-domains
   URLresp, err := http.Get(url)
   if err != nil {
       fmt.Println(err)
   }
   if err == nil {
       fmt.Println(url," OK")
       URLresp.Body.Close()
   }
}

func main() {
    domains := make(chan string, 500)

    inFile, _ := os.Open("F:\\PATH\\TO\\LIST_OF_URLS_SEPARATED_BY_NEWLINE.txt")
    in := bufio.NewReader(inFile)

    for {
        go readLine(in, domains)
        for i := 0; i < 500; i++ { go get(domains) }
        time.Sleep(100000000)
    }
}

我尝试过 pprof,但似乎说我只使用了 50mb 的堆空间,而资源监控的内存消耗正在飙升。

我还尝试在不使用 Keep Alive 的情况下创建自定义 http 传输,因为我发现 net/http 保存连接以供重用,但没有运气。

【问题讨论】:

  • 你在“无休止地”产生 goroutines(睡眠只有 0.1 秒)。您还在不同步的情况下同时使用in,并且您的所有readLine() 函数都从同一个文件中读取,未同步。而且您不检查打开文件的错误。没有什么好的结果。
  • 您的代码充满了错误。最灾难的一个是有效的分叉炸弹。您正在启动无数个在您的域中读取的 goroutine。
  • 如果你想重用连接,你需要阅读响应正文。如果您不需要重用连接,则应关闭它们以尽快释放资源。
  • 您需要管理生成的 goroutine 的数量 - 请参阅此处 gobyexample.com/worker-pools

标签: go web-crawler


【解决方案1】:

让我们考虑一下您的代码有什么问题,重点关注您的 main() 函数。

func main() {
    domains := make(chan string, 500)

这很好。您创建一个缓冲通道来处理域列表输入。没问题。

    inFile, _ := os.Open("F:\\PATH\\TO\\LIST_OF_URLS_SEPARATED_BY_NEWLINE.txt")

您打开输入文件。您永远不应该忽略错误,但我们现在将忽略它。

    in := bufio.NewReader(inFile)

    for {

在这里你开始一个无限循环。为什么?

        go readLine(in, domains)

这里你从in文件中读取了接下来的500行,将它们传递给domains通道,但是你在后台执行,这意味着下一行将在readLine有机会之前执行完成。

        for i := 0; i < 500; i++ { go get(domains) }

在这里你同时调用get(domains) 500 次。但是如上所述,您在readLine 完成之前执行此操作,因此(至少在第一次通过外循环时),大多数对get() 的调用都会失败,因为domains 通道可能是空的。 get() 函数不能正确处理这种情况,但我会留给你考虑。

        time.Sleep(100000000)

然后在再次开始无限循环之前睡眠 0.1 秒。

    }
}

然后无限循环将再次尝试在后台再次从您的文件中读取接下来的 500 个项目。如果对readLine 的第一次调用需要超过0.1 秒才能完成,那么您将有两个readLine 副本同时尝试读取文件,这可能会导致恐慌。

假设它的行为符合您的预期(尽管很明显不是),在读取文件中的所有 URL 后,程序将永远继续,每 0.1 生成额外的 501 个 go 例程秒。 一个 go 例程尝试从文件中读取更多行,发现没有更多行,然后立即退出。其他 500 个 go 例程最终将永远等待从 domains 通道读取不存在的结果。这是你的内存“泄漏”。

【讨论】:

  • 我在抱怨内存使用情况,您还没有解决我似乎遇到的一个问题。就像我说的,即使我的生成速度很慢,内存使用量也会增长一点点,但还是有泄漏的地方。
  • @geraldog,这是你的内存泄漏! Goroutines 不是免费的,而且你运行的数量越来越多。
  • 但是他们应该退出,不是吗?除非 goroutines 由于某种原因没有退出?关键是如果我生成缓慢,代码不应该加载到内存中,但它确实会加载到内存中,那么有什么用呢?为什么 goroutines 没有退出?
  • 我假设以上所有内容都不完全符合预期 - 如果您的程序行为不可预测,您如何对内存使用有预期 - 当您了解您的程序在做什么时(并且这是有道理的)您可以开始对内存消耗做出假设
  • 它的行为并非不可预测,尽管@Flimzy 有很好的注释,它仍按预期工作。它只是在内存中加载得非常糟糕。
【解决方案2】:

问题是 golang net Dial 中缺少默认超时。它会通过不让 goroutine 死掉来占用资源。以下作品:

c := &http.Client{
 Transport: &http.Transport{
     DisableKeepAlives: true,
     Dial: (&net.Dialer{
             Timeout:   30 * time.Second,
             KeepAlive: 30 * time.Second,
     }).Dial,
     TLSHandshakeTimeout:   10 * time.Second,
     ResponseHeaderTimeout: 10 * time.Second,
     ExpectContinueTimeout: 1 * time.Second,}}

URLresp, err := c.Get(url)

【讨论】:

  • 我怀疑这真的是问题所在,因为DefaultTransport 已经设置了拨号超时。这也暗示此代码在您的 get goroutine 中,但您应该永远丢弃 http.Transport,因为这也会导致内存泄漏。
  • 是的,我也读过,除了这篇文章的重点是设置Client.Timeout,你没有这样做。 ResponseHeaderTimeout 部分涵盖了这种情况,但仍然不会中止缓慢/停滞的响应。设置客户端超时或使用带有取消上下文的请求将防止任何请求情况无限期挂起。
  • 设置超时不会解决你的 goroutine 在文件被完全读取后永远等待domains 通道上的更多输入的问题。 url := &lt;-domains 是你的程序永远挂起的地方。
猜你喜欢
  • 1970-01-01
  • 2012-07-12
  • 1970-01-01
  • 2021-07-22
  • 2016-12-23
  • 2018-03-20
  • 2014-03-02
  • 2018-10-16
  • 1970-01-01
相关资源
最近更新 更多