基于 Yii 2.0 实现的命令行脚本,持续运行期间,占用内存过高的分析与解决

1、当在 MySQL 与 Redis 中的数据量较小时,Docker 容器的 CPU:0.02%,内存:298MB,如图1、图2

当在 MySQL 与 Redis 中的数据量较小时,Docker 容器的 CPU:0.02%,内存:298MB

图1

当在 MySQL 与 Redis 中的数据量较小时,Docker 容器的 CPU:0.02%,内存:298MB

图2

2、查看 MySQL 的实例监控情况,内存占用:2328MB,如图3

查看 MySQL 的实例监控情况,内存占用:2328MB

图3

3、查看 Redis 的实例监控情况,已使用容量:33MB,如图4

查看 Redis 的实例监控情况,已使用容量:33MB

图4

4、命令行脚本 /console/controllers/CmcConsoleUserController.php 、/console/controllers/ConfigColumnUserController.php 代码如下:

<?php
/**
 * Created by PhpStorm.
 * User: Qiang Wang
 * Date: 2019/01/09
 * Time: 13:55
 */

namespace console/controllers;

use Yii;
use console/models/redis/cmc_console/User as RedisCmcConsoleUser;
use console/services/CmcConsoleUserService;
use yii/console/Controller;
use yii/console/ExitCode;
use yii/helpers/ArrayHelper;
use yii/web/HttpException;
use yii/web/ServerErrorHttpException;
use common/logics/http/im/IMIndependentMode;

/**
 * 框架服务控制台的用户
 *
 * @author Qiang Wang <shuijingwanwq@163.com>
 * @since  1.0
 */
class CmcConsoleUserController extends Controller
{
    /**
     * 同步框架服务控制台的用户模型(Http)至框架服务控制台的用户模型(Redis)
     *
     * @throws ServerErrorHttpException
     * @throws HttpException 如果登录超时
     */
    public function actionSync()
    {
        // 查询框架服务控制台的用户模型(Redis)的租户ID(以租户 ID 索引结果集)
        $redisCmcConsoleUserIndexByGroupIdItems = RedisCmcConsoleUser::find()->indexBy('group_id')->all();
        
        /* 判断 $redisCmcConsoleUserIndexByGroupIdItems 是否为空,如果为空,则成功退出 */
        if (empty($redisCmcConsoleUserIndexByGroupIdItems)) {
            // 延缓执行 60 * 60 秒
            sleep(Yii::$app->params['cmcConsoleUser']['isEmptyYesSleepTime']);
            
            return ExitCode::OK;
        }
        
        foreach ($redisCmcConsoleUserIndexByGroupIdItems as $redisCmcConsoleUserIndexByGroupIdItemKey => $redisCmcConsoleUserIndexByGroupIdItemValue) {

            $isLockExist = RedisCmcConsoleUser::isLockExist($redisCmcConsoleUserIndexByGroupIdItemKey);
            // 返回 true,表示锁定存在,即已经被其他客户端锁定
            if ($isLockExist === true) {
                continue;
            }

            // HTTP请求,获取租户ID下的用户列表
            $httpGetUserListData = [
                'groupId'  => $redisCmcConsoleUserIndexByGroupIdItemKey,
                'loginId'  => '',
                'loginTid' => '',
            ];
            $getUserListData = CmcConsoleUserService::httpGetUserList($httpGetUserListData);
            
            // 框架服务控制台的用户模型(Http),重建数组索引(以 ID 索引结果集)
            $httpCmcConsoleUserItems = ArrayHelper::index($getUserListData['list'], 'id');
            
            // 基于租户ID查询框架服务控制台的用户模型(Redis)(以 ID 索引结果集)
            $redisCmcConsoleUserItems = RedisCmcConsoleUser::find()->where([ 'group_id' => $redisCmcConsoleUserIndexByGroupIdItemKey ])->indexBy('id')->all();
            
            // 使用键名比较计算数组的差集,如果不为空,则删除出现在 Redis 中但是未出现在 Http 中的记录
            $redisArrayDiffItems = array_diff_key($redisCmcConsoleUserItems, $httpCmcConsoleUserItems);
            if (!empty($redisArrayDiffItems)) {
                $redisArrayDiffIds = array_keys($redisArrayDiffItems);
                if (RedisCmcConsoleUser::deleteAllByIds($redisCmcConsoleUserIndexByGroupIdItemKey, $redisArrayDiffIds) === false) {
                    continue;
                }
            }
            $im = new IMIndependentMode();
            // 遍历框架服务控制台的用户模型(Http),判断在 Redis 中是否存在,如果不存在,则插入,如果存在且更新时间不相等,则更新
            foreach ($httpCmcConsoleUserItems as $httpCmcConsoleUserItemValue) {
                $redisCmcConsoleUserItem = RedisCmcConsoleUser::find()->where([ 'id' => $httpCmcConsoleUserItemValue['id'] ])->one();
                
                $attributes = [
                    'id' => $httpCmcConsoleUserItemValue['id'],
                    'group_id' => $getUserListData['group_info']['group_id'],
                    'login_name' => $httpCmcConsoleUserItemValue['login_name'],
                    'user_token' => $httpCmcConsoleUserItemValue['user_token'],
                    'user_nick' => $httpCmcConsoleUserItemValue['user_nick'],
                    'user_pic' => $httpCmcConsoleUserItemValue['user_pic'],
                    'user_mobile' => $httpCmcConsoleUserItemValue['user_mobile'] ? $httpCmcConsoleUserItemValue['user_mobile'] : '',
                    'user_email' => $httpCmcConsoleUserItemValue['user_email'] ? $httpCmcConsoleUserItemValue['user_email'] : '',
                    'user_sex' => $httpCmcConsoleUserItemValue['user_sex'],
                    'user_type' => $httpCmcConsoleUserItemValue['user_type'],
                    'user_birthday' => $httpCmcConsoleUserItemValue['user_birthday'],
                    'user_chat_id' => $httpCmcConsoleUserItemValue['user_chat_id'] ? $httpCmcConsoleUserItemValue['user_chat_id'] : '',
                    'is_open' => $httpCmcConsoleUserItemValue['is_open'],
                    'add_time' => $httpCmcConsoleUserItemValue['add_time'],
                    'update_time' => $httpCmcConsoleUserItemValue['update_time'],
                    'im_identity' => md5($getUserListData['group_info']['group_id'] . $httpCmcConsoleUserItemValue['login_name']),
                ];
                
                if (!isset($redisCmcConsoleUserItem)) {
                    
                    $redisCmcConsoleUser = new RedisCmcConsoleUser();
                    $redisCmcConsoleUser->attributes = $attributes;
                    $redisCmcConsoleUser->insert();
                    
                } else if (isset($redisCmcConsoleUserItem) && $httpCmcConsoleUserItemValue['update_time'] != $redisCmcConsoleUserItem['update_time']) {
                    
                    $redisCmcConsoleUserItem->attributes = $attributes;
                    $redisCmcConsoleUserItem->save();
                    
                }
                $this->addIMAccount($im, $attributes['im_identity'], $attributes['user_nick'], $attributes['user_pic']);
            }
        }
        
        // 延缓执行 60 秒
        sleep(Yii::$app->params['cmcConsoleUser']['isEmptyNoSleepTime']);
        
        return ExitCode::OK;
        
    }
    
    private function addIMAccount(IMIndependentMode $im, $identifier, $nick, $faceUrl)
    {
        $im->generateUserSig();
        $ret = $im->getAccountProfile($identifier);
        if (isset($ret['ActionStatus']) && $ret['ActionStatus'] != 'OK') {
            $im->accountImport($identifier, $nick, $faceUrl);
        }
    }
}
<?php
/**
 * Created by PhpStorm.
 * User: Qiang Wang
 * Date: 2019/03/01
 * Time: 13:17
 */

namespace console/controllers;

use Yii;
use console/models/redis/cmc_console/User as RedisCmcConsoleUser;
use console/models/ConfigColumnUser;
use yii/console/Controller;
use yii/console/ExitCode;

/**
 * 栏目人员配置
 *
 * @author Qiang Wang <shuijingwanwq@163.com>
 * @since  1.0
 */
class ConfigColumnUserController extends Controller
{
    /**
     * 同步框架服务控制台的用户模型(Redis)至栏目人员配置模型(MySQL)
     *
     */
    public function actionSync()
    {
        // 查询框架服务控制台的用户模型(Redis)的租户ID(以租户 ID 索引结果集)
        $redisCmcConsoleUserIndexByGroupIdItems = RedisCmcConsoleUser::find()->indexBy('group_id')->all();
        
        /* 判断 $redisCmcConsoleUserIndexByGroupIdItems 是否为空,如果为空,则成功退出 */
        if (empty($redisCmcConsoleUserIndexByGroupIdItems)) {
            // 延缓执行 60 * 60 秒
            sleep(Yii::$app->params['configColumnUser']['isEmptyYesSleepTime']);
            
            return ExitCode::OK;
        }
        
        foreach ($redisCmcConsoleUserIndexByGroupIdItems as $redisCmcConsoleUserIndexByGroupIdItemKey => $redisCmcConsoleUserIndexByGroupIdItemValue) {

            $isLockExist = RedisCmcConsoleUser::isLockExist($redisCmcConsoleUserIndexByGroupIdItemKey);
            // 返回 true,表示锁定存在,即已经被其他客户端锁定
            if ($isLockExist === true) {
                continue;
            }
            
            // 基于租户ID查询框架服务控制台的用户模型(Redis)(以 ID 索引结果集)
            $redisCmcConsoleUserItems = RedisCmcConsoleUser::find()->where([ 'group_id' => $redisCmcConsoleUserIndexByGroupIdItemKey ])->indexBy('id')->all();

            // 基于租户ID查询栏目人员配置模型(MySQL)(以 用户ID 索引结果集)
            $configColumnUserItems = ConfigColumnUser::find()->where([ 'group_id' => $redisCmcConsoleUserIndexByGroupIdItemKey ])->isDeletedNo()->indexBy('user_id')->all();
            
            // 使用键名比较计算数组的差集,如果不为空,则删除 (软删除) 出现在 栏目人员配置模型(MySQL) 中但是未出现在 框架服务控制台的用户模型(Redis) 中的记录
            $diffItems = array_diff_key($configColumnUserItems, $redisCmcConsoleUserItems);
            if (!empty($diffItems)) {
                foreach ($diffItems as $diffItem) {
                    /* @var $diffItem /console/models/ConfigColumnUser */
                    $diffItem->softDelete();
                }
                continue;
            }
        }
        
        // 延缓执行 60 秒
        sleep(Yii::$app->params['configColumnUser']['isEmptyNoSleepTime']);
        
        return ExitCode::OK;
        
    }
}

5、命令行脚本的运行基于 Supervisor 提供支持,/etc/supervisord.d/yii-cmc-console-user-sync.ini、/etc/supervisord.d/yii-config-column-user-sync.ini,如图5

命令行脚本的运行基于 Supervisor 提供支持,/etc/supervisord.d/yii-cmc-console-user-sync.ini、/etc/supervisord.d/yii-config-column-user-sync.ini

图5

[program:yii-cmc-console-user-sync]
command = php /sobey/www/pcs-api/yii cmc-console-user/sync
autorestart = true
startsecs = 0
stopwaitsecs = 10
stderr_logfile = /data/logs/yii-cmc-console-user-sync-stderr.log
stdout_logfile = /data/logs/yii-cmc-console-user-sync-stdout.log
[program:yii-config-column-user-sync]
command = php /sobey/www/pcs-api/yii config-column-user/sync
autorestart = true
startsecs = 0
stopwaitsecs = 10
stderr_logfile = /data/logs/yii-config-column-user-sync-stderr.log
stdout_logfile = /data/logs/yii-config-column-user-sync-stdout.log

6、在 8 个租户下批量导入用户数据,让每个租户下的导入的用户数量为 999 个,总计 8000 左右,如图6

租户名称:广东省、广州市、深圳市、南山区、罗湖区、蛇口街道、招商街道、DEFAULT租户

在 8 个租户下批量导入用户数据,让每个租户下的导入的用户数量为 999 个,总计 8000 左右

图6

7、在本地环境的 Redis 中,用户数量为 8223,在开发环境的 Redis 中,对接的同一个框架,但用户数量一直为 3000 左右,始终未达到 8223,如图7、图8

在本地环境的 Redis 中,用户数量为 8223

图7

在开发环境的 Redis 中,对接的同一个框架,但用户数量一直为 3000 左右,始终未达到 8223

图8

