如何计算最后一秒、一分钟和一小时内的请求数?

Posted

技术标签:

【中文标题】如何计算最后一秒、一分钟和一小时内的请求数?【英文标题】:How can I count the number of requests in the last second, minute and hour? 【发布时间】:2013-07-10 03:41:52 【问题描述】:

我有一个 Web 服务器,它只支持一个非常简单的 API——计算过去一小时、一分钟和一秒收到的请求数。 该服务器在世界范围内非常流行,每秒接收数千个请求。

旨在找到如何准确地将这 3 个值返回给每个请求?

请求一直在到来,因此每个请求的一小时、一分钟和一秒的窗口是不同的。 如何管理每个请求的不同窗口,以便每个请求的计数正确?

【问题讨论】:

Efficient way to compute number of hits to a server within the last minute, in real time 的可能重复项 【参考方案1】:

如果需要 100% 的准确度:

拥有一个包含所有请求和 3 个计数的链接列表 - 最后一小时、最后一分钟和最后一秒。

您将有 2 个指向链接列表的指针 - 一分钟前和一秒前。

一小时前将在列表末尾。每当最后一次请求的时间比当前时间早一个多小时时,将其从列表中删除并减少小时计数。

分钟和秒指针将分别指向一分钟和一秒前发生的第一个请求。每当请求的时间比当前时间早一分钟/秒以上时,将指针上移并减少分钟/秒计数。

当有新请求进来时,将其添加到所有 3 个计数中,并将其添加到链表的前面。

对计数的请求只涉及返回计数。

以上所有操作都是摊销常数时间。

如果低于 100% 的准确度是可以接受的:

上面的空间复杂度可能有点多,具体取决于您通常每秒收到多少请求;您可以通过稍微牺牲精度来减少这种情况,如下所示:

有一个如上所述的链表,但仅限于最后一秒。也有3个计数。

然后有一个由 60 个元素组成的圆形数组,指示过去 60 秒中每一秒的计数。每当经过一秒时,从分钟计数中减去数组的最后一个(最旧的)元素,然后将最后一秒计数添加到数组中。

在过去 60 分钟内有一个类似的圆形数组。

准确性损失:一秒钟内所有请求都可以关闭分钟计数,而一分钟内所有请求都可以关闭小时计数。

如果您每秒只有一个或更少的请求,这显然是没有意义的。在这种情况下,您可以将最后一分钟保留在链表中,并且只保留最后 60 分钟的循环数组。

对此还有其他变体 - 可以根据需要调整精度与空间使用比率。

删除旧元素的计时器:

如果你只在新元素进来时移除旧元素,它将被摊销恒定时间(某些操作可能需要更长的时间,但它会平均到恒定时间)。

如果您想要真正的恒定时间,您还可以运行一个计时器来删除旧元素,并且每次调用它(当然还有插入和检查计数)只会花费恒定时间,因为您最多删除自上次计时器滴答以来以恒定时间插入的元素数量。

【讨论】:

在“100%准确率”的方法中,数组移位和递减操作的时间常数如何?通过列表进行线性扫描以删除所有旧请求将是 O(n),例如想象在 0.1 秒内有 1000 个请求的最坏情况,1 秒后您需要扫描并删除 1000 个条目,现在想象一百万个请求,您将需要扫描并删除 100 万个条目 - 所需的工作随着需要删除的请求数,不是恒定的时间。即使列表中过期请求的数量是 n 的某个恒定分数,它仍然是 O(n)。 @bain “链表”数据结构的好处是您不需要像使用简单数组和推/移那样传递所有元素来添加/删除。要删除一个元素,您可以简单地删除或重新排列“链接”。 @patmood 在链表中添加或删除节点是 O(1),但遍历链表是 O(n)。这里描述的“指针移位和计数器递减”需要遍历链表,直到我们找到第一个节点存储的请求距离当前时间不到一小时(或分钟或秒)。在最坏的情况下,这需要遍历列表中的所有 n 个节点并调用递减运算符 n 次。 bain - 无需遍历...创建一个名为 sum 的变量并添加新元素并减去旧元素.. 这就是为什么他/她的意思是 O(1) @RohitashwaNigam 当第二次过去时,您会执行我的回答中描述的更新。如果您在这个确切点检查计数,它将是准确的。但是,如果您在一秒钟的中途检查计数,那么 60 秒前的那一秒将是一个问题 - 您将不知道前半部分(应该排除)和后半部分(应该包括在内)发生了多少请求) 那一秒。所有这些都可能发生在上半场或下半场,或者在两半场发生的次数相同。当然你可以只显示准确的计数,但是会有延迟。【参考方案2】:

要在 T 秒的时间窗口内执行此操作,请使用队列数据结构,您可以在其中将各个请求到达时的时间戳排入队列。当您想读取在最近的 T 秒窗口内到达的请求数时,首先从队列的“旧”端删除那些早于 T 秒的时间戳,然后读取队列的大小。您还应该在向队列添加新请求时删除元素以保持其大小有界(假设传入请求的速率有界)。

此解决方案可以达到任意精度,例如毫秒精度。如果您对返回近似答案感到满意,例如,您可以对于 T = 3600(一小时)的时间窗口,将同一秒内的请求合并到一个队列元素中,使队列大小以 3600 为界。我认为这会很好,但理论上会失去准确性。对于 T = 1,您可以根据需要在毫秒级别进行合并。

在伪代码中:

queue Q

proc requestReceived()
  Q.insertAtFront(now())
  collectGarbage()

proc collectGarbage()
  limit = now() - T
  while (! Q.empty() && Q.lastElement() < limit)
    Q.popLast()

proc count()
  collectGarbage()
  return Q.size()

【讨论】:

【参考方案3】:

为什么不直接使用循环数组呢? 我们在该数组中有 3600 个元素。

index = 0;
Array[index % 3600] = count_in_one_second. 
++index;

如果你想要最后一秒,返回这个数组的最后一个元素。 如果你想要最后一分钟,返回最后 60 个元素的总和。 如果你想最后一小时,返回整个数组的总和(3600 个元素)。

他不是一个简单有效的解决方案吗?

谢谢

德里克

【讨论】:

我喜欢。使用当前时间(例如 System.currentTimeMillis())而不是索引如何? 这很简单,但我们还需要一个单独的每秒计数器。每秒之后,它应该更新 Array 元素中的值那一秒? 我喜欢这个想法,当你每秒都有连续的请求时它工作得很好,但是当你没有请求数组中的某些条目时会发生什么?如果我们减少只为最后一秒和最后一分钟提供计数器的问题,那么假设您在第 4 秒有 n 个请求,但在第 3 秒没有请求。在这种情况下,您应该为第 3 秒清理旧数据,并且通常清理回来,直到您达到秒 0 或同一分钟的条目。前面的数据清理应该只针对早于当前分钟的陈旧数据 - 1。 如果担心过时数据,我们可以有一个大小为 7200 的数组。一旦达到第 3600 个和第 7200 个元素的索引,就清理下一个 3600 个元素。【参考方案4】:

一个解决方案是这样的:

1) 使用长度为 3600 的圆形数组(每小时 60 * 60 秒)保存上一小时每秒的数据。

要记录下一秒的数据,请通过移动循环数组的头指针将最后一秒的数据放入循环数组中。

2) 在循环数组的每个元素中,我们不是保存特定秒内的请求数,而是记录我们之前看到的请求数的累积和,以及一个周期的请求可以通过requests_sum.get(current_second) - requests_sum.get(current_second - number_of_seconds_in_this_period)来计算

increament()getCountForLastMinute()getCountForLastHour() 等所有操作都可以在O(1) 时间内完成。

================================================ ============================

这是一个如何工作的示例。

如果我们在最近 3 秒内有这样的请求计数: 1st second: 2 requests 2nd second: 4 requests 3rd second: 3 requests

循环数组将如下所示: sum = [2, 6, 9] 其中 6 = 4 + 2 和 9 = 2 + 4 + 3

在这种情况下:

1)如果要获取最后一秒的请求数(第3秒的请求数),只需计算sum[2] - sum[1] = 9 - 6 = 3

2)如果要获取最后两秒​​的请求数(第3秒的请求数和第2秒的请求数),只需计算sum[2] - sum[0] = 9 - 2 = 7

【讨论】:

如果几秒钟内没有请求,将如何处理场景?我们可能会错误地读取旧数据?【参考方案5】:

您可以每小时创建一个大小为 60x60 的数组,并将其用作循环缓冲区。每个条目包含给定秒的请求数。当你移动到下一秒时,清除它并开始计数。当您处于数组的末尾时,您又从 0 开始,因此有效地清除了 1 小时之前的所有计数。

    对于小时:返回所有元素的总和 分钟:返回最后 60 个条目的总和(来自 currentIndex) 对于第二个:返回当前索引的计数

所以这三个都有 O(1) 的空间和时间复杂度。唯一的缺点是,它忽略了毫秒,但您也可以应用相同的概念来包含毫秒。

【讨论】:

我们不能有滑动寡妇类型的解决方案而不是固定间隔吗?【参考方案6】:

Following 代码在 JS 中。它将返回 O(1) 中的计数。我为一次面试编写了这个程序,其中时间被预先定义为 5 分钟。但是您可以修改此代码几秒钟、几分钟等。告诉我进展如何。

    创建一个以毫秒为键,计数器为值的对象 添加一个名为 totalCount 的属性并将其预定义为 0 在步骤 1 中定义的每个命中增量计数器的日志和总计数 添加一个名为 clean_hits 的方法,每毫秒调用一次该方法

    在 clean_hits 方法中,从我们创建的对象中删除每个条目(在我们的时间范围之外),并在删除条目之前从 totalCount 中减去该计数

    this.hitStore = "totalCount" : 0;

【讨论】:

【参考方案7】:

我必须在 Go 中解决这个问题,我认为我还没有看到这种方法,但它也可能非常适合我的用例。

由于它连接到第 3 方 API 并且需要限制自己的请求,我只是保留了最后一秒的计数器和最后 2 分钟的计数器(我需要的两个计数器)

