Kurangi yak shaving saat menangani queue dan cache produksi berarti menahan diri agar tidak langsung membenahi hal-hal sekunder sebelum bottleneck utama terbukti. Dalam insiden nyata, gejala seperti job menumpuk, retry tak terkendali, cache stale, lock macet, atau eksekusi ganda sering membuat engineer lompat ke banyak arah: tuning worker, mengganti driver cache, menambah retry, atau merapikan code path yang terlihat janggal. Masalahnya, langkah-langkah itu sering tidak menyentuh penyebab utama.

Tujuan artikel ini sederhana: memberi kerangka kerja operasional agar tim backend dan DevOps bisa mendiagnosis dulu, menambal secukupnya, lalu memperbaiki dengan data. Fokusnya bukan teori umum, tetapi langkah yang bisa dipakai saat produksi sedang bermasalah.

Apa itu yak shaving dalam konteks queue dan cache

Dalam operasi produksi, yak shaving terjadi ketika tim terdistraksi oleh pekerjaan sampingan yang tampak berguna, tetapi tidak mengurangi dampak insiden saat ini. Contohnya:

  • Job menumpuk karena satu dependency eksternal lambat, tetapi tim malah sibuk mengganti format payload job.
  • Retry membanjir karena timeout downstream, tetapi solusi pertama yang dicoba justru menambah jumlah worker.
  • Cache stale muncul karena invalidasi tidak konsisten, tetapi tim malah menurunkan TTL semua key tanpa memetakan alur write.
  • Lock macet karena proses crash tanpa release, tetapi investigasi melebar ke refactor abstraksi locking.
  • Duplikasi eksekusi muncul karena job tidak idempoten, tetapi yang diubah duluan adalah monitoring dashboard.

Semua aktivitas di atas mungkin berguna di masa depan, tetapi saat insiden berlangsung, prioritasnya adalah: menghentikan kerusakan, mengukur dampak, membuktikan bottleneck, lalu memilih perubahan dengan risiko paling kecil.

Gejala nyata yang paling sering memicu distraksi

1. Job menumpuk di queue

Gejala utamanya backlog naik terus, latency pemrosesan membengkak, dan SLA async gagal. Penyebab yang umum:

  • Worker terlalu sedikit dibanding laju masuk job.
  • Ada satu jenis job lambat yang memonopoli worker.
  • Dependency eksternal lambat atau error.
  • Job saling menunggu resource yang sama, misalnya lock atau koneksi database.
  • Poison message: satu payload selalu gagal dan terus di-retry.

2. Retry tak terkendali

Retry yang tidak dibatasi bisa mengubah kegagalan kecil menjadi banjir beban. Gejalanya meliputi kenaikan tajam jumlah attempt, CPU worker naik, queue makin panjang, dan dependency downstream makin tertekan. Akar masalahnya sering ada pada kombinasi timeout yang terlalu pendek, retry tanpa backoff, atau klasifikasi error yang salah.

3. Cache stale atau tidak konsisten

Cache stale biasanya bukan soal "cache lambat", tetapi soal invalidasi dan model konsistensi. Gejala yang terlihat:

  • User melihat data lama setelah update.
  • Node A dan B memberi respons berbeda untuk entity yang sama.
  • Hit rate tinggi, tetapi banyak komplain data tidak akurat.

Penyebabnya bisa berupa write-through yang tidak konsisten, invalidasi yang gagal diam-diam, key namespace rancu, atau multi-step update yang tidak atomik.

4. Lock macet

Distributed lock sering dipakai untuk mencegah kerja ganda, tetapi masalah muncul ketika lease terlalu panjang, proses crash, atau release tidak dijamin di semua jalur eksekusi. Hasilnya adalah antrian pekerjaan berhenti tanpa error yang jelas.

5. Duplikasi eksekusi

Jika satu job diproses lebih dari sekali, akar masalahnya sering ada pada setidaknya satu dari hal ini:

  • Worker crash setelah side effect terjadi tetapi sebelum ack.
  • Visibility timeout atau lease lock lebih pendek dari durasi kerja nyata.
  • Publisher mengirim job yang sama lebih dari sekali.
  • Konsumen tidak idempoten.

Kesalahan umum di sini adalah menganggap "queue harus exactly-once". Dalam banyak sistem, asumsi yang lebih aman adalah at-least-once delivery, sehingga handler harus tahan terhadap duplikasi.

Kerangka prioritas diagnosis: tahan dorongan untuk membenahi semuanya

Saat insiden aktif, gunakan urutan prioritas berikut.

1. Tentukan dampak bisnis lebih dulu

  • Fungsi apa yang gagal: checkout, notifikasi, sinkronisasi data, billing, atau internal batch?
  • Apakah kegagalan bersifat total, parsial, atau hanya lambat?
  • Apakah ada risiko kehilangan data, duplikasi transaksi, atau hanya keterlambatan?

Ini penting agar tim tidak menghabiskan 45 menit memperbaiki metrik yang buruk tetapi dampaknya kecil, sementara jalur kritis tetap rusak.

2. Buktikan bottleneck dominan

Jangan mulai dari solusi. Mulailah dari pertanyaan: komponen mana yang benar-benar membatasi throughput atau menyebabkan inkonsistensi? Untuk queue dan cache, kandidat utamanya biasanya:

  • Broker atau backend queue.
  • Worker process.
  • Database utama.
  • Dependency eksternal seperti API atau object storage.
  • Cache store dan mekanisme invalidasi.
  • Locking layer.

3. Pisahkan gejala primer dan sekunder

Contoh: backlog queue sering hanya gejala. Penyebab primernya bisa latency database, lock contention, atau API pihak ketiga yang timeout. Jika gejala primer belum jelas, menambah worker bisa memperparah beban di dependency yang sebenarnya sudah jenuh.

4. Utamakan perubahan yang reversibel

Selama triage, pilih tindakan yang mudah dibatalkan:

  • Menurunkan concurrency per jenis job.
  • Mem-pause consumer tertentu.
  • Mengalihkan job non-kritis ke dead-letter atau delay queue.
  • Memperpanjang timeout secara terbatas.
  • Menonaktifkan invalidasi agresif yang terbukti salah.

Hindari refactor besar, migrasi komponen, atau perubahan arsitektur di tengah insiden kecuali tidak ada opsi lain.

Triage 30–60 menit yang bisa dipakai tim

Langkah berikut dirancang untuk insiden queue, cache, worker, locking, dan consistency yang sedang berlangsung.

Menit 0–10: stabilkan dan batasi blast radius

  1. Tetapkan incident owner dan satu jalur komunikasi.
  2. Bekukan perubahan non-esensial agar sinyal tidak makin bising.
  3. Identifikasi fungsi bisnis yang terdampak dan putuskan apakah perlu mode degradasi.
  4. Jika retry membanjir, pertimbangkan pause sementara consumer tertentu atau turunkan concurrency.
  5. Jika duplikasi berbahaya, prioritaskan penghentian source job atau side effect non-idempoten sebelum throughput dikejar.

Menit 10–25: kumpulkan metrik minimum yang wajib ada

Jangan langsung baca semua dashboard. Ambil metrik yang menjawab empat pertanyaan: apakah masukannya naik, prosesnya melambat, error meningkat, atau hasilnya tidak konsisten?

  • Queue depth / backlog: jumlah job tertunda per queue.
  • Oldest message age / queue latency: umur job tertua atau waktu tunggu sebelum diproses.
  • Incoming rate vs processing rate: job masuk per menit dibanding job selesai per menit.
  • Success, failure, retry rate: dipisah per jenis job.
  • Worker concurrency dan utilization: jumlah worker aktif, idle, busy, restart.
  • Job duration percentile: p50, p95, p99 jika tersedia.
  • Dependency latency/error: database, API eksternal, cache store.
  • Cache hit/miss rate dan key invalidation error bila ada.
  • Lock acquisition time, timeout, dan stale lock count.
  • Duplicate execution signal: request ID atau job ID yang muncul lebih dari sekali.

Menit 25–40: bentuk hipotesis, lalu buktikan satu per satu