8、最终发现问题所在,原因在于有 2 个容器在同时运行,每个容器中皆在运行命令行:cmc-console-user/sync,数据相互冲突覆盖,停止掉另一个容器中的命令行脚本(计划后续将命令行单独部署至一个容器中,隔离开,以为后续集群部署做准备),如图9

最终发现问题所在,原因在于有 2 个容器在同时运行,每个容器中皆在运行命令行:cmc-console-user/sync,数据相互冲突覆盖,停止掉另一个容器中的命令行脚本(计划后续将命令行单独部署至一个容器中,隔离开,以为后续集群部署做准备)

图9

[root@79af01f496bb pcs-api]# supervisorctl status
cronolog                         RUNNING   pid 426, uptime 2:08:17
nginx                            RUNNING   pid 422, uptime 2:08:17
php-fpm                          RUNNING   pid 424, uptime 2:08:17
yii-cmc-console-user-sync        RUNNING   pid 20685, uptime 0:04:42
yii-config-column-user-sync      RUNNING   pid 23504, uptime 0:00:45
[root@79af01f496bb pcs-api]# supervisorctl stop yii-cmc-console-user-sync
yii-cmc-console-user-sync: stopped
[root@79af01f496bb pcs-api]# supervisorctl status
cronolog                         RUNNING   pid 426, uptime 2:08:41
nginx                            RUNNING   pid 422, uptime 2:08:41
php-fpm                          RUNNING   pid 424, uptime 2:08:41
yii-cmc-console-user-sync        STOPPED   Jul 08 01:32 PM
yii-config-column-user-sync      RUN

9、在本地环境的 Redis 中,用户数量为 8223,在开发环境的 Redis 中,用户数量为 8223,已经一致,如图10

在本地环境的 Redis 中,用户数量为 8223,在开发环境的 Redis 中,用户数量为 8223,已经一致

图10

10、在 8 个租户的栏目设置中,每个租户下添加 2 个栏目,每个栏目中添加全部用户,即每个租户下添加的数据数量为 2000 左右,总计 16000 左右,如图11

在 8 个租户的栏目设置中,每个租户下添加 2 个栏目,每个栏目中添加全部用户,即每个租户下添加的数据数量为 2000 左右,总计 16000 左右

图11

11、在 第 6 步骤执行完毕后,Docker 容器的 CPU:0.05%,内存:382MB,内存增加了 80MB 左右,每 1000 条数据的添加,内存会增加 10 MB 左右。在 第 10 步骤执行完毕后,Docker 容器的 CPU:9.9%,内存:541MB,内存增加了 160MB 左右,每 1000 条数据的添加,内存会增加 20 MB 左右。因此,优化的重点在于第 2 个命令行脚本。如图12、图13

在 第 6 步骤执行完毕后,Docker 容器的 CPU:0.05%,内存:382MB,内存增加了 80MB 左右,每 1000 条数据的添加,内存会增加 10 MB 左右。

图12

在 第 10 步骤执行完毕后,Docker 容器的 CPU:9.9%,内存:541MB,内存增加了 160MB 左右,每 1000 条数据的添加,内存会增加 20 MB 左右。因此,优化的重点在于第 2 个命令行脚本。

图13

12、编辑 /console/controllers/CmcConsoleUserController.php、/console/controllers/ConfigColumnUserController.php,查看实际使用的内存量、系统分配总的内存量

        file_put_contents('E:/wwwroot/pcs-api/console/runtime/logs/cmc-console-user-sync-memory-get-usage-' . time() . '.txt', memory_get_usage() / 1024 / 1024 . 'MB');
        file_put_contents('E:/wwwroot/pcs-api/console/runtime/logs/cmc-console-user-sync-memory-get-peak-usage-' . time() . '.txt', memory_get_peak_usage() / 1024 / 1024 . 'MB');

        return ExitCode::OK;
        file_put_contents('E:/wwwroot/pcs-api/console/runtime/logs/config-column-user-sync-memory-get-usage-' . time() . '.txt', memory_get_usage() / 1024 / 1024 . 'MB');
        file_put_contents('E:/wwwroot/pcs-api/console/runtime/logs/config-column-user-sync-memory-get-peak-usage-' . time() . '.txt', memory_get_peak_usage() / 1024 / 1024 . 'MB');

        return ExitCode::OK;

13、运行第 1 个命令行脚本,日志如下:

8.9912643432617MB
40.378349304199MB

14、运行第 2 个命令行脚本,日志如下:总结:与第 1 个命令行脚本的主要差异在于实际使用的内存量上,相差 17 MB 左右。

26.464874267578MB
40.366722106934MB

15、分析优化第 1 个命令行脚本,/console/controllers/CmcConsoleUserController.php,基于 unset() 实现,实际使用的内存量减少 2 MB 左右,系统分配总的内存量无变化

<?php
/**
 * Created by PhpStorm.
 * User: Qiang Wang
 * Date: 2019/01/09
 * Time: 13:55
 */

namespace console/controllers;

use Yii;
use console/models/redis/cmc_console/User as RedisCmcConsoleUser;
use console/services/CmcConsoleUserService;
use yii/console/Controller;
use yii/console/ExitCode;
use yii/helpers/ArrayHelper;
use yii/web/HttpException;
use yii/web/ServerErrorHttpException;
use common/logics/http/im/IMIndependentMode;

/**
 * 框架服务控制台的用户
 *
 * @author Qiang Wang <shuijingwanwq@163.com>
 * @since  1.0
 */
class CmcConsoleUserController extends Controller
{
    /**
     * 同步框架服务控制台的用户模型(Http)至框架服务控制台的用户模型(Redis)
     *
     * @throws ServerErrorHttpException
     * @throws HttpException 如果登录超时
     */
    public function actionSync()
    {
        // 查询框架服务控制台的用户模型(Redis)的租户ID(以租户 ID 索引结果集)
        $redisCmcConsoleUserIndexByGroupIdItems = RedisCmcConsoleUser::find()->indexBy('group_id')->all();

        /* 判断 $redisCmcConsoleUserIndexByGroupIdItems 是否为空,如果为空,则成功退出 */
        if (empty($redisCmcConsoleUserIndexByGroupIdItems)) {
            // 延缓执行 60 * 60 秒
            sleep(Yii::$app->params['cmcConsoleUser']['isEmptyYesSleepTime']);

            return ExitCode::OK;
        }

        foreach ($redisCmcConsoleUserIndexByGroupIdItems as $redisCmcConsoleUserIndexByGroupIdItemKey => $redisCmcConsoleUserIndexByGroupIdItemValue) {

            $isLockExist = RedisCmcConsoleUser::isLockExist($redisCmcConsoleUserIndexByGroupIdItemKey);
            // 返回 true,表示锁定存在,即已经被其他客户端锁定
            if ($isLockExist === true) {
                continue;
            }

            // HTTP请求,获取租户ID下的用户列表
            $httpGetUserListData = [
                'groupId' => $redisCmcConsoleUserIndexByGroupIdItemKey,
                'loginId' => '',
                'loginTid' => '',
            ];
            $getUserListData = CmcConsoleUserService::httpGetUserList($httpGetUserListData);

            // 框架服务控制台的用户模型(Http),重建数组索引(以 ID 索引结果集)
            $httpCmcConsoleUserItems = ArrayHelper::index($getUserListData['list'], 'id');

            // 基于租户ID查询框架服务控制台的用户模型(Redis)(以 ID 索引结果集)
            $redisCmcConsoleUserItems = RedisCmcConsoleUser::find()->where(['group_id' => $redisCmcConsoleUserIndexByGroupIdItemKey])->indexBy('id')->all();

            // 使用键名比较计算数组的差集,如果不为空,则删除出现在 Redis 中但是未出现在 Http 中的记录
            $redisArrayDiffItems = array_diff_key($redisCmcConsoleUserItems, $httpCmcConsoleUserItems);
            // 销毁变量
            unset($redisCmcConsoleUserItems);
            if (!empty($redisArrayDiffItems)) {
                $redisArrayDiffIds = array_keys($redisArrayDiffItems);
                // 销毁变量
                unset($redisArrayDiffItems);
                if (RedisCmcConsoleUser::deleteAllByIds($redisCmcConsoleUserIndexByGroupIdItemKey, $redisArrayDiffIds) === false) {
                    continue;
                }
            }
            $im = new IMIndependentMode();
            // 遍历框架服务控制台的用户模型(Http),判断在 Redis 中是否存在,如果不存在,则插入,如果存在且更新时间不相等,则更新
            foreach ($httpCmcConsoleUserItems as $httpCmcConsoleUserItemValue) {
                $redisCmcConsoleUserItem = RedisCmcConsoleUser::find()->where(['id' => $httpCmcConsoleUserItemValue['id']])->one();

                $attributes = [
                    'id' => $httpCmcConsoleUserItemValue['id'],
                    'group_id' => $getUserListData['group_info']['group_id'],
                    'login_name' => $httpCmcConsoleUserItemValue['login_name'],
                    'user_token' => $httpCmcConsoleUserItemValue['user_token'],
                    'user_nick' => $httpCmcConsoleUserItemValue['user_nick'],
                    'user_pic' => $httpCmcConsoleUserItemValue['user_pic'],
                    'user_mobile' => $httpCmcConsoleUserItemValue['user_mobile'] ? $httpCmcConsoleUserItemValue['user_mobile'] : '',
                    'user_email' => $httpCmcConsoleUserItemValue['user_email'] ? $httpCmcConsoleUserItemValue['user_email'] : '',
                    'user_sex' => $httpCmcConsoleUserItemValue['user_sex'],
                    'user_type' => $httpCmcConsoleUserItemValue['user_type'],
                    'user_birthday' => $httpCmcConsoleUserItemValue['user_birthday'],
                    'user_chat_id' => $httpCmcConsoleUserItemValue['user_chat_id'] ? $httpCmcConsoleUserItemValue['user_chat_id'] : '',
                    'is_open' => $httpCmcConsoleUserItemValue['is_open'],
                    'add_time' => $httpCmcConsoleUserItemValue['add_time'],
                    'update_time' => $httpCmcConsoleUserItemValue['update_time'],
                    'im_identity' => md5($getUserListData['group_info']['group_id'] . $httpCmcConsoleUserItemValue['login_name']),
                ];

                if (!isset($redisCmcConsoleUserItem)) {

                    $redisCmcConsoleUser = new RedisCmcConsoleUser();
                    $redisCmcConsoleUser->attributes = $attributes;
                    $redisCmcConsoleUser->insert();

                } else {
                    if (isset($redisCmcConsoleUserItem) && $httpCmcConsoleUserItemValue['update_time'] != $redisCmcConsoleUserItem['update_time']) {

                        $redisCmcConsoleUserItem->attributes = $attributes;
                        $redisCmcConsoleUserItem->save();

                    }
                }
                $this->addIMAccount($im, $attributes['im_identity'], $attributes['user_nick'], $attributes['user_pic']);
            }
        }

        // 延缓执行 60 秒
        sleep(Yii::$app->params['cmcConsoleUser']['isEmptyNoSleepTime']);

        file_put_contents('E:/wwwroot/pcs-api/console/runtime/logs/cmc-console-user-sync-memory-get-usage-' . time() . '.txt', memory_get_usage() / 1024 / 1024 . 'MB');
        file_put_contents('E:/wwwroot/pcs-api/console/runtime/logs/cmc-console-user-sync-memory-get-peak-usage-' . time() . '.txt', memory_get_peak_usage() / 1024 / 1024 . 'MB');

        return ExitCode::OK;

    }

    private function addIMAccount(IMIndependentMode $im, $identifier, $nick, $faceUrl)
    {
        $im->generateUserSig();
        $ret = $im->getAccountProfile($identifier);
        if (isset($ret['ActionStatus']) && $ret['ActionStatus'] != 'OK') {
            $im->accountImport($identifier, $nick, $faceUrl);
        }
    }
}
8.9912643432617MB //优化前
6.6167755126953MB //优化后
40.378349304199MB //优化前
40.378349304199MB //优化后

16、分析优化第 2 个命令行脚本,/console/controllers/ConfigColumnUserController.php,基于 unset() 实现,实际使用的内存量减少 9 MB 左右,系统分配总的内存量无变化

<?php
/**
 * Created by PhpStorm.
 * User: Qiang Wang
 * Date: 2019/03/01
 * Time: 13:17
 */

namespace console/controllers;

use Yii;
use console/models/redis/cmc_console/User as RedisCmcConsoleUser;
use console/models/ConfigColumnUser;
use yii/console/Controller;
use yii/console/ExitCode;

