Export users 500k+ lines, seed, supervisord setup
This commit is contained in:
@@ -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"]
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
+14
-1
@@ -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 тысяч элементов
|
||||||
@@ -75,3 +75,16 @@ mysqldump -u root -psecret test_fhr clubs players > clubs_and_players_dump.sql
|
|||||||
Тестовый дамп лежит в корне проекта **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',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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',
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
*
|
||||||
|
!.gitignore
|
||||||
File diff suppressed because one or more lines are too long
@@ -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']);
|
||||||
|
});
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
1
|
||||||
@@ -44,6 +44,6 @@ server {
|
|||||||
}
|
}
|
||||||
|
|
||||||
location / {
|
location / {
|
||||||
try_files $uri /index.php?$args;
|
try_files $uri $uri/ /index.php?$query_string;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user