当前位置:首页 > CMS教程 > 其它CMS > 列表

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

发布:smiling 来源: PHP粉丝网  添加日期:2022-03-24 15:05:34 浏览: 评论:0 

这篇文章主要给大家介绍了关于Laravel中GraphQL接口请求频率的相关资料,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧。

前言

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

项目环境:

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

  1. <?php 
  2.  
  3. namespace App\Http\Middleware; 
  4.  
  5. use Closure; 
  6.  
  7. class GraphQLRecord 
  8. { 
  9.   /** 
  10.    * Handle an incoming request. 
  11.    * 
  12.    * @param \Illuminate\Http\Request $request 
  13.    * @param \Closure $next 
  14.    * @return mixed 
  15.    */ 
  16.   public function handle($request, Closure $next) 
  17.   { 
  18.     return $next($request); 
  19.   } 
  20. } 

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

  1. 'middleware' => [ 
  2.   \App\Http\Middleware\GraphQLRecord::class, 
  3.   \Nuwave\Lighthouse\Support\Http\Middleware\AcceptJson::class, 
  4. ], 

获取 GraphQL Operation Name

  1. public function handle($request, Closure $next) 
  2. { 
  3.     $opName = $request->get('operationName'); 
  4.     return $next($request); 
  5. } 

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

添加接口计数器

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

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

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

  1. /** 
  2.  * 更新请求计数器 
  3.  * 
  4.  * @param string $opName 
  5.  * @param integer $count 
  6.  * @return void 
  7.  */ 
  8. public function updateRequestCounter(string $opName, $count = 1) 
  9. { 
  10.   $now  = microtime(true); 
  11.   $redis = self::getRedisConn(); 
  12.   if ($redis) { 
  13.     $pipe = $redis->pipeline(); 
  14.     foreach (self::PRECISION as $prec) { 
  15.       //计算时间片 
  16.       $pnow = intval($now / $prec) * $prec; 
  17.       //生成一个hash key标识 
  18.       $hash = "request:counter:{$prec}:$opName"; 
  19.       //增长接口请求数 
  20.       $pipe->hincrby($hash, $pnow, 1); 
  21.       // 添加到集合中,方便后续数据查询 
  22.       $pipe->zadd('request:counter', [$hash => 0]); 
  23.     } 
  24.     $pipe->execute(); 
  25.   } 
  26. } 
  27.  
  28. /** 
  29.  * 获取Redis连接 
  30.  * 
  31.  * @return object 
  32.  */ 
  33. public static function getRedisConn() 
  34. { 
  35.   $redis = Redis::connection('cache'); 
  36.   try { 
  37.     $redis->ping(); 
  38.   } catch (Exception $ex) { 
  39.     $redis = null; 
  40.     //丢给sentry报告 
  41.     app('sentry')->captureException($ex); 
  42.   } 
  43.  
  44.   return $redis; 
  45. } 

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

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

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

查询、分析数据

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

  1. /** 
  2.  * 获取接口访问计数 
  3.  * 
  4.  * @param string $opName 
  5.  * @param integer $prec 
  6.  * @return array 
  7.  */ 
  8. public static function getRequestCounter(string $opName, int $prec) 
  9. { 
  10.   $data = []; 
  11.   $redis = self::getRedisConn(); 
  12.   if ($redis) { 
  13.     $hash   = "request:counter:{$prec}:$opName"; 
  14.     $hashData = $redis->hgetall($hash); 
  15.     foreach ($hashData as $k => $v) { 
  16.       $date  = date("Y/m/d", $k); 
  17.       $data[] = ['timestamp' => $k, 'value' => $v, 'date' => $date]; 
  18.     } 
  19.   } 
  20.  
  21.   return $data; 
  22. } 

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

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