/**
 * 栏目人员配置
 *
 * @author Qiang Wang <shuijingwanwq@163.com>
 * @since  1.0
 */
class ConfigColumnUserController extends Controller
{
    /**
     * 同步框架服务控制台的用户模型(Redis)至栏目人员配置模型(MySQL)
     *
     */
    public function actionSync()
    {
        // 查询框架服务控制台的用户模型(Redis)的租户ID(以租户 ID 索引结果集)
        $redisCmcConsoleUserIndexByGroupIdItems = RedisCmcConsoleUser::find()->indexBy('group_id')->all();

        /* 判断 $redisCmcConsoleUserIndexByGroupIdItems 是否为空,如果为空,则成功退出 */
        if (empty($redisCmcConsoleUserIndexByGroupIdItems)) {
            // 延缓执行 60 * 60 秒
            sleep(Yii::$app->params['configColumnUser']['isEmptyYesSleepTime']);

            return ExitCode::OK;
        }

        foreach ($redisCmcConsoleUserIndexByGroupIdItems as $redisCmcConsoleUserIndexByGroupIdItemKey => $redisCmcConsoleUserIndexByGroupIdItemValue) {

            $isLockExist = RedisCmcConsoleUser::isLockExist($redisCmcConsoleUserIndexByGroupIdItemKey);
            // 返回 true,表示锁定存在,即已经被其他客户端锁定
            if ($isLockExist === true) {
                continue;
            }

            // 基于租户ID查询框架服务控制台的用户模型(Redis)(以 ID 索引结果集)
            $redisCmcConsoleUserItems = RedisCmcConsoleUser::find()->where(['group_id' => $redisCmcConsoleUserIndexByGroupIdItemKey])->indexBy('id')->all();

            // 基于租户ID查询栏目人员配置模型(MySQL)(以 用户ID 索引结果集)
            $configColumnUserItems = ConfigColumnUser::find()->where(['group_id' => $redisCmcConsoleUserIndexByGroupIdItemKey])->isDeletedNo()->indexBy('user_id')->all();

            // 使用键名比较计算数组的差集,如果不为空,则删除 (软删除) 出现在 栏目人员配置模型(MySQL) 中但是未出现在 框架服务控制台的用户模型(Redis) 中的记录
            $diffItems = array_diff_key($configColumnUserItems, $redisCmcConsoleUserItems);
            // 销毁变量
            unset($redisCmcConsoleUserItems, $configColumnUserItems);
            if (!empty($diffItems)) {
                foreach ($diffItems as $diffItem) {
                    /* @var $diffItem ConfigColumnUser */
                    $diffItem->softDelete();
                }
                continue;
            }
        }

        // 延缓执行 60 秒
        sleep(Yii::$app->params['configColumnUser']['isEmptyNoSleepTime']);

        file_put_contents('E:/wwwroot/pcs-api/console/runtime/logs/config-column-user-sync-memory-get-usage-' . time() . '.txt', memory_get_usage() / 1024 / 1024 . 'MB');
        file_put_contents('E:/wwwroot/pcs-api/console/runtime/logs/config-column-user-sync-memory-get-peak-usage-' . time() . '.txt', memory_get_peak_usage() / 1024 / 1024 . 'MB');

        return ExitCode::OK;

    }
}
26.464874267578MB //优化前
17.696464538574MB //优化后
40.366722106934MB //优化前
40.366722106934MB //优化后

17、升级至开发环境,Docker 容器的 CPU:5.4%,内存:371MB,内存减少了 (541MB – 371MB) = 170MB 左右,证明基于 unset() 实现的方案是可行的。如图14

升级至开发环境,Docker 容器的 CPU:5.4%,内存:371MB,内存减少了 (541MB - 371MB) = 170MB 左右,证明基于 unset() 实现的方案是可行的。

图14

18、分析优化第 1 个命令行脚本,/console/controllers/CmcConsoleUserController.php,以减少执行时间为目标,从 30 分钟左右减少至 1 分钟左右。删除掉:addIMAccount() 相关的实现,其在遍历过程中,执行 HTTP 请求,消耗大量的时间。实际使用的内存量减少 0.09 MB 左右,系统分配总的内存量减少 0.002 MB 左右

<?php
/**
 * Created by PhpStorm.
 * User: Qiang Wang
 * Date: 2019/01/09
 * Time: 13:55
 */

namespace console/controllers;

use Yii;
use console/models/redis/cmc_console/User as RedisCmcConsoleUser;
use console/services/CmcConsoleUserService;
use yii/console/Controller;
use yii/console/ExitCode;
use yii/helpers/ArrayHelper;
use yii/web/HttpException;
use yii/web/ServerErrorHttpException;

/**
 * 框架服务控制台的用户
 *
 * @author Qiang Wang <shuijingwanwq@163.com>
 * @since  1.0
 */
class CmcConsoleUserController extends Controller
{
    /**
     * 同步框架服务控制台的用户模型(Http)至框架服务控制台的用户模型(Redis)
     *
     * @throws ServerErrorHttpException
     * @throws HttpException 如果登录超时
     */
    public function actionSync()
    {
        // 查询框架服务控制台的用户模型(Redis)的租户ID(以租户 ID 索引结果集)
        $redisCmcConsoleUserIndexByGroupIdItems = RedisCmcConsoleUser::find()->indexBy('group_id')->all();

        /* 判断 $redisCmcConsoleUserIndexByGroupIdItems 是否为空,如果为空,则成功退出 */
        if (empty($redisCmcConsoleUserIndexByGroupIdItems)) {
            // 延缓执行 60 * 60 秒
            sleep(Yii::$app->params['cmcConsoleUser']['isEmptyYesSleepTime']);

            return ExitCode::OK;
        }

        foreach ($redisCmcConsoleUserIndexByGroupIdItems as $redisCmcConsoleUserIndexByGroupIdItemKey => $redisCmcConsoleUserIndexByGroupIdItemValue) {

            $isLockExist = RedisCmcConsoleUser::isLockExist($redisCmcConsoleUserIndexByGroupIdItemKey);
            // 返回 true,表示锁定存在,即已经被其他客户端锁定
            if ($isLockExist === true) {
                continue;
            }

            // HTTP请求,获取租户ID下的用户列表
            $httpGetUserListData = [
                'groupId' => $redisCmcConsoleUserIndexByGroupIdItemKey,
                'loginId' => '',
                'loginTid' => '',
            ];
            $getUserListData = CmcConsoleUserService::httpGetUserList($httpGetUserListData);

            // 框架服务控制台的用户模型(Http),重建数组索引(以 ID 索引结果集)
            $httpCmcConsoleUserItems = ArrayHelper::index($getUserListData['list'], 'id');

            // 基于租户ID查询框架服务控制台的用户模型(Redis)(以 ID 索引结果集)
            $redisCmcConsoleUserItems = RedisCmcConsoleUser::find()->where(['group_id' => $redisCmcConsoleUserIndexByGroupIdItemKey])->indexBy('id')->all();

            // 使用键名比较计算数组的差集,如果不为空,则删除出现在 Redis 中但是未出现在 Http 中的记录
            $redisArrayDiffItems = array_diff_key($redisCmcConsoleUserItems, $httpCmcConsoleUserItems);
            // 销毁变量
            unset($redisCmcConsoleUserItems);
            if (!empty($redisArrayDiffItems)) {
                $redisArrayDiffIds = array_keys($redisArrayDiffItems);
                // 销毁变量
                unset($redisArrayDiffItems);
                if (RedisCmcConsoleUser::deleteAllByIds($redisCmcConsoleUserIndexByGroupIdItemKey, $redisArrayDiffIds) === false) {
                    continue;
                }
            }

            // 遍历框架服务控制台的用户模型(Http),判断在 Redis 中是否存在,如果不存在,则插入,如果存在且更新时间不相等,则更新
            foreach ($httpCmcConsoleUserItems as $httpCmcConsoleUserItemValue) {
                $redisCmcConsoleUserItem = RedisCmcConsoleUser::find()->where(['id' => $httpCmcConsoleUserItemValue['id']])->one();

                $attributes = [
                    'id' => $httpCmcConsoleUserItemValue['id'],
                    'group_id' => $getUserListData['group_info']['group_id'],
                    'login_name' => $httpCmcConsoleUserItemValue['login_name'],
                    'user_token' => $httpCmcConsoleUserItemValue['user_token'],
                    'user_nick' => $httpCmcConsoleUserItemValue['user_nick'],
                    'user_pic' => $httpCmcConsoleUserItemValue['user_pic'],
                    'user_mobile' => $httpCmcConsoleUserItemValue['user_mobile'] ? $httpCmcConsoleUserItemValue['user_mobile'] : '',
                    'user_email' => $httpCmcConsoleUserItemValue['user_email'] ? $httpCmcConsoleUserItemValue['user_email'] : '',
                    'user_sex' => $httpCmcConsoleUserItemValue['user_sex'],
                    'user_type' => $httpCmcConsoleUserItemValue['user_type'],
                    'user_birthday' => $httpCmcConsoleUserItemValue['user_birthday'],
                    'user_chat_id' => $httpCmcConsoleUserItemValue['user_chat_id'] ? $httpCmcConsoleUserItemValue['user_chat_id'] : '',
                    'is_open' => $httpCmcConsoleUserItemValue['is_open'],
                    'add_time' => $httpCmcConsoleUserItemValue['add_time'],
                    'update_time' => $httpCmcConsoleUserItemValue['update_time'],
                    'im_identity' => md5($getUserListData['group_info']['group_id'] . $httpCmcConsoleUserItemValue['login_name']),
                ];

                if (!isset($redisCmcConsoleUserItem)) {

                    $redisCmcConsoleUser = new RedisCmcConsoleUser();
                    $redisCmcConsoleUser->attributes = $attributes;
                    $redisCmcConsoleUser->insert();

                } else {
                    if (isset($redisCmcConsoleUserItem) && $httpCmcConsoleUserItemValue['update_time'] != $redisCmcConsoleUserItem['update_time']) {

                        $redisCmcConsoleUserItem->attributes = $attributes;
                        $redisCmcConsoleUserItem->save();

                    }
                }

            }
        }

        // 延缓执行 60 秒
        sleep(Yii::$app->params['cmcConsoleUser']['isEmptyNoSleepTime']);

        file_put_contents('E:/wwwroot/pcs-api/console/runtime/logs/cmc-console-user-sync-memory-get-usage-' . time() . '.txt', memory_get_usage() / 1024 / 1024 . 'MB');
        file_put_contents('E:/wwwroot/pcs-api/console/runtime/logs/cmc-console-user-sync-memory-get-peak-usage-' . time() . '.txt', memory_get_peak_usage() / 1024 / 1024 . 'MB');

        return ExitCode::OK;

    }
}
6.6167755126953MB //优化前
6.5267791748047MB //优化后
40.378349304199MB //优化前
40.37670135498MB //优化后

19、升级至开发环境,Docker 容器的 CPU:5.4%,内存:280MB,内存减少了 (371MB – 371MB) = 0MB 左右,证明减少命令行的执行时间,对于内存的使用优化无意义。虽然在本地开发环境中,实际使用的内存量减少 0.09 MB 左右,系统分配总的内存量减少 0.002 MB 左右,减少的内存量很小。如图15

升级至开发环境,Docker 容器的 CPU:5.4%,内存:280MB,内存减少了 (371MB - 371MB) = 0MB 左右,证明减少命令行的执行时间,对于内存的使用优化无意义。虽然在本地开发环境中,实际使用的内存量减少 0.09 MB 左右,系统分配总的内存量减少 0.002 MB 左右,减少的内存量很小。

图15

20、分析优化第 1 个命令行脚本,/console/controllers/CmcConsoleUserController.php,以数组形式获取数据,在查询方法前调用 asArray() 方法,来获取 PHP 数组形式的结果。实际使用的内存量减少 0.09 MB 左右,系统分配总的内存量减少 8.96 MB 左右

        // 查询框架服务控制台的用户模型(Redis)的租户ID(以租户 ID 索引结果集)
        $redisCmcConsoleUserIndexByGroupIdItems = RedisCmcConsoleUser::find()->indexBy('group_id')->asArray()->all();

            // 基于租户ID查询框架服务控制台的用户模型(Redis)(以 ID 索引结果集)
            $redisCmcConsoleUserItems = RedisCmcConsoleUser::find()->where(['group_id' => $redisCmcConsoleUserIndexByGroupIdItemKey])->indexBy('id')->asArray()->all();
6.5267791748047MB //优化前
6.4357604980469MB //优化后
40.37670135498MB //优化前
31.42064666748MB //优化后

