web服务下游代码端限流Laravel+Redis+Apache-jmete并发测试

一、前言

公司因为战略问题将钉钉办公软件切换成飞书,对接飞书接口实现接口秒级限流。协助职能组开发,之前历史使用的Laravel框架和Redis缓存,钉钉是分钟级限流,飞书是秒级限流,限流颗粒度更细,我们使用的是滑动窗口实现限流,用到的是Redis的list类型来处理。

二、准备工具

Laravel框架、Redis服务、jmete(5.4.1)并发测试工具和Java(1.8.0)运行环境。大家自行百度大巴安装教程。 

 三、代码实现

① 新建文件 laravel框架 app/Http/Middleware/SlidingWindowGrantMiddleware.php

<?php

namespace App\Http\Middleware;

use App\Models\InterfaceList;
use App\Models\SystemConfig;
use Closure;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Redis;

class SlidingWindowGrantMiddleware
{
    protected $limit = 50; //默认限制次数
    protected $interval = 15; //窗口秒数 因为下游代码接收的http请求是延迟在处理,所以时间颗粒度不能太细建议10秒以上
    private $interFaceId = 'id';
    private $interFaceConcurrent = 'concurrent';
    private $interFaceStatus = 'status';

    /**
     * 基于redis的滑动窗口限流
     * @url https://caihongtengxu.github.io/2019/20190611/index.html
     * @param  \Illuminate\Http\Request  $request
     * @param  \Closure  $next
     * @return mixed
     */
    public function handle($request, Closure $next)
    {
        $systemKey = $request->header('systemKey',''); //获取对接系统名
        if($systemKey){
            if (Redis::hexists($systemKey, 'bing_limit')) {
                $limit = (int)Redis::hget($systemKey, 'bing_limit'); //应用最大限制次数
                $systemStatus = (int)Redis::hget($systemKey, 'status');//应用限制是否开启
                $systemInterFaceId = Redis::hget($systemKey, 'interface_id');//单个接口id
            } else {
                $system = SystemConfig::where('system_key', $systemKey)->first(); //获取应用限流配置
                if (!empty($system)){
                    $limit  = $system->bing_limit;
                    $systemStatus = $system->status;
                    $systemInterFaceId = $system->interface_id;
                    Redis::hset($systemKey, 'bing_limit', $system->bing_limit); //不存在就缓存进redis
                    Redis::hset($systemKey, 'status', $system->status);
                    Redis::hset($systemKey, 'interface_id', $system->interface_id);
                } else {
                    return response()->json(['code'=> 400,'msg'=> '系统未配置,请联系管理员']);
                }
            }
            if($systemStatus == 0){
                return response()->json(['code'=> 400,'msg'=> '系统已禁用,不可以调用']);
            }
            if(empty($systemInterFaceId)){
                return response()->json(['code'=> 400,'msg'=> '系统未配置接口权限,请联系管理员']);
            }
            // 没有设置就没有限制
            if (empty($limit)){
                return $next($request);
            }

            $path = 'window:'. $systemKey;
            $result = $this->slidingWindowGrant($path, $limit);
            if (!$result) {
                return response()->json(['code'=> 400,'msg'=> '系统繁忙,请稍后重试'], 500); //响应代码为了jmter断言
            }
        }
       // 接口限流逻辑
        //获取接口请求路径
        $interface_url = $request->path();
        //获取接口信息
        $interFaceKey = str_replace('/', '_', $interface_url);
        if (Redis::hexists($interFaceKey, $this->interFaceId)) {
            $interFaceId = Redis::hget($interFaceKey, $this->interFaceId);
            $interFaceConcurrent = (int)Redis::hget($interFaceKey, $this->interFaceConcurrent);

            $interFaceStatus = Redis::hget($interFaceKey, $this->interFaceStatus);
        } else {
            $interface = InterfaceList::where('interface_url', $interface_url)->first();
            if (!$interface) {
                return response()->json(['code'=> 400,'msg'=> '接口未配置,请联系管理员']);
            }
            //从接口表中获取接口信息
            $interFaceId = $interface->id;
            $interFaceConcurrent = $interface->concurrent;

            $interFaceStatus = $interface->status;
            //Redis存储接口信息
            Redis::hset($interFaceKey, $this->interFaceId, $interface->id);
            Redis::hset($interFaceKey, $this->interFaceConcurrent, $interface->concurrent);
            Redis::hset($interFaceKey, $this->interFaceStatus, $interface->status);
        }
        //接口状态
        if($interFaceStatus == 0){
            return response()->json(['code'=> 400,'msg'=> '接口已禁用,不可以调用']);
        }
        //没有设置就没有限制
        if (empty($interFaceConcurrent)){
            return $next($request);
        }
        //对接系统是否配置接口权限,免登流程接口不验证系统的权限
        if(isset($systemInterFaceId)){
            $ids =explode(',', $systemInterFaceId);
            if(!in_array($interFaceId, $ids)){
                return response()->json(['code'=> 400,'msg'=> '没有调用权限,请联系管理员']);
            }
        }
        $questionLocation = md5($interface_url);
        $path = 'window:'. $questionLocation;
        $result = $this->slidingWindowGrant($path, $interFaceConcurrent);
        if (!$result) {
            return response()->json(['code'=> 401,'msg'=> '接口繁忙,请稍后重试'], 500);
        }

        return $next($request);
    }

