【问题标题】:Go HTTP Server Performance IssueGo HTTP 服务器性能问题
【发布时间】:2018-10-05 14:19:15
【问题描述】:

我正在编写一个负载很重的事件收集器 http 服务器。因此,在 http 处理程序中,我只是反序列化事件,然后在 goroutine 中的 http 请求-响应周期之外运行实际处理。

有了这个,我发现如果我以每秒 400 个请求的速度访问服务器,那么对于 99 个百分位,延迟低于 20 毫秒。但是,一旦我将请求率提高到每秒 500 次,延迟就会飙升至超过 800 毫秒。

任何人都可以帮助我了解原因可能是什么,以便我可以探索更多。

package controller

import (
    "net/http"
    "encoding/json"
    "event-server/service"
    "time"
)

func CollectEvent() http.Handler {
    handleFunc := func(w http.ResponseWriter, r *http.Request) {
        startTime := time.Now()
        stats.Incr("TotalHttpRequests", nil, 1)
        decoder := json.NewDecoder(r.Body)
        var event service.Event
        err := decoder.Decode(&event)
        if err != nil {
            http.Error(w, "Invalid json: " + err.Error(), http.StatusBadRequest)
            return
        }
        go service.Collect(&event)
        w.Write([]byte("Accepted"))
        stats.Timing("HttpResponseDuration", time.Since(startTime), nil, 1)
    }

    return http.HandlerFunc(handleFunc)
}

我以每秒 1000 个请求运行测试并对其进行了分析。以下是结果。

(pprof) top20
Showing nodes accounting for 3.97s, 90.85% of 4.37s total
Dropped 89 nodes (cum <= 0.02s)
Showing top 20 nodes out of 162
      flat  flat%   sum%        cum   cum%
     0.72s 16.48% 16.48%      0.72s 16.48%  runtime.mach_semaphore_signal
     0.65s 14.87% 31.35%      0.66s 15.10%  syscall.Syscall
     0.54s 12.36% 43.71%      0.54s 12.36%  runtime.usleep
     0.46s 10.53% 54.23%      0.46s 10.53%  runtime.cgocall
     0.34s  7.78% 62.01%      0.34s  7.78%  runtime.mach_semaphore_wait
     0.33s  7.55% 69.57%      0.33s  7.55%  runtime.kevent
     0.30s  6.86% 76.43%      0.30s  6.86%  syscall.RawSyscall
     0.10s  2.29% 78.72%      0.10s  2.29%          runtime.mach_semaphore_timedwait
     0.07s  1.60% 80.32%      1.25s 28.60%  net.dialSingle
     0.06s  1.37% 81.69%      0.11s  2.52%  runtime.notetsleep
     0.06s  1.37% 83.07%      0.06s  1.37%  runtime.scanobject
     0.06s  1.37% 84.44%      0.06s  1.37%  syscall.Syscall6
     0.05s  1.14% 85.58%      0.05s  1.14%  internal/poll.convertErr
     0.05s  1.14% 86.73%      0.05s  1.14%  runtime.memmove
     0.05s  1.14% 87.87%      0.05s  1.14%  runtime.step
     0.04s  0.92% 88.79%      0.09s  2.06%  runtime.mallocgc
     0.03s  0.69% 89.47%      0.58s 13.27%  net.(*netFD).connect
     0.02s  0.46% 89.93%      0.40s  9.15%  net.sysSocket
     0.02s  0.46% 90.39%      0.03s  0.69%  net/http.(*Transport).getIdleConn
     0.02s  0.46% 90.85%      0.13s  2.97%  runtime.gentraceback
(pprof) top --cum
Showing nodes accounting for 70ms, 1.60% of 4370ms total
Dropped 89 nodes (cum <= 21.85ms)
Showing top 10 nodes out of 162
      flat  flat%   sum%        cum   cum%
         0     0%     0%     1320ms 30.21%  net/http.(*Transport).getConn.func4
         0     0%     0%     1310ms 29.98%  net.(*Dialer).Dial
         0     0%     0%     1310ms 29.98%  net.(*Dialer).Dial-fm
         0     0%     0%     1310ms 29.98%  net.(*Dialer).DialContext
         0     0%     0%     1310ms 29.98%  net/http.(*Transport).dial
         0     0%     0%     1310ms 29.98%  net/http.(*Transport).dialConn
         0     0%     0%     1250ms 28.60%  net.dialSerial
      70ms  1.60%  1.60%     1250ms 28.60%  net.dialSingle
         0     0%  1.60%     1170ms 26.77%  net.dialTCP
         0     0%  1.60%     1170ms 26.77%  net.doDialTCP
(pprof) 