21、分析优化第 2 个命令行脚本,/console/controllers/ConfigColumnUserController.php,以数组形式获取数据,在查询方法前调用 asArray() 方法,来获取 PHP 数组形式的结果。实际使用的内存量减少 12.72 MB 左右,系统分配总的内存量减少 8.95 MB 左右

<?php
/**
 * Created by PhpStorm.
 * User: Qiang Wang
 * Date: 2019/03/01
 * Time: 13:17
 */

namespace console/controllers;

use Yii;
use console/models/redis/cmc_console/User as RedisCmcConsoleUser;
use console/models/ConfigColumnUser;
use yii/console/Controller;
use yii/console/ExitCode;

/**
 * 栏目人员配置
 *
 * @author Qiang Wang <shuijingwanwq@163.com>
 * @since  1.0
 */
class ConfigColumnUserController extends Controller
{
    /**
     * 同步框架服务控制台的用户模型(Redis)至栏目人员配置模型(MySQL)
     *
     */
    public function actionSync()
    {
        // 查询框架服务控制台的用户模型(Redis)的租户ID(以租户 ID 索引结果集)
        $redisCmcConsoleUserIndexByGroupIdItems = RedisCmcConsoleUser::find()->indexBy('group_id')->asArray()->all();

        /* 判断 $redisCmcConsoleUserIndexByGroupIdItems 是否为空,如果为空,则成功退出 */
        if (empty($redisCmcConsoleUserIndexByGroupIdItems)) {
            // 延缓执行 60 * 60 秒
            sleep(Yii::$app->params['configColumnUser']['isEmptyYesSleepTime']);

            return ExitCode::OK;
        }

        foreach ($redisCmcConsoleUserIndexByGroupIdItems as $redisCmcConsoleUserIndexByGroupIdItemKey => $redisCmcConsoleUserIndexByGroupIdItemValue) {

            $isLockExist = RedisCmcConsoleUser::isLockExist($redisCmcConsoleUserIndexByGroupIdItemKey);
            // 返回 true,表示锁定存在,即已经被其他客户端锁定
            if ($isLockExist === true) {
                continue;
            }

            // 基于租户ID查询框架服务控制台的用户模型(Redis)(以 ID 索引结果集)
            $redisCmcConsoleUserItems = RedisCmcConsoleUser::find()->where(['group_id' => $redisCmcConsoleUserIndexByGroupIdItemKey])->indexBy('id')->asArray()->all();

            // 基于租户ID查询栏目人员配置模型(MySQL)(以 用户ID 索引结果集)
            $configColumnUserItems = ConfigColumnUser::find()->where(['group_id' => $redisCmcConsoleUserIndexByGroupIdItemKey])->isDeletedNo()->indexBy('user_id')->asArray()->all();

            // 使用键名比较计算数组的差集,如果不为空,则删除 (软删除) 出现在 栏目人员配置模型(MySQL) 中但是未出现在 框架服务控制台的用户模型(Redis) 中的记录
            $diffItems = array_diff_key($configColumnUserItems, $redisCmcConsoleUserItems);

            // 销毁变量
            unset($redisCmcConsoleUserItems, $configColumnUserItems);
            if (!empty($diffItems)) {
                // 基于租户ID、用户ID查询栏目人员配置模型(MySQL)(待删除)
                $toBeDeletedConfigColumnUserItems = ConfigColumnUser::find()->where([
                    'and',
                    ['group_id' => $redisCmcConsoleUserIndexByGroupIdItemKey],
                    ['in', 'user_id', $diffItems],
                ])->isDeletedNo()->all();
                foreach ($toBeDeletedConfigColumnUserItems as $toBeDeletedConfigColumnUserItem) {
                    /* @var $toBeDeletedConfigColumnUserItem ConfigColumnUser */
                    $toBeDeletedConfigColumnUserItem->softDelete();
                }
                continue;
            }
        }

        // 延缓执行 60 秒
        sleep(Yii::$app->params['configColumnUser']['isEmptyNoSleepTime']);

        // file_put_contents('E:/wwwroot/pcs-api/console/runtime/logs/config-column-user-sync-memory-get-usage-' . time() . '.txt', memory_get_usage() / 1024 / 1024 . 'MB');
        // file_put_contents('E:/wwwroot/pcs-api/console/runtime/logs/config-column-user-sync-memory-get-peak-usage-' . time() . '.txt', memory_get_peak_usage() / 1024 / 1024 . 'MB');

        return ExitCode::OK;

    }
}
17.696464538574MB //优化前
4.9719772338867MB //优化后
40.366722106934MB //优化前
31.416854858398MB //优化后

22、分析优化第 1 个命令行脚本,/console/controllers/CmcConsoleUserController.php,变量 $attributes 在每一次遍历中皆有定义,实际上在大部分遍历中皆未使用,因此,仅在有使用的情况下才定义。实际使用的内存量减少 0.0007 MB 左右,系统分配总的内存量无变化

            // 遍历框架服务控制台的用户模型(Http),判断在 Redis 中是否存在,如果不存在,则插入,如果存在且更新时间不相等,则更新
            foreach ($httpCmcConsoleUserItems as $httpCmcConsoleUserItemValue) {
                $redisCmcConsoleUserItem = RedisCmcConsoleUser::find()->where(['id' => $httpCmcConsoleUserItemValue['id']])->one();

                if (!isset($redisCmcConsoleUserItem)) {

                    $redisCmcConsoleUser = new RedisCmcConsoleUser();
                    $attributes = $this->getAttributes($getUserListData['group_info']['group_id'], $httpCmcConsoleUserItemValue);
                    $redisCmcConsoleUser->attributes = $attributes;
                    $redisCmcConsoleUser->insert();

                } else {
                    if ($httpCmcConsoleUserItemValue['update_time'] != $redisCmcConsoleUserItem['update_time']) {

                        $attributes = $this->getAttributes($getUserListData['group_info']['group_id'], $httpCmcConsoleUserItemValue);
                        $redisCmcConsoleUserItem->attributes = $attributes;
                        $redisCmcConsoleUserItem->save();

                    }
                }

            }

    /**
     * 获取属性列表
     * @param string $groupId 租户ID
     * @param array $httpCmcConsoleUser 框架服务控制台的用户模型(Http)
     *
     * @return array
     */
    private function getAttributes($groupId, $httpCmcConsoleUser) {
        return [
            'id' => $httpCmcConsoleUser['id'],
            'group_id' => $groupId,
            'login_name' => $httpCmcConsoleUser['login_name'],
            'user_token' => $httpCmcConsoleUser['user_token'],
            'user_nick' => $httpCmcConsoleUser['user_nick'],
            'user_pic' => $httpCmcConsoleUser['user_pic'],
            'user_mobile' => $httpCmcConsoleUser['user_mobile'] ? $httpCmcConsoleUser['user_mobile'] : '',
            'user_email' => $httpCmcConsoleUser['user_email'] ? $httpCmcConsoleUser['user_email'] : '',
            'user_sex' => $httpCmcConsoleUser['user_sex'],
            'user_type' => $httpCmcConsoleUser['user_type'],
            'user_birthday' => $httpCmcConsoleUser['user_birthday'],
            'user_chat_id' => $httpCmcConsoleUser['user_chat_id'] ? $httpCmcConsoleUser['user_chat_id'] : '',
            'is_open' => $httpCmcConsoleUser['is_open'],
            'add_time' => $httpCmcConsoleUser['add_time'],
            'update_time' => $httpCmcConsoleUser['update_time'],
            'im_identity' => md5($groupId . $httpCmcConsoleUser['login_name']),
        ];
    }
6.4357604980469MB //优化前
6.4350357055664MB //优化后
31.42064666748MB //优化前
31.42064666748MB //优化后

23、升级至开发环境,Docker 容器的 CPU:0.04%,内存:283MB,内存减少了 (371MB – 283MB) = 90MB 左右,证明以数组形式获取数据,在查询方法前调用 asArray() 方法,来获取 PHP 数组形式的结果。优化的方案是可行的。如图16

升级至开发环境,Docker 容器的 CPU:0.04%,内存:283MB,内存减少了 (371MB - 283MB) = 90MB 左右,证明以数组形式获取数据,在查询方法前调用 asArray() 方法,来获取 PHP 数组形式的结果。优化的方案是可行的。

图16

24、分析优化第 1 个命令行脚本,/console/controllers/CmcConsoleUserController.php,基于 unset() 实现,实际使用的内存量减少 0.067 MB 左右,系统分配总的内存量无变化

            // 框架服务控制台的用户模型(Http),重建数组索引(以 ID 索引结果集)
            $httpCmcConsoleUserItems = ArrayHelper::index($getUserListData['list'], 'id');

            // 销毁变量
            unset($getUserListData['list']);
6.4350357055664MB //优化前
6.3685455322266MB //优化后
31.42064666748MB //优化前
31.42064666748MB //优化后

25、分析优化第 1 个命令行脚本,/console/controllers/CmcConsoleUserController.php,得出结论,内存占用过大的根本原因在于 RedisCmcConsoleUser::find()。其在 8000 左右的 Redis AR 模型记录中查找出 8 条记录。

        // 查询框架服务控制台的用户模型(Redis)的租户ID(以租户 ID 索引结果集)
        $redisCmcConsoleUserIndexByGroupIdItems = RedisCmcConsoleUser::find()->indexBy('group_id')->asArray()->all();

        file_put_contents('E:/wwwroot/pcs-api/console/runtime/logs/cmc-console-user-sync-memory-get-usage-' . time() . '.txt', memory_get_usage() / 1024 / 1024 . 'MB');
        file_put_contents('E:/wwwroot/pcs-api/console/runtime/logs/cmc-console-user-sync-memory-get-peak-usage-' . time() . '.txt', memory_get_peak_usage() / 1024 / 1024 . 'MB');
3.6807098388672MB //RedisCmcConsoleUser::find() 执行后
6.3685455322266MB //命令行执行后
31.42064666748MB //RedisCmcConsoleUser::find() 执行后
31.42064666748MB //命令行执行后

26、分析优化第 1 个命令行脚本,/console/controllers/CmcConsoleUserController.php,删除 RedisCmcConsoleUser::find(),以另外一种形式来获取租户ID列表。

        // HTTP 请求,获取开通有效服务的租户ID列表
        $cmcApiGroupIds = CmcApiGroupService::httpGetGroupIds();

        file_put_contents('E:/wwwroot/pcs-api/console/runtime/logs/cmc-console-user-sync-memory-get-usage-' . time() . '.txt', memory_get_usage() / 1024 / 1024 . 'MB');
        file_put_contents('E:/wwwroot/pcs-api/console/runtime/logs/cmc-console-user-sync-memory-get-peak-usage-' . time() . '.txt', memory_get_peak_usage() / 1024 / 1024 . 'MB');

        exit;
3.6807098388672MB //RedisCmcConsoleUser::find() 执行后
3.1283721923828MB //CmcApiGroupService::httpGetGroupIds() 执行后
31.42064666748MB //RedisCmcConsoleUser::find() 执行后
3.1799774169922MB //CmcApiGroupService::httpGetGroupIds() 执行后

