我正在写一个事件收集器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/