Studi Debugging Timeout Job PHP Menggulung Resource di Produksi
Masalah utama adalah job PHP yang timeout di lingkungan produksi sambil menumpuk proses worker dan pegging CPU/memori—itu sebabnya resource mengeras saat retry melonjak. Untuk mengatasinya perlu dibaca langsung log timeout, metrik worker, dan tracing untuk mengungkap bagian kode yang blocking.
Pada kasus ini queue worker PHP (misalnya Laravel Horizon atau Symfony Messenger) menjalankan job sinkron yang menunggu respons eksternal. Ketika limit timeout tidak dikonfigurasi atau terlalu tinggi, setiap job menggulung resource karena proses tidak selesai, membuat Supervisor restart terus-menerus dan menyebabkan latensi sistem. Dengan observability yang tepat, kita bisa menjawab mengapa worker tidak selesai, berapa lama blocking I/O berlangsung, dan parameter mana yang perlu diubah.
Gejala Produksi dan Observability yang Muncul
Log dan Trace yang Terlihat
Log worker menunjukkan job dimulai dengan timestamp yang normal, lalu menunggu tanpa keluar selama durasi yang sama dengan batas supervisor. Trace distributed (OpenTelemetry atau Sentry Performance) memperlihatkan span job yang memanggil HTTP eksternal dengan durasi puluhan detik, tanpa terbuka lagi. Log stack trace mencatat titik terakhir sebelum blocking, misalnya file_get_contents atau curl_exec ke endpoint billing internal.
Metrik Queue dan Infrastruktur
Metrik menunjukkan antrean job aktif menumpuk di Redis/Beanstalkd, uptime worker turun-per-bagi saat timeout, dan CPU serta memori container naik secara perlahan hingga Full GC. Job retry tak terduga menyebabkan spike networking karena koneksi terus dibuka ulang. Ini memberikan sinyal bahwa job gagal segera dan diulang, bukan job baru yang lama.
Diagnosis dan Root Cause
Root cause ditemukan dari kombinasi konfigurasi timeout yang tidak memotong blocking I/O, dan kode job yang tidak menekan dependency eksternal. Dalam kasus nyata kami, job memanggil API pihak ketiga tanpa timeout sama sekali. PHP stream atau cURL default menunggu secara tidak terbatas, sehingga worker tidak pernah menutup proses sampai supervisor memaksa timeout global (misalnya 60 detik). Resource worker tetap aktif, memori teralokasi untuk buffer, dan tidak ada circuit breaker saat respons lama.
Akibatnya, ketika endpoint external tidak responsif, job menghabiskan 60 detik, lalu supervisor membunuh worker, lalu queue mengirim ulang job yang sama. Satu job blocking menyebabkan multiplier terhadap jumlah worker karena setiap job menunggu dan retry, sehingga PHP Worker queue secara kolektif menggulung resource.
Langkah Debugging Terperinci
- Profiling dan Tracing untuk Menentukan Blocking: Jalankan XHGui/XHProf pada job di lingkungan staging untuk melihat durasi span dan fungsi yang paling lama. Trace distributed memetakan panggilan eksternal—jika span HTTP lebih dari 15 detik, itu penyebab utama.
- Observasi Log Timeout: Pastikan log mencatat
timeoutdanretry. Kecerobohan seperti tidak mencatat body request membuat debugging susah; tambahkan logging status sebelum dan sesudah panggilan eksternal. - Isolasi Request: Jalankan job secara manual (php artisan queue:work --once) dengan endpoint simulasi lambat. Catat bagaimana timeout bekerja dan apakah worker tetap memakai resource setelah job selesai.
- Mengukur Dependency dengan Curl/HttpClient Timeout: Gunakan script kecil untuk memanggil endpoint yang sama dengan timeout 3 detik. Jika job tetap menunggu, berarti timeout belum aktif.
Debugger tip: jangan langsung menaikkan timeout supervisor. Sebaliknya, cari parameter timeout di config/queue.php atau supervisor.conf yang mengatur durasi maksimal job. Tanda-tanda lain termasuk backlog queue dan log Worker timeout yang sama persis di semua job.
Perbaikan Konfigurasi dan Refactor
Konfigurasi Timeout dan Retry Yang Ketat
Atur timeout HTTP dan worker agar tidak menunggu lama. Contoh implementasi cURL dalam job:
$client = new \GuzzleHttp\Client(['timeout' => 5, 'connect_timeout' => 2]);
$response = $client->post('https://internal.api/payment', ['json' => $payload]);Dengan cara ini job akan melempar exception setelah 5 detik, lalu worker bisa menangkapnya dan men-trigger retry terkontrol (misalnya retryAfter yang lebih panjang). Pastikan retry_after di config/queue.php lebih besar daripada timeout worker.
Refactor ke Asynchronous/Non-blocking
Untuk operasi I/O berat, gunakan queue terpisah atau sistem event (misalnya job yang menulis event ke Kafka lalu diproses consumer yang berbeda). Alternatif lain, job cukup menempatkan data ke table pending dan worker lain memanggil API secara batch. Ini mengurangi waktu blocking pada job utama.
Implementasi Circuit Breaker
Tambahkan circuit breaker sederhana di wrapper HTTP: jika job mendeteksi error berulang dari dependency dalam window waktu tertentu, skip panggilan dan laporkan ke monitoring. Gunakan library seperti php-circuit-breaker atau mekanisme kustom dengan Redis counter. Ini memutus job dari blocking saat dependency down.
Pengujian dan Monitoring Setelah Perbaikan
Validasi dengan test load di staging menggunakan data nyata. Jalankan skrip yang mensimulasikan job dan pantau metrik CPU, memory, antrean, serta timeouts. Pastikan job dengan dependency slow masih gagal cepat (timeout < retry).
Setelah deploy, tambahkan alert pada:
- Jumlah job failed yang meningkat drastis.
- Durasi span eksternal yang melebihi threshold (OpenTelemetry atau New Relic).
- Utilisasi worker > 80% selama lebih dari 2 menit.
Tambahkan log berupa job_start, job_end, dan dependency_response_time agar observability memadai. Gunakan dashboard Grafana untuk memvisualisasi retries dan job latency.
Pelajaran Utama dan Rekomendasi Pencegahan
Pelajaran utama: timeout job PHP tidak hanya soal parametri supervisor, tetapi menyangkut titik I/O yang paling lama. Tanpa timeout dependency dan monitoring yang memadai, job akan menumpuk resource dan merusak kestabilan. Rekomendasi pencegahan meliputi:
- Selalu set timeout eksplisit pada semua HTTP dan database call dalam job.
- Gunakan retries dengan backoff dan pastikan
retry_aftersinkron dengantimeout. - Monitoring job latency, retry rate, dan resource worker untuk mendeteksi pola gulung resource.
- Implementasikan circuit breaker atau fallback untuk dependency yang sering timeout.
Dengan pendekatan ini, job PHP tidak lagi mengumpulkan resource saat timeout, dan produksi dapat tetap stabil walau dependency mengalami gangguan.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!