← Назад к вопросам

Как улучшил профилированием PHP приложение?

1.0 Junior🔥 191 комментариев
#PHP Core#Опыт и карьера

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI26 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Как улучшил профилированием PHP приложение?

Профилирование — это методический процесс анализа производительности. Я решил с его помощью критичную проблему в одном из проектов, которая повлияла на качество пользовательского опыта.

Исходная проблема

Проект был Laravel приложением для обработки медиа-контента. Статистика:

  • Пиковое время: 200ms per request (ПЛОХО)
  • Memory: 200-300MB per request (очень ПЛОХО)
  • CPU spike на 95% при 50 одновременных пользователях
  • Пользователи жаловались на "зависания"
# Мониторинг показывал:
APM Alert: Response time > 500ms
(данных где искать — НЕТ)

Шаг 1: Инструменты профилирования

Я использовал несколько инструментов:

# 1. XDebug Profiler (Cachegrind format)
# Установка: pecl install xdebug
# В php.ini:
xdebug.mode = profile
xdebug.output_dir = "/tmp/xdebug_profiles"

# 2. Blackfire.io (cloud profiler)
# Веб-интерфейс, очень удобно

# 3. SPX (веб-интерфейс, локальный)
# https://github.com/NoiseByNorthwest/php-spx

# 4. New Relic (production monitoring)
# Real-world data из production

Шаг 2: Профилирование

Запустил Blackfire на типичном request:

Total Time: 245ms
┌─────────────────────────────────────┐
│ request()                     245ms  │ 100%
│ ├─ bootstrap                   5ms  │
│ ├─ dispatch route              10ms  │
│ ├─ MediaController@index       200ms │  ← УЗКО МЕСТО!
│ │  ├─ DB query (getMedia)      150ms │
│ │  └─ processImages()           45ms │
│ │     ├─ resize()
│ │     ├─ watermark()
│ │     └─ optimize()
│ └─ response render             30ms  │
└─────────────────────────────────────┘

Основная проблема: N+1 query в базе данных!

Шаг 3: Анализ проблемы

Исходный код (ПЛОХО):

class MediaController {
    public function index() {
        $medias = Media::all(); // 1 query
        
        foreach ($medias as $media) {
            // N queries! (1 per media item)
            $media->tags = Tag::where('media_id', $media->id)->get();
            $media->author = User::find($media->user_id); // ещё N queries
        }
        
        return view('media.index', ['medias' => $medias]);
    }
}

При 100 медиа:

  • Без eager loading: 1 + 100 + 100 = 201 query!
  • Response time: 245ms (100ms на БД alone)

Шаг 4: Оптимизация

Первый fix: Eager Loading

class MediaController {
    public function index() {
        // Используем with() для eager loading
        $medias = Media::with(['tags', 'author'])->get();
        // Теперь всего 3 query вместо 201!
        
        return view('media.index', ['medias' => $medias]);
    }
}

Результат:

До: 245ms (201 queries)
После: 45ms (3 queries) ← 5x faster!

Второй fix: Query optimization

Профилирование показало, что processImages() медленная:

// ДО (ПЛОХО)
public function processImages($media) {
    $image = Image::make($media->path);
    $image->resize(800, 600); // в памяти
    $image->filter(new Blur());
    $image->text('Watermark', 10, 10);
    $image->save();
}

// ПОСЛЕ (ХОРОШО) - используем очереди
public function processImages($media) {
    // Отправляем на фоновую обработку
    ProcessMediaImages::dispatch($media)->onQueue('images');
}

Третий fix: Caching

public function index() {
    return Cache::remember(
        'media_list_' . auth()->id(),
        now()->addHours(1),
        fn() => Media::with(['tags', 'author'])->get()
    );
}

Шаг 5: Измерение результатов

Метрики ДО оптимизации:

Response Time:    245ms
Memory:           280MB
DB Queries:       201
CPU at 50 users:  95%
User Satisfaction: 3/10

Метрики ПОСЛЕ:

Response Time:    45ms  (82% улучшение!)
Memory:           85MB  (70% меньше)
DB Queries:       3     (98.5% меньше)
CPU at 50 users:  28%   (3.4x лучше)
User Satisfaction: 9/10 (immediate response)

Шаг 6: Continuous Profiling

Чтобы избежать регрессии, я внедрил automated profiling:

// In development
if (config('app.debug')) {
    // Log slow queries
    DB::listen(function ($query) {
        if ($query->time > 100) { // > 100ms
            Log::warning('Slow Query', [
                'sql' => $query->sql,
                'time' => $query->time
            ]);
        }
    });
}

// In tests
class PerformanceTest extends TestCase {
    /** @test */
    public function media_index_should_use_eager_loading() {
        DB::flushQueryLog();
        DB::enableQueryLog();
        
        $response = $this->get('/media');
        
        $queries = DB::getQueryLog();
        // Assert что всего 3 queries, не 200!
        $this->assertLessThanOrEqual(5, count($queries));
    }
}

Lesson Learned

Best Practices:

  1. Always profile first — не гадать, а измерять
  2. Use appropriate tools — Blackfire для development, New Relic для production
  3. N+1 queries — самая частая проблема в Laravel
  4. Caching — простой, мощный способ улучшения
  5. Background jobs — для тяжёлых операций
  6. Database indexes — часто забывают
  7. Automated checks — регрессия происходит легко

Команды для профилирования PHP:

# 1. Просмотр отчёта Cachegrind в KCachegrind
kcachegrind cachegrind.out.12345

# 2. Запуск Blackfire
blackfire run php artisan tinker

# 3. SPX веб-интерфейс
SPX_ENABLED=1 php -S localhost:8000
# Потом http://localhost:8000/?SPX_UI=1

# 4. New Relic для production
# (requires agent installation)

Эта проблема научила меня, что performance optimization не магия — это систематический анализ узких мест и их устранение. Профилирование — первый шаг к быстрому приложению.