Export users 500k+ lines, seed, supervisord setup

This commit is contained in:
Ilya Rogozhin
2026-06-09 18:52:21 +02:00
parent 299ab41b7d
commit 792e4d7f28
15 changed files with 469 additions and 223 deletions
+9 -1
View File
@@ -22,6 +22,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
libzip-dev \ libzip-dev \
unzip \ unzip \
git \ git \
supervisor \
&& rm -rf /var/lib/apt/lists/* && rm -rf /var/lib/apt/lists/*
# Configure and install PHP extensions # Configure and install PHP extensions
@@ -55,7 +56,14 @@ USER www-data
# 6. Download and unpack the Laravel installer directly into www-data's profile folder # 6. Download and unpack the Laravel installer directly into www-data's profile folder
RUN composer global require laravel/installer --no-interaction RUN composer global require laravel/installer --no-interaction
# 7. Switch back to root user
USER root
# Supervisor
RUN mkdir -p /var/log/supervisor
COPY ./supervisord.conf /etc/supervisor/conf.d/supervisord.conf
# Expose PHP-FPM default communication port # Expose PHP-FPM default communication port
EXPOSE 9000 EXPOSE 9000
CMD ["php-fpm"] CMD ["/usr/bin/supervisord", "-n", "-c", "/etc/supervisor/conf.d/supervisord.conf"]
+2 -1
View File
@@ -4,5 +4,6 @@ opcache.memory_consumption=256
opcache.interned_strings_buffer=16 opcache.interned_strings_buffer=16
opcache.max_accelerated_files=20000 opcache.max_accelerated_files=20000
opcache.revalidate_freq=0 opcache.revalidate_freq=0
opcache.validate_timestamps=0 opcache.validate_timestamps=1
opcachee.revalidate_freq=0
opcache.fast_shutdown=1 opcache.fast_shutdown=1
+24
View File
@@ -0,0 +1,24 @@
[supervisord]
nodaemon=true
user=root
[program:php-fpm]
command=php-fpm
autostart=true
autorestart=true
priority=5
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0
[program:laravel-worker]
process_name=%(program_name)s_%(process_num)02d
command=php /var/www/test-fhr/artisan queue:work --sleep=3 --tries=3
autostart=true
autorestart=true
user=www-data
numprocs=4
redirect_stderr=true
stdout_logfile=/var/www/test-fhr/storage/logs/worker.log
stopwaitsecs=3600
+15 -2
View File
@@ -30,7 +30,7 @@ docker compose up
alias fhr-mysql='docker exec -it mysql_fhr /bin/bash' alias fhr-mysql='docker exec -it mysql_fhr /bin/bash'
alias fhr-nginx='docker exec -it nginx_fhr /bin/bash' alias fhr-nginx='docker exec -it nginx_fhr /bin/bash'
alias fhr-redis='docker exec -it redis_fhr /bin/sh' alias fhr-redis='docker exec -it redis_fhr /bin/sh'
alias fhr-php='docker exec -it php8_fhr /bin/bash' alias fhr-php='docker exec -u www-data:www-data -it php8_fhr /bin/bash'
``` ```
### Написать сортировку для массива числовых данных от 200 тысяч элементов ### Написать сортировку для массива числовых данных от 200 тысяч элементов
@@ -74,4 +74,17 @@ mysqldump -u root -psecret test_fhr clubs players > clubs_and_players_dump.sql
После такой выгрузки, дамп появится в папке MySQL-files После такой выгрузки, дамп появится в папке MySQL-files
Тестовый дамп лежит в корне проекта **clubs_and_players_dump.sql** Тестовый дамп лежит в корне проекта **clubs_and_players_dump.sql**
**Время: 1 час 25 минут.** **Время: 1 час 25 минут.**
### Выгрузка БД пользователей, более 500к+ строк
Сидируем БД
```bash
# Долгий сид достаточно, минут 15-20 возможно
php artisan db:seed --class=UserSeeder
```
Дальше открываем главную страницу http://test-fhr.ldev/
Хост **test-fhr.ldev** нужно будет прописать в hosts в зависимости от вашей ОС.
**Время: 2 часа 30 минут.**
@@ -0,0 +1,53 @@
<?php
namespace App\Http\Controllers;
use App\Models\User;
use App\Jobs\UsersExport;
use Illuminate\Support\Facades\Cache;
class UserExportController extends Controller
{
public function status()
{
$status = Cache::remember('export_status', now()->addMinutes(2), function () {
return 'standby';
});
$lastExportFile = Cache::get('last_export_file');
return [
'export_status' => $status,
'last_export_file' => $lastExportFile
];
}
public function counter()
{
$currentCount = Cache::remember('users_count', now()->addMinutes(2), function () {
return User::count();
});
return [
'count' => $currentCount,
];
}
public function export()
{
$status = cache('export_status');
if ($status === 'progress') {
return [
'export_status' => $status,
];
}
Cache::forever('export_status', 'progress');
UsersExport::dispatch();
return [
'export_status' => 'started',
];
}
}
+63
View File
@@ -0,0 +1,63 @@
<?php
namespace App\Jobs;
use App\Models\User;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\File;
use Throwable;
class UsersExport implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
/**
* Execute the job.
*/
public function handle(): void
{
try {
$exportPath = public_path('exports');
if (!File::isDirectory($exportPath)) {
File::makeDirectory($exportPath, 0755, true);
}
$oldFiles = File::glob($exportPath . '/users_export_*.csv');
foreach ($oldFiles as $file) {
File::delete($file);
}
$filename = 'users_export_' . now()->format('Y-m-d_H-i-s') . '.csv';
$filePath = $exportPath . '/' . $filename;
$handle = fopen($filePath, 'w');
fputcsv($handle, ['Имя', 'Фамилия', 'Телефон', 'E-mail']);
foreach (User::select('name', 'surname', 'phone_number', 'email')->lazy() as $user) {
fputcsv($handle, [
$user->name,
$user->surname,
$user->phone_number,
$user->email,
]);
}
fclose($handle);
Cache::forever('export_status', 'standby');
Cache::forever('last_export_file', 'exports/' . $filename);
} catch (Throwable $e) {
Cache::forever('export_status', 'failed');
report($e);
throw $e;
}
}
}
@@ -26,6 +26,8 @@ class UserFactory extends Factory
{ {
return [ return [
'name' => fake()->name(), 'name' => fake()->name(),
'surname' => fake()->lastName(),
'phone_number' => fake()->phoneNumber(),
'email' => fake()->unique()->safeEmail(), 'email' => fake()->unique()->safeEmail(),
'email_verified_at' => now(), 'email_verified_at' => now(),
'password' => static::$password ??= Hash::make('password'), 'password' => static::$password ??= Hash::make('password'),
@@ -0,0 +1,30 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('users', function (Blueprint $table) {
$table->string('surname')->after('name');
$table->string('phone_number')->after('surname');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('users', function (Blueprint $table) {
$table->dropColumn('surname');
$table->dropColumn('phone_number');
});
}
};
@@ -15,11 +15,6 @@ class DatabaseSeeder extends Seeder
*/ */
public function run(): void public function run(): void
{ {
// User::factory(10)->create(); //
User::factory()->create([
'name' => 'Test User',
'email' => 'test@example.com',
]);
} }
} }
+31
View File
@@ -0,0 +1,31 @@
<?php
namespace Database\Seeders;
use App\Models\User;
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder;
class UserSeeder extends Seeder
{
/**
* Run the database seeds.
*/
public function run(): void
{
$totalToCreate = 525000;
$chunkSize = 1000;
\Illuminate\Support\Facades\DB::disableQueryLog();
$this->command->getOutput()->progressStart($totalToCreate);
for ($i = 0; $i < $totalToCreate; $i += $chunkSize) {
$createCount = min($chunkSize, $totalToCreate - $i);
User::factory($createCount)->create();
$this->command->getOutput()->progressAdvance($createCount);
}
$this->command->getOutput()->progressFinish();
}
}
+2
View File
@@ -0,0 +1,2 @@
*
!.gitignore
File diff suppressed because one or more lines are too long
+7
View File
@@ -1,7 +1,14 @@
<?php <?php
use Illuminate\Support\Facades\Route; use Illuminate\Support\Facades\Route;
use \App\Http\Controllers\UserExportController;
Route::get('/', function () { Route::get('/', function () {
return view('welcome'); return view('welcome');
}); });
Route::prefix('users-export')->group(function () {
Route::get('/counter', [UserExportController::class, 'counter']);
Route::get('/status', [UserExportController::class, 'status']);
Route::post('/export', [UserExportController::class, 'export']);
});
+1
View File
@@ -0,0 +1 @@
1
+1 -1
View File
@@ -44,6 +44,6 @@ server {
} }
location / { location / {
try_files $uri /index.php?$args; try_files $uri $uri/ /index.php?$query_string;
} }
} }