清除数据

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

  1. /** 
  2.    * 清理请求计数 
  3.    * 
  4.    * @param integer $clearDay 
  5.    * @return void 
  6.    */ 
  7.   public function clearRequestCounter($clearDay = 7) 
  8.   { 
  9.     $index   = 0; 
  10.     $startTime = microtime(true); 
  11.     $redis   = self::getRedisConn(); 
  12.     if ($redis) { 
  13.       //可以清理的情况下 
  14.       while ($index < $redis->zcard('request:counter')) { 
  15.         $hash = $redis->zrange('request:counter', $index, $index); 
  16.         $index++; 
  17.  
  18.         //当前hash存在 
  19.         if ($hash) { 
  20.           $hash = $hash[0]; 
  21.           //计算删除截止时间 
  22.           $cutoff = intval(microtime(true) - ($clearDay * 24 * 60 * 60)); 
  23.  
  24.           //优先删除时间较远的数据 
  25.           $samples = array_map('intval', $redis->hkeys($hash)); 
  26.           sort($samples); 
  27.  
  28.           //需要删除的数据 
  29.           $removes = array_filter($samples, function ($item) use (&$cutoff) { 
  30.             return $item <= $cutoff; 
  31.           }); 
  32.           if (count($removes)) { 
  33.             $redis->hdel($hash, ...$removes); 
  34.             //如果整个数据都过期了的话,就清除掉统计的数据 
  35.             if (count($removes) == count($samples)) { 
  36.               $trans = $redis->transaction(['cas' => true]); 
  37.               try { 
  38.                 $trans->watch($hash); 
  39.                 if (!$trans->hlen($hash)) { 
  40.                   $trans->multi(); 
  41.                   $trans->zrem('request:counter', $hash); 
  42.                   $trans->execute(); 
  43.                   $index--; 
  44.                 } else { 
  45.                   $trans->unwatch(); 
  46.                 } 
  47.               } catch (\Exception $ex) { 
  48.                 dump($ex); 
  49.               } 
  50.             } 
  51.           } 
  52.  
  53.         } 
  54.       } 
  55.       dump('清理完成'); 
  56.     } 
  57.  
  58.   } 

清理一个30天前的数据:

$this->clearRequestCounter(30);

