performance - 转到HTTP服务器性能问题

标签 performance http go server latency

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

这样,我看到如果我以每秒400个请求的速度命中服务器,那么对于99%的延迟,其延迟将低于20ms。但是,一旦我将请求速率提高到每秒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) 

最佳答案

问题

I am using another goroutine because I dont want the processing to happen in the http request-response cycle.



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

问题是“其他地方”仍然是一些代码
与其余的请求处理流失同时运行。
因此,如果该代码的运行速度低于入口请求的速度,
您的处理goroutine基本上会堆积一空,或者
更多资源。确切地取决于实际处理:
如果它受CPU限制,则会为CPU创建自然竞争
在所有这些GOMAXPROCS执行硬件线程之间;
如果绑定(bind)到网络I/O,它将在Go运行时scheruler上创建负载,该负载必须划分其具有的可用执行量
在所有想要执行的goroutine之间进行操作;
如果绑定(bind)到磁盘I/O或其他系统调用,您将拥有
创建的OS线程数量激增,依此类推……

本质上,您是排队,这些工作单位是从
入口HTTP请求,但队列不能解决过载。
它们可能用于吸收过载的短暂峰值,
但这仅在这些峰值被周期“包围”时有效
的负载至少略低于您提供的最大容量
系统。
您正在排队的事实并未直接显示在您的案例中,而是
在那里,通过将系统压过自然状态来展示它
容量-您的“队列”开始无限增长。

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

怎么办呢?

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

在最广泛的范围内,尝试看看您是否容易
系统中目前尚无法看到的明显瓶颈。
例如,如果所有这些并发工作程序goroutine最终
与RDBM实例通信时,其磁盘I/O可能很容易序列化
所有那些等待轮到他们的goroutines
他们的数据被接受。
瓶颈可能更简单-例如,在每个 worker goroutine中
按住锁不小心执行了一些长时间运行的操作
所有这些goroutines竞争
这显然将它们全部序列化。

下一步将是实际衡量(我的意思是,写一个基准)
一个 worker 完成其工作单元需要多少时间。
然后,您需要测量此数字在增加
并发因子。
收集完这些数据后,您就可以
关于您的系统的实际速率的有根据的预测
能够处理请求。

下一步是仔细考虑您制作系统的策略
满足那些计算得出的期望。通常这意味着限制速率
入口请求。有多种方法可以实现此目的。
golang.org/x/time/rate
基于时间的速率限制器,但可以从技术含量较低的产品开始
诸如使用缓冲 channel 作为计数信号量的方法。
可能会超出您的能力的请求可能会被拒绝
(通常使用HTTP状态代码429,请参见this)。
您也可以考虑对它们进行短暂排队,但我只会尝试这样做
充当馅饼上的樱桃-也就是说,当您剩下的时候
完全整理出来。

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

让我重复一遍,一般性问题没有神奇的解决方案:
如果您的完整系统(使用此HTTP服务,您正在写
仅其前端,网关部分)只能处理N RPS负载,
没有任何数量的go processRequest()可以实现
更快地处理请求。 Go提供的简单并发不是
一个silver bullet
这是机关枪。

关于performance - 转到HTTP服务器性能问题,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/50016617/

相关文章:

c - 集成到静态库中的可重定位列表

go - 读取文件时,为什么ReadBytes会影响后面的Read?

c# - 将非托管内存复制到托管字节数组

c# - 更好的填充 ListBox DataTemplate 的性能

ios - iOS 上异步调用的最佳性能是什么?

javascript - Angular $http - 在接收响应数据时处理它

asp.net - 仅在 Page_Load 中使用 ASPX 页面上的响应下载 PDF

c++ - 是否值得将局部变量设为静态以防止不断重新创建?

angularjs - 更新模型时 Angular 2 中的双向绑定(bind)

xml - 如何使用 Golang 解码 Reddit 的 RSS?