在 Sentinel 中,主要是通过 LeapArray 类来实现滑动时间窗口的实现和选择。在 sentinel 的这个获取时间窗口并为时间窗口添加指标的过程中,主要的流程为:
- 根据当前时间选择当前时间应该定位当前时间应该属于的时间窗口 id。
- 根据时间窗口 id 获取时间窗口。这里可能会存在三种情况:
- 时间窗口还未建立,那么将会为此次流量的进入建立一个新的时间窗口返回,并且接下来这个时间窗口内的获取请求都将返回该窗口。
- 时间窗口已经建立的情况下,将会直接获取已经存在的符合条件的时间窗口。
- 时间窗口可能已经存在,但是当前获取的时间窗口已经过期,需要加锁,并重置当前时间窗口。
- 当前进入的时间已经远远落后当前的时间,目标时间窗口已经被 reset 更新成更新的时间窗口,那么将不会返回目标时间窗口,而是返回一个新的空的时间窗口进行统计,这个时间窗口不会再被重复利用。
其中的第四个情况表明,sentinel 的滑动时间窗口是有时间范围的,这也是为了尽量减少 sentinel 的所占用的内存,默认情况下 sentinel 的采取的时间长度为 1 分钟和 1 秒钟。这里的实现与 LeapArray 类的结构非常有关系。
protected final AtomicReferenceArray<WindowWrap<T>> array;
在 LeapArray 中,时间窗口的存放通过一个由 AtomicReferenceArray 实现的 array 来实现。AtomicReferenceArray 支持原子读取和写入,并支持通过 cas 来为指定位置的成员进行更新。在时间窗口的创建并放回 array 的过程中,也就是上文的第一步,就是通过 AtomicReferenceArray 的 compareAndSet()方法来实现,保证并发下的线程安全。并发情况下,通过 cas 更新失败的线程将会回到就绪态,在下一次婚欢得到已经初始化完成的时间窗口。
private final ReentrantLock updateLock = new ReentrantLock();
此处的 updateLock 是专门在上述的第三个情况来进行加锁的,只有成功得到锁的线程才会对过期的时间窗口进行 reset 操作,其他没有成功获取的线程将不会挂起等待,而是通过 yield()方法回到就绪态在下一次的循环尝试重新获取该位置的时间窗口。在下一次获取该锁的线程可能已经完成了,那么将会执行上述第二步,否则继续回到就绪态等待下一次循环中再次获取该时间窗口。
以上两个数据结构是 LeapArray 类实现时间窗口在高并发下准确获取时间窗口并更新的关键。
在 sentinel 默认的秒级别时间窗口中,array 的大小为 2,也就是每 500ms 为一个时间窗口的大小。
因此当一个线程试图获取一个时间窗口来记录指标数据的时候,将会根据单个时间窗口的时间跨度进行取模,来得到 array 上对应的时间窗口的下标,在这个情况下,将为 0 或者 1,之后计算当前线程时间指标所属的时间窗口的起始时间,以此为依据来判断如果在后面如果获取到的时间窗口是过期还是正好所需要的。
最后,将会不断循环从 array 尝试获取之前计算得到下标位置处的时间窗口,可能发生的 4 种情况如上所示。在这个情况,如果 cas 失败或事没有尝试获取到更新锁,都不会阻塞或是挂起,而是通过 yield 重新回到就绪态等待下一次循环获取。
在指标集合类的实现 MetricBucket 中,通过 LongAdder 类来记录单个指标的值而不是 AtomicLong,LongAdder 内部的核心思路是为各个线程分配一个专属变量进行更新,在需要总数的时候对这一系列进行累加,因此在更新值的时候相比 AtomicLong 会尽可能减少线程间的竞争,达到高效的 metric 更新。