Contoh hipotesis yang baik:

  • “Backlog naik karena satu jenis job melakukan panggilan ke API eksternal yang timeout.”
  • “Retry membanjir karena semua error diperlakukan retriable, termasuk validation error.”
  • “Cache stale terjadi karena update database berhasil tetapi event invalidasi tidak terkirim.”
  • “Lock macet karena worker crash dan lease lock lebih panjang dari durasi failover.”

Hindari hipotesis kabur seperti “Redis mungkin lambat” kecuali ada bukti dari latency, saturation, atau error rate.

Menit 40–60: pilih patch operasional berisiko rendah

Setelah satu hipotesis dominan terbukti, lakukan patch yang paling kecil risikonya dan paling cepat menurunkan dampak. Contohnya:

  • Memisahkan job lambat ke queue khusus agar job kritis tetap berjalan.
  • Menonaktifkan retry otomatis untuk error yang jelas tidak retriable.
  • Menambah backoff dan jitter untuk mencegah retry storm.
  • Memperbaiki atau menghapus lock stale secara selektif.
  • Mengganti sementara pembacaan cache ke fallback database untuk subset endpoint tertentu.
  • Mengaktifkan idempotency guard di level penyimpanan hasil atau side effect.

Tujuan triage bukan membuat sistem sempurna dalam satu jam. Tujuannya adalah mengembalikan kendali, menurunkan dampak, dan menghindari perubahan yang memperbesar ketidakpastian.

Metrik yang wajib dicek dan cara membacanya

Queue depth tanpa oldest age sering menipu

Queue depth tinggi belum tentu buruk jika throughput tetap lebih tinggi dari laju masuk dan umur job masih sesuai SLA. Yang lebih penting adalah umur job tertua atau waktu tunggu aktual. Jika depth stabil tetapi oldest age naik, ada starvation atau head-of-line blocking.

Retry rate harus dibaca bersama error class

Retry 1.000 kali tidak memberi informasi jika tidak dibagi berdasarkan penyebab. Pisahkan minimal menjadi:

  • Transient: timeout, connection reset, rate limit.
  • Permanent: validation error, payload rusak, resource tidak ditemukan permanen.
  • Unknown: exception tak terklasifikasi.

Jika error permanent ikut di-retry, itu masalah desain kontrol alur, bukan kapasitas.

Worker CPU tinggi tidak selalu berarti perlu tambah worker

CPU tinggi bisa berarti serialisasi payload besar, kompresi berlebihan, loop retry lokal, atau polling terlalu agresif. Menambah worker tanpa tahu bottleneck bisa menaikkan kontensi database atau lock.

Cache hit rate tinggi tidak menjamin benar

Hit rate bagus sering menipu saat data stale. Tambahkan indikator kualitas cache, misalnya perbandingan versi data, umur entri saat dibaca, atau sampling validasi terhadap source of truth.

Lock timeout perlu dibaca bersama durasi kerja nyata

Jika lease lock 30 detik tetapi pekerjaan sering butuh 45 detik, duplikasi atau lock expiry prematur bisa terjadi. Sebaliknya, lease terlalu panjang membuat stale lock lebih menyiksa saat worker mati.

Contoh alur investigasi: job menumpuk dan retry tak terkendali

Misalkan ada queue send-notification. Gejalanya: backlog naik tajam, worker busy terus, alert error dari provider notifikasi meningkat, dan beberapa user menerima pesan ganda.

Langkah 1: lihat distribusi job dan durasi

Dari dashboard, terlihat 80% backlog berasal dari satu tipe job yang memanggil provider eksternal. Durasi job melonjak, sementara queue lain normal. Ini memberi sinyal bahwa masalahnya terlokalisasi, bukan gangguan umum broker.

Langkah 2: klasifikasikan error

Log menunjukkan campuran timeout dan error 4xx dari provider. Timeout layak di-retry dengan backoff. Sebagian 4xx kemungkinan permanen dan seharusnya langsung gagal tanpa retry berulang.

Langkah 3: hentikan amplifikasi

Patch cepat yang masuk akal:

  • Turunkan concurrency job notifikasi agar provider tidak makin tertekan.
  • Tambahkan backoff dan jitter untuk timeout.
  • Matikan retry untuk error yang pasti permanen.
  • Aktifkan dead-letter queue untuk payload bermasalah.

Langkah 4: tangani duplikasi

Karena ada pesan ganda, cek apakah side effect notifikasi punya kunci idempoten. Jika belum, tambahkan guard sederhana berbasis unique key pada kombinasi event ID dan channel tujuan.

function handleNotification(job) {
  const key = `notif:${job.eventId}:${job.recipient}:${job.channel}`;

  if (idempotencyStore.exists(key)) {
    return;
  }

  idempotencyStore.put(key, { status: 'processing' }, { ttlSeconds: 3600 });

  try {
    provider.send(job.payload);
    idempotencyStore.put(key, { status: 'done' }, { ttlSeconds: 86400 });
  } catch (err) {
    idempotencyStore.delete(key); // hati-hati: sesuaikan dengan model retry Anda
    throw err;
  }
}

Contoh di atas bukan solusi universal, tetapi menunjukkan prinsip penting: anggap handler bisa dipanggil lebih dari sekali.

Langkah 5: cek apakah backlog mulai turun

Setelah patch, lihat tiga sinyal: processing rate naik, retry rate turun, dan oldest message age mulai membaik. Jika backlog tetap naik, kemungkinan bottleneck ada di tempat lain, misalnya database atau koneksi jaringan keluar.

Cache stale: fokus pada alur write, bukan hanya TTL

Ketika data stale muncul, respons yang paling sering keliru adalah menurunkan TTL semua cache key. Ini bisa menurunkan durasi stale, tetapi sering menaikkan beban database dan belum tentu menyelesaikan akar masalah.

Pertanyaan diagnosis yang lebih tepat

  • Apakah write ke source of truth dan invalidasi cache berada dalam alur yang sama?
  • Jika invalidasi berbasis event, apakah event bisa hilang, tertunda, atau diproses tidak berurutan?
  • Apakah ada lebih dari satu key yang merepresentasikan entity yang sama?
  • Apakah pembacaan memakai cache-aside, write-through, atau pola lain yang tidak konsisten antarlayanan?

Patch cepat yang aman

  • Invalidasi key yang diketahui salah, bukan flush seluruh namespace kecuali benar-benar darurat.
  • Tambahkan versi pada key untuk menghindari bentrok format lama dan baru.
  • Untuk endpoint kritis, gunakan fallback terkontrol ke database bila versi cache tidak cocok.
function getUserProfile(userId) {
  const version = profileVersionStore.get(userId) || 1;
  const cacheKey = `user-profile:${userId}:v${version}`;

  const cached = cache.get(cacheKey);
  if (cached) return cached;

  const profile = db.loadUserProfile(userId);
  cache.set(cacheKey, profile, { ttlSeconds: 300 });
  return profile;
}

function updateUserProfile(userId, changes) {
  db.updateUserProfile(userId, changes);
  profileVersionStore.increment(userId);
}

Pendekatan versioned key seperti ini membantu menghindari race pada invalidasi, walau tetap ada trade-off: key lama akan hidup sampai TTL habis dan konsumsi memori bisa naik.

Lock macet dan duplikasi eksekusi: desain aman lebih penting daripada lock “sempurna”

Masalah umum pada lock

  • Lease terlalu panjang sehingga stale lock lama hilang.
  • Lease terlalu pendek sehingga pekerjaan valid kehilangan lock di tengah proses.
  • Release lock tidak ada di jalur exception.
  • Lock dipakai untuk menutupi handler yang seharusnya idempoten.

Prinsip operasional yang lebih aman

  • Gunakan lock untuk mengurangi kontensi, bukan satu-satunya pagar terhadap duplikasi.
  • Buat side effect penting idempoten bila memungkinkan.
  • Pastikan ada observabilitas: siapa pemegang lock, sejak kapan, dan lease berapa lama.
  • Sediakan prosedur manual untuk membersihkan stale lock dengan verifikasi.

Membersihkan lock secara manual tanpa verifikasi adalah anti-pattern. Jika worker sebenarnya masih aktif tetapi lambat, menghapus lock bisa memicu eksekusi ganda.

Kapan menambal, kapan refactor

Tambal dulu jika

  • Insiden masih aktif dan dampak bisnis belum terkendali.
  • Akar masalah sudah cukup jelas, dan ada perubahan kecil yang bisa mengurangi dampak.
  • Perubahan bisa dibalik cepat jika salah.
  • Tim belum punya cukup data untuk desain jangka panjang.

