基于 Yii 2.0 实现的命令行脚本,持续运行期间,占用内存过高的分析与解决
1、当在 MySQL 与 Redis 中的数据量较小时,Docker 容器的 CPU:0.02%,内存:298MB,如图1、图2
2、查看 MySQL 的实例监控情况,内存占用:2328MB,如图3
3、查看 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
[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租户
7、在本地环境的 Redis 中,用户数量为 8223,在开发环境的 Redis 中,对接的同一个框架,但用户数量一直为 3000 左右,始终未达到 8223,如图7、图8
8、最终发现问题所在,原因在于有 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
10、在 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
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
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
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
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
30、分析具体原因,基于 Yii 2 的 HTTP 客户端扩展在 Linux 中必须添加:setData([]),否则响应 400,如图19、图20
31、解决了 响应 400 的 Bug 之后,升级至开发环境,Docker 容器的 CPU:0.04%,内存:341MB,CPU无变化,内存增加了 (341MB – 283MB) = 60MB 左右。证明实际使用的内存量增加,系统分配总的内存量减少,Docker 的内存占用是在增加的(主要受到实际使用的内存量的影响)。如图21、图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
36、仔细对比与第 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
38、决定再次降低同步的执行频率,同步成功后的 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
43、查看 MySQL 的实例监控情况,内存占用:6166MB,参考价值不大,因为是与其他产品共享同一个数据库,如图31
44、查看 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 容器升级后,性能指标未变化。优化的意义不大了的,虽然命令行同步一次的运行时间大幅度减少(原因可能在于开发环境的租户数量不多,总的数据量不大,优化的意义未体现出来。当租户数量更多时,内存占用的优化意义才能够体现出来)
近期评论