Queue

Decoupling, buffering, dan worker pool sebagai alat dasar pemrosesan asinkron.

Daftar isi

Topik ini membahas queue sebagai alat dasar pemrosesan asinkron dalam desain sistem, yaitu perannya sebagai alat decoupling antarkomponen, penyangga lonjakan trafik, dan tempat kerja bagi worker pool. Pembahasan ditutup dengan simulasi producer-consumer yang dapat dieksekusi untuk membandingkan queue tanpa batas dan queue berbatas, sehingga konsekuensi pilihan kapasitas terlihat pada angka nyata.

Masalah yang dijawab queue#

Pemrosesan sinkron mengaitkan nasib dua komponen pada satu koneksi. Aplikasi web yang memproses unggahan berkas secara langsung harus menunggu konversi format selesai, dan kegagalan tahap konversi berarti kegagalan permintaan itu juga. Ketika trafik melonjak, jumlah permintaan yang menumpuk pada server aplikasi naik secepat laju kedatangan, sedangkan kapasitas pengerjaan tetap. Pengguna yang menunggu tidak mendapat informasi berapa lama pengerjaan berlangsung, dan satu layanan yang lambat dapat memperlambat seluruh alur. Pola seperti ini memaksa kapasitas setiap komponen diukur terhadap puncak beban, bukan terhadap beban rata-rata.

Queue memutus keterkaitan itu dengan menempatkan tugas di tengah, sehingga pengirim hanya perlu menyimpan tugas dan penerima mengerjakannya pada laju masing-masing. Tugas tidak hilang ketika penerima sedang sibuk, karena tugas tersimpan pada queue sampai diambil. Pengirim tidak perlu mengetahui jumlah penerima yang berjalan maupun alamatnya. Pemisahan peran seperti ini disebut decoupling, yakni pelepasan ketergantungan langsung antarkomponen, sedangkan penampungan lonjakan pada queue disebut buffering.

Producer, consumer, dan worker pool#

Tiga peran muncul pada setiap penggunaan queue. Producer adalah pihak yang memasukkan pesan atau tugas ke queue, sedangkan consumer adalah pihak yang mengambil dan mengerjakannya. Di antara keduanya, queue menyimpan pesan dengan prinsip masuk pertama, keluar pertama (FIFO), sehingga pesan yang datang lebih dulu biasanya dikerjakan lebih dulu. Perangkat lunak yang menyediakan queue secara permanen, menyimpan isinya ke penyimpanan, dan melayani banyak producer sekaligus disebut message broker, contohnya RabbitMQ, Apache Kafka, dan Amazon SQS.

Untuk menaikkan throughput, yaitu jumlah tugas yang diselesaikan per satuan waktu, pengerjaan tidak dilakukan oleh satu consumer tunggal, melainkan oleh sekumpulan worker identik yang menarik tugas dari queue yang sama. Kumpulan ini disebut worker pool. Setiap tugas hanya diambil satu worker pada satu waktu, sehingga menambah worker secara langsung menaikkan kapasitas pengerjaan tanpa mengubah producer. Skala penambahan bersifat horizontal, misalnya dari dua worker menjadi lima worker saat beban naik, lalu dikurangi kembali saat beban turun. Worker yang gagal pada satu tugas dapat melanjutkan ke tugas berikutnya, sedangkan nasib tugas yang gagal berulang kali ditentukan oleh jaminan pengiriman yang dibahas pada topik Semantik Pengiriman.

Simulasi queue berbatas dan tanpa batas#

Simulasi berikut membandingkan dua konfigurasi pada pola kedatangan yang sama. Producer menghasilkan dua permintaan per tick pada kondisi normal, kemudian empat permintaan per tick selama lima tick ketika beban melonjak. Jumlah worker adalah dua, dan setiap worker menyelesaikan satu permintaan per tick. Konfigurasi pertama memakai queue tanpa batas, sedangkan konfigurasi kedua memakai queue berbatas dengan kapasitas empat. Jalankan berkas berikut untuk melihat perilaku keduanya.

python queue_sim.py
"""Simulasi producer-consumer dengan queue tanpa batas dan queue berbatas.

Satu tick mewakili satu satuan waktu. Producer menghasilkan sejumlah
permintaan pada tick tertentu sesuai DAFTAR_DATANG, lalu setiap worker
menyelesaikan satu permintaan per tick sehingga kapasitas proses sistem
adalah JUMLAH_WORKER permintaan per tick.
"""

from collections import deque

DAFTAR_DATANG = [2, 2, 4, 4, 4, 2, 2, 0, 0, 0]  # permintaan baru per tick
JUMLAH_WORKER = 2


class Antrian:
    """Queue FIFO sederhana dengan kapasitas opsional."""

    def __init__(self, kapasitas=None):
        self.kapasitas = kapasitas
        self.isi = deque()
        self.ditolak = 0

    def masukkan(self, item):
        """Memasukkan item; mengembalikan False bila antrian sudah penuh."""
        if self.kapasitas is not None and len(self.isi) >= self.kapasitas:
            self.ditolak += 1
            return False
        self.isi.append(item)
        return True

    def ambil(self):
        """Mengambil item paling depan, atau None bila kosong."""
        return self.isi.popleft() if self.isi else None