整合代码

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

  1. <?php 
  2. namespace App\Helpers; 
  3.  
  4. use Illuminate\Support\Facades\Redis; 
  5.  
  6. class RequestCounter 
  7. { 
  8.   const PRECISION = [5, 60, 1800, 3600, 86400]; 
  9.  
  10.   const REQUEST_COUNTER_CACHE_KEY = 'request:counter'; 
  11.  
  12.   /** 
  13.    * 更新请求计数器 
  14.    * 
  15.    * @param string $opName 
  16.    * @param integer $count 
  17.    * @return void 
  18.    */ 
  19.   public static function updateRequestCounter(string $opName, $count = 1) 
  20.   { 
  21.     $now  = microtime(true); 
  22.     $redis = self::getRedisConn(); 
  23.     if ($redis) { 
  24.       $pipe = $redis->pipeline(); 
  25.       foreach (self::PRECISION as $prec) { 
  26.         //计算时间片 
  27.         $pnow = intval($now / $prec) * $prec; 
  28.         //生成一个hash key标识 
  29.         $hash = self::counterCacheKey($opName, $prec); 
  30.         //增长接口请求数 
  31.         $pipe->hincrby($hash, $pnow, 1); 
  32.         // 添加到集合中,方便后续数据查询 
  33.         $pipe->zadd(self::REQUEST_COUNTER_CACHE_KEY, [$hash => 0]); 
  34.       } 
  35.       $pipe->execute(); 
  36.     } 
  37.   } 
  38.  
  39.   /** 
  40.    * 获取Redis连接 
  41.    * 
  42.    * @return object 
  43.    */ 
  44.   public static function getRedisConn() 
  45.   { 
  46.     $redis = Redis::connection('cache'); 
  47.     try { 
  48.       $redis->ping(); 
  49.     } catch (Exception $ex) { 
  50.       $redis = null; 
  51.       //丢给sentry报告 
  52.       app('sentry')->captureException($ex); 
  53.     } 
  54.  
  55.     return $redis; 
  56.   } 
  57.  
  58.   /** 
  59.    * 获取接口访问计数 
  60.    * 
  61.    * @param string $opName 
  62.    * @param integer $prec 
  63.    * @return array 
  64.    */ 
  65.   public static function getRequestCounter(string $opName, int $prec) 
  66.   { 
  67.     $data = []; 
  68.     $redis = self::getRedisConn(); 
  69.     if ($redis) { 
  70.       $hash   = self::counterCacheKey($opName, $prec); 
  71.       $hashData = $redis->hgetall($hash); 
  72.       foreach ($hashData as $k => $v) { 
  73.         $date  = date("Y/m/d", $k); 
  74.         $data[] = ['timestamp' => $k, 'value' => $v, 'date' => $date]; 
  75.       } 
  76.     } 
  77.  
  78.     return $data; 
  79.   } 
  80.  
  81.   /** 
  82.    * 清理请求计数 
  83.    * 
  84.    * @param integer $clearDay 
  85.    * @return void 
  86.    */ 
  87.   public static function clearRequestCounter($clearDay = 7) 
  88.   { 
  89.     $index   = 0; 
  90.     $startTime = microtime(true); 
  91.     $redis   = self::getRedisConn(); 
  92.     if ($redis) { 
  93.       //可以清理的情况下 
  94.       while ($index < $redis->zcard(self::REQUEST_COUNTER_CACHE_KEY)) { 
  95.         $hash = $redis->zrange(self::REQUEST_COUNTER_CACHE_KEY, $index, $index); 
  96.         $index++; 
  97.  
  98.         //当前hash存在 
  99.         if ($hash) { 
  100.           $hash = $hash[0]; 
  101.           //计算删除截止时间 
  102.           $cutoff = intval(microtime(true) - ($clearDay * 24 * 60 * 60)); 
  103.  
  104.           //优先删除时间较远的数据 
  105.           $samples = array_map('intval', $redis->hkeys($hash)); 
  106.           sort($samples); 
  107.  
  108.           //需要删除的数据 
  109.           $removes = array_filter($samples, function ($item) use (&$cutoff) { 
  110.             return $item <= $cutoff; 
  111.           }); 
  112.           if (count($removes)) { 
  113.             $redis->hdel($hash, ...$removes); 
  114.             //如果整个数据都过期了的话,就清除掉统计的数据 
  115.             if (count($removes) == count($samples)) { 
  116.               $trans = $redis->transaction(['cas' => true]); 
  117.               try { 
  118.                 $trans->watch($hash); 
  119.                 if (!$trans->hlen($hash)) { 
  120.                   $trans->multi(); 
  121.                   $trans->zrem(self::REQUEST_COUNTER_CACHE_KEY, $hash); 
  122.                   $trans->execute(); 
  123.                   $index--; 
  124.                 } else { 
  125.                   $trans->unwatch(); 
  126.                 } 
  127.               } catch (\Exception $ex) { 
  128.                 dump($ex); 
  129.               } 
  130.             } 
  131.           } 
  132.  
  133.         } 
  134.       } 
  135.       dump('清理完成'); 
  136.     } 
  137.  
  138.   } 
  139.  
  140.   public static function counterCacheKey($opName, $prec) 
  141.   { 
  142.     $key = "request:counter:{$prec}:$opName"; 
  143.  
  144.     return $key; 
  145.   } 
  146. } 

在Middleware中使用.

  1. <?php 
  2.  
  3. namespace App\Http\Middleware; 
  4.  
  5. use App\Helpers\RequestCounter; 
  6. use Closure; 
  7.  
  8. class GraphQLRecord 
  9. { 
  10.  
  11.   /** 
  12.    * Handle an incoming request. 
  13.    * 
  14.    * @param \Illuminate\Http\Request $request 
  15.    * @param \Closure $next 
  16.    * @return mixed 
  17.    */ 
  18.   public function handle($request, Closure $next) 
  19.   { 
  20.     $opName = $request->get('operationName'); 
  21.     if (!emptyempty($opName)) { 
  22.       RequestCounter::updateRequestCounter($opName); 
  23.     } 
  24.  
  25.     return $next($request); 
  26.   } 
  27. } 

结尾

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

Tags: GraphQL Laravel接口请求频率

分享到: