我怎样才能有效地“最大化”'并发HTTP请求?

时间:2014-04-27 01:38:16

标签: http concurrency go

我目前正在尝试使用Go进行一些实验。这就是我试图做的事情:

我已经运行了一个REST API服务,我想在尽可能多的Goroutines中反复查询特定的URL,看看这些响应的性能如何(通过查看我的REST) API服务器日志)。我想在退出程序之前发送总共100万个HTTP请求 - 执行与我的计算机允许的数量相同的数量。

我知道有一些工具可以做到这一点,但我主要对如何在Go with goroutines中最大化我的HTTP并发感兴趣。

这是我的代码:

package main

import (
    "fmt"
    "net/http"
    "runtime"
    "time"
)

func main() {
    runtime.GOMAXPROCS(runtime.NumCPU())
    transport := &http.Transport{}

    for i := 0; i < 1000000; i++ {
        go func() {
            req, _ := http.NewRequest("GET", "http://myapi.com", nil)
            req.Header.Set("User-Agent", "custom-agent")
            req.SetBasicAuth("xxx", "xxx")
            resp, err := transport.RoundTrip(req)
            if err != nil {
                panic("HTTP request failed.")
            }
            defer resp.Body.Close()

            if resp.StatusCode != 302 {
                panic("Unexpected response returned.")
            }

            location := resp.Header.Get("Location")
            if location == "" {
                panic("No location header returned.")
            }
            fmt.Println("Location Header Value:", location)
        }()
    }

    time.Sleep(60 * time.Second)
}

我期待此代码的作用是:

  • 启动1,000,000个goroutines,每个goroutine向我的API服务发出HTTP请求。
  • 在我的所有CPU上同时运行goroutine(因为我使用运行时包来增加GOMAXPROCS设置)。

然而,会发生以下错误(粘贴太多,所以我只包括一些输出):

goroutine 16680 [IO wait]:
net.runtime_pollWait(0xcb1d878, 0x77, 0x0)
        /usr/local/Cellar/go/1.2/libexec/src/pkg/runtime/netpoll.goc:116 +0x6a
net.(*pollDesc).Wait(0xc212a86ca0, 0x77, 0x55d0c0, 0x24)
        /usr/local/Cellar/go/1.2/libexec/src/pkg/net/fd_poll_runtime.go:81 +0x34
net.(*pollDesc).WaitWrite(0xc212a86ca0, 0x24, 0x55d0c0)
        /usr/local/Cellar/go/1.2/libexec/src/pkg/net/fd_poll_runtime.go:90 +0x30
net.(*netFD).connect(0xc212a86c40, 0x0, 0x0, 0xb4c97e8, 0xc212a84500, ...)
        /usr/local/Cellar/go/1.2/libexec/src/pkg/net/fd_unix.go:86 +0x166
net.(*netFD).dial(0xc212a86c40, 0xb4c87d8, 0x0, 0xb4c87d8, 0xc212a878d0, ...)
        /usr/local/Cellar/go/1.2/libexec/src/pkg/net/sock_posix.go:121 +0x2fd
net.socket(0x2402c0, 0x3, 0x2, 0x1, 0x0, ...)
        /usr/local/Cellar/go/1.2/libexec/src/pkg/net/sock_posix.go:91 +0x40b
net.internetSocket(0x2402c0, 0x3, 0xb4c87d8, 0x0, 0xb4c87d8, ...)
        /usr/local/Cellar/go/1.2/libexec/src/pkg/net/ipsock_posix.go:136 +0x161
net.dialTCP(0x2402c0, 0x3, 0x0, 0xc212a878d0, 0x0, ...)
        /usr/local/Cellar/go/1.2/libexec/src/pkg/net/tcpsock_posix.go:155 +0xef
net.dialSingle(0x2402c0, 0x3, 0xc210d161e0, 0x15, 0x0, ...)
        /usr/local/Cellar/go/1.2/libexec/src/pkg/net/dial.go:225 +0x3d8
net.func·015(0x0, 0x0, 0x0, 0x2402c0, 0x3, ...)
        /usr/local/Cellar/go/1.2/libexec/src/pkg/net/dial.go:158 +0xde
net.dial(0x2402c0, 0x3, 0xb4c8748, 0xc212a878d0, 0xafbbcd8, ...)
        /usr/local/Cellar/go/1.2/libexec/src/pkg/net/fd_unix.go:40 +0x45
net.(*Dialer).Dial(0xafbbd78, 0x2402c0, 0x3, 0xc210d161e0, 0x15, ...)
        /usr/local/Cellar/go/1.2/libexec/src/pkg/net/dial.go:165 +0x3e0
net.Dial(0x2402c0, 0x3, 0xc210d161e0, 0x15, 0x0, ...)
        /usr/local/Cellar/go/1.2/libexec/src/pkg/net/dial.go:138 +0x75
net/http.(*Transport).dial(0xc210057280, 0x2402c0, 0x3, 0xc210d161e0, 0x15, ...)
        /usr/local/Cellar/go/1.2/libexec/src/pkg/net/http/transport.go:401 +0xd4
net/http.(*Transport).dialConn(0xc210057280, 0xc2112efa80, 0x0, 0x0, 0x0)
        /usr/local/Cellar/go/1.2/libexec/src/pkg/net/http/transport.go:444 +0x6e
net/http.func·014()
        /usr/local/Cellar/go/1.2/libexec/src/pkg/net/http/transport.go:419 +0x3e
