zoco

动态流量控制(滑动窗口)在api日志中的应用

2016-06-18


现在api的所有php-fpm日志都打到了elasticsearch里,我们可以拿这些日志来做很多东西,但首先第一步是怎么取这些数据。

这些日志的量大概每天有几百万条的规模。

首先我尝试的是通过from+size的方式取数据,但是elasticsearch默认情况下只可以取from+size<10000的数据(当然也可以调大from+size,但是会带来一个问题就是数据取到最后越来越慢)

Result window is too large, from + size must be less than or equal to: [10000] but was [10020]. See the scroll api for a more efficient way to request large data sets. This limit can be set by changing the [index.max_result_window] index level parameter

那么就采取第二个方案,就是按时间来取数据。

那么问题来了,按多久取一次数据呢?

之前的取数据一般是按分钟取,但是会遇到一个问题。

先看下面的图,是redis_warning一天(20170110)的分布情况:

Paste_Image.png

从图里我们可以看到两个特点:

  1. 请求包括报错的分布都是有高峰的。
  2. 正常情况下数据的涨跌都不是跳崖式的波动,都是有一个涨跌的过渡过程(当然突然某个redis或者moa挂掉会导致一个突发的增长,这种情况也应该考虑到)。

我们的请求包括报错的分布都是有高峰的,所以说在高峰的时候按分钟取也可能出现from+size>10000的情况,那么我们就来按秒取,按秒取的话一天相当于要请求elasticsearch的接口86400次,在非高峰的时候这样无疑是对带宽的一种浪费。而且执行一次脚本所需要的时间很长,测试和出数据的的时候极其不方便(redis_warning按秒执行一次的时间大概是90分钟)。

下面就是我现在所采用的方案,就是动态流量控制(滑动窗口)。

既然非高峰的时候我可以多取一段时间的数据,非高峰的时候少取一些数据,那么我就建立一个模型让脚本动态地根据上一次获得的数据来决定下一次请求所要获取的数据多少。

if(count($res) == 10000) {
    //说明此次请求获取的数据超过阈值
    //丢弃掉此次请求的数据
    //将下一次请求的时间间隔/2(滑动窗口缩小)
} else if (count($res) < 2000) {
    //说明此次请求的数据太少
    //保留此次数据
    //将下一次请求的时间间隔*2(滑动窗口扩大)
}

上面例子中的2000是可以调整的。

经过测试,这样能够较优地平衡带宽、时间和数据。

现在redis_warning脚本的执行时间已经降低到平均2分钟。

这其中细节有很多

  1. 每次请求的时长应该是2的倍数,否则在除以2(窗口缩小)的时候会出现小数。
  2. 窗口不能太大,否则出现突然的波动窗口会多次调整,容易出现内存爆了的情况。
  3. 防止出现死锁的情况,就是说窗口扩大后不满足要求然后窗口缩小后也不满足要求的情况,这样会导致窗口不断的扩大缩小导致最终内存爆了。防止这种情况的出现是纪录窗口扩大和缩小的次数,超过一定次数就不再对窗口进行操作,保留现在的数据并请求下一个时间窗口的数据。

这种模型也不是我想出来的,在很多地方都在用,最著名的就是TCP的流量控制(http://blog.jobbole.com/105500/)。还有手机QQ的上传模型(http://weibo.com/ttarticle/p/show?id=2309404050024081266569)。

在上传一个文件(图片)的过程中,应当尽可能动态增大分片大小(例如,后一片是前一片的 N 倍),以减少分片数量。

这种模型可以应用到很多地方,这里就不一一例举。

这也给我们带来一个值得去努力的地方:能自动化的,要自动化;不能自动化的,要半自动化。我们可以用弱智能+人工确认的方式,来实现“半智能化”:用机器帮你做预选,你来做最终选择,虽然依然包含了人工干预,但却可以把生产效率提升几十倍。