def jalankan(kapasitas):
    antrian = Antrian(kapasitas)
    diproses = 0
    puncak = 0
    label = "tanpa batas" if kapasitas is None else str(kapasitas)
    print(f"=== Kapasitas {label} ===")
    print("tick  datang  panjang  diproses  ditolak")
    for t, datang in enumerate(DAFTAR_DATANG, start=1):
        for _ in range(datang):
            antrian.masukkan(f"tugas-{t}")
        for _ in range(JUMLAH_WORKER):
            if antrian.ambil() is not None:
                diproses += 1
        puncak = max(puncak, len(antrian.isi))
        print(f"{t:>4}  {datang:>6}  {len(antrian.isi):>7}  {diproses:>8}  {antrian.ditolak:>7}")
    print(f"Sisa antrean {len(antrian.isi)}, puncak {puncak}, diproses {diproses}, ditolak {antrian.ditolak}")
    print()


jalankan(None)
jalankan(4)
text Keluaran
=== Kapasitas tanpa batas ===
tick  datang  panjang  diproses  ditolak
   1       2        0         2        0
   2       2        0         4        0
   3       4        2         6        0
   4       4        4         8        0
   5       4        6        10        0
   6       2        6        12        0
   7       2        6        14        0
   8       0        4        16        0
   9       0        2        18        0
  10       0        0        20        0
Sisa antrean 0, puncak 6, diproses 20, ditolak 0

=== Kapasitas 4 ===
tick  datang  panjang  diproses  ditolak
   1       2        0         2        0
   2       2        0         4        0
   3       4        2         6        0
   4       4        2         8        2
   5       4        2        10        4
   6       2        2        12        4
   7       2        2        14        4
   8       0        0        16        4
   9       0        0        16        4
  10       0        0        16        4
Sisa antrean 0, puncak 2, diproses 16, ditolak 4

Kedua skenario menerima 20 permintaan yang sama, tetapi konsekuensinya berbeda. Pada queue tanpa batas, permintaan menumpuk hingga panjang queue mencapai puncak enam pada tick 5 sampai 7, dan permintaan yang datang pada tick 7 menunggu hingga tiga tick sebelum selesai dikerjakan. Penyangga itu tidak menghilangkan keterlambatan, melainkan hanya memindahkannya dari pengguna ke antrean tunggu. Pada queue berbatas kapasitas empat, empat permintaan ditolak saat antrean penuh, yaitu dua pada tick 4 dan dua pada tick 5, sedangkan permintaan lain tetap terlayani dan panjang queue tidak pernah melebihi dua setelah puncak. Penolakan tersebut bukan kehilangan diam-diam, karena producer menerima sinyal bahwa sistem penuh dan dapat mencoba lagi nanti.

Pilihan kapasitas pada production#

Pada production, kapasitas queue adalah keputusan desain, bukan efek samping. Queue tanpa batas memindahkan seluruh tekanan ke memori, karena lonjakan yang bertahan lama membuat backlog tumbuh tanpa henti sampai proses kehabisan memori dan berhenti. Queue berbatas memaksa keputusan eksplisit ketika penuh, misalnya menunda, menolak, atau membuang permintaan paling ringan. Kapasitas yang layak biasanya diturunkan dari throughput worker dan durasi lonjakan yang ingin diserap, bukan dipilih secara asal. Banyak broker juga menyimpan isi queue ke penyimpanan yang tahan kegagalan, sehingga tugas yang sudah diterima tidak hilang ketika broker dimulai ulang, dan sifat inilah yang membedakan queue dari cache yang isinya boleh hilang.

Kesalahan umum#

  1. Menganggap queue menaikkan throughput. Queue hanya memindahkan waktu tunggu, dan jika consumer lebih lambat daripada producer, backlog tumbuh beserta latensi ujung ke ujungnya. Perbaikannya adalah menambah worker atau mempercepat pengerjaan, bukan memperbesar queue.
  2. Membiarkan queue tanpa batas pada production. Lonjakan yang panjang menghabiskan memori broker, dan kegagalan broker memengaruhi seluruh jalur pemesanan. Perbaikannya adalah kapasitas batas, alarm backlog, dan strategi penolakan yang disengaja.
  3. Mengasumsikan urutan selalu terjaga. Banyak worker yang menarik dari queue yang sama membuat dua permintaan dari satu pengguna dapat dikerjakan bersamaan atau terbalik urutannya. Apabila urutan penting, kelompokkan pesan berdasarkan kunci partisi sehingga satu kunci selalu berada pada satu partisi.

Ringkasan#

  • Queue memisahkan producer dan consumer, menyerap lonjakan trafik lewat buffering, dan memungkinkan kapasitas pengerjaan ditambah dengan worker pool.
  • Queue tanpa batas memindahkan tekanan ke memori dan menyembunyikan kelebihan beban, sedangkan queue berbatas memaksa keputusan eksplisit berupa menunda, menolak, atau membuang permintaan.
  • Kapasitas queue diturunkan dari throughput worker serta durasi lonjakan yang ingin diserap, dan alarm pada ambang tingginya lebih berguna daripada ukuran yang sebesar mungkin.

Referensi#