PHP中循环使用curl调用接口导致内存溢出如何解决

1. 运行环境

lnmp

1). 当前使用的 Laravel 版本?

laravel 版本:5.4

2). 当前使用的 php版本?

PHP 版本:7.1

3). 当前系统 ubuntu

2. 问题描述?

在laravel中循环调用接口拉取抖音数据导致内存溢出,每次内存溢出都是在curl_exec函数中,问一下这种情况有什么好的解决方法吗?循环调用是不可避免的

private function curl($url, $postData)
    {
        $ch = curl_init($url);
        $options = [
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_POST           => true,
            CURLOPT_SSL_VERIFYPEER => false,
            CURLOPT_SSL_VERIFYHOST => false,
            CURLOPT_POSTFIELDS     => $postData
        ];
        curl_setopt_array($ch, $options);
        $response = curl_exec($ch);
        if (curl_errno($ch)) {
            Log::info(curl_error($ch));
        } else {
            $data = json_decode($response, true);
        }
        curl_reset($ch);
        curl_close($ch);
        unset($ch);
        return $data;
    }

贴一下目前的代码

$pullDay = date('Y-m-d', strtotime('-3 day'));
        $pullDay = '2023-10-26';
        $pageSize = 10000; //单次查询数量 抖音平台支持最大10000
        $url = 'https://developer.toutiao.com/api/apps/taskbox/query_task_video_daily_data/';
        //从数据库获取查询的记录
        $record = DistributionDyTaskPullRecord::query()
            ->where('billing_date', $pullDay)
            ->orderByDesc('id')
            ->first();
        $lastRequestEndTime = strtotime($pullDay) + 86399; //视频发布结束参数的最大时间
        if (!$record) {
            $pageNo = 1;
            $publishStartTime = strtotime(date('Y-m-d', strtotime('-81 day')));
            $publishEndTime = strtotime('+1 week -1 second', $publishStartTime);
        } else {
            if ($record->is_need_page) {
                $pageNo = $record->page + 1;
                $publishStartTime = $record->publish_start_time;
                $publishEndTime = $record->publish_end_time;
            } else {
                $pageNo = 1;
                $publishStartTime = $record->publish_end_time + 1;
                $publishEndTime = strtotime('+1 week -1 second', $publishStartTime);
            }
            if ($record->status == 504 || $record->error_code == 4001010) {
                $pageNo = $record->page;
                $publishStartTime = $record->publish_start_time;
                $publishEndTime = $record->publish_end_time;
            }
            //当所有条件满足的时候 就不在继续拉取
            if ($record->status == 1 && $record->error_code == 0
                && $record->publish_end_time >= $lastRequestEndTime
                && $record->is_need_page == 0) return;
        }
        if ($publishEndTime > $lastRequestEndTime) $publishEndTime = $lastRequestEndTime;