27、分析优化第 1 个命令行脚本,/console/controllers/CmcConsoleUserController.php,删除 RedisCmcConsoleUser::find(),以另外一种形式来获取租户ID列表,实际使用的内存量减少 1.72 MB 左右,系统分配总的内存量减少 21.17 MB 左右

    /**
     * 同步框架服务控制台的用户模型(Http)至框架服务控制台的用户模型(Redis)
     *
     * @throws ServerErrorHttpException
     * @throws HttpException 如果登录超时
     */
    public function actionSync()
    {
        // HTTP 请求,获取开通有效服务的租户ID列表
        $cmcApiGroupIds = CmcApiGroupService::httpGetGroupIds();

        /* 判断 $cmcApiGroupIds 是否为空,如果为空,则成功退出 */
        if (empty($cmcApiGroupIds)) {
            // 延缓执行 60 * 60 秒
            sleep(Yii::$app->params['cmcConsoleUser']['isEmptyYesSleepTime']);

            return ExitCode::OK;
        }

        foreach ($cmcApiGroupIds as $cmcApiGroupId) {

            $isLockExist = RedisCmcConsoleUser::isLockExist($cmcApiGroupId);
            // 返回 true,表示锁定存在,即已经被其他客户端锁定
            if ($isLockExist === true) {
                continue;
            }

            // HTTP请求,获取租户ID下的用户列表
            $httpGetUserListData = [
                'groupId' => $cmcApiGroupId,
                'loginId' => '',
                'loginTid' => '',
            ];
            $getUserListData = CmcConsoleUserService::httpGetUserList($httpGetUserListData);

            // 框架服务控制台的用户模型(Http),重建数组索引(以 ID 索引结果集)
            $httpCmcConsoleUserItems = ArrayHelper::index($getUserListData['list'], 'id');

            // 销毁变量
            unset($getUserListData['list']);

            // 基于租户ID查询框架服务控制台的用户模型(Redis)(以 ID 索引结果集)
            $redisCmcConsoleUserItems = RedisCmcConsoleUser::find()->where(['group_id' => $cmcApiGroupId])->indexBy('id')->asArray()->all();

            // 使用键名比较计算数组的差集,如果不为空,则删除出现在 Redis 中但是未出现在 Http 中的记录
            $redisArrayDiffItems = array_diff_key($redisCmcConsoleUserItems, $httpCmcConsoleUserItems);
            // 销毁变量
            unset($redisCmcConsoleUserItems);
            if (!empty($redisArrayDiffItems)) {
                $redisArrayDiffIds = array_keys($redisArrayDiffItems);
                // 销毁变量
                unset($redisArrayDiffItems);
                if (RedisCmcConsoleUser::deleteAllByIds($cmcApiGroupId, $redisArrayDiffIds) === false) {
                    continue;
                }
            }

            // 遍历框架服务控制台的用户模型(Http),判断在 Redis 中是否存在,如果不存在,则插入,如果存在且更新时间不相等,则更新
            foreach ($httpCmcConsoleUserItems as $httpCmcConsoleUserItemValue) {
                $redisCmcConsoleUserItem = RedisCmcConsoleUser::find()->where(['id' => $httpCmcConsoleUserItemValue['id']])->one();

                if (!isset($redisCmcConsoleUserItem)) {

                    $redisCmcConsoleUser = new RedisCmcConsoleUser();
                    $attributes = $this->getAttributes($getUserListData['group_info']['group_id'], $httpCmcConsoleUserItemValue);
                    $redisCmcConsoleUser->attributes = $attributes;
                    $redisCmcConsoleUser->insert();

                } else {
                    if ($httpCmcConsoleUserItemValue['update_time'] != $redisCmcConsoleUserItem['update_time']) {

                        $attributes = $this->getAttributes($getUserListData['group_info']['group_id'], $httpCmcConsoleUserItemValue);
                        $redisCmcConsoleUserItem->attributes = $attributes;
                        $redisCmcConsoleUserItem->save();

                    }
                }

            }
        }

        // 延缓执行 60 秒
        sleep(Yii::$app->params['cmcConsoleUser']['isEmptyNoSleepTime']);

        file_put_contents('E:/wwwroot/pcs-api/console/runtime/logs/cmc-console-user-sync-memory-get-usage-' . time() . '.txt', memory_get_usage() / 1024 / 1024 . 'MB');
        file_put_contents('E:/wwwroot/pcs-api/console/runtime/logs/cmc-console-user-sync-memory-get-peak-usage-' . time() . '.txt', memory_get_peak_usage() / 1024 / 1024 . 'MB');

        return ExitCode::OK;

    }
6.3685455322266MB //优化前
4.6499099731445MB //优化后
31.42064666748MB //优化前
10.250221252441MB //优化后

28、分析优化第 2 个命令行脚本,/console/controllers/ConfigColumnUserController.php,删除 RedisCmcConsoleUser::find(),以另外一种形式来获取租户ID列表,实际使用的内存量增加 0.37 MB 左右,系统分配总的内存量减少 19.39 MB 左右

    /**
     * 同步框架服务控制台的用户模型(Redis)至栏目人员配置模型(MySQL)
     *
     */
    public function actionSync()
    {
        // HTTP 请求,获取开通有效服务的租户ID列表
        $cmcApiGroupIds = CmcApiGroupService::httpGetGroupIds();

        /* 判断 $cmcApiGroupIds 是否为空,如果为空,则成功退出 */
        if (empty($cmcApiGroupIds)) {
            // 延缓执行 60 * 60 秒
            sleep(Yii::$app->params['configColumnUser']['isEmptyYesSleepTime']);

            return ExitCode::OK;
        }

        foreach ($cmcApiGroupIds as $cmcApiGroupId) {

            $isLockExist = RedisCmcConsoleUser::isLockExist($cmcApiGroupId);
            // 返回 true,表示锁定存在,即已经被其他客户端锁定
            if ($isLockExist === true) {
                continue;
            }

            // 基于租户ID查询框架服务控制台的用户模型(Redis)(以 ID 索引结果集)
            $redisCmcConsoleUserItems = RedisCmcConsoleUser::find()->where(['group_id' => $cmcApiGroupId])->indexBy('id')->asArray()->all();

            // 基于租户ID查询栏目人员配置模型(MySQL)(以 用户ID 索引结果集)
            $configColumnUserItems = ConfigColumnUser::find()->where(['group_id' => $cmcApiGroupId])->isDeletedNo()->indexBy('user_id')->asArray()->all();

            // 使用键名比较计算数组的差集,如果不为空,则删除 (软删除) 出现在 栏目人员配置模型(MySQL) 中但是未出现在 框架服务控制台的用户模型(Redis) 中的记录
            $diffItems = array_diff_key($configColumnUserItems, $redisCmcConsoleUserItems);

            // 销毁变量
            unset($redisCmcConsoleUserItems, $configColumnUserItems);
            if (!empty($diffItems)) {
                // 基于租户ID、用户ID查询栏目人员配置模型(MySQL)(待删除)
                $toBeDeletedConfigColumnUserItems = ConfigColumnUser::find()->where([
                    'and',
                    ['group_id' => $cmcApiGroupId],
                    ['in', 'user_id', $diffItems],
                ])->isDeletedNo()->all();
                foreach ($toBeDeletedConfigColumnUserItems as $toBeDeletedConfigColumnUserItem) {
                    /* @var $toBeDeletedConfigColumnUserItem ConfigColumnUser */
                    $toBeDeletedConfigColumnUserItem->softDelete();
                }
                continue;
            }
        }

        // 延缓执行 60 秒
        sleep(Yii::$app->params['configColumnUser']['isEmptyNoSleepTime']);

        file_put_contents('E:/wwwroot/pcs-api/console/runtime/logs/config-column-user-sync-memory-get-usage-' . time() . '.txt', memory_get_usage() / 1024 / 1024 . 'MB');
        file_put_contents('E:/wwwroot/pcs-api/console/runtime/logs/config-column-user-sync-memory-get-peak-usage-' . time() . '.txt', memory_get_peak_usage() / 1024 / 1024 . 'MB');

        return ExitCode::OK;

    }
4.9719772338867MB //优化前
5.3451461791992MB //优化后
31.416854858398MB //优化前
12.024124145508MB //优化后

29、升级至开发环境,Docker 容器的 CPU:92%,内存:435MB,CPU 增加了 (92% – 0.04%) = 91.96% 左右,内存增加了 (435MB – 283MB) = 152MB 左右。如图17、图18

升级至开发环境,Docker 容器的 CPU:92%,内存:435MB,CPU 增加了 (92% - 0.04%) = 91.96% 左右

图17

 

升级至开发环境,Docker 容器的 CPU:92%,内存:435MB,内存增加了 (435MB - 283MB) = 152MB 左右

图18

30、分析具体原因,基于 Yii 2 的 HTTP 客户端扩展在 Linux 中必须添加:setData([]),否则响应 400,如图19、图20

响应 400

图19

 

分析具体原因,基于 Yii 2 的 HTTP 客户端扩展在 Linux 中必须添加:setData([]),否则响应 400

图20

31、解决了 响应 400 的 Bug 之后,升级至开发环境,Docker 容器的 CPU:0.04%,内存:341MB,CPU无变化,内存增加了 (341MB – 283MB) = 60MB 左右。证明实际使用的内存量增加,系统分配总的内存量减少,Docker 的内存占用是在增加的(主要受到实际使用的内存量的影响)。如图21、图22

解决了 响应 400 的 Bug 之后,升级至开发环境,Docker 容器的 CPU:0.04%,内存:341MB,CPU无变化

图21

 

解决了 响应 400 的 Bug 之后,升级至开发环境,Docker 容器的 CPU:0.04%,内存:341MB,CPU无变化,内存增加了 (341MB - 283MB) = 60MB 左右。证明实际使用的内存量增加,系统分配总的内存量减少,Docker 的内存占用是在增加的(主要受到实际使用的内存量的影响)。

图22

32、延缓执行 60 秒,决定不再使用 sleep(),因为,在 sleep() 期间,内存一直处于占用中的,实际上延长了内存占用的时间。虽然可能降低了 CPU 的占用百分比。

4.6499099731445MB //延缓执行 60 秒前
4.6499099731445MB //延缓执行 60 秒后
10.250221252441MB //延缓执行 60 秒前
10.250221252441MB //延缓执行 60 秒后

33、分析优化第 1 个命令行脚本,/console/controllers/CmcConsoleUserController.php,设置同步标识的缓存,如果不存在,则同步,将数据在缓存中保留 60 秒。如果存在,则不同步(注:在同步成功后的 60 秒内,内存占用会很小,见第 2 个优化后的内存占用情况)。

<?php
/**
 * Created by PhpStorm.
 * User: Qiang Wang
 * Date: 2019/01/09
 * Time: 13:55
 */

namespace console/controllers;

use Yii;
use console/models/redis/cmc_console/User as RedisCmcConsoleUser;
use console/services/CmcApiGroupService;
use console/services/CmcConsoleUserService;
use yii/console/Controller;
use yii/console/ExitCode;
use yii/helpers/ArrayHelper;
use yii/web/HttpException;
use yii/web/ServerErrorHttpException;

/**
 * 框架服务控制台的用户
 *
 * @author Qiang Wang <shuijingwanwq@163.com>
 * @since  1.0
 */
