Kafka, RabbitMQ and Pulsar measured side by side on ordering, replay and consumer scaling by driving the three client libraries directly, plus an operational-footprint measurement of each broker's own distribution. Nine tests, three brokers started without Docker, and every number in the documentation regenerated by scripts/run-all.sh.
67 lines
3.1 KiB
Markdown
67 lines
3.1 KiB
Markdown
prev: [Three models](01-three-models.md) · [README](../README.md) · next: [Replay](03-replay.md)
|
|
|
|
# 2. Ordering
|
|
|
|
Every broker here is FIFO with one producer and one consumer. The question that matters is what
|
|
survives the second consumer, because that is the first thing you add.
|
|
|
|
## Kafka: ordered within a partition, and a key picks the partition
|
|
|
|
Twelve records, three keys, three partitions
|
|
([`docs/output/kafka-ordering.txt`](output/kafka-ordering.txt)):
|
|
|
|
```
|
|
F -> partition [2] values [2, 5, 8, 11]
|
|
A -> partition [1] values [1, 4, 7, 10]
|
|
D -> partition [0] values [3, 6, 9, 12]
|
|
```
|
|
|
|
Each key's values come back in the order they were produced. The global sequence does not: the
|
|
consumer drains one partition's buffer before moving to the next, so the delivered order is
|
|
2, 5, 8, 11, 1, 4, 7, 10, 3, 6, 9, 12.
|
|
|
|
That is the whole Kafka ordering guarantee, and it is usually enough, because "in order" nearly
|
|
always means "in order per customer / per account / per device" rather than globally.
|
|
|
|
**A detail worth stealing.** The keys in that transcript are D, A and F, not A, B and C. With
|
|
three partitions, murmur2 sends A, B *and* C all to partition 1. Picking three obvious keys to
|
|
demonstrate partitioning would have produced a transcript in which global order was accidentally
|
|
preserved. Before you conclude that your keys spread evenly, compute
|
|
`Utils.toPositive(Utils.murmur2(key)) % partitions` for the ones you actually use.
|
|
|
|
## RabbitMQ: FIFO per queue, and no key at all
|
|
|
|
One consumer sees the queue in order. Two consumers do not
|
|
([`docs/output/rabbit-ordering.txt`](output/rabbit-ordering.txt)):
|
|
|
|
```
|
|
one queue, one consumer : [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]
|
|
one queue, two consumers : [2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 1, 12]
|
|
```
|
|
|
|
Message 1 went to the slower consumer and finished tenth. There is no key and no partition, so
|
|
there is nothing to pin related messages to one consumer. Ordering across related messages in
|
|
RabbitMQ means one queue and one consumer — which means no horizontal scaling of that queue —
|
|
or the consistent-hash exchange, which is a plugin and effectively reintroduces partitions by
|
|
hand.
|
|
|
|
## Pulsar: the subscription type decides, per subscriber
|
|
|
|
Two consumers on a `Shared` subscription and two on `Key_Shared`, same topic, same messages
|
|
([`docs/output/pulsar-ordering.txt`](output/pulsar-ordering.txt)):
|
|
|
|
```
|
|
Shared A -> [consumer-1, consumer-2] B -> [consumer-1, consumer-2] C -> [consumer-1, consumer-2]
|
|
Key_Shared A -> [consumer-2] B -> [consumer-1] C -> [consumer-2]
|
|
```
|
|
|
|
Under `Shared`, every key was handled by both consumers, so two messages with the same key can be
|
|
in flight at once and no ordering claim survives. Under `Key_Shared`, each key went to exactly one
|
|
consumer — Kafka's guarantee, but chosen by the subscriber rather than fixed by a partition count
|
|
someone picked when the topic was created, and recomputed as consumers join and leave.
|
|
|
|
That last property is the strongest single argument for Pulsar: two teams reading the same topic
|
|
can make different ordering-versus-throughput trades without negotiating.
|
|
|
|
next: [Replay](03-replay.md)
|