//        DistributionDyTaskSubmissionVideoDetail::query()->where('date', $pullDay)->delete();
        $disTask = DistributionTask::all()->pluck('payment_allocate_ratio', 'task_id');
        $packSetting = PackSetting::query()->where('id', 34)->first();
        $post_data = [
            'access_token'             => $packSetting->access_token,
            'appid'                    => $packSetting->appid,
            'page_no'                  => $pageNo,
            'page_size'                => $pageSize,
            'video_publish_start_time' => $publishStartTime,
            'video_publish_end_time'   => $publishEndTime,
            'billing_date'             => $pullDay,
        ];
        $insertRecord = [
            'page'               => $pageNo,
            'publish_start_time' => $publishStartTime,
            'publish_end_time'   => $publishEndTime,
            'billing_date'       => $pullDay,
        ];
        try {
            $data = GuzzleHelper::post($url, [ 'query' => $post_data ]);
            $insertRecord['error_code'] = $data['error'] ?? 0;
            $insertRecord['data_count'] = count($data['results']);
            $insertData = [];
            if ($data['error'] == 0 && count($data['results']) > 0) {
                foreach ($data['results'] as $val) {
                    $new = [
                        'video_id'               => $val['VideoId'] ?? 0,
                        'task_id'                => $val['TaskId'] ?? 0,
                        'task_name'              => $val['TaskName'] ?? '',
                        'publish_time'           => $val['PublishTime'] ?? 0,
                        'author'                 => $val['Author'] ?? '',
                        'douyin_id'              => $val['DouyinID'] ?? '',
                        'video_title'            => $val['VideoTitle'] ?? '',
                        'micro_app_title'        => $val['MicroAppTitle'] ?? '',
                        'video_views'            => $val['video_views'] ?? 0,
                        'likes'                  => $val['Likes'] ?? 0,
                        'comments'               => $val['Comments'] ?? 0,
                        'shares'                 => $val['Shares'] ?? 0,
                        'video_link'             => $val['VideoLink'] ?? '',
                        'expected_profit'        => $val['ExpectedProfit'] ?? 0,
                        'gmv_1d'                 => $val['GMV1d'] ?? 0,
                        'refund_gmv_1d'          => $val['RefundGMV1d'] ?? 0,
                        'billing_gmv_1d'         => $val['BillingGMV1d'] ?? 0,
                        'billing_refund_gmv_1d'  => $val['BillingRefundGMV1d'] ?? 0,
                        'ad_share_cost_1d'       => $val['AdShareCost1d'] ?? 0,
                        'feed_ad_share_cost_1d'  => $val['FeedAdShareCost1d'] ?? 0,
                        'active_cnt_1d'          => $val['ActiveCnt1d'] ?? 0,
                        'date'                   => $val['date'] ?? '',
                        'client_name'            => $val['ClientName'] ?? '',
                        'payment_allocate_ratio' => $disTask[$val['TaskId']] ?? 0,
                    ];
                    $insertData[] = $new;
                }
                $batches = array_chunk($insertData, 1000);
                foreach ($batches as $batch) {
                    DistributionDyTaskSubmissionVideoDetail::insert($batch);
                }
            }
            if (count($data['results']) < $pageSize) {
                $insertRecord['is_need_page'] = 0;
            } else {
                $insertRecord['is_need_page'] = 1;
            }
        } catch (\Exception $e) {
            Log::info('抖音数据拉取异常,拉取参数为' . json_encode($post_data) . '。异常信息' . $e->getMessage());
            if ($e->getCode() == 504) {
                $insertRecord['status'] = $e->getCode();
            }
        }
        $insertRecord['created_at'] = Carbon::now();
        DistributionDyTaskPullRecord::insert($insertRecord);
    }

之前分页是从1-N 的样子和$pullDay这个参数是递增的类似10-21,10-22这样循环

《L04 微信小程序从零到发布》
从小程序个人账户申请开始,带你一步步进行开发一个微信小程序,直到提交微信控制台上线发布。
《L02 从零构建论坛系统》
以构建论坛项目 LaraBBS 为线索,展开对 Laravel 框架的全面学习。应用程序架构思路贴近 Laravel 框架的设计哲学。
最佳答案

找到原因了兄弟们

file

file 是我框架他们封装的sql日志记录的原因,这里一直往$sqlMessage中存 mysql信息 导致内存一直增长

1年前 评论
anticipate (作者) (楼主) 1年前
讨论数量: 35

curl 和 guzzeHttp都用过,都会内存溢出

1年前 评论

file

1年前 评论
anticipate (楼主) 1年前
  • 问题:内存溢出如何解决?
  • 报错信息:具体内存溢出多少啊?
  • PHP:当前脚本允许内存限制是多少?
  • 比较:要么就是你请求接口返回的数据太大了,而你当前脚本的内存又不够?
  • 结论:初步判断跟循环没关系,你这个是一个方法,每次调用会释放的

报错类似这种

E_ERROR "Allowed memory size of 1073741824 bytes exhausted (tried to allocate 2097152000

php.ini

cat  /usr/local/etc/php/php.ini| grep memory_limit
memory_limit = 256M
1年前 评论
anticipate (楼主) 1年前
anticipate (楼主) 1年前

用迭代器试一下,如果还不行就只能修改 php.ini 最大运行内存了。

function curl($url, $postData)
{
    $ch = curl_init($url);
    $options = [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_POST => true,
        CURLOPT_SSL_VERIFYPEER => false,
        CURLOPT_SSL_VERIFYHOST => false,
        CURLOPT_POSTFIELDS => $postData,
    ];
    curl_setopt_array($ch, $options);
    $response = curl_exec($ch);
    if (curl_errno($ch)) {
        Log::info(curl_error($ch));
    } else {
        $data = json_decode($response, true);
    }
    curl_reset($ch);
    curl_close($ch);
    unset($ch);
    return $data;
}

