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
<?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檢視一下資料。
查詢、分析資料
資料記錄完善後,可以通過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,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,方便後續資料查詢 $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",'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,就清除掉統計的資料 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介面請求頻率內容請搜尋我們以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援我們!