【问题标题】:Timing of requests in goroutinesgoroutine 中的请求时间
【发布时间】:2015-09-25 10:55:24
【问题描述】:

我正在尝试计算发出并发请求所需的时间。我的计时结果比ab2 报告的慢四倍左右。

我尝试了两种不同的计时请求,这两种方法都导致相似的结果(与 ab2 的结果相差甚远)。举个例子,ab2 将在本地服务器上报告最长的请求持续 2 毫秒,而此代码将报告最多 4.5 毫秒。顺便说一句,整个代码库都可用here

我如何正确地为 go 中的请求计时?

方法一:计时不仅仅包括请求

来自this commit

// Let's spawn all the requests, with their respective concurrency.
wg.Add(r.Repeat)
r.doneWg.Add(r.Repeat)
for rno := 1; rno <= r.Repeat; rno++ {
    go func(no int, greq goreq.Request) {
        r.ongoingReqs <- struct{}{} // Adding sentinel value to limit concurrency.
        startTime := time.Now()
        greq.Uri = r.URL.Generate()
        gresp, err := greq.Do()
        if err != nil {
            log.Critical("could not send request to #%d %s: %s", no, r.URL, err)
        } else if no < r.Repeat {
            // We're always using the last response for the next batch of requests.
            gresp.Body.Close()
        }
        <-r.ongoingReqs // We're done, let's make room for the next request.
        resp := Response{Response: gresp, duration: time.Now().Sub(startTime)}
        // Let's add that request to the list of completed requests.
        r.doneChan <- &resp
        runtime.Gosched()
    }(rno, greq)
}

方法2:使用带有defer语句的内部函数

来自this commit

// Let's spawn all the requests, with their respective concurrency.
wg.Add(r.Repeat)
r.doneWg.Add(r.Repeat)
for rno := 1; rno <= r.Repeat; rno++ {
    go func(no int, greq goreq.Request) {
        r.ongoingReqs <- struct{}{} // Adding sentinel value to limit concurrency.
        greq.Uri = r.URL.Generate()
        var duration time.Duration
        gresp, err := func(dur *time.Duration) (gresp *goreq.Response, err error) {
            defer func(startTime time.Time) { *dur = time.Now().Sub(startTime) }(time.Now())
            return greq.Do()
        }(&duration)

        if err != nil {
            log.Critical("could not send request to #%d %s: %s", no, r.URL, err)
        } else if no < r.Repeat {
            // We're always using the last response for the next batch of requests.
            gresp.Body.Close()
        }
        <-r.ongoingReqs // We're done, let's make room for the next request.
        resp := Response{Response: gresp, duration: duration}
        // Let's add that request to the list of completed requests.
        r.doneChan <- &resp
        runtime.Gosched()
    }(rno, greq)
}

我查看了this question,但没有帮助。

【问题讨论】:

  • 如果你想创建一个基准,用“goreq”在 http 包上添加另一层似乎适得其反。首先尝试使用标准的 http 包,然后再配置文件(如果还有时间无法计算)。已经用 Go 编写了许多 http 负载生成器,效果很好。
  • 服务器是否在响应正文中返回 any 内容?
  • @JimB,是的,服务器可能会返回内容。而这个负载生成器实际上可以将返回的数据用于后续请求。
  • 我没有看到您在哪里阅读正文,但如果您不阅读该内容并尽快关闭正文,则该连接将被阻止发出更多请求,需要您创建新的连接。 (这也被额外的 goreq 包所掩盖)
  • 您需要阅读完整的回复尽快关闭正文。在您阅读整个响应之前,该请求尚未完成。我不确定回答第二部分,如果你以后想要身体,你需要把它存放在某个地方。

标签: go timing goroutine


【解决方案1】:

当您的 goroutine 进入系统调用(写入套接字)时,它们会被抢占。这意味着它们被中断,另一个 goroutine 将在它们的位置运行。最终,你被抢占的 goroutine 将再次被调度,它将继续从它停止的地方运行。不过,这并不一定会在系统调用完成之后发生。

在 goroutine 中计时很困难,因为即使您以串行方式完成所有操作,Go 1.5 的垃圾收集器也会偶尔运行,从而中断您理论上的串行循环。

唯一真正的解决方案有点复杂:

  1. 直接使用RawSyscall
  2. 使用 //go:nosplit 注释您的函数,以防止它被抢占。
  3. 禁用垃圾收集器。

即便如此,我也可能会忘记一些事情。

【讨论】:

  • 感谢您的快速回答。我在//go:nosplit 上找不到太多文档,我应该把它放在哪里?如果我把它放在go func 指令之上,甚至是func (r *Request) Spawn 函数之上,我会得到一个链接时间堆栈溢出错误。有什么想法吗?
  • 记录在案here。我认为它只适用于命名函数。我已经要求对 golang-nuts 上的这个答案进行同行评审。也许有人想出了更聪明的办法。
  • 您打算使用 RawSyscall 做什么?重写所有的网络 IO,这是一个已经做了很多性能调整的领域?这是尝试发出 http 请求,这是一个相当复杂的过程,而不是单个系统调用。
  • @JimB 基于this question 似乎有道理。
  • 我仍然不明白它是如何应用的。如果您重写 net 包以某种方式仅使用 RawSyscall,则运行时将无法在同一线程中调度并发 IO。您的时间可能更具确定性,但您必须将每个请求固定到它自己的线程以防止阻塞。您不妨使用runtime.LockOSThread 并直接在您自己的自定义 net.Conn 上使用阻塞 IO。
猜你喜欢
  • 1970-01-01
  • 2014-02-26
  • 1970-01-01
  • 2016-05-04
  • 1970-01-01
  • 2020-09-04
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多