created by net/http.(*Transport).getConn
        /usr/local/Cellar/go/1.2/libexec/src/pkg/net/http/transport.go:421 +0x11a

我在配备16GB内存和2.6GHz Intel Core i5处理器的Mac OSX 10.9.2笔记本电脑上运行此脚本。

我能做些什么来淹没&#39;我的笔记本电脑有尽可能多的并发HTTP请求吗?

2 个答案:

答案 0 :(得分:14)

正如Rob Napier建议的那样,你几乎肯定会达到文件描述符限制。

编辑:改进的并发版本:

此程序创建一个max goroutines的工作池,从一个频道提取请求,处理它们,并在响应通道上发送它们。请求由dispatcher排队,goroutines由workerPool启动,worker每次处理一个作业,直到请求通道为空,并且{{1处理响应通道,直到成功响应的数量等于请求数。

consumer

产地:

  

连接:1000000
  同时:200
  总大小:15000000字节
  总时间:36m39.6778103s
  平均时间:2.199677ms

警告:此非常会快速达到系统资源限制。在我的笔记本电脑上,超过206个并发工作者导致我的本地测试Web服务器崩溃!

Playground

原始回答: 下面的程序使用缓冲的package main import ( "flag" "fmt" "log" "net/http" "runtime" "time" ) var ( reqs int max int ) func init() { flag.IntVar(&reqs, "reqs", 1000000, "Total requests") flag.IntVar(&max, "concurrent", 200, "Maximum concurrent requests") } type Response struct { *http.Response err error } // Dispatcher func dispatcher(reqChan chan *http.Request) { defer close(reqChan) for i := 0; i < reqs; i++ { req, err := http.NewRequest("GET", "http://localhost/", nil) if err != nil { log.Println(err) } reqChan <- req } } // Worker Pool func workerPool(reqChan chan *http.Request, respChan chan Response) { t := &http.Transport{} for i := 0; i < max; i++ { go worker(t, reqChan, respChan) } } // Worker func worker(t *http.Transport, reqChan chan *http.Request, respChan chan Response) { for req := range reqChan { resp, err := t.RoundTrip(req) r := Response{resp, err} respChan <- r } } // Consumer func consumer(respChan chan Response) (int64, int64) { var ( conns int64 size int64 ) for conns < int64(reqs) { select { case r, ok := <-respChan: if ok { if r.err != nil { log.Println(r.err) } else { size += r.ContentLength if err := r.Body.Close(); err != nil { log.Println(r.err) } } conns++ } } } return conns, size } func main() { flag.Parse() runtime.GOMAXPROCS(runtime.NumCPU()) reqChan := make(chan *http.Request) respChan := make(chan Response) start := time.Now() go dispatcher(reqChan) go workerPool(reqChan, respChan) conns, size := consumer(respChan) took := time.Since(start) ns := took.Nanoseconds() av := ns / conns average, err := time.ParseDuration(fmt.Sprintf("%d", av) + "ns") if err != nil { log.Println(err) } fmt.Printf("Connections:\t%d\nConcurrent:\t%d\nTotal size:\t%d bytes\nTotal time:\t%s\nAverage time:\t%s\n", conns, max, size, took, average) } 作为信号量通道,它限制了并发请求的数量。您可以调整此数字以及请求总数,以便对系统进行压力测试并确定最大值。

chan bool

这将打印如下内容:

  

连接:100000
  总时间:6m8.2554629s

此测试在本地Web服务器上完成,每个请求返回的响应总大小为85B,因此这不是一个真实的结果。此外,我没有对响应进行任何处理,只是关闭它的正文。

在最多1000个并发请求中,我的笔记本电脑花了超过6分钟就完成了100,000个请求,因此我猜测一百万个会占用一个小时。调整package main import ( "fmt" "net/http" "runtime" "time" ) type Resp struct { *http.Response err error } func makeResponses(reqs int, rc chan Resp, sem chan bool) { defer close(rc) defer close(sem) for reqs > 0 { select { case sem <- true: req, _ := http.NewRequest("GET", "http://localhost/", nil) transport := &http.Transport{} resp, err := transport.RoundTrip(req) r := Resp{resp, err} rc <- r reqs-- default: <-sem } } } func getResponses(rc chan Resp) int { conns := 0 for { select { case r, ok := <-rc: if ok { conns++ if r.err != nil { fmt.Println(r.err) } else { // Do something with response if err := r.Body.Close(); err != nil { fmt.Println(r.err) } } } else { return conns } } } } func main() { reqs := 100000 maxConcurrent := 1000 runtime.GOMAXPROCS(runtime.NumCPU()) rc := make(chan Resp) sem := make(chan bool, maxConcurrent) start := time.Now() go makeResponses(reqs, rc, sem) conns := getResponses(rc) end := time.Since(start) fmt.Printf("Connections: %d\nTotal time: %s\n", conns, end) } 变量可以帮助您恢复系统的最高性能。

答案 1 :(得分:2)

您几乎肯定会遇到文件描述符限制。默认限制是2560(旧限制是256,但我认为他们在某些时候x10)。我相当肯定你能设定的最高值是10,000。

我不知道你是否能够以这种方式从一台机器上同时获得一百万个同时连接。您可能希望尝试混合流程和goroutine:每个流程1000个goroutine的10k流程,但如果您无论如何都遇到系统范围限制,我也不会感到惊讶。

为了得到你想要的东西,我相信你需要加速限制(使用缓冲的通道信号量),这样你就不会同时制造超过几千个连接,如果目标只是打击尽可能简单地从一个主机(和一个网卡)中使用API​​。