var callsSinceLastSecond, callsSinceLast2Minutes uint64

然后,当调用计数器低于我的允许限制时,我会在单独的 go 例程中启动我的请求

for callsSinceLastSecond > 20 || callsSinceLast2Minutes > 100 
    time.Sleep(10 * time.Millisecond)

在每个 go 例程结束时,我会自动递减计数器。

go func() 
    time.Sleep(1 * time.Second)
    atomic.AddUint64(&callsSinceLastSecond, ^uint64(0))
()

go func() 
    time.Sleep(2 * time.Minute)
    atomic.AddUint64(&callsSinceLast2Minutes, ^uint64(0))
()

到目前为止,这似乎工作没有任何问题,到目前为止进行了一些相当繁重的测试。

【讨论】:

【参考方案8】:

这是一个通用的 Java 解决方案,可以跟踪最后一分钟的事件数。

我使用ConcurrentSkipListSet 的原因是因为它保证了搜索、插入和删除操作的平均时间复杂度为 O(log N)。您可以轻松更改下面的代码以使持续时间(默认为 1 分钟)可配置。

正如上面的答案所建议的,定期清理过时的条目是个好主意,例如使用调度程序。

@Scope(value = "prototype")
@Component
@AllArgsConstructor
public class TemporalCounter 

    @Builder
    private static class CumulativeCount implements Comparable<CumulativeCount> 

        private final Instant timestamp;
        private final int cumulatedValue;

        @Override
        public int compareTo(CumulativeCount o) 
            return timestamp.compareTo(o.timestamp);
        
    

    private final CurrentDateTimeProvider currentDateTimeProvider;
    private final ConcurrentSkipListSet<CumulativeCount> metrics = new ConcurrentSkipListSet<>();

    @PostConstruct
    public void init() 
        Instant now = currentDateTimeProvider.getNow().toInstant();
        metrics.add(new CumulativeCount(now, 0));
    

    public void increment() 
        Instant now = currentDateTimeProvider.getNow().toInstant();
        int previousCount = metrics.isEmpty() ? 0 : metrics.last().cumulatedValue;
        metrics.add(new CumulativeCount(now, previousCount + 1));
    

    public int getLastCount() 
        if (!metrics.isEmpty()) 
            cleanup();

            CumulativeCount previousCount = metrics.first();
            CumulativeCount mostRecentCount = metrics.last();
            if (previousCount != null && mostRecentCount != null) 
                return mostRecentCount.cumulatedValue - previousCount.cumulatedValue;
            
        
        return 0;
    

    public void cleanup() 
        Instant upperBoundInstant = currentDateTimeProvider.getNow().toInstant().minus(Duration.ofMinutes(1));
        CumulativeCount c = metrics.lower(CumulativeCount.builder().timestamp(upperBoundInstant).build());
        if (c != null) 
            metrics.removeIf(o -> o.timestamp.isBefore(c.timestamp));
            if (metrics.isEmpty()) 
                init();
            
        
    

    public void reset() 
        metrics.clear();
        init();
    

【讨论】:

【参考方案9】:

简单的时间戳列表怎么样?每次发出请求时,都会将当前时间戳附加到列表中。每次你想检查你是否低于速率限制时,你首先删除超过 1 小时的时间戳以防止堆栈溢出(呵呵),然后你计算最后一秒、分钟等时间戳的数量。

这可以在 Python 中轻松完成:

import time

requestsTimestamps = []

def add_request():
    requestsTimestamps.append(time.time())

def requestsCount(delayInSeconds):
    requestsTimestamps = [t for t in requestsTimestamps if t >= time.time() - 3600]
    return len([t for t in requestsTimestamps if t >= time.time() - delayInSeconds])

我想这可以优化,但你看到了这个想法。

【讨论】:

【参考方案10】:

我的解决方案:

    维护一个 3600 的哈希,其中包含一个计数、时间戳作为字段。

    对于每个请求:

    按时间戳%3600 获取 idx(当前元素的数组索引)。 如果 hash[idx].count=0,那么 hash[idx].count=1 并且 hash[idx].timestamp=inputTimeStamp 如果 hash[idx].count>0 ,那么

    案例(1) : if i/p timestamp==hash[idx].timestamp,hash[count]++;

    案例(2):如果 i/p 时间戳>hash[idx].timestamp,则 hash[idx].count=1 和 hash[idx].timestamp=inputTimeStamp

    Case(3): : if i/p timestamp

现在对于最后一秒、分钟、小时的任何查询: 如上找到idx,只要时间戳在给定的秒/范围/分钟内匹配,就继续循环从idx返回。

【讨论】:

以上是关于如何计算最后一秒、一分钟和一小时内的请求数?的主要内容,如果未能解决你的问题,请参考以下文章

Java计算两个日期时间相差几天,几小时,几分钟等

Java计算两个日期时间相差几天,几小时,几分钟等

js 关于时间方面的通用函数(时间格式化,分钟数转换为小时+分钟,计算天数差的函数)

如何计算给定间隔内的记录?

统计接口QPS

Pandas 时间序列数据 - 每 30 分钟计算过去 24 小时内的唯一值