使用Easyswoole 开发项目有一段时间了,官方的ip 限流方法比较简陋,我这里自己手动实现了一套基于令牌桶算法限流的方案,这里记录分享下
实现功能
1:根据ip 限速
2:可以配置缓冲池,防止突发流量
3:可以配置黑白名单,或者及时调整指定ip的流量
实现流程
一:创建IpAccess 类
<?php
/**
* Created by PhpStorm.
* User: 05
* Date: 2020/8/18
* Time: 10:14
*
* ip限流模块
*/
namespace App\Model;
use EasySwoole\Component\TableManager;
use Swoole\Exception;
use Swoole\Table;
class IpAccess
{
/** @var IpAccess */
private static $install=null;
protected $table;
protected $burst=0;
protected $rate=0;
protected $ips=[];
private function __construct(int $burs,int $rate,$ips)
{
$this->burst=$burs;
$this->rate=$rate;
$this->ips=$ips;
//初始化一个保存最大50w条ip 的内存表
TableManager::getInstance()->add('ipList', [
'ip' => [
'type' => Table::TYPE_STRING,
'size' => 16
],
'burst' => [
'type' => Table::TYPE_INT,
'size' => 8
],
'rate' => [
'type' => Table::TYPE_INT,
'size' => 6
],
'rest' => [
'type' => Table::TYPE_INT,
'size' => 8
],
'lastAccessTime' => [
'type' => Table::TYPE_INT,
'size' => 8
]
], 1024 * 512);
$this->table = TableManager::getInstance()->get('ipList');
}
/**
* @param array ['rate'=>10,'burst'=>100,'ips'=>['127.0.0.1'=>['rate'=>10,'burst'=>100],'xxx.xxx.xx.x'=>['rate'=>10,'burst'=>100]]]
* @return IpAccess|Table
* @throws Exception
*/
public static function regist(array $conf=[]) {
if (empty(self::$install)){
$rate=50;
$burst=200;
$ips=[];
if (isset($conf['rate'])){
$rate=$conf['rate'];
}
if (isset($conf['burst'])){
$burst=$conf['burst'];
}
if (isset($conf['ips'])){
$ips=$conf['ips'];
}
self::$install=new static($burst,$rate,$ips);
}
return self::$install;
}
public static function getInstall() :IpAccess {
return self::$install;
}
/**
* 手动添加ip信息,及时黑名单或者调整白名单流速
* @param array $ips ['127.0.0.1'=>['rate'=>10,'burst'=>100],'xxx.xxx.xx.x'=>['rate'=>10,'burst'=>100]]
*/
public function addIps(array $ips){
if (!empty($ips)){
$this->ips=array_merge($this->ips,$ips);
foreach ($ips as $key=> $v) {
$tkey = substr(md5($key), 8, 16);
//删除key 下次重新创建
$this->table->del($tkey);
}
return true;
}
return false;
}
/**
* @param string $ip 客户端ip
* @return int false 超速,int =剩余次数
*/
public function access(string $ip): int
{
$key = substr(md5($ip), 8, 16);
$info = $this->table->get($key);
$tempBurst=$this->burst;
$tempRate=$this->rate;
$tempIps=$this->ips;
if (in_array($ip,$tempIps)){
$tempBurst=$tempIps['burst'];
$tempRate=$tempIps['rate'];
}
if ($info) {
$nowRest=$info['rest']-1;
//判断是否超流
if ($nowRest<0){
return false;
}
$this->table->set($key, [
'lastAccessTime' => time(),
'rest' => $nowRest,
]);
return $nowRest;
} else {
$this->table->set($key, [
'ip' => $ip,
'lastAccessTime' => time(),
'burst' => $tempBurst,
'rate' => $tempRate,
'rest' => $tempBurst+$tempRate
]);
return $tempBurst+$tempRate-1;
}
}
//定时增加令牌,外部勿主动调用
public function tick($token=''){
if ($token!=='IpAccessTask'){
return false;
}
foreach ($this->table as $key => $item) {
//如果没用到缓冲池 则直接删除ip,等待下次累计,如果已经用到缓冲池 则往池子里增加
if($item['rest']<$item['burst']){
$rest=$item['rest']+$item['rate'];
$this->table->set($key, [
'rest' => $rest,
]);
}else{
$this->table->del($key);
}
}
}
//删除所有的
public function clear()
{
foreach ($this->table as $key => $item) {
$this->table->del($key);
}
}
/**
* 访问记录
* @param int $count 5s内访问频率大于这个数据的
* @return array
*/
public function accessList($count = 10): array
{
$ret = [];
foreach ($this->table as $key => $item) {
if ($item['rest'] < $item['burst'] +$item['rate'] -$count) {
$ret[] = $item;
}
}
return $ret;
}
}
二:创建定时任务添加令牌类
<?php
/**
* Created by PhpStorm.
* User: 05
* Date: 2020/8/18
* Time: 17:05
*/
namespace App\Model;
use EasySwoole\Component\Process\AbstractProcess;
use EasySwoole\Component\TableManager;
use EasySwoole\EasySwoole\Config;
use EasySwoole\Session\Session;
use EasySwoole\Task\AbstractInterface\TaskInterface;
use Swoole\Coroutine;
use Swoole\Exception;
use Swoole\Table;
class IpAccessTask implements TaskInterface
{
function onException(\Throwable $throwable, int $taskId, int $workerIndex)
{
// TODO: Implement onException() method.
}
function run(int $taskId, int $workerIndex)
{
try {
IpAccess::getInstall()->tick('IpAccessTask');
} catch (Exception $e) {
}
}
}
三:启动server 时 在 EasySwooleEven->mainServerCreate 注册限流,并启动令牌桶任务
//启动限流任务,这里先初始化创建内存表,不要再onrequest里创建,不然进程不共享
try {
//每个ip 5s 内最多访问50次,缓冲池500,本机随便访问
$ipConf=['rate'=>50,'burst'=>300,'ips'=>['127.0.0.1'=>['rate'=>5000,'burst'=>10000]]];
IpAccess::regist($ipConf);
} catch (Exception $e) {
var_dump($e->getMessage());
}
//这里每5s 检测一遍令牌,可以自己根据需求定义,时间太频繁注意性能
Timer::tick(5*1000,function (){
$task =TaskManager::getInstance();
$task->async(new IpAccessTask());
});
4:在 EasySwooleEven->onRequest 里 进行限流判断
//判断限流
$ipAccess=IpAccess::getInstall();
$head= $request->getHeaders();
$realIp=$head['x-real_ip'][0];
//超出速度流量
if (!$ipAccess->access($realIp)){
//可以进行适当提醒,发邮件啥的
$response->write("server is busy,plase try later");
return false;
}
三:性能测试
这里测试了添加10w 个ip 跟检测10w 个ip 所需的时间
环境 腾讯云 1核 2g 主机
添加10w 个IP 大概耗时 400 ms
检测10w 个IP 大概耗时 178ms