Sebuah benchmark butuh lebih dari sekadar angka throughput. Hardware, ukuran payload, ukuran batch, warmup, dan aktivitas lain di mesin itu memengaruhi hasilnya. Tanpa kondisi-kondisi itu, sebuah perbandingan susah ditafsirkan.
Setelah membangun stock ticker yang sama dua kali, saya ingin membandingkan Kafka dan Redis Streams di mesin saya. Saya menulis test harness untuk mengukur throughput dan latency.
Artikel ini menjelaskan harness tersebut beserta batasannya. Di sini nggak ada hasil pengukuran throughput atau latency. Hasil dari laptop saya bakal butuh tes lanjutan sebelum bisa jadi dasar keputusan di production.
Dua jalan masuk ke function yang sama
Ada dua entry point yang memanggil runBenchmark(). CLI menerima jumlah message dan ukuran payload, dengan default 15.000 message dan 64 byte. Dashboard mengirim nilai yang dipilih ke sebuah API route, yang memanggil function yang sama di server:
export async function POST(req: Request): Promise<Response> {
const body = await req.json();
const result = await runBenchmark(body.messages ?? 15000, body.payloadBytes ?? 64);
return Response.json(result);
}
Kedua jalur meng-clamp input-nya sebelum melakukan apa pun:
const n = Math.max(1000, Math.min(50000, Math.floor(messages)));
const bytes = Math.max(8, Math.min(4096, Math.floor(payloadBytes)));
const m = Math.min(1200, n); // how many of those n messages get a latency sample
Jumlah message berkisar dari 1.000 sampai 50.000, dan ukuran payload dari 8 sampai 4.096 byte. Harness mengukur latency untuk paling banyak 1.200 message supaya durasi fase itu terbatas.
Preset di dashboard berkisar dari 5.000 sampai 50.000 message dan dari 16 sampai 1.024 byte. Semuanya tetap di dalam batas input yang sama.
Apa yang diukur, dan dengan urutan apa
Dua dimensi, masing-masing dijalankan terhadap kedua sistem, secara berurutan:
sequenceDiagram
participant H as Harness
participant K as Kafka
participant R as Redis
H->>K: throughput run "batched producer.send"
H->>K: latency run "producer + fresh consumer"
H->>R: throughput run "pipelined XADD"
H->>R: latency run "XADD + blocking XREAD"
Note over H: one phase at a time, never concurrent
Harness menjalankan kedua fase Kafka lebih dulu, baru kemudian kedua fase Redis. Run yang berurutan menghindari persaingan langsung antara kedua tes untuk CPU, Docker network bridge, dan akses disk. Aktivitas lain di laptop saya tetap bisa memengaruhi hasil keduanya.
Throughput produce
Secara struktur, kedua function throughput melakukan hal yang sama: kirim n message dalam batch berisi 1.000, ukur waktu seluruh loop, lalu bagi.
// Kafka: batched producer.send(), keyed across all 6 partitions
const producer = kafka.producer({ idempotent: true });
const BATCH = 1000;
const start = performance.now();
for (let i = 0; i < n; i += BATCH) {
const end = Math.min(n, i + BATCH);
const messages = [];
for (let j = i; j < end; j++) messages.push({ key: `k${j % 6}`, value });
await producer.send({ topic, messages });
}
const perSec = Math.round(n / ((performance.now() - start) / 1000));
// Redis: pipelined XADD, same batch size
const redis = createRedis();
const BATCH = 1000;
const start = performance.now();
for (let i = 0; i < n; i += BATCH) {
const end = Math.min(n, i + BATCH);
const pipe = redis.pipeline();
for (let j = i; j < end; j++) pipe.xadd(key, "*", "p", value);
await pipe.exec();
}
const perSec = Math.round(n / ((performance.now() - start) / 1000));
Run Kafka memutar key k0 sampai k5 di sebuah topic dengan 6 partition. Run ini sudah mencakup paralelisme di level partition yang dijelaskan sebelumnya. Run Redis menulis ke satu stream key.
Perbedaan ini membatasi perbandingannya. Eksperimen lanjutan bisa membagi traffic Redis ke beberapa stream key, dengan hash tag untuk key yang saling berkaitan. Harness yang sekarang nggak melakukan itu.
Producer Kafka memakai idempotent: true dan acks=all. Setup Compose default-nya punya satu node dan membuat topic benchmark dengan replicationFactor: 1. Jadi, "all in-sync replicas" artinya satu replica. Tes ini nggak mengukur acknowledgment di beberapa broker.
Latency end to end
Throughput mengukur jumlah message per satuan waktu. Latency mengukur waktu antara operasi kirim dan saat message diterima. Untuk mengukur latency, saya menjalankan producer dan consumer secara bersamaan. Saya mencatat waktu kirim di setiap message dan menghitung waktu yang berlalu saat message itu diterima.
Untuk Kafka, harness membuat consumer group baru (bench-lat-<timestamp>) dan subscribe ke topic-nya. Harness menunggu 900ms sebelum message pertama. Delay tetap ini mengurangi race saat startup di tes saya, tapi nggak membuktikan kalau consumer-nya udah siap. Setiap message membawa timestamp kirimnya di sebuah header:
await producer.send({
topic,
messages: [{ key: `k${i % 6}`, value, headers: { t: String(performance.now()) } }],
});
// in the consumer's eachMessage handler
const sent = Number(message.headers?.t?.toString() ?? "0");
latencies.push(performance.now() - sent);
Writer Redis mengirim entry dengan XADD. Reader memakai blocking connection terpisah. Kalau nggak, blocking read bakal menunda command lain di connection itu. Reader mengirim call XREAD ... BLOCK 1000 mentah dari $ (cuma entry baru). Writer menaruh waktu kirim di sebuah field entry:
await writer.xadd(key, "*", "t", String(performance.now()), "p", value);
// read loop, dedicated blocking connection
const res = await reader.call("XREAD", "COUNT", "500", "BLOCK", "1000", "STREAMS", key, lastId);
// for each entry returned: latencies.push(performance.now() - Number(map.t))
Pengukuran Redis memakai XREAD, bukan XREADGROUP, jadi pengelolaan pending entries list nggak ikut terukur. Kedua loop latency punya timeout 20 detik. Keduanya mengirim satu message setiap kali, jadi nggak mengukur latency batch.
Percentile, dihitung apa adanya
Begitu harness punya daftar sampel latency, satu function bersama dijalankan di hasil kedua sistem:
function percentiles(samples: number[]): Percentiles {
const s = [...samples].sort((a, b) => a - b);
const at = (q: number) => s[Math.min(s.length - 1, Math.floor(q * s.length))];
const sum = s.reduce((a, b) => a + b, 0);
return {
count: s.length,
avgMs: sum / s.length,
p50Ms: at(0.5),
p95Ms: at(0.95),
p99Ms: at(0.99),
minMs: s[0],
maxMs: s[s.length - 1],
};
}
Function at(q) mengurutkan sampel lalu memilih index floor(q * length). Ini estimasi percentile berbasis index. Hasilnya bisa beda dari interpolasi dan dari konvensi nearest rank, yang memakai ceil(q * length) - 1 untuk quantile positif. Pakai estimator yang sama saat membandingkan hasil.
Harness nggak membuang sampel warmup sama sekali. Biaya startup, misalnya setup connection dan koordinasi consumer group, tetap ikut di mean dan percentile. Dengan sampel yang sedikit, beberapa message yang lambat bisa mengubah mean secara signifikan.
Kenapa bentuk hasilnya cenderung berbeda
Perbedaan arsitektur ini bisa menjelaskan hasil tes. Tapi perbedaan ini nggak menggantikan pengukuran dari deployment yang dituju.
graph TD
subgraph Kafka
KP["Producer batches records"] --> KL["Leader log segment (page cache)"]
KL --> KF["fsync per broker flush policy"]
KF --> KA["acks=all: wait for in-sync replicas"]
end
subgraph Redis
RP["Client pipelines XADD"] --> RE["Single-threaded event loop"]
RE --> RM["In-memory stream (RAM)"]
RM --> RA["Async AOF append (everysec)"]
end
Kafka meng-append record ke log segment dan memakai page cache dari sistem operasi. Biasanya broker nggak menyinkronkan setiap record ke disk. Karena itu, setting replication dan acknowledgment penting untuk durability.
Harness saya mengirim batch berisi 1.000 record per call producer.send(). Batching ini mengurangi overhead request. Jangan berasumsi setting producer Java seperti linger.ms dan batch.size berlaku juga di KafkaJS.
Redis menyimpan data stream di memory dan mengeksekusi command secara berurutan di main command thread-nya. Persistence tergantung konfigurasi. Setup Compose saya memakai AOF dengan appendfsync everysec, yang membuka celah kehilangan data setelah terjadi failure. Pipelining mengurangi pertukaran di network dengan mengirim beberapa command sekaligus.
Kedua sistem punya biaya storage dan replication yang berbeda. Ukur keduanya dengan payload, batching, dan setting durability yang dibutuhkan. Arsitektur saja nggak bisa memastikan mana yang bakal lebih cepat untuk workload itu.
Catatan yang selalu saya sertakan di setiap run
Ada beberapa hal yang bukan cakupan benchmark ini, dan jangan sampai disalahartikan begitu:
- Topologi lokal: setup Compose default punya satu node KRaft dengan topic benchmark di
replicationFactor: 1. Profile tiga broker yang terpisah memakaireplicationFactor: 3untuk tes leader failure. Benchmark ini nggak memakai profile itu maupun Redis Cluster. - Overhead client: KafkaJS dan ioredis berbagi proses Node.js dengan harness. Garbage collection,
JSON.stringify, dan scheduling promise memengaruhi pengukuran. Hasilnya mengukur "Kafka lewat KafkaJS" dan "Redis lewat ioredis," termasuk biaya dari client-nya. - Network lokal: client terhubung lewat port mapping Docker ke
localhost. Setup ini nggak merepresentasikan delay network atau failure antar host, availability zone, atau region. - Single run: harness nggak punya fase warmup atau beban berkelanjutan yang diulang. Setiap run mengukur satu mesin dalam kondisinya saat itu.
Tes untuk deployment sebaiknya memvariasikan concurrency producer dan consumer, jumlah broker, dan sharding Redis. Tes itu juga sebaiknya mencakup perilaku sinkronisasi disk yang dibutuhkan dan payload di atas 4.096 byte kalau aplikasinya butuh. Jalankan cukup lama untuk melihat efek garbage collection dan page cache pada tail latency.
Membaca output tanpa membohongi diri sendiri
Saya melihat distribusinya dulu sebelum rata-ratanya. p50 yang dekat dengan mean dan p99 yang jauh menandakan ada request lambat di bagian tail. Saya membandingkan jumlah message dan ukuran payload yang sama, lalu mengulang run-nya. Satu run 15.000 message dengan browser dan IDE yang terbuka cuma menguji setup lokal saya.
Pakai pengukuran dari hardware, payload, dan beban yang representatif sebelum mengambil keputusan deployment. Harness lokal membantu mengeksplorasi perilaku sistem, tapi hasilnya butuh konteks tambahan itu.
Post terakhir memakai perilaku dari seri ini untuk membandingkan di mana masing-masing sistem cocok dipakai.

Memuat komentar...