Debugging backend saat decoding video menghabiskan RAM server biasanya tidak selesai hanya dengan menaikkan limit memory. Jika setelah migrasi library, player, atau pipeline codec worker mulai terkena OOM, antrean job macet, latensi API naik, dan container sering restart, masalahnya hampir selalu ada pada perubahan perilaku decode, buffering, atau paralelisme yang tidak terkontrol.

Dalam studi kasus ini, kita bahas pola investigasi yang praktis untuk layanan upload/transcoding video. Fokusnya bukan pada player desktop, tetapi pada backend yang menerima file, men-decode frame, men-generate thumbnail, dan melakukan transcoding. Konteksnya relevan dengan tren software media modern yang makin berat, serta kenyataan bahwa codec tertentu bisa memicu fallback atau perilaku pipeline yang lebih mahal dari perkiraan.

Studi kasus: gejala setelah migrasi pipeline media

Sebuah layanan backend memiliki alur seperti ini:

  1. User mengunggah video ke object storage.
  2. API membuat job untuk validasi metadata, ekstraksi thumbnail, dan transcoding.
  3. Worker queue memproses file menggunakan library pembungkus FFmpeg dan decoder tambahan.
  4. Hasil akhir disimpan kembali ke storage, lalu status dipublikasikan ke aplikasi.

Sebelum migrasi, sistem stabil. Setelah tim mengganti salah satu komponen—misalnya library metadata, wrapper transcoding, atau player/decoder yang dipakai untuk preview—muncul gejala berikut:

  • Worker OOM killed pada beban upload normal.
  • Antrean macet karena job besar memonopoli worker.
  • Latensi naik pada endpoint yang sebenarnya tidak berat, karena node kehabisan memory dan mulai swap atau dibunuh orchestrator.
  • Container restart berulang sehingga retry job makin memperparah backlog.

Gejala ini sering menipu. Tim biasanya curiga ke ukuran file video, padahal akar masalahnya adalah perubahan cara file dibaca, jumlah proses decode yang berjalan bersamaan, atau fallback codec yang membuat pipeline mengambil jalur paling boros memory.

Mengapa decoding video bisa tiba-tiba boros RAM

1. File dipreload penuh ke memory

Masalah klasik setelah migrasi adalah perubahan dari model streaming menjadi read-all-into-memory. Ini sering terjadi tanpa disadari karena API library baru menerima Buffer, byte[], atau BytesIO alih-alih path file atau stream.

Akibatnya, satu file video berukuran ratusan MB bisa dimuat penuh di RAM. Jika proses berikutnya juga membuat salinan tambahan untuk probing, thumbnail, dan upload ulang, penggunaan memory bisa berlipat.

Gejala khas:

  • RSS process naik tajam begitu job dimulai.
  • Lonjakan memory hampir sebanding dengan ukuran file input.
  • Heap aplikasi mungkin terlihat normal, tetapi total RSS tetap besar karena buffer native dan child process.

2. Decode paralel berlebihan

Setelah migrasi, worker mungkin memproses lebih banyak job bersamaan. Ini bisa berasal dari:

  • Jumlah worker dinaikkan, tetapi ukuran node tidak berubah.
  • Library baru membuka thread decode internal lebih agresif.
  • Satu job membuat banyak child process untuk probe, thumbnail, waveform, subtitle burn-in, dan transcode sekaligus.

Decoding video adalah pekerjaan berat. Beberapa file yang diproses paralel bisa membuat memory melonjak meskipun setiap proses terlihat "wajar" secara individual.

3. Fallback codec yang salah atau tidak efisien

Masalah lain adalah file tertentu tidak diproses melalui jalur hardware acceleration atau decoder yang diharapkan. Sistem lalu jatuh ke fallback software decode yang lebih berat. Dalam beberapa kasus, codec/container yang tidak didukung penuh membuat library melakukan decode tambahan hanya untuk membaca metadata atau seek ke frame tertentu.

Ini sangat relevan bila migrasi mengubah:

  • Urutan pemilihan codec/decoder.
  • Format preview yang dihasilkan.
  • Cara thumbnail diambil dari frame tertentu.
  • Library/player yang memaksa decode lebih banyak data untuk kompatibilitas.

Anda tidak perlu menebak detail lisensi codec untuk sampai ke akar masalah. Yang penting adalah memverifikasi apakah file yang sama sekarang diproses dengan jalur decode berbeda dibanding pipeline lama.

4. Cache buffer tidak dibatasi

Beberapa pipeline menyimpan frame, chunk, atau hasil probing di memory cache untuk mempercepat langkah berikutnya. Jika cache ini tidak punya batas ukuran, TTL, atau mekanisme eviction yang jelas, beban burst dari upload video akan langsung memakan RAM.

