Laravel Hostinger共享主机队列报MaxAttemptsExceededException异常
Hostinger共享主机部署Laravel队列任务执行失败/卡死问题
问题现象
- 部署环境:Hostinger共享主机,本地localhost环境Excel导入队列功能运行完全正常,仅服务器端出现异常
- 异常表现:终端执行
php artisan queue:work命令后进程被强制终止,队列任务要么直接失败,要么长期处于运行中状态,多次排查未解决 - 报错信息:
Illuminate\Queue\MaxAttemptsExceededException: App\Jobs\ImportJob has been attempted too many times or run too long. The job may have previously timed out.
- 业务调用链路:Livewire组件Import调度ImportJob队列任务,最终调用ProductImport完成Maatwebsite Excel批量数据导入
核心业务代码
Import.php(Livewire调度类)
<?php namespace App\Http\Livewire; use App\Jobs\ImportJob; use Modules\Product\Entities\Product; use Modules\Product\Entities\Years; use Modules\Product\Entities\Glasses; use Modules\Product\Entities\Parts; use Modules\Product\Entities\Bodystyles; use Modules\Product\Entities\Models; use Modules\Product\Entities\Makers; use Modules\Product\Entities\Features; use Modules\Product\Entities\BodystylesModels; use Livewire\Component; use Livewire\WithFileUploads; use Illuminate\Support\Facades\Bus; use Illuminate\Support\Facades\Storage; class Import extends Component { use WithFileUploads; public $batchId; public $importFile; public $importing = false; public $importFilePath; public $importFinished = false; public function import() { $this->validate([ 'importFile' => 'required', ]); // Product::query()->truncate(); // Years::query()->truncate(); // Makers::query()->truncate(); // Models::query()->truncate(); // Bodystyles::query()->truncate(); // Glasses::query()->truncate(); // Parts::query()->truncate(); // Features::query()->truncate(); $this->importing = true; $this->importFilePath = $this->importFile->store('imports'); $batch = Bus::batch([ new ImportJob($this->importFilePath), ])->dispatch(); $this->batchId = $batch->id; } public function getImportBatchProperty() { if (!$this->batchId) { return null; } return Bus::findBatch($this->batchId); } public function updateImportProgress() { $this->importFinished = $this->importBatch->finished(); if ($this->importFinished) { Storage::delete($this->importFilePath); $this->importing = false; } } public function render() { return view('livewire.import'); } }
ImportJob.php(队列任务类)
<?php namespace App\Jobs; use Illuminate\Bus\Batchable; use Illuminate\Bus\Queueable; use App\Imports\ProductImport; use Maatwebsite\Excel\Facades\Excel; use Illuminate\Queue\SerializesModels; use Illuminate\Queue\InteractsWithQueue; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Contracts\Queue\ShouldBeUnique; class ImportJob implements ShouldQueue { use Batchable, Dispatchable, InteractsWithQueue, Queueable, SerializesModels; public $uploadFile; /** * Create a new job instance. * * @return void */ public function __construct($uploadFile) { $this->uploadFile = $uploadFile; } /** * Execute the job. * * @return void */ public function handle() { Excel::import(new ProductImport, $this->uploadFile); } /** * The job failed to process. * * @param Exception $exception * @return void */ public function failed(Exception $exception) { dd($exception); } }
ProductImport.php(Excel导入实现类)
<?php namespace App\Imports; use Modules\Product\Entities\Product; use Modules\Product\Entities\Years; use Modules\Product\Entities\Glasses; use Modules\Product\Entities\Parts; use Modules\Product\Entities\Bodystyles; use Modules\Product\Entities\Models; use Modules\Product\Entities\Makers; use Modules\Product\Entities\Features; use Modules\Product\Entities\BodystylesModels; use Maatwebsite\Excel\Concerns\ToModel; use Illuminate\Contracts\Queue\ShouldQueue; use Maatwebsite\Excel\Concerns\WithHeadingRow; use Maatwebsite\Excel\Concerns\WithBatchInserts; use Maatwebsite\Excel\Concerns\WithChunkReading; class ProductImport implements ToModel, WithHeadingRow, WithChunkReading { private $users; public function __construct() { // $this->users = User::all(['id', 'name'])->pluck('id', 'name'); } public function model(array $row) { // MakersFeatures:: $year= Years::where('years_name', trim($row['year'],''))->first(); $make= Makers::where('makers_name', trim($row['make'],''))->first(); $model= Models::where('models_name', trim($row['model'],''))->first(); $style= Bodystyles::where('bodystyles_name', trim($row['style'],''))->first(); $glass= Glasses::where('glasses_name', trim($row['glass'],''))->first(); $part= Parts::where('parts_name', trim($row['partnumber'],''))->first(); $feature= Features::where('features_name', trim($row['feature'],''))->first(); if($year==null) { $year= Years::create(['years_name'=> trim($row['year'],'')]); } if($make==null) { $make= Makers::create(['makers_name'=> trim($row['make'],'')]); } if($model==null) { $model= Models::create( [ 'models_name'=> trim($row['model'],''), 'makers_id'=>$make->id ]); } if($style==null) { $style= Bodystyles::create(['bodystyles_name'=> trim($row['style'],'')]); } if($glass==null) { $glass= Glasses::create(['glasses_name'=>trim($row['glass'],'')]); } if($part==null) { $part= Parts::create(['parts_name'=>trim($row['partnumber'],'')]); } if($feature==null) { $feature= Features::create([ 'features_name'=>trim($row['feature'],''), 'glasses_id'=>$glass->id ]); } $f= [ 'years_id'=>$year->id, 'makers_id'=>$make->id, 'models_id'=>$model->id, 'bodystyles_id'=>$style->id, 'glasses_id'=>$glass->id, 'part_id'=>$part->id, 'product_part_number'=>trim($row['partnumber'],''), 'product_price'=>trim($row['sale_price'],''), 'features_id'=> $feature->id ]; // $d= MakersFeatures::create($f); return new Product($f); } public function chunkSize(): int { return 2000; } }
问题根因及解决方案
该问题是共享主机环境的典型限制导致,和代码逻辑本身无关,按以下优先级调整即可解决:
- 根本原因:Hostinger共享主机会强制杀掉运行时长超过60秒(部分基础套餐为30秒)的CLI进程,同时对单进程内存占用有严格上限。默认
queue:work启动的长驻进程、单任务处理2000条数据的导入逻辑很容易触发限制被杀,进程异常退出后队列会判定任务超时重试,重试次数耗尽后抛出MaxAttemptsExceededException。 - 调整任务配置适配主机限制
在ImportJob类中添加公共属性,覆盖框架默认配置:
启动队列工作进程时不要使用默认长驻模式,改用一次性执行模式:执行命令为// 单任务最大运行时长设置为300秒,建议不超过主机进程存活上限 public $timeout = 300; // 任务失败后重试3次,避免无限重试 public $tries = 3; // 单进程内存占用上限设置为256M,适配共享主机内存限制 public $memory = 256;php artisan queue:work --stop-when-empty,该命令会处理完所有待执行任务后自动退出进程,不会长期驻留被主机查杀。 - 优化导入逻辑降低单任务负载
当前导入逻辑每一行数据都执行7次查表判断,2000条分块单任务要执行14000次SQL查询,很容易超时。做两个调整:- 把
ProductImport的chunkSize从2000改为200,缩小单批次处理的数据量,降低单次任务执行时长 - 实现
WithBatchInserts接口,批量写入Product数据减少SQL请求次数;同时提前把已存在的Years、Makers等关联数据全部查出来放到类属性中做内存缓存,不要每行都重复查库
- 把
- 配置定时任务替代手动运行队列
共享主机无法配置Supervisor守护进程,直接在Hostinger面板的Cron Job中添加定时任务,每分钟执行一次php artisan queue:work --stop-when-empty即可,既可以保证队列任务按时消费,又不会产生长期驻留的进程触发主机查杀规则。 - 注意不要在共享主机上运行
queue:listen,该模式会持续占用进程,一定会被主机系统自动杀掉。
内容的提问来源于stack exchange,提问作者Aqeela Urooj
相关产品推荐
相关产品推荐

