Anatomi Sebuah Pesan
Pesan AMQP terdiri dari tiga bagian: properti (metadata baku), headers (metadata bebas), dan body (byte mentah).
┌─ Properties (baku AMQP) ────────────────────────┐
│ content_type : application/json │
│ delivery_mode : 2 (persistent) │
│ message_id : 8f2c-4a1b-… │
│ correlation_id : untuk RPC / pelacakan │
│ reply_to : nama queue balasan │
│ timestamp : 1785312000 │
│ expiration : "60000" (TTL per pesan, ms) │
│ priority : 0–9 │
│ app_id : layanan-pesanan │
├─ Headers (bebas) ───────────────────────────────┤
│ x-trace-id : 4bf92f3577b34da6 │
│ x-versi-skema : 2 │
│ x-percobaan : 0 │
├─ Body ──────────────────────────────────────────┤
│ {"pesanan_id":"P-1042","total":150000} │
└─────────────────────────────────────────────────┘delivery_mode = 2 saja tidak cukupAgar pesan benar-benar bertahan saat broker restart, ketiganya harus terpenuhi:
1. queue durable = true (queue ada lagi setelah restart)
2. delivery_mode = 2 (pesan ditulis ke disk)
3. publisher confirm (Anda tahu broker benar-benar menerimanya)Melewatkan salah satunya membuat dua lainnya sia-sia. Ini dibahas tuntas di bab 6.
Producer yang Benar
const amqp = require('amqplib');
const { randomUUID } = require('crypto');
let koneksi = null;
let channel = null;
async function siapkan() {
koneksi = await amqp.connect({
protocol: 'amqp',
hostname: process.env.RMQ_HOST || 'localhost',
port: 5672,
username: process.env.RMQ_USER,
password: process.env.RMQ_PASS,
vhost: '/',
heartbeat: 60, // deteksi koneksi mati
clientProperties: {
connection_name: 'layanan-pesanan-producer' // terlihat di UI
}
});
koneksi.on('error', (e) => console.error('koneksi error:', e.message));
koneksi.on('close', () => { channel = null; jadwalkanSambungUlang(); });
// ConfirmChannel: broker mengonfirmasi tiap publish
channel = await koneksi.createConfirmChannel();
await channel.assertExchange('pesanan', 'topic', {
durable: true,
arguments: { 'alternate-exchange': 'pesanan.tak-terkirim' }
});
// pesan yang tak bisa dirutekan dikembalikan ke sini
channel.on('return', (msg) => {
console.error('TIDAK TERKIRIM:', msg.fields.routingKey,
msg.content.toString());
});
}
async function kirimPeristiwa(peristiwa, wilayah, data) {
const routingKey = 'pesanan.' + wilayah + '.' + peristiwa;
const body = Buffer.from(JSON.stringify(data));
return new Promise((resolve, reject) => {
channel.publish('pesanan', routingKey, body, {
persistent: true, // = delivery_mode 2
mandatory: true, // kembalikan bila tak terkirim
contentType: 'application/json',
contentEncoding: 'utf-8',
messageId: randomUUID(), // dasar idempotensi konsumer
timestamp: Math.floor(Date.now() / 1000),
appId: 'layanan-pesanan',
headers: {
'x-versi-skema': 2,
'x-trace-id': ambilTraceId()
}
}, (err) => err ? reject(err) : resolve()); // publisher confirm
});
}Consumer yang Benar
async function mulaiKonsumsi() {
const channel = await koneksi.createChannel();
await channel.assertQueue('pesanan.email', {
durable: true,
arguments: {
'x-queue-type': 'quorum',
'x-dead-letter-exchange': 'pesanan.dlx',
'x-delivery-limit': 5
}
});
await channel.bindQueue('pesanan.email', 'pesanan', 'pesanan.*.dibuat');
// WAJIB: batasi berapa pesan boleh "in-flight" tanpa ack
await channel.prefetch(20);
await channel.consume('pesanan.email', async (msg) => {
if (msg === null) return; // konsumer dibatalkan broker
try {
const data = JSON.parse(msg.content.toString());
await prosesKirimEmail(data, msg.properties.messageId);
channel.ack(msg); // baru ack SETELAH berhasil
} catch (e) {
const bisaDiulang = e.sementara === true;
// requeue=false → jatuh ke DLX, bukan berputar tanpa henti
channel.nack(msg, false, bisaDiulang);
console.error('gagal memproses:', e.message);
}
}, {
noAck: false, // ack manual — WAJIB
consumerTag: 'email-worker-' + process.pid
});
}noAck: true berarti kehilangan pesanDengan noAck: true (disebut juga auto-ack), broker menganggap pesan selesai begitu terkirim ke soket — sebelum konsumer Anda sempat memprosesnya.
Bila proses konsumer mati di tengah jalan, pesan itu sudah hilang dari broker dan tidak akan pernah kembali. Pakai noAck: true hanya untuk data yang memang boleh hilang, seperti metrik sampling.
Serialisasi Payload
| Format | Kelebihan | Kekurangan |
|---|---|---|
| JSON | Mudah dibaca manusia, universal | Boros ukuran, tanpa skema |
| Protobuf / Avro | Ringkas, ada skema, evolusi terkendali | Perlu registry, sulit dibaca saat debug |
| MessagePack | Ringkas, mirip JSON | Kurang umum |
Untuk sebagian besar tim, JSON dengan header x-versi-skema adalah titik awal yang tepat. Yang penting bukan formatnya, melainkan aturan evolusinya: hanya menambah field opsional, tidak pernah mengubah arti field yang sudah ada, dan tidak menghapus field sebelum semua konsumer diperbarui.
connection_name muncul di UI manajemen dan di rabbitmqctl list_connections. Saat sebuah broker melayani 400 koneksi dan salah satunya berperilaku aneh, nama seperti layanan-pesanan-producer mengubah investigasi setengah jam menjadi setengah menit.
Push vs Pull
basic.consume (push) | basic.get (pull) | |
|---|---|---|
| Cara kerja | Broker mendorong pesan | Klien meminta satu per satu |
| Throughput | Tinggi | Sangat rendah |
| Pakai untuk | Hampir semua kasus | Skrip administratif, mengintip isi queue |
basic.get memerlukan satu perjalanan bolak-balik jaringan per pesan. Memakainya di dalam loop adalah kesalahan performa klasik — throughput bisa 100 kali lebih rendah dibanding basic.consume.