先说结论,通过并发请求上传图片,上传效率提升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来调整