【问题讨论】:

  • 显示的代码中没有任何内容表明为什么性能会在 400 或 500 个请求/秒时下降。问题出在您未显示的代码的其他部分。 Profile your code.
  • 代码的其他部分针对 json 模式对事件数据进行 json 验证,然后将其推送到流中。
  • 这不是什么你的代码的其余部分,而是如何。没有看到它,我们将无法为您提供帮助。如前所述,请分析您的代码。
  • 附带说明,处理每个请求已经在单独的 goroutine 中完成,因此您从启动器中没有任何收获,但每个请求都有另一个 goroutine。这只是开销。
  • 个人资料和基准测试,然后在此处发布实际数据,否则您只是要求人们为您猜测。分析将允许您自己识别代码中的慢速路径。

标签: performance http go server latency


【解决方案1】:

问题

我正在使用另一个 goroutine,因为我不希望处理发生在 http 请求-响应周期中。

这是一个常见的谬误(因此是陷阱)。推理线似乎是合理的:您正在尝试“在其他地方”处理请求,以尝试 尽可能快地处理入口 HTTP 请求。

问题是“其他地方”仍然是一些代码 与您的其他请求处理搅动同时运行。 因此,如果该代码的运行速度比入口请求的速度慢, 您的处理 goroutine 将堆积起来,基本上会耗尽一个或 更多资源。究竟哪个——取决于实际处理: 如果它受 CPU 限制,它将为 CPU 创建自然争用 在所有GOMAXPROCS 硬件执行线程之间; 如果它绑定到网络 I/O,它将在 Go 运行时 scheruler 上创建负载,该负载必须划分它拥有的可用执行量 在所有想要执行的 goroutine 之间; 如果它绑定到磁盘 I/O 或其他系统调用,您将拥有 创建的操作系统线程激增,等等……

基本上,您正在排队工作单位从 入口 HTTP 请求,但 队列不能修复过载问题。 它们可能用于吸收过载的短尖峰, 但这仅在这些尖峰被周期“包围”时才有效 负载至少略低于您提供的最大容量 系统。 你排队的事实在你的情况下没有直接看到,但它是 在那里,它通过将你的系统压过它的自然来展示 容量——你的“队列”开始无限增长。

请仔细阅读this classic essay 以了解您的方法为何行不通 在现实的生产环境中工作。 密切关注厨房水槽的那些图片。

该怎么办?

不幸的是,几乎不可能给出简单的解决方案 因为我们没有在您的工作负载设置中使用您的代码。 不过,这里有几个方向可供探索。

在最广泛的范围内,试着看看你是否容易拥有一些 您目前看不到的系统瓶颈。 例如,如果所有这些并发的工作 goroutine 最终 与 RDBM 实例对话,它的磁盘 I/O 可能很容易序列化 所有那些只会等待轮到他们拥有的 goroutine 他们的数据被接受。 瓶颈可能更简单——比如说,在每个 worker goroutine 中 你在持有锁时不小心执行了一些长时间运行的操作 所有这些 goroutine 都在竞争; 这显然将它们全部序列化。

下一步将是实际测量(我的意思是,通过编写基准) 单个工人完成其工作单元需要多少时间。 然后你需要测量这个数字在增加 并发因素。 收集这些数据后,您将能够做到 关于您的系统的现实评级的有根据的预测 能够处理请求。

下一步是考虑制定系统的策略 满足那些计算出来的期望。通常这意味着限制速率 入口请求。 有不同的方法来实现这一点。 看golang.org/x/time/rate 对于基于时间的速率限制器,但可以从较低技术开始 方法,例如使用缓冲通道作为计数信号量。 超出您能力的请求可能会被拒绝 (通常使用 HTTP 状态代码 429,请参阅 this)。 你也可以考虑让他们短暂排队,但我只会尝试这个 充当馅饼上的樱桃——也就是说,当你有剩下的时候 彻底解决了。

如何处理被拒绝的请求取决于您的 环境。通常,您尝试通过部署更多“水平扩展” 不仅仅是一项服务来处理您的请求并教您的客户 切换可用服务。 (我要强调的是,这意味着几个 独立服务——如果它们都共享某个目标接收器,该接收器收集 他们的数据,他们可能会受到该汇的最终容量的限制, 并且添加更多系统不会为您带来任何好处。)

让我重复一遍,一般问题没有神奇的解决方案: 如果您的完整系统(使用您正在编写的这个 HTTP 服务) 只是它的前端、网关、部分)只能处理N RPS 的负载, 没有多少散射go processRequest()会成功 以更快的速度处理请求。 Go 提供的简单并发不是 silver bullet, 这是机关枪。

【讨论】:

  • 感谢您的详细回复。所有这些都是有道理的,而且我确实意识到,由于系统过载,因此需要进行水平缩放。对我来说仍然没有意义的是为什么响应时间突然下降。我想知道是否有办法解决这个问题。
  • 我首先会尝试看看在GODEBUG=shedtrace=N 下运行您的服务——有关背景/如何使用,请参阅thisthis
  • 你考虑过进度条吗?