Masalah menjadi lebih parah bila:

  • Cache disimpan per worker process.
  • Object besar tidak segera dilepas.
  • Retry job membuat file yang sama masuk cache berkali-kali.
  • Tmpfs digunakan untuk file sementara tetapi dianggap "bukan RAM" padahal tetap mengonsumsi memory host/container.

Langkah investigasi yang efektif

Mulai dari gejala operasional, bukan dari asumsi

Urutan investigasi yang paling aman adalah:

  1. Pastikan benar ada tekanan memory, bukan CPU atau I/O semata.
  2. Identifikasi proses mana yang paling boros: aplikasi utama, child process decoder, atau sidecar.
  3. Temukan pola: file tertentu, codec tertentu, ukuran tertentu, atau jam tertentu.
  4. Bandingkan perilaku sebelum dan sesudah migrasi.

Metrik yang perlu diperiksa

Minimal, periksa metrik berikut di level host, container, dan aplikasi:

  • RSS dan working set per process/container.
  • OOM kill count dan restart count.
  • Queue depth, job age, dan retry rate.
  • P95/P99 job duration untuk probe, thumbnail, dan transcode.
  • Input file size dan distribusi codec/container.
  • Jumlah child process per job.
  • Open file descriptors bila ada indikasi kebocoran resource.
  • CPU throttling jika di container, karena throttling sering membuat job lebih lama menahan memory.

Kalau stack observability Anda sederhana, data dari top, ps, dmesg, dan log worker sudah cukup untuk mengonfirmasi pola awal.

Contoh sinyal dari log dan sistem

[worker] job_id=9f2c type=transcode video_id=vid_481 started input=s3://bucket/a.mp4 size_bytes=284391002 codec=h264 container=mp4
[worker] job_id=9f2c step=probe elapsed_ms=842 rss_mb=410
[worker] job_id=9f2c step=thumbnail elapsed_ms=3210 rss_mb=1180 child_procs=3
[worker] job_id=9f2c warning=buffer_cache_growth cache_bytes=536870912
[worker] job_id=9f2c step=transcode profile=preview elapsed_ms=12877 rss_mb=2140
[kernel] Memory cgroup out of memory: Killed process 271 (ffmpeg) total-vm:3281900kB anon-rss:1824400kB file-rss:1208kB
[queue] job_id=9f2c status=retry reason=worker_lost attempt=2

Log semacam ini berguna karena menghubungkan step pipeline dengan konsumsi memory. Tanpa korelasi itu, tim sering hanya melihat bahwa worker mati, tetapi tidak tahu mati pada tahap mana.

Bandingkan path lama vs path baru

Salah satu teknik paling efektif adalah menjalankan file uji yang sama melalui pipeline lama dan baru, lalu membandingkan:

  • Perintah decoder yang dipanggil.
  • Jumlah proses yang lahir.
  • Puncak RSS.
  • Durasi tiap tahap.
  • Apakah input dibaca sebagai stream atau buffer penuh.

Jika setelah migrasi wrapper baru ternyata menyalin file ke memory sebelum memanggil decoder native, perbedaannya biasanya langsung terlihat.

Teknik reproduksi agar bug bisa dibuktikan

Bug memory sulit diperbaiki bila hanya muncul di produksi. Buat reproduksi yang sengaja meniru kondisi berisiko:

Siapkan sampel video yang bervariasi

  • File kecil dan besar.
  • Codec umum dan file yang lebih sulit diproses.
  • Container berbeda, misalnya MP4, MOV, MKV.
  • File dengan resolusi tinggi dan variable bitrate.

Tujuannya bukan mengejar semua kombinasi, tetapi menemukan kelas input yang memicu lonjakan memory.

Jalankan beban konkuren terbatas namun terukur

Uji 1, 2, 4, lalu 8 job paralel. Catat memory puncak dan waktu selesai. Dengan cara ini Anda bisa melihat apakah masalah bersifat linear, eksponensial, atau dipicu oleh ambang tertentu.

Gunakan instrumentasi sederhana

Tambahkan log RSS sebelum dan sesudah setiap tahap. Untuk bahasa yang menjalankan child process, log juga PID proses turunannya. Contohnya:

job=9f2c stage=download rss_mb=220
job=9f2c stage=probe rss_mb=380
job=9f2c stage=thumbnail rss_mb=1020 child_pids=271,272
job=9f2c stage=transcode rss_mb=1890 child_pids=271

