Laravel中GraphQL接口请求频率实战记录


Posted in PHP onSeptember 01, 2020

前言

起源:通常在产品的运行过程,我们可能会做数据埋点,以此来知道用户触发的行为,访问了多少页面,做了哪些操作,来方便产品根据用户喜好的做不同的调整和推荐,同样在服务端开发层面,也要做好“数据埋点”,去记录接口的响应时长、接口调用频率,参数频率等,方便我们从后端角度去分析和优化问题,如果遇到异常行为或者大量攻击来源,我们可以具体针对到某个接口去进行优化。

项目环境:

  • framework:laravel 5.8+
  • cache : redis >= 2.6.0

目前项目中几乎都使用的是 graphql 接口,采用的 package 是 php lighthouse graphql,那么主要的场景就是去统计好,graphql 接口的请求次数即可。

实现GraphQL Record Middleware

首先建立一个middleware 用于稍后记录接口的请求频率,在这里可以使用artisan 脚手架快速创建:

php artisan make:middleware GraphQLRecord
<?php

namespace App\Http\Middleware;

use Closure;

class GraphQLRecord
{
  /**
   * Handle an incoming request.
   *
   * @param \Illuminate\Http\Request $request
   * @param \Closure $next
   * @return mixed
   */
  public function handle($request, Closure $next)
  {
    return $next($request);
  }
}

然后添加到 app/config/lighthouse.php middleware 配置中,或后添加到项目中 app/Http/Kernel.php 中,设置为全局中间件

'middleware' => [
  \App\Http\Middleware\GraphQLRecord::class,
  \Nuwave\Lighthouse\Support\Http\Middleware\AcceptJson::class,
],

获取 GraphQL Operation Name

public function handle($request, Closure $next)
{
    $opName = $request->get('operationName');
    return $next($request);
}

获取到 Operation Name 之后,开始就通过在Redis 来实现一个接口计数器。

添加接口计数器

首先要设置我们需要记录的时间,如5秒,60秒,半小时、一个小时、5个小时、24小时等,用一个数组来实现,具体可以根据自我需求来调整。

const PRECISION = [5, 60, 1800, 3600, 86400];

然后就开始添加对接口计数的逻辑,计数完成后,我们将其添加到zsset中,方便后续进行数据查询等操作。

/**
   * 更新请求计数器
   *
   * @param string $opName
   * @param integer $count
   * @return void
   */
  public function updateRequestCounter(string $opName, $count = 1)
  {
    $now  = microtime(true);
    $redis = self::getRedisConn();
    if ($redis) {
      $pipe = $redis->pipeline();
      foreach (self::PRECISION as $prec) {
        //计算时间片
        $pnow = intval($now / $prec) * $prec;
        //生成一个hash key标识
        $hash = "request:counter:{$prec}:$opName";
        //增长接口请求数
        $pipe->hincrby($hash, $pnow, 1);
        // 添加到集合中,方便后续数据查询
        $pipe->zadd('request:counter', [$hash => 0]);
      }
      $pipe->execute();
    }
  }

  /**
   * 获取Redis连接
   *
   * @return object
   */
  public static function getRedisConn()
  {
    $redis = Redis::connection('cache');
    try {
      $redis->ping();
    } catch (Exception $ex) {
      $redis = null;
      //丢给sentry报告
      app('sentry')->captureException($ex);
    }

    return $redis;
  }

然后请求一下接口,用medis查看一下数据。

Laravel中GraphQL接口请求频率实战记录

Laravel中GraphQL接口请求频率实战记录

查询、分析数据

数据记录完善后,可以通过opName 及 prec两个属性来查询,如查询24小时的tag接口访问数据

/**
   * 获取接口访问计数
   *
   * @param string $opName
   * @param integer $prec
   * @return array
   */
  public static function getRequestCounter(string $opName, int $prec)
  {
    $data = [];
    $redis = self::getRedisConn();
    if ($redis) {
      $hash   = "request:counter:{$prec}:$opName";
      $hashData = $redis->hgetall($hash);
      foreach ($hashData as $k => $v) {
        $date  = date("Y/m/d", $k);
        $data[] = ['timestamp' => $k, 'value' => $v, 'date' => $date];
      }
    }

    return $data;
  }

获取 tag 接口 24小时的访问统计

$data = $this->getRequestCounter('tagQuery', '86400');

清除数据

完善一系列步骤后,我们可能需要将过期和一些不必要的数据进行清理,可以通过定时任务来进行定期清理,相关实现如下:

/**
   * 清理请求计数
   *
   * @param integer $clearDay
   * @return void
   */
  public function clearRequestCounter($clearDay = 7)
  {
    $index   = 0;
    $startTime = microtime(true);
    $redis   = self::getRedisConn();
    if ($redis) {
      //可以清理的情况下
      while ($index < $redis->zcard('request:counter')) {
        $hash = $redis->zrange('request:counter', $index, $index);
        $index++;

        //当前hash存在
        if ($hash) {
          $hash = $hash[0];
          //计算删除截止时间
          $cutoff = intval(microtime(true) - ($clearDay * 24 * 60 * 60));

          //优先删除时间较远的数据
          $samples = array_map('intval', $redis->hkeys($hash));
          sort($samples);

          //需要删除的数据
          $removes = array_filter($samples, function ($item) use (&$cutoff) {
            return $item <= $cutoff;
          });
          if (count($removes)) {
            $redis->hdel($hash, ...$removes);
            //如果整个数据都过期了的话,就清除掉统计的数据
            if (count($removes) == count($samples)) {
              $trans = $redis->transaction(['cas' => true]);
              try {
                $trans->watch($hash);
                if (!$trans->hlen($hash)) {
                  $trans->multi();
                  $trans->zrem('request:counter', $hash);
                  $trans->execute();
                  $index--;
                } else {
                  $trans->unwatch();
                }
              } catch (\Exception $ex) {
                dump($ex);
              }
            }
          }

        }
      }
      dump('清理完成');
    }

  }

清理一个30天前的数据:

$this->clearRequestCounter(30);

整合代码

我们将所有操作接口统计的代码,单独封装到一个类中,然后对外提供静态函数调用,既实现了职责单一,又方便集成到其他不同的模块使用。

<?php
namespace App\Helpers;

use Illuminate\Support\Facades\Redis;

class RequestCounter
{
  const PRECISION = [5, 60, 1800, 3600, 86400];

  const REQUEST_COUNTER_CACHE_KEY = 'request:counter';

  /**
   * 更新请求计数器
   *
   * @param string $opName
   * @param integer $count
   * @return void
   */
  public static function updateRequestCounter(string $opName, $count = 1)
  {
    $now  = microtime(true);
    $redis = self::getRedisConn();
    if ($redis) {
      $pipe = $redis->pipeline();
      foreach (self::PRECISION as $prec) {
        //计算时间片
        $pnow = intval($now / $prec) * $prec;
        //生成一个hash key标识
        $hash = self::counterCacheKey($opName, $prec);
        //增长接口请求数
        $pipe->hincrby($hash, $pnow, 1);
        // 添加到集合中,方便后续数据查询
        $pipe->zadd(self::REQUEST_COUNTER_CACHE_KEY, [$hash => 0]);
      }
      $pipe->execute();
    }
  }

  /**
   * 获取Redis连接
   *
   * @return object
   */
  public static function getRedisConn()
  {
    $redis = Redis::connection('cache');
    try {
      $redis->ping();
    } catch (Exception $ex) {
      $redis = null;
      //丢给sentry报告
      app('sentry')->captureException($ex);
    }

    return $redis;
  }

  /**
   * 获取接口访问计数
   *
   * @param string $opName
   * @param integer $prec
   * @return array
   */
  public static function getRequestCounter(string $opName, int $prec)
  {
    $data = [];
    $redis = self::getRedisConn();
    if ($redis) {
      $hash   = self::counterCacheKey($opName, $prec);
      $hashData = $redis->hgetall($hash);
      foreach ($hashData as $k => $v) {
        $date  = date("Y/m/d", $k);
        $data[] = ['timestamp' => $k, 'value' => $v, 'date' => $date];
      }
    }

    return $data;
  }

  /**
   * 清理请求计数
   *
   * @param integer $clearDay
   * @return void
   */
  public static function clearRequestCounter($clearDay = 7)
  {
    $index   = 0;
    $startTime = microtime(true);
    $redis   = self::getRedisConn();
    if ($redis) {
      //可以清理的情况下
      while ($index < $redis->zcard(self::REQUEST_COUNTER_CACHE_KEY)) {
        $hash = $redis->zrange(self::REQUEST_COUNTER_CACHE_KEY, $index, $index);
        $index++;

        //当前hash存在
        if ($hash) {
          $hash = $hash[0];
          //计算删除截止时间
          $cutoff = intval(microtime(true) - ($clearDay * 24 * 60 * 60));

          //优先删除时间较远的数据
          $samples = array_map('intval', $redis->hkeys($hash));
          sort($samples);

          //需要删除的数据
          $removes = array_filter($samples, function ($item) use (&$cutoff) {
            return $item <= $cutoff;
          });
          if (count($removes)) {
            $redis->hdel($hash, ...$removes);
            //如果整个数据都过期了的话,就清除掉统计的数据
            if (count($removes) == count($samples)) {
              $trans = $redis->transaction(['cas' => true]);
              try {
                $trans->watch($hash);
                if (!$trans->hlen($hash)) {
                  $trans->multi();
                  $trans->zrem(self::REQUEST_COUNTER_CACHE_KEY, $hash);
                  $trans->execute();
                  $index--;
                } else {
                  $trans->unwatch();
                }
              } catch (\Exception $ex) {
                dump($ex);
              }
            }
          }

        }
      }
      dump('清理完成');
    }

  }

  public static function counterCacheKey($opName, $prec)
  {
    $key = "request:counter:{$prec}:$opName";

    return $key;
  }
}

在Middleware中使用.

<?php

namespace App\Http\Middleware;

use App\Helpers\RequestCounter;
use Closure;

class GraphQLRecord
{

