当前位置: 首页 > 后端技术 > Java

Sentinel滑动窗口流量统计

时间:2023-04-01 23:23:55 Java

StatisticSlot主要统计两类数据:线程请求数,即QPS,线程数统计比较简单。是LongAdder内部维护的当前线程数统计。每进入一个线程就加1,线程执行完后减1,得到线程数。QPS的统计比较复杂,它使用了滑动窗口的原理。下面重点分析实现细节。BucketSentinel使用Bucket统计一个窗口时间内的各种指标数据。这些指标数据包括请求总数、成功总数、异常总数、总耗时、最短耗时、最大耗时等,一个Bucket可以记录内的数据1s,也可以是10ms以内的数据,这个时间长度称为窗口时间。publicclassMetricBucket{/***存储每个事件的计数,例如异常总数、请求总数等*/privatefinalLongAdder[]counters;/***本次事件花费的最短时间*/privatevolatilelongminRt;}Bucket使用一个LongAdder数组记录一段时间内的各种指标数据。LongAdder保证了数据修改的原子性,性能优于AtomicInteger。数组的每个元素记录了一个时间窗口内的总请求数、异常数和总耗时。Sentinel使用枚举类型MetricEvent的序号属性作为下标。当需要获取Bucket记录的成功请求总数或异常总数,以及处理请求的总时间时,可以从Bucket的LongAdder数组中获取对应的事件类型(MetricEvent)。LongAdder,并调用sum方法得到://假设事件是MetricEvent.SUCCESSpubliclongget(MetricEventevent){//MetricEvent.SUCCESS.ordinal()is1returncounters[event.ordinal()].sum();}需要记录请求次数的操作如下://假设事件是MetricEvent.RTpublicvoidadd(MetricEventevent,longn){//MetricEvent.RT.ordinal()is2counters[event.序数()].添加(n);}滑动窗口我们想知道某个接口每秒处理成功的请求数(成功QPS)和平均请求时间(avgrt)。我们只需要控制桶统计一秒的索引数据即可。哨兵是如何实现的?定义了一个Bucket数组,根据时间戳定位数组下标。假设我们需要统计每秒处理的请求数,只需要保存最后一分钟的数据,那么Bucket数组的大小可以设置为60,每个Bucket的windowLengthInMs(窗口时间)大小为1000ms.当然,我们不能无限期地存储桶,如果我们只需要保留一分钟的数据,那么我们可以将桶的大小设置为60,然后循环使用,避免频繁创建桶。在这种情况下如何定位Bucket?方法是去掉当前时间戳中的毫秒部分等待当前秒数,然后将得到的秒数和数组长度取余,得到Bucket在当前时间窗口的位置在数组中。例如计算给定时间戳的数组索引:privateintcalculateTimeIdx(longtimeMillis){/***假设当前时间戳为1577017699235*windowLengthInMs为1000毫秒(1秒)*然后将毫秒转换为秒=>1577017699*然后数组长度的余数=>映射到数组的索引*余数用于回收数组**当time增加windowLength的长度时,timeId会增加1,时间窗口会向前滑动一位*/longtimeId=timeMillis/windowLengthInMs;//数组长度为2return(int)(timeId%array.length());}由于数组是循环使用的,当前时间戳和前一分钟的时间戳以及一分钟后的时间戳都会映射到数组中,所以需要能够判断得到的Bucket是否是指标数据中的用于统计的当前时间窗口,需要数组的每个元素存储Bucket时间窗口的起始时间戳。如何计算开始时间?protectedlongcalculateWindowStart(longtimeMillis){/***假设窗口大小为1000毫秒,即数组的每个元素存储1秒的统计数据*timeMillis%windowLengthInMs是获取毫秒部分*timeMillis-毫秒=第二部分*这是获取每一秒的开始时间戳*/returntimeMillis-timeMillis%windowLengthInMs;}WindowWrap由于Bucket本身不保存时间窗口信息,所以Sentinel为Bucket增加了一个包装类WindowWrap来记录Bcuket的时间窗口。publicclassWindowWrap{/***窗口时间长度(毫秒)*/privatefinallongwindowLengthInMs;/***开始时间戳(毫秒)*/privatelongwindowStart;/***时间窗口的内容,在WindowWrap中使用泛型来表示这个值,*但实际上是MetricBucket类*/privateT值;publicWindowWrap(longwindowLengthInMs,longwindowStart,Tvalue){this.windowLengthInMs=windowLengthInMs;this.windowStart=windowStart;这个.value=值;}}我们只需要知道窗口的开始时间和窗口时间的大小,给定一个时间戳,就可以知道时间戳是否在Bucket的窗口时间内。/***检查给定时间戳是否在当前桶中。**@paramtimeMillis时间戳,毫秒*@return*/publicbooleanisTimeInWindow(longtimeMillis){returnwindowStart<=timeMillis&&timeMilliscurrentWindow(longtimeMillis){if(timeMillis<0){returnnull;}//获取时间戳映射到的数组索引intidx=calculateTimeIdx(timeMillis);//计算桶时间窗口的开始时间longwindowStart=calculateWindowStart(timeMillis);//从数组中获取桶while(true){WindowWrapold=array.get(idx);//一般在项目启动的时候,时间还没有到一个周期,数组还没有满。还没有到复用阶段,所以数组元素可能为空newEmptyBucket(timeMillis));//cas写入,保证线程安全,期望数组下标的元素为空,否则不写入,而是重用if(array.compareAndSet(idx,null,window)){returnwindow;}别的{线程.yield();}}//如果WindowWrap的windowStart恰好是当前时间戳计算出的时间窗口的开始时间,那么就是我们想要的bucketelseif(windowStart==old.windowStart()){returnold;}//重用旧桶当前时间窗口的开始时间超过旧窗口的开始时间,//放弃旧窗口,将时间设置为新时间窗口的开始时间,此时窗口向前滑动elseif(windowStart>old.windowStart()){if(updateLock.tryLock()){try{//重置桶并指定桶新时间窗口的开始时间returnresetWindowTo(old,windowStart);}最后{updateLock.unlock();}}else{线程.yield();}}//计算出的当前bucket时间窗的开始时间小于数组中当前存储的bucket的时间窗开始时间,//直接返回一个空bucketelseif(windowStart(windowLengthInMs,windowStart,newEmptyBucket(timeMillis));}}}protectedWindowWrapresetWindowTo(WindowWrapw,longtime){//更新开始时间和重置值.//重置窗口启动w.resetTo(time);MetricBucketborrowBucket=borrowArray.getWindowValue(时间);if(borrowBucket!=null){w.value().reset();w.value().addPass((int)borrowBucket.pass());}else{w.value().reset();}返回w;}上面的代码通过当前时间戳计算出Bucket(新Bucket)在数组中当前时间窗口的索引,以及Bucket时间窗口的开始时间。通过索引从数组中获取Bucket(旧桶)。当索引处不存在Bucket时,创建一个新的Bucket并线程安全的写入到索引中,当旧的Bucket不存在时返回该Bucket。如果为空,且旧Bucket的时间窗开始时间等于当前计算的新Bucket的时间窗开始时间,则该Bucket为当前要查找的Bucket,当查找到时直接返回新Bucket计算出的时间窗开始时间大于当前数组中存储的旧Bucket时间窗开始时间时,可以重用旧Bucket对其进行线程安全的重置,相当于计算滑动窗口中新Bucket时间窗的开始时间小于当前数组中存储的旧Bucket时间窗时间开始时,直接返回一个空的Bucket。如何获取当前时间戳的上一个Bucket?答案是根据当前时间戳计算出当前Bucket的时间窗开始时间,将当前Bucket的时间窗开始时间减去一个windowsize,从而定位到上一个Bucket。需要注意的是数组是循环使用的,所以当前Bucket和计算出来的Bucket可能相差一个滑动窗口,也可能不止一个,所以需要比较Bucket的时间窗口的开始时间和当前时间戳.期间无效。如何记录和修改单位时间内的请求数,可以简单概括为:先根据时间戳获取bucket,然后根据bucket进行记录和修改操作。总结一下,WindowWrap就是用来打包Buckets的。WindowWrap数组与Buckets一起创建,以实现滑动窗口。Buckets只负责各种指标的统计。WindowWrap用于记录Buckets的时间窗口信息。定位Buckets其实就是定位WindowWrap。转至Bucket参考文章:Sentinel限流实现原理常见限流算法