    /**
     * 限流鉴权-基于redis的list
     *
     * @param $url
     * @return boolean
     */
    public function slidingWindowGrant($url, $limit)
    {
        try {
            $interval =  $this->interval; //毫秒
            $size = (int)Redis::lLen($url); //获取list长度
//            $now = $this->getMicroSecond();  //使用这个时间会有个问题 1秒钟打大量http请求会导致fpm进程延迟处理 php执行时间滞后最后判断时间大于设置时间,导致队列被清空重新统计
            $now = isset($_SERVER['REQUEST_TIME']) ? $_SERVER['REQUEST_TIME'] : time();
            if (! $size) {
                return $this->insertSlidingWindow($url, $now);
            }
            $startTime = (float)Redis::lindex($url, 0);
            $check = $size;
//            file_put_contents('yu.log',$size.'__'.date('s',$now) .'__'.date('s',time())."\n", FILE_APPEND);
            while ($check > 0) {
                if ($now >= $startTime + $interval) {
                    $startTime = (int)Redis::lpop($url); //移出队列
                    $check--;
                    continue;
                } else {
                    break;
                }
            }
            if ($now <= $startTime + $interval) {
                if ($size < $limit) {
                    return $this->insertSlidingWindow($url, $now);
                } else {
                    Log::info("SlidingWindowGrantMiddleware|{$size}|{$limit}|{$startTime}");
                    return false;
                }
            } else {
                return $this->insertSlidingWindow($url, $now);
            }
        } catch (\Exception $exception) {
            Log::info('SlidingWindowGrantMiddleware|'.date('Y-m-d H:i:s', time()).'|slidingWindowGrant()方法捕获异常'.'|'.
                $exception->getFile().'|'.$exception->getCode().'|'.$exception->getMessage());
            return false;
        }
    }

    /**
     * 初始化滑动窗口
     *
     * @param $redis
     * @param $url
     * @param $timestamp
     * @return boolean
     */
    public function insertSlidingWindow($url, $timestamp)
    {
        Redis::rPush($url, $timestamp);
        Redis::expire($url, 30);
        return true;
    }
}

② 载入中间件配置 app/Http/Kernel.php

 ③ 在laravel对应的路由处使用即可

四、开始测试

① 打开jmter安装包运行 jmeter.bat

 ② 打开后切换中文

③ 添加线程组

④ 添加Http请求组件
 按照图中的填写接口请求配置信息

请求体参数 

 ⑤ 添加请求头组件

 点击添加填入请求头信息

 ⑥添加响应断言组件

 按照步骤配置响应状态码

 ⑦ 添加同步定时器 不用这个会导致循环发送请求不是并行发送了。注意添加后要拖动到线程组上面。

 

 ⑧ 添加结果报表

 添加表格统计观察吞吐

 ⑨ 设置同步定时器的并发等待数要与线程组一致,不能就无限等待不能发起请求哦。

 设置线程组线程数与同步定时器数量要一致哦。

 ⑩ 选择好观察报表运行

 一直点击保存和同意

 运行结果

 限流成功,在一秒的100毫秒内60次请求过来了。

五、总结

如果要达到秒级限流必须要在web上游去做控制,这时应该采用Hyperf这种swoole运行的框架才能在流量入口时做拦截处理,因为我们这种是通过用户手动配置的应用限流和接口限流也是历史项目上来做增量开发。限流简单的也可以直接在nginx进行限流 参考我另一篇文章 。

刚开始使用的是PHP的time()函数生成计数key发现并发请求过来程序花了几十秒才接入处理导致秒级计数不断重置,所以使用了$_SERVER['REQUEST_TIME'] 来获取http请求接收处理时间,这个误差下降了70%左右,可以满足基本的限流了。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

余祥伟

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值