【问题标题】:Is there a better way limit requests at the "door"?有没有更好的方法限制“门口”的请求?
【发布时间】:2017-08-14 22:17:00
【问题描述】:

现在,我正在 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)
    })
}

【问题讨论】:

  • 信号量本身不会给请求增加任何明显的延迟。您是否检查过您实际上有超过 10000 个并发请求?如果您在该信号量上达到阻塞状态,我的猜测是您从额外的并发中受益,而之前只阻塞 redis 请求。
  • AWS 每分钟报告 30,000 个请求。我在两台服务器上进行负载平衡。我担心的是当我将代码移动到东海岸时,我们的峰值达到每分钟 300,000。此外,我的大部分延迟来自我们的生产 mongo 服务器。尽管我们尽可能使用 redis,但当流量增加时,mongo 会受到猛烈抨击,并且全国延迟会达到峰值。所以我正在关注 mongo (mgo) 文档并试图限制在“门口”。
  • 这已经尽可能高效了,因此您将不得不做一些更好的系统分析(平均延迟基本上不会告诉您任何有用的信息)。阻塞处理程序可能会导致活动连接激增,您可能想尝试直接限制网络连接,看看是否有帮助。
  • 30,000 个请求 每分钟 不等于 10,000 个并发/同时请求(没有直接关联,这实际上取决于为请求提供服务所需的时间)。
  • 我同意@icza。我使用这个数字是因为它是 redis 可以处理的最大并发连接数。所以在精神上我知道它不应该超过那个......仍在试验。

标签: go server semaphore


【解决方案1】:

您概述的解决方案存在一些问题。但首先,让我们退后一步;这里有两个问题,其中一个暗示:

  1. 如何有效地限制入站连接?
  2. 如何防止出站连接导致后端服务过载?

听起来你想做的其实是第二个,防止过多的请求打到 Redis。我将从解决第一个问题开始,然后在第二个问题上制作一些 cmets。

入站连接速率限制

如果您确实想“在门口”对入站连接进行速率限制,您通常应该永远不要通过在处理程序中等待来做到这一点。使用您提出的解决方案,该服务将继续接受请求,这些请求将在 sema &lt;- struct{}{} 语句中排队。如果负载持续存在,它最终会通过耗尽套接字、内存或其他一些资源来关闭您的服务。另请注意,如果您的请求率接近信号量的饱和,您会发现 goroutine 在处理请求之前在信号量处等待会导致延迟增加。

更好的方法是始终尽快响应(尤其是在高负载下)。这可以通过将503 Service Unavailable 发送回客户端或智能负载平衡器来告诉它退出来完成。

在你的情况下,它可能看起来像这样:

select {
case sema <- struct{}{}:
    defer func() { <-sema }()
    h.ServeHTTP(w, r)
default:
    http.Error(w, "Overloaded", http.StatusServiceUnavailable)
}

对后端服务的出站连接进行速率限制

如果限制速率的原因是为了避免后端服务超载,您通常想要做的是对该服务超载做出反应,并通过请求链施加背压

实际上,这可能意味着将与上述相同类型的信号量逻辑放入一个包装器中,以保护对后端的所有调用,并在信号量溢出时通过请求的调用链返回错误。

此外,如果后端发送像503(或等效)这样的状态代码,您通常应该以相同的方式向下传播该指示,或者诉诸其他后备行为来处理传入的请求。

您可能还想考虑将其与circuit breaker 结合使用,如果后端服务似乎无响应或已关闭,则停止尝试快速调用它。

如上所述通过限制并发或排队连接的数量来限制速率通常是处理过载的好方法。当后端服务过载时,请求通常会花费更长的时间,这将减少每秒的有效请求数。但是,如果出于某种原因,您希望对每秒的请求数进行固定限制,则可以使用 rate.Limiter 而不是信号量来实现。

性能评论

在通道上发送和接收琐碎对象的成本应该是亚微秒级。即使在高度拥塞的通道上,仅与通道同步的额外延迟也不会接近 150 毫秒。因此,假设在处理程序中完成的工作是相同的,无论您的延迟增加来自什么,它几乎肯定与等待某处的 goroutine 相关联(例如,在 I/O 上或访问被其他 goroutine 阻塞的同步区域) .

如果您收到的传入请求的速度接近您设置的并发限制 10000 可以处理的速度,或者您收到的请求高峰,您可能会看到 goroutine 导致的平均延迟增加在频道的等待队列中。

无论哪种方式,这都应该很容易衡量;例如,您可以在处理路径中的某些点跟踪时间戳。我会对所有请求的样本(例如 0.1%)执行此操作,以避免日志输出影响性能。

【讨论】:

    【解决方案2】:

    我会为此使用稍微不同的机制,可能是这里描述的工作池:

    https://gobyexample.com/worker-pools

    我实际上是说保持 10000 个 goroutine 运行,(它们会在阻塞通道上等待接收,所以这并不是真正的资源浪费),并在它们进入时将请求+响应发送到池.

    如果您想要在池已满时响应错误的超时,您也可以使用select 块来实现它。

    【讨论】:

    • 预建池还具有立即告诉您可能的资源利用率并保持不变的优势。因此,您不太可能对负载峰值感到惊讶。
    猜你喜欢
    • 2020-07-27
    • 2016-10-28
    • 1970-01-01
    • 2010-09-15
    • 2022-01-22
    • 1970-01-01
    • 1970-01-01
    • 2020-03-12
    • 1970-01-01
    相关资源
    最近更新 更多