PHP扩展--ratelimit本地服务器限流

应用限流

本文讲的应用限流,是限制单位时间PHP应用服务器总访问量,不针对接口,不针对个人用户。原做法,是通过Redis记录访问总量,通过过期时间淘汰(单位时间访问量清零)。做法简单,效果也不错,但缺点也明显,一次访问量判断和设置,就至少要一次网络开销(Redis Incr)。优化的思路是,可否省掉这一次Redis操作(直接去掉不限流了hahahaha)。限制总访问量,分摊到每台应用机上,不就是它们自己的指标了吗。对,那就应用服务器本地记录状态,本地判断。那本地怎么记录状态?多着,本地文件、APC缓存。但要考虑性能问题,本地文件就…当我没说过。APC不错,实现起来会复杂点,要考虑修改记录的冲突问题。

Redis 版本

下面给个Redis的简版,官方思路,但有bug

1
2
3
4
5
6
7
8
9
10
11
12
$old_count = $redis->incr($key);
if($old_count == 1) {
expire($key, 1); // bug.Why?如果在expire前,退出了,那么这个$key就不会过期了,至少短期内过期不了,后续达到了limit上限,服务就无法使用了。
}
if($old_count > $limit) {
echo "1秒内太多人访问了";
return false;
} else {
return true;
}

Redis的版本可以setnx锁来实现会更稳

我的限流PHP扩展

开头说,使用APC来实现Redis上的这个功能是可以的,只是要多的事会多点,不说了,说说APC缓存的原理吧,APC使用内存共享的来实现进程间通信,咋说,php-fpm是多进程模型,同一进程可以处理多次(不是同时处理)php请求,换句话说,php-fpm的worker进程是常驻内存的,那么,php-fpm woker进程就可以通过内存共享来达到缓存数据的功能。

那么利用内存共享不就可以在多个进程间共享本地服务器的访问量了,就可以保存访问记录了。

1
2
3
4
5
6
7
8
9
10
$solt = 0; // 使用那个solt比对
$limit = 100; // 每秒访问量上限
$rl = new M_ratelimit($solt, $limit);
if($rl->acquire()) {
echo "允许访问";
return true;
} else {
echo "超过了{$limit}上限了";
return false;
}

扩展源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
// solt结构体
typedef struct {
m_lck_t lock;
size_t visit;
size_t timeout;
} rlimit;
// module init
if(MUTILS_G(ratelimit_enable)) {
if(MUTILS_G(ratelimit_slot_nums) > 10) {
php_error(E_ERROR, "Ratelimit slot nums(%d) bigger than 10", MUTILS_G(ratelimit_slot_nums));
return FAILURE;
}
size = MUTILS_G(ratelimit_slot_nums) * sizeof(rlimit);
ah = &alloc_handler;
// 创建内存共享
if(!(ah->create_segments((void **)&rlimit_slots, size, &error))) {
php_error(E_ERROR, "Shared memory allocator failed '%s': %s", error, strerror(errno));
return FAILURE;
}
for (i = 0; i < MUTILS_G(ratelimit_slot_nums); ++i) {
// 初始化solt
if(CREATE_LOCK(&(rlimit_slots[i].lock), &error)) {
php_error(E_ERROR, "%s", error);
ah->detach_segment((void **)&rlimit_slots, size);
return FAILURE;
}
rlimit_slots[i].visit = 0;
rlimit_slots[i].timeout = 0;
}
}
// M_ratelimit::acquire
// 上锁
if(LOCK(&(rlimit_slots[Z_LVAL_P(slot)].lock), &error)) {
error = error? error: "rate limit LOCK error";
php_error(E_ERROR, "%s", error);
RETURN_FALSE;
}
// 判断是否达到上限
if(tv < rlimit_slots[Z_LVAL_P(slot)].timeout) {
rlimit_slots[Z_LVAL_P(slot)].visit += 1;
if( rlimit_slots[Z_LVAL_P(slot)].visit <= Z_LVAL_P(limit)) {
goto allow;
} else {
goto deny;
}
} else {
rlimit_slots[Z_LVAL_P(slot)].visit = 1;
rlimit_slots[Z_LVAL_P(slot)].timeout = tv + 1;
if( rlimit_slots[Z_LVAL_P(slot)].visit <= Z_LVAL_P(limit)) {
goto allow;
} else {
goto deny;
}
}
allow:
UNLOCK(&(rlimit_slots[Z_LVAL_P(slot)].lock), &error);
RETURN_TRUE;

扩展源码