前言

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

我的项目环境:

  • 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
<?phpnamespace 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);

整合代码

咱们将所有操作接口统计的代码,独自封装到一个类中,而后对外提供动态函数调用,既实现了职责繁多,又不便集成到其余不同的模块应用。

<?phpnamespace 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中应用.

<?phpnamespace 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即可。