-
Notifications
You must be signed in to change notification settings - Fork 106
Description
问题发现与测试用例补充
在对应 issue #144 (目前已关闭)的时候,我发现了滑动窗口流量控制存在窗口更新不及时的问题。
发现问题的具体代码在这个 branch:
https://github.com/f1akeee/trpc-cpp/tree/sliding_window_limiter_test
(仅据参考,直接看下面的 case即可)
稍加整理,设计出一个 adversary case 😈
相关单元测试用例:
TEST_F(SmoothLimiterTest, Overload) {
ServerContextPtr context = MakeRefCounted<ServerContext>();
ProtocolPtr req_msg = std::make_shared<TrpcRequestProtocol>();
context->SetRequestMsg(std::move(req_msg));
context->SetFuncName("trpc.test.flow_control.smooth_limiter");
ASSERT_EQ(smooth_limiter_->GetMaxCounter(), 3);
for (int i{0}; i < 4; ++i) {
ASSERT_EQ(smooth_limiter_->GetCurrCounter(), 0);
ASSERT_EQ(smooth_limiter_->CheckLimit(context), false);
ASSERT_EQ(smooth_limiter_->CheckLimit(context), false);
ASSERT_EQ(smooth_limiter_->GetCurrCounter(), 2);
ASSERT_EQ(smooth_limiter_->CheckLimit(context), false);
ASSERT_EQ(smooth_limiter_->CheckLimit(context), true);
ASSERT_EQ(smooth_limiter_->CheckLimit(context), true);
std::this_thread::sleep_for(std::chrono::seconds(1));
}
}单元测试说明:
单元测试中的 smooth_limiter_ 滑窗对象配置为每秒允许通过3个请求。故而对于1秒内的5个请求,前3个请求是准许通过的,后两个请求应该被拒绝。1秒过后,滑窗应该更新,对于新到的5个请求,应该也是通过3个,拒绝两个。
实际测试结果:
exec ${PAGER:-/usr/bin/less} "$0" || exit 1
Executing tests from //trpc/overload_control/flow_control:smooth_limiter_test
-----------------------------------------------------------------------------
Running main() from gmock_main.cc
[==========] Running 3 tests from 2 test suites.
[----------] Global test environment set-up.
[----------] 1 test from SmoothLimiter
[ RUN ] SmoothLimiter.Contruct
[ OK ] SmoothLimiter.Contruct (30 ms)
[----------] 1 test from SmoothLimiter (30 ms total)
[----------] 2 tests from SmoothLimiterTest
[ RUN ] SmoothLimiterTest.CheckLimit
[ OK ] SmoothLimiterTest.CheckLimit (1007 ms)
[ RUN ] SmoothLimiterTest.Overload
trpc/overload_control/flow_control/smooth_limiter_test.cc:75: Failure
Expected equality of these values:
smooth_limiter_->GetCurrCounter()
Which is: 3
0
[ FAILED ] SmoothLimiterTest.Overload (1006 ms)
[----------] 2 tests from SmoothLimiterTest (2013 ms total)
[----------] Global test environment tear-down
[==========] 3 tests from 2 test suites ran. (2044 ms total)
[ PASSED ] 2 tests.
[ FAILED ] 1 test, listed below:
[ FAILED ] SmoothLimiterTest.Overload
1 FAILED TEST在 1000ms(1s)后,smooth_limiter_ 的当前窗口的活跃连接数依然为 3。
trpc::smooth_limiter 目前的机制
将单位时间 1S 分为 N 个部分,每一个部分称为1帧(frame),默认配置的 fps 为100,即每10ms 为一帧。帧和秒的关系,类比于现实中秒和分钟、分钟和小时的关系,只不过是进制单位不同,帧与秒采用百进制。类比时钟,不难想到用 ringbuffer,实际上,trpc 也是这样实现的。其中 ringbuffer 中每个 slot 对某一帧的计数器,对 ringbuffer 的 slot 计数器求和,即可得到 1s 的有效请求计数。引入外部定时器,用于驱动帧的“滴答”切换。
这是一个符合滑动窗口定义的朴素实现。下面分析一下这个朴素实现存在的问题🤔
机制问题分析
-
精度问题
使用“分帧分边沿”的滑动窗口的计算精度跟帧的大小有关:
-
极限状态下,将 fps 设置为1后退化为固定窗口算法(无法处理窗口边沿点的突发流量)
-
滑动窗口的左右边沿帧可能存在误 reject 的情况(如之前的问题描述),简单的画个图描述如下,其中数字表示帧计数器的值,绿色表示通过,红色表示拒绝:
-
-
效率问题
-
引入外部定时器线程
-
对于每个请求都要做 HitQueue::ActiveSum(),这是一个 O(n) 的运算。(其实,针对 这个 O(n) 的处理可以用前缀和的方式进行优化,优化后 ActiveSum() 可以降到 O(1),数据类型使用 uint64_t,当 uint64_t 耗尽的时候可以利用减法下溢的特性得到正确的结果)
int64_t HitQueue::ActiveSum() const { const int begin = window_begin_.load(std::memory_order_acquire); const int end = (begin + WindowSize()) % frame_hits_.size(); int64_t sum = 0; for (int i = begin; i != end; i = NextIndex(i)) { sum += frame_hits_.at(i).load(std::memory_order_acquire); } return sum; }
-