在 "door" 有没有更好的方法来限制请求?
Is there a better way limit requests at the "door"?
现在我正在 AWS 的一个生产区域测试一个非常简单的信号量。在部署时,延迟从 150 毫秒跃升至 300 毫秒。我假设会发生延迟,但如果它可以被删除那就太好了。这对我来说有点新,所以我正在试验。我已将信号量设置为允许 10000 个连接。这与 Redis 设置的最大连接数相同。下面的代码是最优的吗?如果没有,有人可以帮我优化它,如果我做错了什么等等。我想把它作为一个中间件,这样我就可以在服务器上像这样简单地调用它 n.UseHandler(wrappers.DoorMan(wrappers.DefaultHeaders(myRouter), 10000))
.
package wrappers
import "net/http"
// DoorMan limit requests
func DoorMan(h http.Handler, n int) http.Handler {
sema := make(chan struct{}, n)
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
sema <- struct{}{}
defer func() { <-sema }()
h.ServeHTTP(w, r)
})
}
我会为此使用稍微不同的机制,可能是此处描述的工作池:
https://gobyexample.com/worker-pools
我实际上会说保留 10000 个 goroutines 运行,(他们会在阻塞通道上等待接收,所以这不是真正的资源浪费),并将请求+响应发送到他们进来时的游泳池。
如果您想要在池已满时响应错误的超时,您也可以使用 select
块来实现它。
您概述的解决方案存在一些问题。但首先,让我们退后一步;这里面有两个问题,其中一个是隐含的:
- 您如何有效地限制入站连接?
- 如何防止后端服务因出站连接而过载?
听起来你想做的其实是第二种,防止太多的请求打到Redis。我将从解决第一个问题开始,然后对第二个问题发表一些评论。
限制入站连接的速率
如果您真的想对入站连接进行速率限制"at the door",您通常应该永远不要通过在处理程序中等待来做到这一点。使用您提出的解决方案,该服务将继续接受请求,这些请求将在 sema <- struct{}{}
语句处排队。如果负载持续存在,它最终会通过 运行 套接字、内存或其他一些资源来关闭您的服务。另请注意,如果您的请求率接近信号量的饱和度,您会看到在处理请求之前等待信号量的 goroutines 导致延迟增加。
更好的方法是始终尽快响应(尤其是在负载很重的情况下)。这可以通过将 503 Service Unavailable
发送回客户端或智能负载平衡器,告诉它退出来完成。
在你的情况下,它可能看起来像这样:
select {
case sema <- struct{}{}:
defer func() { <-sema }()
h.ServeHTTP(w, r)
default:
http.Error(w, "Overloaded", http.StatusServiceUnavailable)
}
速率限制到后端服务的出站连接
如果速率限制的原因是为了避免后端服务超载,您通常想要做的是对超载的服务做出反应,并通过 背压 请求链。
实际上,这可能意味着将与上述相同类型的信号量逻辑放在一个包装器中,以保护对后端的所有调用,并且 return 通过请求的调用链出错如果信号量溢出。
此外,如果后端发送 503
之类的状态代码(或等效代码),您通常应该以相同的方式向下传播该指示,或者诉诸其他一些回退行为来处理传入的请求。
您可能还想考虑将此与 circuit breaker 结合使用,如果后端服务似乎无响应或出现故障,则停止尝试快速调用后端服务。
如上所述通过限制并发或排队连接的数量来限制速率通常是处理过载的好方法。当后端服务过载时,请求通常会花费更长的时间,这将减少每秒的有效请求数。但是,如果出于某种原因,您希望对每秒的请求数有一个固定的限制,您可以使用 rate.Limiter
而不是信号量来实现。
对性能的评论
在通道上发送和接收普通对象的成本应该是亚微秒。即使在高度拥塞的信道上,也不会出现接近 150 毫秒的额外延迟,只是为了与信道同步。因此,假设在处理程序中完成的工作在其他方面是相同的,无论您的延迟增加来自于它几乎肯定与等待某处的 goroutines 相关联(例如 I/O 或访问被其他阻止的同步区域goroutines).
如果您收到传入请求的速度接近您设置的并发限制 10000 可以处理的速度,或者如果您收到请求激增,您可能会看到平均延迟的增加来自通道等待队列中的 goroutines。
无论如何,这应该很容易衡量;例如,您可以在处理路径的某些点跟踪时间戳。我会在所有请求的样本(例如 0.1%)上执行此操作,以避免日志输出影响性能。
现在我正在 AWS 的一个生产区域测试一个非常简单的信号量。在部署时,延迟从 150 毫秒跃升至 300 毫秒。我假设会发生延迟,但如果它可以被删除那就太好了。这对我来说有点新,所以我正在试验。我已将信号量设置为允许 10000 个连接。这与 Redis 设置的最大连接数相同。下面的代码是最优的吗?如果没有,有人可以帮我优化它,如果我做错了什么等等。我想把它作为一个中间件,这样我就可以在服务器上像这样简单地调用它 n.UseHandler(wrappers.DoorMan(wrappers.DefaultHeaders(myRouter), 10000))
.
package wrappers
import "net/http"
// DoorMan limit requests
func DoorMan(h http.Handler, n int) http.Handler {
sema := make(chan struct{}, n)
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
sema <- struct{}{}
defer func() { <-sema }()
h.ServeHTTP(w, r)
})
}
我会为此使用稍微不同的机制,可能是此处描述的工作池:
https://gobyexample.com/worker-pools
我实际上会说保留 10000 个 goroutines 运行,(他们会在阻塞通道上等待接收,所以这不是真正的资源浪费),并将请求+响应发送到他们进来时的游泳池。
如果您想要在池已满时响应错误的超时,您也可以使用 select
块来实现它。
您概述的解决方案存在一些问题。但首先,让我们退后一步;这里面有两个问题,其中一个是隐含的:
- 您如何有效地限制入站连接?
- 如何防止后端服务因出站连接而过载?
听起来你想做的其实是第二种,防止太多的请求打到Redis。我将从解决第一个问题开始,然后对第二个问题发表一些评论。
限制入站连接的速率
如果您真的想对入站连接进行速率限制"at the door",您通常应该永远不要通过在处理程序中等待来做到这一点。使用您提出的解决方案,该服务将继续接受请求,这些请求将在 sema <- struct{}{}
语句处排队。如果负载持续存在,它最终会通过 运行 套接字、内存或其他一些资源来关闭您的服务。另请注意,如果您的请求率接近信号量的饱和度,您会看到在处理请求之前等待信号量的 goroutines 导致延迟增加。
更好的方法是始终尽快响应(尤其是在负载很重的情况下)。这可以通过将 503 Service Unavailable
发送回客户端或智能负载平衡器,告诉它退出来完成。
在你的情况下,它可能看起来像这样:
select {
case sema <- struct{}{}:
defer func() { <-sema }()
h.ServeHTTP(w, r)
default:
http.Error(w, "Overloaded", http.StatusServiceUnavailable)
}
速率限制到后端服务的出站连接
如果速率限制的原因是为了避免后端服务超载,您通常想要做的是对超载的服务做出反应,并通过 背压 请求链。
实际上,这可能意味着将与上述相同类型的信号量逻辑放在一个包装器中,以保护对后端的所有调用,并且 return 通过请求的调用链出错如果信号量溢出。
此外,如果后端发送 503
之类的状态代码(或等效代码),您通常应该以相同的方式向下传播该指示,或者诉诸其他一些回退行为来处理传入的请求。
您可能还想考虑将此与 circuit breaker 结合使用,如果后端服务似乎无响应或出现故障,则停止尝试快速调用后端服务。
如上所述通过限制并发或排队连接的数量来限制速率通常是处理过载的好方法。当后端服务过载时,请求通常会花费更长的时间,这将减少每秒的有效请求数。但是,如果出于某种原因,您希望对每秒的请求数有一个固定的限制,您可以使用 rate.Limiter
而不是信号量来实现。
对性能的评论
在通道上发送和接收普通对象的成本应该是亚微秒。即使在高度拥塞的信道上,也不会出现接近 150 毫秒的额外延迟,只是为了与信道同步。因此,假设在处理程序中完成的工作在其他方面是相同的,无论您的延迟增加来自于它几乎肯定与等待某处的 goroutines 相关联(例如 I/O 或访问被其他阻止的同步区域goroutines).
如果您收到传入请求的速度接近您设置的并发限制 10000 可以处理的速度,或者如果您收到请求激增,您可能会看到平均延迟的增加来自通道等待队列中的 goroutines。
无论如何,这应该很容易衡量;例如,您可以在处理路径的某些点跟踪时间戳。我会在所有请求的样本(例如 0.1%)上执行此操作,以避免日志输出影响性能。