限制每个时间段的事件数

时间:2012-09-21 12:03:12

标签: algorithm rate-limiting

我需要在 deltaT 的时间段内限制 n 允许的事件数量。我能想到的任何方法,空间是 O(m),其中 m 是每个 deltaT O(deltaT / r),其中 r 是可接受的分辨率。

编辑:deltaT是相对于时间戳的滑动时间窗口。

例如:保留事件时间戳的循环缓冲区。在事件裁剪所有早于时间戳的时间戳比 t-deltaT 。如果时间戳数超过 n ,则拒绝事件。将时间戳添加到缓冲区。

或者,初始化一个大小为 deltaT / r 的整数的循环桶缓冲区,其时间相对于当前分辨率为 r 的当前索引。保持指针 i 。在事件中,自上次事件除以 r 以来,按时间递增 i 。将原始 i 与新原始之间的缓冲区归零。增加 i 。拒绝,如果bugger的总和超过 n

什么是更好的方法?


我刚刚在c#中实现了我的第二个建议,其中固定的 deltaT 为1 s,固定分辨率为10 ms。

public class EventCap
{
    private const int RES = 10; //resolution in ms

    private int _max;
    private readonly int[] _tsBuffer;
    private int p = 0;
    private DateTime? _lastEventTime;
    private int _length = 1000 / RES;

    public EventCap(int max)
    {
        _max = max;

        _tsBuffer = new int[_length];
    }

    public EventCap()
    {
    }

    public bool Request(DateTime timeStamp)
    {
        if (_max <= 0)
            return true;

        if (!_lastEventTime.HasValue)
        {
            _lastEventTime = timeStamp;
            _tsBuffer[0] = 1;
            return true;
        }

        //A
        //Mutually redundant with B
        if (timeStamp - _lastEventTime >= TimeSpan.FromSeconds(1))
        {
            _lastEventTime = timeStamp;
            Array.Clear(_tsBuffer, 0, _length);
            _tsBuffer[0] = 1;
            p = 0;
            return true;
        }

        var newP = (timeStamp - _lastEventTime.Value).Milliseconds / RES + p;

        if (newP < _length)
            Array.Clear(_tsBuffer, p + 1, newP - p);

        else if (newP > p + _length)
        {
            //B
            //Mutually redundant with A
            Array.Clear(_tsBuffer, 0, _length);
        }
        else
        {
            Array.Clear(_tsBuffer, p + 1, _length - p - 1);
            Array.Clear(_tsBuffer, 0, newP % _length);
        }

        p = newP % _length;
        _tsBuffer[p]++;
        _lastEventTime = timeStamp;

        var sum = _tsBuffer.Sum();

        return sum <= 10;
    }
}

4 个答案:

答案 0 :(得分:12)

如何使用这些变量:num_events_allowed,time_before,time_now,time_passed

在初始阶段,您将执行:time_before = system.timer(), num_events_allowed = n

收到活动时,请执行以下操作:

  time_now = system.timer()
  time_passed = time_now - time_before
  time_before = time_now

  num_events_allowed += time_passed * (n / deltaT);

  if num_events_allowed > n 
      num_events_allowed = n

  if num_events_allowed >= 1
      let event through, num_events_allowed -= 1
  else
      ignore event

这个算法的好处是num_events_allowed实际上增加了自上次事件以来已经过去的时间以及可以接收事件的速率,这样你就可以增加你可以发送的事件数量。 time_passed是为了保持在n的极限。因此,如果您过早地收到某个事件,则会将其增加小于1,如果它经过太多时间后会增加一个以上。当然,如果事件发生了,你刚刚得到一个事件就会减少1。如果允许通过最大事件n,则将其返回到n,因为在任何时间阶段都不允许超过n。如果津贴小于1,你就不能发送整个事件,不要让它通过!

这是漏桶算法:https://en.wikipedia.org/wiki/Leaky_bucket

答案 1 :(得分:3)

保持滑动窗口的一种方法仍然是每个传入请求的O(1)+非常小的O(n)是制作一个合适大小的整数数组并将其保持为循环缓冲区并将传入的请求离散化(请求与A / D转换器中的采样电平一样集成,或者如果您是统计数据,则作为直方图)并跟踪循环缓冲区的总和,如下所示

assumptions: 
"there can be no more than 1000 request per minute" and 
"we discretize on every second"

int[] buffer = new zeroed int-array with 60 zeroes
int request-integrator = 0 (transactional)
int discretizer-integrator = 0 (transactional)

for each request:
    check if request-integrator < 1000 then
         // the following incs could be placed outside 
         // the if statement for saturation on to many
         // requests (punishment)
         request-integrator++                     // important
         discretizer-integrator++
         proceed with request