class CmcConsoleUserController extends Controller
{
    /**
     * 同步框架服务控制台的用户模型(Http)至框架服务控制台的用户模型(Redis)
     *
     * @throws ServerErrorHttpException
     * @throws HttpException 如果登录超时
     */
    public function actionSync()
    {
        // 设置同步标识、租户ID列表的缓存键
        $redisCache = Yii::$app->redisCache;
        $redisCacheIdentityKey = 'cmc_console_user_sync';
        $redisCacheGroupIdsKey = 'cmc_api_group_ids';

        // 从缓存中取回同步标识、租户ID列表
        $redisCacheIdentityData = $redisCache[$redisCacheIdentityKey];
        $redisCacheGroupIdsData = $redisCache[$redisCacheGroupIdsKey];

        if ($redisCacheIdentityData === false) {
            if ($redisCacheGroupIdsData === false) {
                // HTTP 请求,获取开通有效服务的租户ID列表
                $cmcApiGroupIds = CmcApiGroupService::httpGetGroupIds();
                // 将租户ID列表存放到缓存供下次使用,将数据在缓存中保留 60 * 60 秒
                $redisCache->set($redisCacheGroupIdsKey, $cmcApiGroupIds, Yii::$app->params['cmcConsoleUser']['isEmptyYesSleepTime']);
            } else {
                $cmcApiGroupIds = $redisCacheGroupIdsData;
            }

            /* 判断 $cmcApiGroupIds 是否为空,如果为空,则成功退出 */
            if (empty($cmcApiGroupIds)) {
                // 延缓执行 60 * 60 秒
                sleep(Yii::$app->params['cmcConsoleUser']['isEmptyYesSleepTime']);

                return ExitCode::OK;
            }

            foreach ($cmcApiGroupIds as $cmcApiGroupId) {

                $isLockExist = RedisCmcConsoleUser::isLockExist($cmcApiGroupId);
                // 返回 true,表示锁定存在,即已经被其他客户端锁定
                if ($isLockExist === true) {
                    continue;
                }

                // HTTP请求,获取租户ID下的用户列表
                $httpGetUserListData = [
                    'groupId' => $cmcApiGroupId,
                    'loginId' => '',
                    'loginTid' => '',
                ];
                $getUserListData = CmcConsoleUserService::httpGetUserList($httpGetUserListData);

                // 框架服务控制台的用户模型(Http),重建数组索引(以 ID 索引结果集)
                $httpCmcConsoleUserItems = ArrayHelper::index($getUserListData['list'], 'id');

                // 销毁变量
                unset($getUserListData['list']);

                // 基于租户ID查询框架服务控制台的用户模型(Redis)(以 ID 索引结果集)
                $redisCmcConsoleUserItems = RedisCmcConsoleUser::find()->where(['group_id' => $cmcApiGroupId])->indexBy('id')->asArray()->all();

                // 使用键名比较计算数组的差集,如果不为空,则删除出现在 Redis 中但是未出现在 Http 中的记录
                $redisArrayDiffItems = array_diff_key($redisCmcConsoleUserItems, $httpCmcConsoleUserItems);
                // 销毁变量
                unset($redisCmcConsoleUserItems);
                if (!empty($redisArrayDiffItems)) {
                    $redisArrayDiffIds = array_keys($redisArrayDiffItems);
                    // 销毁变量
                    unset($redisArrayDiffItems);
                    if (RedisCmcConsoleUser::deleteAllByIds($cmcApiGroupId, $redisArrayDiffIds) === false) {
                        continue;
                    }
                }

                // 遍历框架服务控制台的用户模型(Http),判断在 Redis 中是否存在,如果不存在,则插入,如果存在且更新时间不相等,则更新
                foreach ($httpCmcConsoleUserItems as $httpCmcConsoleUserItemValue) {
                    $redisCmcConsoleUserItem = RedisCmcConsoleUser::find()->where(['id' => $httpCmcConsoleUserItemValue['id']])->one();

                    if (!isset($redisCmcConsoleUserItem)) {

                        $redisCmcConsoleUser = new RedisCmcConsoleUser();
                        $attributes = $this->getAttributes($getUserListData['group_info']['group_id'],
                            $httpCmcConsoleUserItemValue);
                        $redisCmcConsoleUser->attributes = $attributes;
                        $redisCmcConsoleUser->insert();

                    } else {
                        if ($httpCmcConsoleUserItemValue['update_time'] != $redisCmcConsoleUserItem['update_time']) {

                            $attributes = $this->getAttributes($getUserListData['group_info']['group_id'],
                                $httpCmcConsoleUserItemValue);
                            $redisCmcConsoleUserItem->attributes = $attributes;
                            $redisCmcConsoleUserItem->save();

                        }
                    }

                }
            }

            // 延缓执行 60 秒
            // sleep(Yii::$app->params['cmcConsoleUser']['isEmptyNoSleepTime']);
            // 将同步标识存放到缓存供下次使用,将数据在缓存中保留 60 秒
            $redisCache->set($redisCacheIdentityKey, $redisCacheIdentityKey, Yii::$app->params['cmcConsoleUser']['isEmptyNoSleepTime']);

            // file_put_contents('E:/wwwroot/pcs-api/console/runtime/logs/cmc-console-user-sync-memory-get-usage-' . time() . '.txt', memory_get_usage() / 1024 / 1024 . 'MB');
            // file_put_contents('E:/wwwroot/pcs-api/console/runtime/logs/cmc-console-user-sync-memory-get-peak-usage-' . time() . '.txt', memory_get_peak_usage() / 1024 / 1024 . 'MB');

            return ExitCode::OK;
        } else {
            return ExitCode::OK;
        }
    }

    /**
     * 获取属性列表
     * @param string $groupId 租户ID
     * @param array $httpCmcConsoleUser 框架服务控制台的用户模型(Http)
     *
     * @return array
     */
    private function getAttributes($groupId, $httpCmcConsoleUser) {
        return [
            'id' => $httpCmcConsoleUser['id'],
            'group_id' => $groupId,
            'login_name' => $httpCmcConsoleUser['login_name'],
            'user_token' => $httpCmcConsoleUser['user_token'],
            'user_nick' => $httpCmcConsoleUser['user_nick'],
            'user_pic' => $httpCmcConsoleUser['user_pic'],
            'user_mobile' => $httpCmcConsoleUser['user_mobile'] ? $httpCmcConsoleUser['user_mobile'] : '',
            'user_email' => $httpCmcConsoleUser['user_email'] ? $httpCmcConsoleUser['user_email'] : '',
            'user_sex' => $httpCmcConsoleUser['user_sex'],
            'user_type' => $httpCmcConsoleUser['user_type'],
            'user_birthday' => $httpCmcConsoleUser['user_birthday'],
            'user_chat_id' => $httpCmcConsoleUser['user_chat_id'] ? $httpCmcConsoleUser['user_chat_id'] : '',
            'is_open' => $httpCmcConsoleUser['is_open'],
            'add_time' => $httpCmcConsoleUser['add_time'],
            'update_time' => $httpCmcConsoleUser['update_time'],
        ];
    }
}
4.6499099731445MB //优化前
4.6722030639648MB //优化后(有 HTTP 请求,有同步)
2.8900680541992MB //优化后(无 HTTP 请求,无同步)
4.6064605712891MB //优化后(无 HTTP 请求,有同步)
10.250221252441MB //优化前
10.41194152832MB //优化后(有 HTTP 请求,有同步)
2.9416198730469MB //优化后(无 HTTP 请求,无同步)
10.346199035645MB //优化后(无 HTTP 请求,有同步)

34、分析优化第 2 个命令行脚本,/console/controllers/ConfigColumnUserController.php,设置同步标识的缓存,如果不存在,则同步,将数据在缓存中保留 60 秒。如果存在,则不同步(注:在同步成功后的 60 秒内,内存占用会很小,见第 2 个优化后的内存占用情况)。

<?php
/**
 * Created by PhpStorm.
 * User: Qiang Wang
 * Date: 2019/03/01
 * Time: 13:17
 */

namespace console/controllers;

use Yii;
use console/models/redis/cmc_console/User as RedisCmcConsoleUser;
use console/models/ConfigColumnUser;
use console/services/CmcApiGroupService;
use console/services/CmcConsoleUserService;
use yii/console/Controller;
use yii/console/ExitCode;
use yii/helpers/ArrayHelper;
use yii/web/HttpException;
use yii/web/ServerErrorHttpException;

/**
 * 栏目人员配置
 *
 * @author Qiang Wang <shuijingwanwq@163.com>
 * @since  1.0
 */
class ConfigColumnUserController extends Controller
{
    /**
     * 同步框架服务控制台的用户模型(Http)至栏目人员配置模型(MySQL)
     *
     * @throws ServerErrorHttpException
     * @throws HttpException 如果登录超时
     */
    public function actionSync()
    {
        // 设置同步标识、租户ID列表的缓存键
        $redisCache = Yii::$app->redisCache;
        $redisCacheIdentityKey = 'config_column_user_sync';
        $redisCacheGroupIdsKey = 'cmc_api_group_ids';

        // 从缓存中取回同步标识、租户ID列表
        $redisCacheIdentityData = $redisCache[$redisCacheIdentityKey];
        $redisCacheGroupIdsData = $redisCache[$redisCacheGroupIdsKey];

        if ($redisCacheIdentityData === false) {
            if ($redisCacheGroupIdsData === false) {
                // HTTP 请求,获取开通有效服务的租户ID列表
                // $cmcApiGroupIds = CmcApiGroupService::httpGetGroupIds();
                // 将租户ID列表存放到缓存供下次使用,将数据在缓存中保留 60 * 60 秒
                // $redisCache->set($redisCacheGroupIdsKey, $cmcApiGroupIds, Yii::$app->params['cmcConsoleUser']['isEmptyYesSleepTime']);

                return ExitCode::OK;
            } else {
                $cmcApiGroupIds = $redisCacheGroupIdsData;
            }

            /* 判断 $cmcApiGroupIds 是否为空,如果为空,则成功退出 */
            if (empty($cmcApiGroupIds)) {
                // 延缓执行 60 * 60 秒
                sleep(Yii::$app->params['configColumnUser']['isEmptyYesSleepTime']);

                return ExitCode::OK;
            }

            foreach ($cmcApiGroupIds as $cmcApiGroupId) {

                // HTTP请求,获取租户ID下的用户列表
                $httpGetUserListData = [
                    'groupId' => $cmcApiGroupId,
                    'loginId' => '',
                    'loginTid' => '',
                ];
                $getUserListData = CmcConsoleUserService::httpGetUserList($httpGetUserListData);

                // 框架服务控制台的用户模型(Http),重建数组索引(以 ID 索引结果集)
                $httpCmcConsoleUserItems = ArrayHelper::index($getUserListData['list'], 'id');

                // 销毁变量
                unset($getUserListData);

                // 基于租户ID查询栏目人员配置模型(MySQL)(以 用户ID 索引结果集)
                $configColumnUserItems = ConfigColumnUser::find()->where(['group_id' => $cmcApiGroupId])->isDeletedNo()->indexBy('user_id')->asArray()->all();

                // 使用键名比较计算数组的差集,如果不为空,则删除 (软删除) 出现在 栏目人员配置模型(MySQL) 中但是未出现在 框架服务控制台的用户模型(Http) 中的记录
                $diffItems = array_diff_key($configColumnUserItems, $httpCmcConsoleUserItems);

                // 销毁变量
                unset($httpCmcConsoleUserItems, $configColumnUserItems);
                if (!empty($diffItems)) {
                    // 基于租户ID、用户ID查询栏目人员配置模型(MySQL)(待删除)
                    $toBeDeletedConfigColumnUserItems = ConfigColumnUser::find()->where([
                        'and',
                        ['group_id' => $cmcApiGroupId],
                        ['in', 'user_id', $diffItems],
                    ])->isDeletedNo()->all();
                    foreach ($toBeDeletedConfigColumnUserItems as $toBeDeletedConfigColumnUserItem) {
                        /* @var $toBeDeletedConfigColumnUserItem ConfigColumnUser */
                        $toBeDeletedConfigColumnUserItem->softDelete();
                    }
                    continue;
                }
            }

            // 延缓执行 60 秒
            // sleep(Yii::$app->params['configColumnUser']['isEmptyNoSleepTime']);
            // 将同步标识存放到缓存供下次使用,将数据在缓存中保留 60 秒
            $redisCache->set($redisCacheIdentityKey, $redisCacheIdentityKey, Yii::$app->params['cmcConsoleUser']['isEmptyNoSleepTime']);

            // file_put_contents('E:/wwwroot/pcs-api/console/runtime/logs/config-column-user-sync-memory-get-usage-' . time() . '.txt', memory_get_usage() / 1024 / 1024 . 'MB');
            // file_put_contents('E:/wwwroot/pcs-api/console/runtime/logs/config-column-user-sync-memory-get-peak-usage-' . time() . '.txt', memory_get_peak_usage() / 1024 / 1024 . 'MB');

            return ExitCode::OK;
        } else {
            return ExitCode::OK;
        }

    }
}
5.3451461791992MB //优化前
5.5944900512695MB //优化后(有 HTTP 请求,有同步)
2.8861389160156MB //优化后(无 HTTP 请求,无同步)
5.3590927124023MB //优化后(无 HTTP 请求,有同步)
12.024124145508MB //优化前
11.753318786621MB //优化后(有 HTTP 请求,有同步)
2.9376907348633MB //优化后(无 HTTP 请求,无同步)
11.67618560791MB //优化后(无 HTTP 请求,有同步)

35、升级至开发环境,Docker 容器的 CPU:16%,内存:486MB,CPU 增加了 (16% – 0.04%) = 15.96% 左右,内存增加了 (486MB – 341MB) = 140MB 左右,证明同步成功后的 60 秒内,避免再次同步。优化的方案是不可行的(与预期不相符)。如图23、图24

升级至开发环境,Docker 容器的 CPU:16%,内存:486MB,CPU 增加了 (16% - 0.04%) = 15.96% 左右

图23

 

升级至开发环境,Docker 容器的 CPU:16%,内存:486MB,内存增加了 (486MB - 341MB) = 140MB 左右

图24

36、仔细对比与第 31 步骤上的差异,在于命令行每次执行快结束时,使用了 sleep(),决定在同步成功后的 60 秒内,使用 sleep(),在同步成功后的 60 秒内,仅会再执行一次命令行(且执行时间长度大于:60 秒),而不会执行多次。查看 supervisord 运行状态,发现命令行执行的时间很短、频率很高,尤其是第 2 个命令行。如图25

仔细对比与第 31 步骤上的差异,在于命令行每次执行快结束时,使用了 sleep(),决定在同步成功后的 60 秒内,使用 sleep(),在同步成功后的 60 秒内,仅会再执行一次命令行(且执行时间长度大于:60 秒),而不会执行多次。查看 supervisord 运行状态,发现命令行执行的时间很短、频率很高,尤其是第 2 个命令行。

