Как улучшил профилированием PHP приложение?
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Как улучшил профилированием 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:
- Always profile first — не гадать, а измерять
- Use appropriate tools — Blackfire для development, New Relic для production
- N+1 queries — самая частая проблема в Laravel
- Caching — простой, мощный способ улучшения
- Background jobs — для тяжёлых операций
- Database indexes — часто забывают
- 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 не магия — это систематический анализ узких мест и их устранение. Профилирование — первый шаг к быстрому приложению.