once every second:                    // in a transactional memory transaction, for God's saké 
    buffer-index++
    if (buffer-index = 60) then buffer-index=0    // for that circular buffer feeling!
    request-integrator -= buffer[buffer-index]    // clean for things happening one minute ago
    buffer[buffer-index] = discretizer-integrator // save this times value
    discretizer-integrator = 0                    // resetting for next sampling period

请注意,请求积分器的增加“可能”每秒只能进行一次,但是会留下一个漏洞,使其满足1000个请求或者更糟,在一秒钟内大约每分钟一次,具体取决于惩罚行为。

答案 2 :(得分:2)

在阅读有关该问题的各种可能解决方案时。我遇到了令牌桶算法(http://en.wikipedia.org/wiki/Token_bucket)。如果我完全理解你的问题,你可以实现一个令牌桶算法,而不是实际上有一个带有N个令牌的存储桶,而是采用一个可以相应递增和递减的计数器。像

syncronized def get_token = 
    if count>0 
       { --count, return true }
    else return false

syncronized def add_token = 
    if count==N
       return;
    else ++count

现在剩下的部分是重复调用deltaT / r时间的add_token。

为了使它完全线程安全,我们需要一个原子引用来计算。但上面的代码是为了显示在O(1)内存中执行它的基本思路。

答案 3 :(得分:1)

我写了下面的类(ActionQueue)来限制函数调用的频率。其中一个好处是它使用一个计时器来弹出队列...所以CPU被最小化使用(如果队列是空的,甚至根本不使用)...而不是任何轮询类型的技术

示例...

    // limit to two call every five seconds
    ActionQueue _actionQueue = new ActionQueue(TimeSpan.FromSeconds(5), 2);
    public void Test()
    {
        for (var i = 0; i < 10; i++)
        {
            _actionQueue.Enqueue((i2) =>
            {
                Console.WriteLineAction " + i2 + ": " + DateTime.UtcNow);
            }, i);
        }
    }

真实世界的例子......

    ActionQueue _actionQueue = new ActionQueue(TimeSpan.FromSeconds(1), 10);

    public override void SendOrderCancelRequest(Order order, SessionID sessionID)
    {
        _actionQueue.Enqueue((state) =>
        {
            var parms = (Tuple<Order, SessionID>)state;
            base.SendOrderCancelRequest(parms.Item1, parms.Item2);
        }, new Tuple<Order, SessionID>(order, sessionID));
    }
    public override void SendOrderMassStatusRequest(SessionID sessionID)
    {
        _actionQueue.Enqueue((state) =>
        {
            var sessionID2 = (SessionID)state;
            base.SendOrderMassStatusRequest(sessionID2);
        }, sessionID);
    }

实际班级......

public class ActionQueue
{
    private class ActionState
    {
        public Action<object> Action;
        public object State;
        public ActionState(Action<object> action, object state)
        {
            Action = action;
            State = state;
        }
    }
    Queue<ActionState> _actions = new Queue<ActionState>();
    Queue<DateTime> _times = new Queue<DateTime>();

    TimeSpan _timeSpan;
    int _maxActions;
    public ActionQueue(TimeSpan timeSpan, int maxActions)
    {
        _timeSpan = timeSpan;
        _maxActions = maxActions;           
    }
    public void Enqueue(Action<object> action, object state)
    {
        lock (_times)
        {
            _times.Enqueue(DateTime.UtcNow + _timeSpan);

            if (_times.Count <= _maxActions)
                action(state);
            else
                _actions.Enqueue(new ActionState(action, state));

            CreateDequeueTimerIfNeeded();
        }
    }

    System.Threading.Timer _dequeueTimer;
    protected void CreateDequeueTimerIfNeeded()
    {
        // if we have no timer and we do have times, create a timer
        if (_dequeueTimer == null && _times.Count > 0) 
        {
            var timeSpan = _times.Peek() - DateTime.UtcNow;
            if (timeSpan.TotalSeconds <= 0)
            {
                HandleTimesQueueChange();
            }
            else
            {
                _dequeueTimer = new System.Threading.Timer((obj) =>
                {
                    lock (_times)
                    {
                        _dequeueTimer = null;
                        HandleTimesQueueChange();
                    }
                }, null, timeSpan, System.Threading.Timeout.InfiniteTimeSpan);
            }
        }
    }

    private void HandleTimesQueueChange()
    {
        _times.Dequeue();
        while (_times.Count > 0 && _times.Peek() < DateTime.UtcNow)
            _times.Dequeue();

        while (_actions.Count > 0 && _times.Count < _maxActions)
        {
            _times.Enqueue(DateTime.UtcNow + _timeSpan);
            var actionState = _actions.Dequeue();
            actionState.Action(actionState.State);
        }

        CreateDequeueTimerIfNeeded();
    }
}