转到HTTP服务器性能问题

vai*_*hav 1 performance latency http go server

我正在编写一个事件收集器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)
}
Run Code Online (Sandbox Code Playgroud)

我以每秒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) 
Run Code Online (Sandbox Code Playgroud)

kos*_*tix 9

问题

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

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

问题是"其他地方"仍然是一些代码, 与你的请求处理流失的其余部分同时运行.因此,如果该代码运行速度低于入口请求的速率,那么您的处理goroutines将堆积起来,从根本上耗尽一个或多个资源.具体取决于实际处理:如果它受CPU限制,它将在所有这些GOMAXPROCS硬件执行线程之间创建CPU的自然争用; 如果它绑定到网络I/O,它将在Go运行时scheruler上创建加载,它必须在它想要执行的所有goroutine之间划分它拥有的可用执行量; 如果它绑定到磁盘I/O或其他系统调用,您将创建OS线程的扩散,依此类推......

实际上,您正在对从入口HTTP请求转换的工作单元进行排队,但队列不会修复过载. 它们可能用于吸收过载的短尖峰,但这仅在这些尖峰被负载周期"包围"时才起作用,至少略低于系统提供的最大容量.您排队的事实并不是直接在您的情况下看到的,但它就在那里,并且通过按下您的系统超过其自然容量来展示它 - 您的"队列"开始无限增长.

请仔细阅读这篇经典论文,了解为什么您的方法无法在现实生产环境中发挥作用.密切关注厨房水槽的照片.

该怎么办?

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

在最广泛的范围内,试着看看你的系统中是否有一些你现在看不到的容易辨别的瓶颈.举例来说,如果所有这些并发工作够程最终跟一个RDBMS实例,它的磁盘I/O可以很容易地连载 所有这些够程,这将只是等待轮到他们有他们的数据被接受.瓶颈可能比较简单 - 比方说,在每个工作人员中,你不小心执行一些长时间运行的操作,同时拿着所有那些goroutines争论的锁; 这显然将它们全部序列化.

下一步是实际衡量(我的意思是,通过编写基准)一个工人完成其工作单位需要多少时间.然后,您需要在增加并发因子时测量此数字的变化方式.收集这些数据后,您将能够对系统能够处理请求的实际速率进行有根据的预测.

下一步是考虑您的策略,使您的系统满足这些计算的期望.通常这意味着限制入口请求的速率.有不同的方法来实现这一目标.查看golang.org/x/time/rate 基于时间的速率限制器,但可以从低技术方法开始,例如使用缓冲通道作为计数信号量.会溢出您的容量的请求可能会被拒绝(通常使用HTTP状态代码429,请参阅此内容).您可能还会考虑对它们进行简短排队,但我会尝试将其作为馅饼中的樱桃 - 也就是说,当您将其余部分完全整理出来时.

如何处理被拒绝的请求的问题取决于您的设置.通常,您尝试通过部署多个服务来"横向扩展"以处理您的请求并教您的客户切换可用服务.(我想强调的是,这意味着一些 独立的服务,如果他们都共享它收集的数据的一些目标信宿,他们可能会通过水槽的最终容量的限制,并增加更多的系统不会得到你任何东西.)

让我再说一遍,一般的问题有没有神奇的解决方案:如果您的完整制度的几点(与你写的是这个HTTP服务仅仅是它的前端,网关,一部分)是唯一能够处理N负载的RPS,无散射的量go processRequest()是将它以更高的速度处理请求.容易并发的Go并不是一个银弹,它是一挺机枪.