如何优化死循环程序占用资源过大的问题

问题描述

死循环或者非常大的循环程序运行一段时间后发现占用CPU和内存巨大,两个程序占用8核16G阿里ECS机器中的60%内存和CPU。

程序作用

一个程序是从阿里redis中取zset数据然后消费删除。
一个程序是订阅redis中的一个消息,也是简单的消费。

主要代码

        $redisQueue = new WechatFollowServices();
        //3600 * 26小时
        $time1 = 93600;
        //3600 * 24小时
        $time2 = 86400;
        if (config('app.push_debug') == true) {
            $time1 = 70;
            $time2 = 60;
        }

        while (true) {
            $now = time();
            $queues = $redisQueue->getQueues(0, 100, true);

            foreach ($queues as $member => $time) {

                $queue = \GuzzleHttp\json_decode($member, true);

                /**
                 * 计算时间差,>= 26小时的数据删除
                 */
                $timeDiff = $now - $time;
                if ($timeDiff >= $time1) {
                    $redisQueue->deleteQueue($member);
                }

                /**
                 * >= 24小时的推送
                 */
                if ($timeDiff >= $time2) {

                    $content = WechatPushServices::getMilPlanTemplate($queue['nickname']);
                    $res = app('GrpcServices')->customByOpenID([$queue['openId']], \GuzzleHttp\json_encode($content), 'news');
                    if ($res) {
                        $msg = '推送成功';
                    } else {
                        $msg = '推送失败';
                    }
                    info($this->description . ': ' . $queue['nickname'] . $msg);

                    //不考虑推送结果,直接删除
                    $redisQueue->deleteQueue($member);

                    unset($content, $res, $msg);
                }

                unset($member, $time, $queue, $timeDiff);
                continue;
            }

            unset($now, $queues);
            //测试模式下推送一个就退出
            if (config('app.push_debug') == true) {
                break;
            }
        }

初步分析

初步考虑是不是程序变量的问题导致占用资源特别多。所以加上了 unset()。那么又有问题了,是unset掉呢还是将变量附空值呢?

对占用资源较大的程序您有什么优化的实践和建议?

补充修改

第二版代码

根据一楼楼主的意见修改如下:

        $redisQueue = new WechatFollowServices();

        // 休眠时间
        $sleepTime = 0;
        // 延迟推送的时间长度 3600 * 24小时
        $delayTime = 86400;
        // 未处理的过期时间长度 3600 * 26小时
        $expireTime = 93600;
        if (config('app.push_debug') == true) {
            $delayTime = 60;
            $expireTime = 70;
        }

        while (true) {
            $now = time();
            $queues = $redisQueue->getQueues(0, 100, true);
            if (empty($queues)) {
                // 休眠两分钟
                $sleepTime = 120;
            }

            /**
             * @var int $time 关注时的具体时间
             */
            foreach ($queues as $member => $time) {

                /**
                 * 已关注的时间长度
                 */
                $followedTime = $now - $time;

                /**
                 * 小于 24小时
                 */
                if ($followedTime < $delayTime) {

                    $sleepTime = $delayTime - $followedTime;
                    unset($member, $time, $followedTime);
                    break;
                }

                /**
                 * >= 26小时的数据删除
                 */
                if ($followedTime >= $expireTime) {
                    $redisQueue->deleteQueue($member);
                    unset($member, $time, $followedTime);
                    continue;
                }

                /**
                 * >= 24小时且 < 26小时的推送
                 */
                $queue = \GuzzleHttp\json_decode($member, true);
                $content = WechatPushServices::getMilPlanTemplate($queue['nickname']);
                $res = app('GrpcServices')->customByOpenID([$queue['openId']], \GuzzleHttp\json_encode($content), 'news');
                if ($res) {
                    $msg = '推送成功';
                } else {
                    $msg = '推送失败';
                }
                info($this->description . ': ' . $queue['nickname'] . $msg);

                //不考虑推送结果,直接删除
                $redisQueue->deleteQueue($member);

                unset($member, $time, $followedTime, $queue, $content, $res, $msg);

                //测试模式下推送一个就退出
                if (config('app.push_debug') == true) {
                    break 2;
                }
            }

            unset($now, $queues);

            /**
             * 开始休眠
             */
            if ($sleepTime) {
                sleep($sleepTime);
            }
        }
阅读 6.1k
3 个回答

应该发现题主的问题出现在: 队列里面的记录需要延迟24小时, 在这个延迟时间内, 循环会一直重复不停的检查队列内容, 判断每一个用户的时间, 但是这里就是一直重复工作, 重复的检查同一批用户, 知道用户时间达到后才会进行发送并清除队列.

假定队列的用户延迟时间是有序的, 只要取出部分前面的用户, 判断用户的额外等待时间, 然后挂起进程等待对应的时间.

下面是改进了一下处理逻辑, 希望可以有帮助.

while (true) {
  $now = time();
  // 这个是默认最长睡眠间隔时间, 10分钟后再检查
  $minSleep = 600;
  // 假定这里的队列时间是有序的, 队列最前面的时间最早, 否则最好这里能够取出所有队列记录
  $queues = $redisQueue->getQueues(0, 100, true);

  foreach ($queues as $member => $time) {
    $timeDiff = $now - $time;
    if ($timeDiff >= $time1) {
      // 假如有达到条件的用户, 我们这次循环后不再挂起进程
      $minSleep = 0;
      // 删除队列
    }
    elseif ($timeDiff >= $time2) {
      $minSleep = 0;
      // 这里才需要用到的数据, 就这里才解析吧, 省点CPU
      $queue = \GuzzleHttp\json_decode($member, true);
      // 原来做什么工作, 就做什么工作
    }
    else {
      // 判断当前要达到24小时延迟, 需要等待多久
      $timeDiff = $time2 - $timeDiff;
      if ($timeDiff < $minSleep) {
        $minSleep = $timeDiff;
      }
    }
  }

  // 加入一个睡眠, 让进程挂起来, 直到下一个用户的延迟时间或者达到最大睡眠时间
  if ($minSleep) {
    sleep($minSleep);
  }
}

使用工具分析一下吧!尝试一下死循环一天停止清理在启动脚本试试呢?一般来说一天的方式问题不会太大

如果资源释放的够快,死循环也没有关系。

撰写回答
你尚未登录,登录后可以
  • 和开发者交流问题的细节
  • 关注并接收问题和回答的更新提醒
  • 参与内容的编辑和改进,让解决方法与时俱进
推荐问题