Jika runtime Anda memiliki profiler heap, gunakan untuk melihat object managed. Namun ingat: pada pipeline media, pemborosan sering ada di native memory, shared library, atau child process, bukan hanya heap aplikasi.

Empat root cause yang paling sering ditemukan

1. Buffer penuh saat download dari object storage

Pola bermasalah:

// Contoh antipola pseudocode
const bytes = await storageClient.downloadToBuffer(objectKey)
await processVideo(bytes)

Pendekatan ini terlihat sederhana, tetapi berbahaya untuk file besar. Jika processVideo juga menyalin buffer, memory bisa naik dua sampai tiga kali ukuran file.

Perbaikan yang lebih aman adalah menggunakan file sementara atau stream:

// Pseudocode yang lebih aman
const tmpPath = await storageClient.downloadToTempFile(objectKey)
await processVideoFromPath(tmpPath)
await cleanup(tmpPath)

Mengapa ini bekerja: decoder native umumnya lebih efisien bila membaca dari file path atau stream karena tidak memaksa aplikasi menahan seluruh isi file di memory managed.

Trade-off: Anda menukar penggunaan RAM dengan I/O disk. Pastikan disk sementara cukup cepat dan memiliki pembersihan file yang disiplin.

2. Thumbnail dan probing dijalankan paralel dengan transcode utama

Pola lain adalah satu job memulai semua langkah sekaligus demi mengejar throughput. Misalnya, probe metadata, ambil thumbnail, dan transcode preview berjalan bersamaan pada file yang sama.

Perbaikan:

  • Jalankan tahap berat secara berurutan bila memory node terbatas.
  • Pisahkan queue untuk light jobs dan heavy jobs.
  • Batasi concurrency worker khusus media.

Mengapa ini bekerja: puncak memory ditentukan oleh jumlah pekerjaan berat yang tumpang tindih, bukan hanya memory rata-rata.

3. Fallback decoder memproses lebih banyak data dari yang diperlukan

Misalnya, sistem lama hanya melakukan metadata probe, tetapi sistem baru diam-diam men-decode sebagian stream untuk memastikan kompatibilitas thumbnail atau preview. Hasilnya, langkah yang tadinya murah menjadi mahal.

Perbaikan:

  • Pastikan probe metadata memakai mode paling ringan yang tersedia di pipeline Anda.
  • Validasi file berdasarkan metadata dulu sebelum masuk decode penuh.
  • Log decoder path yang terpilih agar fallback bisa terlihat jelas di produksi.

Kesalahan umum: tim melihat semua file sebagai "MP4", padahal codec video/audio di dalamnya berbeda dan memicu jalur proses berbeda.

4. Cache frame/buffer tanpa batas

Pola umum:

  • Map/dictionary global menyimpan hasil frame extraction.
  • Cache thumbnail in-memory tanpa batas ukuran.
  • Chunk upload ulang disimpan terlalu lama menunggu langkah lain selesai.

Perbaikan:

  • Tetapkan batas total byte, bukan hanya jumlah item.
  • Gunakan TTL singkat untuk object besar.
  • Jangan cache hasil decode besar di process memory jika object storage atau disk lokal sudah cukup.

Perbaikan kode dan konfigurasi yang praktis

Batasi concurrency secara eksplisit

Jangan biarkan jumlah job media aktif ditentukan hanya oleh jumlah pesan di queue. Terapkan batas concurrency yang sadar memory.

# Contoh konsep konfigurasi worker
MEDIA_MAX_CONCURRENT_JOBS=2
MEDIA_MAX_CHILD_PROCESSES=1
MEDIA_JOB_TIMEOUT_SECONDS=1800

Nilai pastinya bergantung pada ukuran node dan profil video Anda. Yang penting, batas tersebut dihitung dari puncak memory per job, bukan tebakan.

Pisahkan worker media dari worker API umum

Jika worker video berbagi node dengan API, lonjakan decode akan mengganggu request biasa. Pisahkan deployment atau queue agar blast radius lebih kecil.

  • Queue api-default untuk pekerjaan ringan.
  • Queue media-heavy untuk decode/transcode.
  • Node pool berbeda jika perlu.

Gunakan file sementara dengan lifecycle yang jelas

Simpan input ke disk sementara, proses dari path, lalu hapus segera setelah selesai. Tambahkan finally block atau mekanisme cleanup yang tetap berjalan saat job gagal.

tmp_path = download_to_temp_file(object_key)
try:
    metadata = probe(tmp_path)
    generate_thumbnail(tmp_path)
    transcode(tmp_path)
finally:
    safe_delete(tmp_path)

Ini sederhana, tetapi sangat efektif untuk menghindari buffer besar menggantung di memory.