图25

        if ($redisCacheIdentityData === false) {
        } else {
            // 延缓执行 60 秒
            sleep(Yii::$app->params['cmcConsoleUser']['isEmptyNoSleepTime']);
            return ExitCode::OK;
        }
        if ($redisCacheIdentityData === false) {
        } else {
            // 延缓执行 60 秒
            sleep(Yii::$app->params['configColumnUser']['isEmptyNoSleepTime']);
            return ExitCode::OK;
        }

37、升级至开发环境,Docker 容器的 CPU:0.04%,内存:283MB,CPU 无变化,内存减少了 (341MB – 283MB) = 60MB 左右。证明同步成功后的 60 秒内,避免再次同步。优化的方案是可行的。如图26、图27

升级至开发环境,Docker 容器的 CPU:0.04%,内存:283MB,CPU 无变化

图26

 

升级至开发环境,Docker 容器的 CPU:0.04%,内存:283MB,CPU 无变化,内存减少了 (341MB - 283MB) = 60MB 左右。证明同步成功后的 60 秒内,避免再次同步。优化的方案是可行的。

图27

38、决定再次降低同步的执行频率,同步成功后的 5 * 60 秒内,避免再次同步。查看 supervisord 运行状态,发现命令行执行的时间很长、频率很低。升级至开发环境,Docker 容器的监控数据无变化。证明当执行频率达到某个临界值之后,再次降低,优化的意义不大了的,顶多 CPU 的平均值更低一些(理论上)。如图28

决定再次降低同步的执行频率,同步成功后的 5 * 60 秒内,避免再次同步。查看 supervisord 运行状态,发现命令行执行的时间很长、频率很低。升级至开发环境,Docker 容器的监控数据无变化。证明当执行频率达到某个临界值之后,再次降低,优化的意义不大了的,顶多 CPU 的平均值更低一些(理论上)。

图28

39、避免在生产环境中,当租户数量过多时,一次同步所有租户下的用户,进而导致内存占用过大、运行时间过长,决定一次仅同步一个租户下的用户。

40、分析优化第 1 个命令行脚本,/console/controllers/CmcConsoleUserController.php,遍历租户ID列表时,break 结束当前 foreach 结构的执行,即命令行的每一次运行,仅同步成功一个租户下的用户列表。在 有 HTTP 请求,有同步 的情况下,实际使用的内存量增加 1.54 MB 左右,系统分配总的内存量减少 0.75 MB 左右

<?php
/**
 * Created by PhpStorm.
 * User: Qiang Wang
 * Date: 2019/01/09
 * Time: 13:55
 */

namespace console/controllers;

use Yii;
use console/models/redis/cmc_console/User as RedisCmcConsoleUser;
use console/services/CmcApiGroupService;
use console/services/CmcConsoleUserService;
use yii/base/InvalidArgumentException;
use yii/console/Controller;
use yii/console/ExitCode;
use yii/helpers/ArrayHelper;
use yii/web/HttpException;
use yii/web/ServerErrorHttpException;

/**
 * 框架服务控制台的用户
 *
 * @author Qiang Wang <shuijingwanwq@163.com>
 * @since  1.0
 */
class CmcConsoleUserController extends Controller
{
    /**
     * 同步框架服务控制台的用户模型(Http)至框架服务控制台的用户模型(Redis)
     *
     * @throws ServerErrorHttpException
     * @throws HttpException 如果登录超时
     * @throws InvalidArgumentException if the $direction or $sortFlag parameters do not have
     */
    public function actionSync()
    {
        // 设置同步标识的缓存键
        $redisCache = Yii::$app->redisCache;
        $redisCacheIdentityKey = 'cmc_console_user_sync';

        // 从缓存中取回同步标识
        $redisCacheIdentityData = $redisCache[$redisCacheIdentityKey];

        if ($redisCacheIdentityData === false) {
            // HTTP 请求,获取开通有效服务的租户ID列表
            $httpCmcApiGroupIds = CmcApiGroupService::httpGetGroupIds();

            /* 判断 $httpCmcApiGroupIds 是否为空,如果为空,则成功退出 */
            if (empty($httpCmcApiGroupIds)) {
                // 延缓执行 60 * 60 秒
                sleep(Yii::$app->params['cmcConsoleUser']['isEmptyYesSleepTime']);

                return ExitCode::OK;
            }

            // 设置租户ID列表的缓存键
            $redisCacheGroupIdsKey = 'cmc_api_group_ids';

            // 从缓存中取回租户ID列表
            $redisCacheGroupIdsData = $redisCache[$redisCacheGroupIdsKey];

            // 是否设置租户ID列表的缓存,默认:否
            $isSetRedisCacheGroupIds = false;

            if ($redisCacheGroupIdsData === false) {
                $cmcApiGroupIds = [];
                foreach ($httpCmcApiGroupIds as $httpCmcApiGroupId) {
                    $cmcApiGroupIds[] = [
                        'group_id' => $httpCmcApiGroupId,
                        'cmc_console_user_last_synced_at' => 0, //上次同步时间
                        'config_column_user_last_synced_at' => 0, //上次同步时间
                    ];
                }
                // 是否设置租户ID列表的缓存:是
                $isSetRedisCacheGroupIds = true;
            } else {
                // 获取 group_id 值列表
                $redisCacheGroupIds = ArrayHelper::getColumn($redisCacheGroupIdsData, 'group_id');
                $cmcApiGroupIds = $redisCacheGroupIdsData;
                foreach ($httpCmcApiGroupIds as $httpCmcApiGroupId) {
                    if (!in_array($httpCmcApiGroupId, $redisCacheGroupIds)) {
                        $cmcApiGroupIds[] = [
                            'group_id' => $httpCmcApiGroupId,
                            'cmc_console_user_last_synced_at' => 0, //上次同步时间
                            'config_column_user_last_synced_at' => 0, //上次同步时间
                        ];
                        // 是否设置租户ID列表的缓存:是
                        $isSetRedisCacheGroupIds = true;
                    }
                }
            }

            // 判断是否设置租户ID列表的缓存
            if ($isSetRedisCacheGroupIds) {
                // 将 $cmcApiGroupIds 存放到缓存供下次使用,将数据在缓存中永久保留
                $redisCache->set($redisCacheGroupIdsKey, $cmcApiGroupIds);
            }


            // 基于上次同步时间顺序排列,赋值给:$sortCmcApiGroupIds
            $sortCmcApiGroupIds = $cmcApiGroupIds;
            ArrayHelper::multisort($sortCmcApiGroupIds, 'cmc_console_user_last_synced_at', SORT_ASC);

            foreach ($sortCmcApiGroupIds as $sortCmcApiGroupId) {
                $isLockExist = RedisCmcConsoleUser::isLockExist($sortCmcApiGroupId['group_id']);
                // 返回 true,表示锁定存在,即已经被其他客户端锁定
                if ($isLockExist === true) {
                    continue;
                }

                // HTTP请求,获取租户ID下的用户列表
                $httpGetUserListData = [
                    'groupId' => $sortCmcApiGroupId['group_id'],
                    'loginId' => '',
                    'loginTid' => '',
                ];
                $getUserListData = CmcConsoleUserService::httpGetUserList($httpGetUserListData);

                // 框架服务控制台的用户模型(Http),重建数组索引(以 ID 索引结果集)
                $httpCmcConsoleUserItems = ArrayHelper::index($getUserListData['list'], 'id');

                // 销毁变量
                unset($getUserListData['list']);

                // 基于租户ID查询框架服务控制台的用户模型(Redis)(以 ID 索引结果集)
                $redisCmcConsoleUserItems = RedisCmcConsoleUser::find()->where(['group_id' => $sortCmcApiGroupId['group_id']])->indexBy('id')->asArray()->all();

                // 使用键名比较计算数组的差集,如果不为空,则删除出现在 Redis 中但是未出现在 Http 中的记录
                $redisArrayDiffItems = array_diff_key($redisCmcConsoleUserItems, $httpCmcConsoleUserItems);
                // 销毁变量
                unset($redisCmcConsoleUserItems);
                if (!empty($redisArrayDiffItems)) {
                    $redisArrayDiffIds = array_keys($redisArrayDiffItems);
                    // 销毁变量
                    unset($redisArrayDiffItems);
                    if (RedisCmcConsoleUser::deleteAllByIds($sortCmcApiGroupId['group_id'], $redisArrayDiffIds) === false) {
                        continue;
                    }
                }

                // 遍历框架服务控制台的用户模型(Http),判断在 Redis 中是否存在,如果不存在,则插入,如果存在且更新时间不相等,则更新
                foreach ($httpCmcConsoleUserItems as $httpCmcConsoleUserItemValue) {
                    $redisCmcConsoleUserItem = RedisCmcConsoleUser::find()->where(['id' => $httpCmcConsoleUserItemValue['id']])->one();

                    if (!isset($redisCmcConsoleUserItem)) {

                        $redisCmcConsoleUser = new RedisCmcConsoleUser();
                        $attributes = $this->getAttributes($getUserListData['group_info']['group_id'],
                            $httpCmcConsoleUserItemValue);
                        $redisCmcConsoleUser->attributes = $attributes;
                        $redisCmcConsoleUser->insert();

                    } else {
                        if ($httpCmcConsoleUserItemValue['update_time'] != $redisCmcConsoleUserItem['update_time']) {

                            $attributes = $this->getAttributes($getUserListData['group_info']['group_id'],
                                $httpCmcConsoleUserItemValue);
                            $redisCmcConsoleUserItem->attributes = $attributes;
                            $redisCmcConsoleUserItem->save();

                        }
                    }

                }

                // 从缓存中取回租户ID列表
                $cmcApiGroupIds = $redisCache[$redisCacheGroupIdsKey];
                // 设置当前租户的上次同步时间
                foreach ($cmcApiGroupIds as $cmcApiGroupIdKey => $cmcApiGroupId) {
                    if ($cmcApiGroupId['group_id'] == $sortCmcApiGroupId['group_id']) {
                        $cmcApiGroupIds[$cmcApiGroupIdKey]['cmc_console_user_last_synced_at'] = time();
                        break;
                    }
                }

                // 将 $cmcApiGroupIds 存放到缓存供下次使用,将数据在缓存中永久保留
                $redisCache->set($redisCacheGroupIdsKey, $cmcApiGroupIds);

                // break 结束当前 foreach 结构的执行,即命令行的每一次运行,仅同步成功一个租户下的用户列表
                break;
            }

            // 延缓执行 60 秒
            // sleep(Yii::$app->params['cmcConsoleUser']['isEmptyNoSleepTime']);
            // 将同步标识存放到缓存供下次使用,将数据在缓存中保留 60 秒
            $redisCache->set($redisCacheIdentityKey, $redisCacheIdentityKey, Yii::$app->params['cmcConsoleUser']['isEmptyNoSleepTime']);

            // file_put_contents('E:/wwwroot/pcs-api/console/runtime/logs/cmc-console-user-sync-memory-get-usage-' . time() . '.txt', memory_get_usage() / 1024 / 1024 . 'MB');
            // file_put_contents('E:/wwwroot/pcs-api/console/runtime/logs/cmc-console-user-sync-memory-get-peak-usage-' . time() . '.txt', memory_get_peak_usage() / 1024 / 1024 . 'MB');

            return ExitCode::OK;
        } else {
            // 延缓执行 60 秒
            sleep(Yii::$app->params['cmcConsoleUser']['isEmptyNoSleepTime']);
            return ExitCode::OK;
        }
    }

    /**
     * 获取属性列表
     * @param string $groupId 租户ID
     * @param array $httpCmcConsoleUser 框架服务控制台的用户模型(Http)
     *
     * @return array
     */
    private function getAttributes($groupId, $httpCmcConsoleUser) {
        return [
            'id' => $httpCmcConsoleUser['id'],
            'group_id' => $groupId,
            'login_name' => $httpCmcConsoleUser['login_name'],
            'user_token' => $httpCmcConsoleUser['user_token'],
            'user_nick' => $httpCmcConsoleUser['user_nick'],
            'user_pic' => $httpCmcConsoleUser['user_pic'],
            'user_mobile' => $httpCmcConsoleUser['user_mobile'] ? $httpCmcConsoleUser['user_mobile'] : '',
            'user_email' => $httpCmcConsoleUser['user_email'] ? $httpCmcConsoleUser['user_email'] : '',
            'user_sex' => $httpCmcConsoleUser['user_sex'],
            'user_type' => $httpCmcConsoleUser['user_type'],
            'user_birthday' => $httpCmcConsoleUser['user_birthday'],
            'user_chat_id' => $httpCmcConsoleUser['user_chat_id'] ? $httpCmcConsoleUser['user_chat_id'] : '',
            'is_open' => $httpCmcConsoleUser['is_open'],
            'add_time' => $httpCmcConsoleUser['add_time'],
            'update_time' => $httpCmcConsoleUser['update_time'],
        ];
    }
}
4.6722030639648MB //优化前(有 HTTP 请求,有同步)
2.8900680541992MB //优化前(无 HTTP 请求,无同步)
4.6064605712891MB //优化前(无 HTTP 请求,有同步)
6.2168731689453MB //优化后(有 HTTP 请求,有同步)
2.8925552368164MB //优化后(无 HTTP 请求,无同步)
10.41194152832MB //优化前(有 HTTP 请求,有同步)
2.9416198730469MB //优化前(无 HTTP 请求,无同步)
10.346199035645MB //优化前(无 HTTP 请求,有同步)
9.6655578613281MB //优化后(有 HTTP 请求,有同步)
2.9470977783203MB //优化后(无 HTTP 请求,无同步)