Refactor setelah insiden jika

  • Masalah berasal dari asumsi dasar yang salah, misalnya mengandalkan exactly-once padahal sistem at-least-once.
  • Retry, timeout, dan idempotency tersebar tidak konsisten di banyak layanan.
  • Model cache dan invalidasi sulit dipahami atau tidak bisa diuji.
  • Queue kritis dan non-kritis masih bercampur sehingga starvation berulang.

Pertanyaan keputusan yang berguna

  • Apakah patch ini mengurangi dampak dalam jam ini?
  • Apakah patch ini menambah utang operasional besar?
  • Jika insiden serupa terulang minggu depan, apakah patch ini masih membantu?
  • Apakah refactor yang diusulkan benar-benar menyasar akar masalah yang terbukti?

Anti-pattern operasional yang sering membuang waktu

  • Menambah worker tanpa melihat downstream. Ini sering hanya memindahkan kemacetan ke database atau API eksternal.
  • Memperbesar retry limit saat error rate naik. Jika penyebabnya permanent error, Anda hanya memperpanjang antrean.
  • Flush semua cache sebagai respons pertama. Dampaknya bisa lebih buruk daripada stale terbatas.
  • Menghapus semua lock tanpa verifikasi. Ini berisiko memicu side effect ganda.
  • Mengubah banyak parameter sekaligus. Sulit tahu mana yang membantu dan mana yang merusak.
  • Membuka refactor besar saat insiden aktif. Risiko perubahan lebih tinggi daripada manfaat langsung.
  • Mengandalkan log mentah tanpa korelasi ID. Sulit membedakan retry, duplikasi, dan alur normal.

Checklist pasca-insiden

Setelah sistem stabil, jangan berhenti di “sudah normal”. Gunakan checklist ini agar perbaikan tidak hilang jadi niat.

1. Rekonstruksi timeline

  • Kapan gejala pertama muncul?
  • Kapan alert aktif?
  • Kapan backlog, retry, atau stale data mulai meningkat?
  • Perubahan apa yang terjadi sebelum insiden?

2. Dokumentasikan akar masalah dan mekanisme amplifikasi

Bedakan pemicu awal dari mekanisme yang memperparah. Contoh: provider lambat adalah pemicu, retry tanpa backoff adalah amplifier.

3. Tambahkan guardrail

  • Retry policy yang membedakan transient dan permanent error.
  • Dead-letter queue untuk payload bermasalah.
  • Idempotency key untuk side effect penting.
  • Per-queue concurrency limit.
  • Alert pada oldest message age, bukan hanya queue depth.
  • Monitoring stale lock dan cache invalidation failure.

4. Tutup celah observabilitas

  • Tambahkan correlation ID dari producer ke consumer.
  • Log attempt number, job ID, dan klasifikasi error.
  • Catat durasi dependency eksternal di level job.
  • Tambahkan metrik untuk lock acquisition dan lock expiration.

5. Putuskan item refactor yang benar-benar perlu

Jangan jadikan postmortem sebagai daftar keinginan tanpa prioritas. Pilih refactor yang menghilangkan pola insiden, misalnya pemisahan queue per kelas pekerjaan, standardisasi idempotency, atau redesain invalidasi cache.

Penutup

Insiden queue dan cache di produksi jarang gagal karena satu komponen saja. Yang sering memperburuk keadaan adalah yak shaving: tim memperbaiki hal yang tampak relevan tetapi belum terbukti menjadi bottleneck utama. Cara menguranginya adalah dengan disiplin terhadap urutan kerja: ukur dampak, buktikan bottleneck, hentikan amplifikasi, pilih patch reversibel, lalu refactor berdasarkan data pasca-insiden.

Jika tim Anda sering menghadapi job menumpuk, retry tak terkendali, cache stale, lock macet, atau duplikasi eksekusi, gunakan artikel ini sebagai runbook awal. Bukan untuk menggantikan pemahaman sistem, tetapi untuk menjaga fokus saat tekanan produksi membuat semua hal terasa mendesak sekaligus.