分享一次Guzzlehttp上传批量图片优化的经历

先说结论,通过并发请求上传图片,上传效率提升10倍左右,从原来的通过同步方式上传同样的19张图片耗时31秒到后来通过异步方式(10个并发数)缩短到3秒多一点,因自己水平比较菜加上之前很少遇到这种场景,实现过程中踩了一些坑,分享出来希望能帮到遇到同样问题或在学guzzlehttp的伙伴。

因公司需要接某里汇的支付方式,该支付方式需要先上传订单产品图片,不过该上传图片接口只支持单次上传一张图片,如果图片少的话无所谓,但如果一件多件的话,用同步的方式上传就比较耗时了,亲测过一单19件,上传19次图片用时31秒,这是客户不能接受的。

如何大幅缩短上传时间,提升用户体验是目前要解决的问题,想到了Guzzlehttp可以发送并发请求,但在写代码时也踩了一些坑,还是按顺序同步请求,而不是预期的异步

服务器生产环境: php 版本5.6 ,guzzlehttp版本 6.3,laravel 版本5.1

先贴一下原来的代码并以注释的方式说明存在的问题

php 复制代码
//以下为原代码,有问题的地方用注释写了出来
public function uploadAttachmentAsync($fileUrlArr, $bizType = '') {
                //定义yield生成器函数
		$uploadResult = function() use($fileUrlArr, $bizType){
			foreach($fileUrlArr as $index => $fileUrl){
				$pathInfo = pathinfo($fileUrl);
				$data = [
					'bizType' => $bizType,
					'fileName' => $pathInfo['basename'],
                                        //file_get_contents是同步下载,这是没有异步的一个原因
					'attachmentContent' => base64_encode(file_get_contents($fileUrl))
				];
                                //这也是不能异步上传的一个原因,因为它返回的是一个结果而不是promise
				yield $this->sendRequest('post', '/api/uploadAttachment', $data);
			}
		};
		$client = new \GuzzleHttp\Client();	
		$fileInfo = [];

		$pool = new \GuzzleHttp\Pool($client, $uploadResult(), [
                    'concurrency' => 5,//并发数
                    'fulfilled' => function ($responseAtta, $index) {
                                // 处理成功响应
				if (array_get($responseAtta, 'result.resultStatus') == 'S'){
                                   $fileKey = array_get($responseAtta, 'fileKey');
                                   $fileName = array_get($responseAtta, 'fileName');
                               }else{
                                   //不应该在这里抛出异常,若一张图片有问题会导致其他图片上传中断
                                   throw new \Exception('上传订单图片失败' . print_r($responseAtta, true));
                               }
				$fileInfo[$index] = [
					'fileId' => $fileKey,
					'fileName' => $fileName,
				];
                                 \Log::info('Request ' . $index . ' completed successfully');
            },
                    'rejected' => function ($reason, $index) {
                       // 处理失败响应
                       \Log::error('Request ' . $index . ' failed: ' . $reason);
                     },
         ]);
        //等待所有图片上传完成
        $pool->promise()->wait();
	return $fileInfo;
    }  

public function sendRequest($method, $uri, $data, $requestTime = null) {
        $requestTime = $requestTime ?: \Carbon\Carbon::now()->toIso8601String();
       
        $jsonData = json_encode($data, JSON_UNESCAPED_UNICODE);
        
        $signData = $this->generateSignData($method, $uri, $jsonData, $requestTime);

        $signature = $this->getRsaSign($signData);
        
        // 不应该在这边创建实例,因为每次调用本方法都会重复创建实例,会造成性能消耗大
        $client = new \GuzzleHttp\Client();

        $url = $this->requestUrl . $uri;
        try {
            // 这是同步请求,应该要返回待执行的promise,而不是执行的结果,导致不会异步上传的主要原因
            $response = $client->request($method, $url, [
                'headers' => [
                    'Content-Type'  => $this->headerContentType,
                    'Client-Id'     => $this->clientId,
                    'Request-Time'  => $requestTime,
                    'Signature'     => 'algorithm=' . $this->signAlgorithm . ', keyVersion=' . $this->keyVersion . ', signature=' . $signature  
                ],
                'body' => $jsonData
            ]);
            
            return json_decode($response->getBody()->getContents(), true);
        } catch (GuzzleException $e) {
            //这里也不应该抛出异常,会导致其他图片上传中断
            throw new \Exception('HTTP 请求失败: ' . $e->getMessage(), $e->getCode(), $e);
        }
    }  

以下是改进后的代码,改进的部分以注释的方式说明