41、分析优化第 2 个命令行脚本,/console/controllers/ConfigColumnUserController.php,遍历租户ID列表时,break 结束当前 foreach 结构的执行,即命令行的每一次运行,仅同步成功一个租户下的用户列表。在 无 HTTP 请求,有同步 的情况下,实际使用的内存量减少 0.45 MB 左右,系统分配总的内存量无变化

<?php
/**
 * Created by PhpStorm.
 * User: Qiang Wang
 * Date: 2019/03/01
 * Time: 13:17
 */

namespace console/controllers;

use Yii;
use console/models/ConfigColumnUser;
use console/services/CmcConsoleUserService;
use yii/console/Controller;
use yii/console/ExitCode;
use yii/helpers/ArrayHelper;
use yii/web/HttpException;
use yii/web/ServerErrorHttpException;

/**
 * 栏目人员配置
 *
 * @author Qiang Wang <shuijingwanwq@163.com>
 * @since  1.0
 */
class ConfigColumnUserController extends Controller
{
    /**
     * 同步框架服务控制台的用户模型(Http)至栏目人员配置模型(MySQL)
     *
     * @throws ServerErrorHttpException
     * @throws HttpException 如果登录超时
     */
    public function actionSync()
    {
        // 设置同步标识的缓存键
        $redisCache = Yii::$app->redisCache;
        $redisCacheIdentityKey = 'config_column_user_sync';

        // 从缓存中取回同步标识
        $redisCacheIdentityData = $redisCache[$redisCacheIdentityKey];

        if ($redisCacheIdentityData === false) {

            // 设置租户ID列表的缓存键
            $redisCacheGroupIdsKey = 'cmc_api_group_ids';

            // 从缓存中取回租户ID列表
            $redisCacheGroupIdsData = $redisCache[$redisCacheGroupIdsKey];

            if ($redisCacheGroupIdsData === false) {
                return ExitCode::OK;
            } else {
                $cmcApiGroupIds = $redisCacheGroupIdsData;
            }

            /* 判断 $cmcApiGroupIds 是否为空,如果为空,则成功退出 */
            if (empty($cmcApiGroupIds)) {
                // 延缓执行 60 * 60 秒
                sleep(Yii::$app->params['configColumnUser']['isEmptyYesSleepTime']);

                return ExitCode::OK;
            }

            // 基于上次同步时间顺序排列,赋值给:$sortCmcApiGroupIds
            $sortCmcApiGroupIds = $cmcApiGroupIds;
            ArrayHelper::multisort($sortCmcApiGroupIds, 'config_column_user_last_synced_at', SORT_ASC);

            foreach ($sortCmcApiGroupIds as $sortCmcApiGroupId) {

                // HTTP请求,获取租户ID下的用户列表
                $httpGetUserListData = [
                    'groupId' => $sortCmcApiGroupId['group_id'],
                    'loginId' => '',
                    'loginTid' => '',
                ];
                $getUserListData = CmcConsoleUserService::httpGetUserList($httpGetUserListData);

                // 框架服务控制台的用户模型(Http),重建数组索引(以 ID 索引结果集)
                $httpCmcConsoleUserItems = ArrayHelper::index($getUserListData['list'], 'id');

                // 销毁变量
                unset($getUserListData);

                // 基于租户ID查询栏目人员配置模型(MySQL)(以 用户ID 索引结果集)
                $configColumnUserItems = ConfigColumnUser::find()->where(['group_id' => $sortCmcApiGroupId['group_id']])->isDeletedNo()->indexBy('user_id')->asArray()->all();

                // 使用键名比较计算数组的差集,如果不为空,则删除 (软删除) 出现在 栏目人员配置模型(MySQL) 中但是未出现在 框架服务控制台的用户模型(Http) 中的记录
                $diffItems = array_diff_key($configColumnUserItems, $httpCmcConsoleUserItems);

                // 销毁变量
                unset($httpCmcConsoleUserItems, $configColumnUserItems);
                if (!empty($diffItems)) {
                    // 基于租户ID、用户ID查询栏目人员配置模型(MySQL)(待删除)
                    $toBeDeletedConfigColumnUserItems = ConfigColumnUser::find()->where([
                        'and',
                        ['group_id' => $sortCmcApiGroupId['group_id']],
                        ['in', 'user_id', $diffItems],
                    ])->isDeletedNo()->all();
                    foreach ($toBeDeletedConfigColumnUserItems as $toBeDeletedConfigColumnUserItem) {
                        /* @var $toBeDeletedConfigColumnUserItem ConfigColumnUser */
                        $toBeDeletedConfigColumnUserItem->softDelete();
                    }
                }

                // 从缓存中取回租户ID列表
                $cmcApiGroupIds = $redisCache[$redisCacheGroupIdsKey];
                // 设置当前租户的上次同步时间
                foreach ($cmcApiGroupIds as $cmcApiGroupIdKey => $cmcApiGroupId) {
                    if ($cmcApiGroupId['group_id'] == $sortCmcApiGroupId['group_id']) {
                        $cmcApiGroupIds[$cmcApiGroupIdKey]['config_column_user_last_synced_at'] = time();
                        break;
                    }
                }

                // 将 $cmcApiGroupIds 存放到缓存供下次使用,将数据在缓存中永久保留
                $redisCache->set($redisCacheGroupIdsKey, $cmcApiGroupIds);

                // break 结束当前 foreach 结构的执行,即命令行的每一次运行,仅同步成功一个租户下的用户列表
                break;
            }

            // 延缓执行 60 秒
            // sleep(Yii::$app->params['configColumnUser']['isEmptyNoSleepTime']);
            // 将同步标识存放到缓存供下次使用,将数据在缓存中保留 60 秒
            $redisCache->set($redisCacheIdentityKey, $redisCacheIdentityKey, Yii::$app->params['cmcConsoleUser']['isEmptyNoSleepTime']);

            // file_put_contents('E:/wwwroot/pcs-api/console/runtime/logs/config-column-user-sync-memory-get-usage-' . time() . '.txt', memory_get_usage() / 1024 / 1024 . 'MB');
            // file_put_contents('E:/wwwroot/pcs-api/console/runtime/logs/config-column-user-sync-memory-get-peak-usage-' . time() . '.txt', memory_get_peak_usage() / 1024 / 1024 . 'MB');

            return ExitCode::OK;
        } else {
            // 延缓执行 60 秒
            sleep(Yii::$app->params['configColumnUser']['isEmptyNoSleepTime']);
            return ExitCode::OK;
        }

    }
}
5.5944900512695MB //优化前(有 HTTP 请求,有同步)
2.8861389160156MB //优化前(无 HTTP 请求,无同步)
5.3590927124023MB //优化前(无 HTTP 请求,有同步)
4.9042816162109MB //优化后(无 HTTP 请求,有同步)
2.8837127685547MB //优化后(无 HTTP 请求,无同步)
11.753318786621MB //优化前(有 HTTP 请求,有同步)
2.9376907348633MB //优化前(无 HTTP 请求,无同步)
11.67618560791MB //优化前(无 HTTP 请求,有同步)
10.693077087402MB //优化后(无 HTTP 请求,有同步)
2.9382553100586MB //优化后(无 HTTP 请求,无同步)

42、升级至开发环境,Docker 容器的 CPU:0.07%,内存:283MB,CPU 增加了 (0.07% – 0.04%) = 0.03% 左右,内存减少了 (283MB – 276MB) = 7MB 左右。证明遍历租户ID列表时,break 结束当前 foreach 结构的执行,即命令行的每一次运行,仅同步成功一个租户下的用户列表。优化的方案是不可行的,意义不大。如图29、图30

升级至开发环境,Docker 容器的 CPU:0.07%,内存:283MB,CPU 增加了 (0.07% - 0.04%) = 0.03% 左右

图29

升级至开发环境,Docker 容器的 CPU:0.07%,内存:283MB,CPU 增加了 (0.07% - 0.04%) = 0.03% 左右,内存减少了 (283MB - 276MB) = 7MB 左右。证明遍历租户ID列表时,break 结束当前 foreach 结构的执行,即命令行的每一次运行,仅同步成功一个租户下的用户列表。优化的方案是不可行的,意义不大。

图30

43、查看 MySQL 的实例监控情况,内存占用:6166MB,参考价值不大,因为是与其他产品共享同一个数据库,如图31

查看 MySQL 的实例监控情况,内存占用:6166MB,参考价值不大,因为是与其他产品共享同一个数据库

图31

44、查看 Redis 的实例监控情况,已使用容量:75MB,如图32

查看 Redis 的实例监控情况,已使用容量:75MB

图32

45、占用内存的减少实现方案,总结如下:

(1)数据量的增加,会导致内存的持续增加,后期当数据量达致一定的量级时,需要支持集群部署
(2)Docker 容器的 CPU:5.4%,内存:371MB,内存减少了 (541MB – 371MB) = 170MB 左右,证明基于 unset() 实现的方案是可行的
(3)Docker 容器的 CPU:5.4%,内存:280MB,内存减少了 (371MB – 371MB) = 0MB 左右,证明减少命令行的执行时间,对于内存的使用优化无意义。
(4)Docker 容器的 CPU:0.04%,内存:283MB,内存减少了 (371MB – 283MB) = 90MB 左右,证明变量仅在使用时才定义,以数组形式获取数据,在查询方法前调用 asArray() 方法,来获取 PHP 数组形式的结果。优化的方案是可行的。
(5)Docker 容器的 CPU:92%,内存:435MB,CPU 增加了 (92% – 0.04%) = 91.96% 左右,内存增加了 (435MB – 283MB) = 152MB 左右。HTTP 请求响应 400 时,命令行抛出异常时,CPU 与内存皆增加得很厉害
(6)解决了 响应 400 的 Bug 之后,升级至开发环境,Docker 容器的 CPU:0.04%,内存:341MB,CPU无变化,内存增加了 (341MB – 283MB) = 60MB 左右。删除 RedisCmcConsoleUser::find(),以另外一种形式(HTTP 请求)来获取租户ID列表,实际使用的内存量增加 0.37 MB 左右,系统分配总的内存量减少 19.39 MB 左右。证明实际使用的内存量增加,系统分配总的内存量减少,Docker 的内存占用是在增加的(主要受到实际使用的内存量的影响)
(7)Docker 容器的 CPU:16%,内存:486MB,CPU 增加了 (16% – 0.04%) = 15.96% 左右,内存增加了 (486MB – 341MB) = 140MB 左右,查看 supervisord 运行状态,发现命令行执行的时间很短、频率很高时,CPU 与内存皆增加得一般厉害
(8)Docker 容器的 CPU:0.04%,内存:283MB,CPU 无变化,内存减少了 (341MB – 283MB) = 60MB 左右。证明同步成功后的 60 秒内,避免再次同步。优化的方案是可行的
(9)决定再次降低同步的执行频率,同步成功后的 5 * 60 秒内,避免再次同步。当执行频率达到某个临界值之后,再次降低,优化的意义不大了的,顶多 CPU 的平均值更低一些(理论上)
(10)遍历租户ID列表时,break 结束当前 foreach 结构的执行,即命令行的每一次运行,仅同步成功一个租户下的用户列表。Docker 容器升级后,性能指标未变化。优化的意义不大了的,虽然命令行同步一次的运行时间大幅度减少(原因可能在于开发环境的租户数量不多,总的数据量不大,优化的意义未体现出来。当租户数量更多时,内存占用的优化意义才能够体现出来)

 

原创文章,作者:ItWorker,如若转载,请注明出处:https://blog.ytso.com/tech/pnotes/250469.html

(0)
上一篇 2022年4月29日 22:59
下一篇 2022年4月29日 22:59

相关推荐

发表回复

登录后才能评论