Laravel中Guzzle并发POST请求REST API及401错误排查
Laravel Guzzle并发计费请求401报错修复方案
问题背景
- 业务需求:在Laravel项目中通过Guzzle请求REST API完成10万量级用户计费,原有同步POST请求方式处理全量数据需6小时
- 请求规则:无回调逻辑,仅需提交包含用户msisdn、业务唯一标识的JSON数据,目标配置50并发请求大幅压缩处理时长
- 故障现象:参考Guzzle官方并发文档编写代码后,接口固定返回
"status":401,"error":"Unauthorized",已确认基础账号参数正确,无法定位错误原因
根因定位
原代码存在多处语法、逻辑错误,其中直接触发401的硬错误有3处:
- 鉴权变量拼写错误:代码提前计算了Basic Auth的base64值存入
$auth变量,但请求头Authorization字段传入的是未定义的$authorizaton,实际没有传入有效鉴权值 - 鉴权头格式不符合规范:HTTP Basic Auth要求头值格式为
Basic base64编码串,代码完全遗漏了Basic前缀,即便传入正确的base64值也会被接口判定为未授权 - Request实例传参格式错误:Guzzle的
Request构造函数第三个参数直接接收头信息键值对,第四个参数接收请求体,原代码将头、body嵌套在数组中传入第三个参数,导致所有自定义头(包括Content-Type、Authorization)根本没有被挂载到请求上,实际发出的请求无任何鉴权信息
其余影响功能正常运行的逻辑错误:
- 作用域错误:请求池
Pool的初始化代码写在了$requests生成器函数内部,循环执行阶段永远不会走到Pool初始化逻辑,并发请求根本不会触发 - 并发配置不达标:当前
concurrency配置为5,远低于目标50并发的要求 - 内存风险:未做数据分块处理,一次性加载10万条数据会导致PHP内存溢出
- 变量未定义:循环内使用的
$msisdn、$subscription、$subscriptionInfo没有绑定实际用户数据源,请求体内容无效
修复后可直接落地的代码
<?php use GuzzleHttp\Client; use GuzzleHttp\Pool; use GuzzleHttp\Psr7\Request; use GuzzleHttp\Exception\RequestException; use Psr\Http\Message\ResponseInterface; // 初始化Guzzle客户端,统一配置超时等公共参数 $client = new Client([ 'timeout' => 10, 'connect_timeout' => 3 ]); // 接口配置项 $apiUrl = "替换为实际计费接口地址"; $apiUser = "替换为接口鉴权账号"; $apiPwd = "替换为接口鉴权密码"; $basicAuthStr = base64_encode($apiUser . ":" . $apiPwd); $concurrencyNum = 50; // 目标并发数 $perChunk = 200; // 每次从数据源加载的用户数量,控制内存占用 // 生成器:分块读取用户数据,yield返回请求实例,避免全量加载内存溢出 $requestGenerator = function () use ($apiUrl, $basicAuthStr, $perChunk) { // 替换为你实际的用户数据源读取逻辑,这里以Laravel模型分块查询为例 User::where('charge_status', 0)->chunk($perChunk, function ($users) use ($apiUrl, $basicAuthStr) { foreach ($users as $user) { $requestBody = json_encode([ 'msisdn' => $user->msisdn, $user->subscription_field => $user->subscription_value ]); // 按正确参数顺序构造请求实例:方法、地址、头数组、请求体 yield new Request( 'POST', $apiUrl, [ 'Content-Type' => 'application/json', 'Authorization' => 'Basic ' . $basicAuthStr ], $requestBody ); } }); }; // 初始化请求池,逻辑移到生成器外部 $pool = new Pool($client, $requestGenerator(), [ 'concurrency' => $concurrencyNum, 'fulfilled' => function (ResponseInterface $response, $index) { // 成功响应处理:建议写入日志而非直接输出,方便后续对账 $res = json_decode($response->getBody()->getContents(), true); \Log::info("第{$index}条计费请求处理成功", $res); // 可选:更新对应用户的计费状态为成功 }, 'rejected' => function (RequestException $e, $index) { // 失败响应处理:记录错误信息用于后续补单 \Log::error("第{$index}条计费请求处理失败", [ 'code' => $e->getCode(), 'msg' => $e->getMessage() ]); // 可选:更新对应用户的计费状态为失败,记录错误原因 } ]); // 启动请求池,等待所有请求处理完成 $pool->promise()->wait();
10万量级场景额外优化建议
- 执行入口:不要在web路由下运行该逻辑,封装为Laravel Artisan自定义命令执行,配合supervisor做进程守护,避免web请求超时中断
- 内存控制:必须使用分块查询、生成器yield的方式读取用户数据,禁止一次性加载10万全量数据到内存,正常优化后进程内存可稳定控制在30MB以内
- 流控保护:如果接口侧有QPS限制,可增加令牌桶限流逻辑,避免短时间请求量过大被接口侧拦截
- 失败重试:配置Guzzle重试中间件,对网络抖动、5xx类错误自动重试2-3次,降低人工补单成本
- 断点续跑:给用户数据增加计费状态字段,处理完成后更新对应状态,进程中断重启后可自动从未处理的用户开始执行,无需从头跑全量数据
内容的提问来源于stack exchange,提问作者Muhammad Hassan Saleem
相关产品推荐
相关产品推荐