Tambahkan pembatas ukuran input dan early reject

Jangan terima semua file ke jalur yang sama. Buat guardrail seperti:

  • Batas ukuran file maksimum.
  • Batas durasi maksimum untuk tier tertentu.
  • Whitelist codec/container yang benar-benar didukung pipeline.
  • Fallback ke jalur async penuh untuk file berisiko tinggi.

Mengapa ini bekerja: file "aneh" sering menjadi outlier yang menghabiskan resource secara tidak proporsional.

Matikan cache memory yang tidak esensial

Jika cache hanya menghemat beberapa detik tetapi memakan ratusan MB saat burst, biasanya lebih baik dipindahkan ke disk atau dihapus sama sekali. Backend video lebih sering dibatasi oleh stabilitas resource daripada mikro-optimasi cache.

Guardrail operasional agar masalah tidak terulang

1. Budget memory per job

Tentukan perkiraan budget memory untuk satu job berdasarkan profil file yang umum. Gunakan budget ini untuk menghitung concurrency maksimum per node.

Contoh pemikiran operasional:

  • Node worker punya memory efektif sekian.
  • Sisakan headroom untuk OS, runtime, dan lonjakan kecil.
  • Sisanya dibagi dengan puncak memory aman per job.

Tidak perlu angka yang terlalu presisi; yang penting pendekatannya konservatif.

2. Alert pada gejala awal, bukan hanya saat OOM

Jangan menunggu container mati. Buat alert jika:

  • RSS worker melewati ambang tertentu selama beberapa menit.
  • Queue age meningkat terus.
  • Retry rate job video naik.
  • Restart container media bertambah.

3. Canary untuk migrasi library media

Setiap perubahan library decoder, wrapper transcode, atau pipeline preview sebaiknya dirilis bertahap. Kirim sebagian kecil traffic ke jalur baru, lalu bandingkan memory, error rate, dan durasi job.

Ini penting karena perubahan media pipeline sering terlihat kompatibel secara fungsional, tetapi mahal secara resource.

4. Simpan sampel file masalah

Setiap insiden perlu menghasilkan corpus file uji internal: file besar, file dengan codec yang memicu fallback, file dengan metadata aneh, dan file yang menyebabkan retry. Sampel ini menjadi regression suite untuk pipeline berikutnya.

Checklist debugging dan pencegahan

Saat insiden berlangsung

  • Konfirmasi apakah container benar-benar OOM killed.
  • Identifikasi langkah pipeline yang terakhir berjalan sebelum crash.
  • Lihat apakah memory naik seiring ukuran file atau jumlah job paralel.
  • Bandingkan jalur proses file yang gagal vs yang sukses.
  • Turunkan concurrency untuk menstabilkan sistem lebih dulu.
  • Hentikan retry tak terbatas pada job yang jelas memicu OOM.

Saat melakukan root cause analysis

  • Audit apakah input dibaca sebagai stream, file, atau buffer penuh.
  • Periksa child process decoder yang lahir dari satu job.
  • Validasi apakah ada fallback codec atau mode decode tak terduga.
  • Tinjau cache in-memory, tmpfs, dan object besar yang tidak dibersihkan.
  • Bandingkan implementasi dan metrik sebelum/sesudah migrasi.

Sebelum rilis perubahan pipeline berikutnya

  • Uji dengan corpus file bermasalah.
  • Uji beban paralel realistis, bukan hanya satu file.
  • Tambahkan log per tahap dengan memory snapshot.
  • Terapkan canary dan rollback plan.
  • Pastikan ada limit concurrency, timeout, dan cleanup file sementara.

Penutup

Dalam kasus debugging backend saat decoding video menghabiskan RAM server, solusi yang benar jarang berupa sekadar menambah ukuran instance. Penyebab paling umum justru perubahan perilaku pipeline: file dipreload ke memory, decode berjalan terlalu paralel, fallback codec mengambil jalur yang lebih mahal, atau cache buffer dibiarkan tanpa batas.

Pendekatan yang paling efektif adalah menghubungkan gejala operasional dengan tahap decode yang spesifik, lalu memperbaiki arsitektur di titik yang benar: streaming/file-based processing, pembatasan concurrency, observability per tahap, dan guardrail input. Dengan begitu, layanan upload/transcoding tetap stabil meski format media dan library terus berubah.

Catatan praktis: bila Anda baru selesai migrasi library media dan tiba-tiba melihat OOM, jangan mulai dari optimasi kecil. Pertama-tama pastikan apakah pipeline baru mengubah cara membaca file, jumlah child process, atau jalur codec yang dipilih. Tiga hal itu paling sering menjadi akar masalah.