php 复制代码
public function uploadAttachmentAsync($fileUrlArr,$bizType) {

                //创建guzzlehttp客户端,所有的请求都可共享,相比改进前的代码减少资源的消耗
		$client = new \GuzzleHttp\Client();	
		$uploadPromise = function() use($fileUrlArr, $bizType, $client){
			foreach($fileUrlArr as $index => $fileUrl){
                                //生成器,因为图片要和产品名称对应起来,这边加了$index,开始这边yield一个guzzlehttp的promise但出现异常,"exception 'InvalidArgumentException' with message 'Each value yielded by the iterator must be a Psr7\Http\Message\RequestInterface or a callable that returns a promise that fulfills with a Psr7\Message\Http\ResponseInterface object"对应一个callback,如果
                                //guzzlehttp 7以上的版本应该可以直接返回一个guzzlehttp的promise而不是callback(猜测,没实际验证过)
				yield $index => function () use($fileUrl, $bizType, $client) {
                                    //这里有个promise链,先异步方式下载fileUrl图片再上传,一定要在then的匿名函数里return 一个要上传的promise,不然promise链就断了得不到想要的结果 
                                    return $client->getAsync($fileUrl)->then(function ($response) use ($fileUrl, $bizType, $client) {
                                    $pathInfo = pathinfo($fileUrl);
                                    $data = [
                                      'bizType' => $bizType,
                                      'fileName' => $pathInfo['basename'],
                                      'attachmentContent' => base64_encode($response->getBody()->getContents())
                                    ];
                                 //返回一个待执行的promise
                                 return $this->sendRequestAsync(
                                    'post',
                                    '/api/uploadAttachment',
                                    $data,
                                    $client
                                 );
                    });
                };
            }
        };
		$fileInfo = [];
		
		$logger = new \Monolog\Logger('test');
        $logger->pushHandler(new \Monolog\Handler\StreamHandler(storage_path('paymentlogs/test.log'), 90));

		$pool = new \GuzzleHttp\Pool($client, $uploadPromise(), [
                    'concurrency' => 10,//并发数
                    'fulfilled' => function ($responseAtta, $index) use(&$fileInfo, $logger){
				try{
					// 处理成功响应
					if (array_get($responseAtta, 'result.resultStatus') == 'S'){
						$fileKey = array_get($responseAtta, 'fileKey');
						$fileName = array_get($responseAtta, 'fileName');
						$fileInfo[$index] = [
							'fileKey' => $fileKey,
							'fileName' => $fileName,
							'success' => true,
						];
						$logger->info("Request {$index} completed successfully");
					}else{
						$errorMsg = '上传订单图片失败: ' . print_r($responseAtta, true);
						$fileInfo[$index] = [
							'fileKey' => '',
							'fileName' => '',
							'success' => false,
							'errorMsg' => $errorMsg,
						];
						$logger->error('uploadAttachment: ' . json_encode($responseAtta, JSON_UNESCAPED_UNICODE));
					}
				}catch (\Exception $exception){
					$logger->error('uploadAttachment: ' . $exception->getMessage());
				}
            },
            'rejected' => function ($reason, $index) use(&$fileInfo, $logger){
                // 处理失败响应
				$errorBody = $reason instanceof \GuzzleHttp\Exception\RequestException && $reason->hasResponse()
                ? (string) $reason->getResponse()->getBody()
                : $reason->getMessage();
				$fileInfo[$index] = [
					'fileKey' => '',
					'fileName' => '',
					'success' => false,
					'errorMsg' => $errorBody,
				];
                $logger->error("Request {$index} failed: {$errorBody}");
            },
        ]);
        $pool->promise()->wait();
	return $fileInfo;
    }





concurrency 参数改了几次,发现它为10的时候,上传19张图片耗时3秒多,
这个可以根据服务器带宽、内存大小、php-fpm的memoey_limit来调整
相关推荐
cxyxiaokui00117 分钟前
论如何优雅地让AI“闭嘴”:深入SpringAI的流式停止与记忆难题
java·后端
bobz96521 分钟前
关于 “涌现” 的最初的定义
后端
Warren9826 分钟前
Spring Boot 整合网易163邮箱发送邮件实现找回密码功能
数据库·vue.js·spring boot·redis·后端·python·spring
秦禹辰39 分钟前
本地Docker部署开源Web相册图库Piwigo与在线远程访问实战方案
开发语言·后端·golang
一乐小哥42 分钟前
五分钟就能搭好的socks5为啥我装了一个小时😭 进来看小丑
linux·后端
HyggeBest1 小时前
Golang 并发原语 Sync Pool
后端·go
Java水解1 小时前
【RabbitMq C++】消息队列组件
后端·rabbitmq
灵魂猎手1 小时前
10. Mybatis XML配置到SQL的转换之旅
java·后端·源码
用户4099322502121 小时前
如何让FastAPI在百万级任务处理中依然游刃有余?
后端·ai编程·trae