Easyswoole 令牌桶IP限流实现(企业实用)

6 篇文章 0 订阅

使用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
在这里插入图片描述

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值