聊聊Guzzlehttp上传批量图片优化的经历

时间:2025-09-05 12:45:01来源:互联网

下面小编就为大家分享一篇聊聊Guzzlehttp上传批量图片优化的经历,具有很好的参考价值,希望对大家有所帮助。

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

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

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

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

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

//以下为原代码,有问题的地方用注释写了出来
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 GuzzleHttpClient();	
		$fileInfo = [];

		$pool = new GuzzleHttpPool($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 ?: CarbonCarbon::now()->toIso8601String();
       
        $jsonData = json_encode($data, JSON_UNESCAPED_UNICODE);
        
        $signData = $this->generateSignData($method, $uri, $jsonData, $requestTime);

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

        $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);
        }
    }  

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

public function uploadAttachmentAsync($fileUrlArr,$bizType) {

                //创建guzzlehttp客户端,所有的请求都可共享,相比改进前的代码减少资源的消耗
		$client = new GuzzleHttpClient();	
		$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 Psr7HttpMessageRequestInterface or a callable that returns a promise that fulfills with a Psr7MessageHttpResponseInterface 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 MonologLogger('test');
        $logger->pushHandler(new MonologHandlerStreamHandler(storage_path('paymentlogs/test.log'), 90));

		$pool = new GuzzleHttpPool($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 GuzzleHttpExceptionRequestException && $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来调整
本站部分内容转载自互联网,如果有网站内容侵犯了您的权益,可直接联系我们删除,感谢支持!