function processUrls(array $urls)
{
    foreach ($urls as $url => $postData) {
        yield curl($url, $postData); // 你的 curl() 方法
    }
}

$urls = ['your-request-url' => ['your-post-data']];

foreach (processUrls($urls) as $item) {
    // TODO
    dump($item);
}
1年前 评论
anticipate (楼主) 1年前
sanders 1年前
keyboby 1年前
keyboby 1年前

file 内存基本上会越来越大@

1年前 评论

数量多的话,这种问题基本上都是用队列去干了,还有一个问题,对方有并发控制,用队列还能解决此类问题

1年前 评论
anticipate (楼主) 1年前
xxbi 1年前

建议的方法:

  1. 按照楼主描述,每次这个函数curl_exec()执行后内存占用会变高,可能是因为在每次循环后内存不会立马释放,可以将每次循环的数据先入库后,再强制清除内存缓冲区的内容。
  2. 具体查清楚内存占用高的原因,可以在可能的行前后打印出内存的占用的数据,再精确分析。
  3. curl也可能是异步模式。
1年前 评论

使用 PHP 原生扩展 pcntl 来完成,每次调用 curl 都是吊起一个新的线程去处理,处理完的内容丢到其他介质中贮存,待收到信号后继续下一个就好了。可以理解成一个实时阻塞的多线程。

1年前 评论

会不会和 json_decode 有关系 ?

1年前 评论
sanders

看样子像是内存泄漏,从代码里没看出哪里会导致 curl_exec 内存泄漏,那就有可能是 php 、 curl 模块或依赖库版本的问题。建议从两个方面试一下:

  1. 更换 php 环境 升级依赖库 (不建议一上来就在生产环境进行);
  2. 使用队列任务相互触发将内存泄漏的风险控制下,但一定要注意队列任务的消费进程一定要做熔断和重新拉起的配置。
1年前 评论

file 艰难找到了一个遇到同样问题的情况

1年前 评论

最终换了一种思路,建一个数据库保存一下每次请求的参数

file,这样,然后通过脚本每分钟去调用一下接口,这样能很有效的去解决内存溢出的问题

1年前 评论
小学毕业生 1年前
anticipate (作者) (楼主) 1年前

你是爬的数据越来越多关curl什么事,分批爬完就存就行了

1年前 评论
anticipate (楼主) 1年前

这种就是爬虫程序的事了,基本爬虫的库都有解决内存爆的问题。可以直接用爬虫库

1年前 评论

试试通过 gc_disable 停用gc然后手动回收?

1年前 评论

既然是爬虫 那应该是cli模式处理的,curl_exec 导致的内存溢出估计是curl模块有内存没及时回收呗,那把脚本放守护进程运行 定时把进程kill掉重启 回收内存不行吗

1年前 评论
anticipate (楼主) 1年前

内存溢出原因有很多,建议你贴出业务代码,只给个curl代码是无法准确分析问题的;几年前有一个帖子,也是一样的问题(https://www.oschina.net/question/569427_2234979?sort=time&p=1),分析原因也不是curl导致的,而是orm的原因。

最后我谈下我的个人看法:

  1. 抖音数据响应数据量太大,抖音数据接口难道不能设置最大数据行数吗?或者抖音没有提供下载数据文件的接口吗?请求接口控制最大数据行数,或者分片下载文件,脚本单独处理文件。
  2. 根据你的表述,请求页数比较多,为什么不共用curl句柄,不需要每次请求完毕后销毁句柄,另外curl也可以输出到文件,然后跑脚本处理文件,数据接收和处理分开执行。
    // 示例
    $fp = fopen('demo.csv', 'w');
    curl_setopt($c, CURLOPT_RETURNTRANSFER, false);
    curl_setopt($c, CURLOPT_FILE, $fp);
  3. 最差的方案,就是脚本进程控制,脚本每次执行多少页后杀掉进程,自动重启执行。
1年前 评论
anticipate (楼主) 1年前
anticipate (楼主) 1年前

找到原因了兄弟们

file

file 是我框架他们封装的sql日志记录的原因,这里一直往$sqlMessage中存 mysql信息 导致内存一直增长

1年前 评论
anticipate (作者) (楼主) 1年前

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!