  /**
   * Handle an incoming request.
   *
   * @param \Illuminate\Http\Request $request
   * @param \Closure $next
   * @return mixed
   */
  public function handle($request, Closure $next)
  {
    $opName = $request->get('operationName');
    if (!empty($opName)) {
      RequestCounter::updateRequestCounter($opName);
    }

    return $next($request);
  }
}

结尾

上诉代码就实现了基于GraphQL的请求频率记录,但是使用不止适用于GraphQL接口,也可以基于Rest接口、模块计数等统计行为,只要有唯一的operation name即可。

到此这篇关于Laravel中GraphQL接口请求频率的文章就介绍到这了,更多相关Laravel中GraphQL接口请求频率内容请搜索三水点靠木以前的文章或继续浏览下面的相关文章希望大家以后多多支持三水点靠木!

PHP 相关文章推荐
PHP 高手之路(一)
Oct 09 PHP
php中的观察者模式
Mar 24 PHP
PHP项目开发中最常用的自定义函数整理
Dec 02 PHP
探讨:如何使用PhpDocumentor生成文档
Jun 25 PHP
php接口与接口引用的深入解析
Aug 09 PHP
linux系统下php安装mbstring扩展的二种方法
Jan 20 PHP
destoon之一键登录设置
Jun 21 PHP
PHP实现批量上传单个文件
Dec 29 PHP
PHP实现的线索二叉树及二叉树遍历方法详解
Apr 25 PHP
PHP实现页面静态化的超简单方法
Sep 06 PHP
PHP unlink与rmdir删除目录及目录下所有文件实例代码
Feb 07 PHP
Laravel框架Request、Response及Session操作示例
May 06 PHP
PHP实现Snowflake生成分布式唯一ID的方法示例
Aug 30 #PHP
Yii实现微信公众号场景二维码的方法实例
Aug 30 #PHP
Swoole源码中如何查询Websocket的连接问题详解
Aug 30 #PHP
PHP常用header头定义代码示例汇总
Aug 29 #PHP
PHP isset()及empty()用法区别详解
Aug 29 #PHP
PHP实现简单日历类编写
Aug 28 #PHP
PHP实现文件上传与下载
Aug 28 #PHP
You might like
用PHP编写PDF文档生成器
2006/10/09 PHP
PHP中SQL查询语句的id=%d解释(推荐)
2016/12/10 PHP
实例讲解php将字符串输出到HTML
2019/01/27 PHP
window.open()弹出居中的窗口
2007/02/01 Javascript
基于jquery的实现简单的表格中增加或删除下一行
2010/08/01 Javascript
JavaScript立即执行函数的三种不同写法
2014/09/05 Javascript
javascript使用prototype完成单继承
2014/12/24 Javascript
jQuery中toggleClass()方法用法实例
2015/01/05 Javascript
JavaScript中实现继承的三种方式和实例
2015/01/29 Javascript
js性能优化技巧
2015/11/29 Javascript
JavaScript保留关键字汇总
2015/12/01 Javascript
详解JavaScript中localStorage使用要点
2016/01/13 Javascript
JavaScript中的时间处理小结
2016/02/24 Javascript
vue开发心得和技巧分享
2016/10/27 Javascript
基于Bootstrap的Java开发问题汇总(Spring MVC)
2017/01/15 Javascript
nodejs使用express获取get和post传值及session验证的方法
2017/11/09 NodeJs
使用nodejs+express实现简单的文件上传功能
2017/12/27 NodeJs
Vue 2.5.2下axios + express 本地请求404的解决方法
2018/02/21 Javascript
解决vue+webpack打包路径的问题
2018/03/06 Javascript
nodejs读取图片返回给浏览器显示
2019/07/25 NodeJs
微信小程序实现上传图片裁剪图片过程解析
2019/08/22 Javascript
[02:19]DOTA2女子战队FOX视频专访:希望更多美眉一起加入
2013/10/15 DOTA
[02:55]含熏伴清风,风行者至宝、屠夫身心及典藏宝瓶二展示
2020/09/08 DOTA
Python基于Tkinter的HelloWorld入门实例
2015/06/17 Python
使用Python写一个小游戏
2018/04/02 Python
python 读取文件并把矩阵转成numpy的两种方法
2019/02/12 Python
对Python 中矩阵或者数组相减的法则详解
2019/08/26 Python
python爬虫开发之PyQuery模块详细使用方法与实例全解
2020/03/09 Python
Python docutils文档编译过程方法解析
2020/06/23 Python
Pat McGrath Labs官网:世界上最有影响力的化妆师推出的彩妆品牌
2018/01/07 全球购物
Harrods美国:英国最大的百货公司
2018/11/04 全球购物
某/etc/fstab文件中的某行如下: /dev/had5 /mnt/dosdata msdos defaults,usrquota 1 2 请解释其含义
2013/04/11 面试题
网络维护中文求职信
2014/01/03 职场文书
追悼会答谢词
2015/01/05 职场文书
2015年度销售个人工作总结
2015/03/31 职场文书
详解如何修改nginx的默认端口
2021/03/31 Servers