Irbisa · Шпаргалка

Брокеры сообщений: Kafka и RabbitMQ

Партиции, consumer groups и оффсеты, репликация и acks, гарантии доставки, идемпотентный продюсер, retention и compaction, exchange и подтверждения в RabbitMQ, схемы событий

Middle-разработчик · 19 вопросов

  1. 01

    Объясните, что такое топик, партиция, consumer group и оффсет в Kafka и чем Kafka отличается от RabbitMQ.

    Kafka устроена как распределённый append-only лог: топик разбит на партиции, каждая из которых является упорядоченным логом, а консьюмеры группы делят партиции между собой и хранят прогресс как оффсет на каждую партицию.

  2. 02

    Какие типы exchange есть в RabbitMQ и как биндинги и routing key решают, в какие очереди попадёт сообщение?

    Продюсер публикует сообщение в exchange, а не в очередь, и exchange копирует его в каждую очередь, чей биндинг подходит.

    func setup(ch *amqp.Channel) error {	// durable: the exchange, the queue and the binding survive a broker restart	if err := ch.ExchangeDeclare("orders", amqp.ExchangeTopic, true, false, false, false, nil); err != nil {		return err	}	q, err := ch.QueueDeclare("billing", true, false, false, false, nil)…
  3. 03

    Что делал ZooKeeper для Kafka и что изменилось, когда Kafka 4.0 перешла на KRaft?

    ZooKeeper хранил метаданные кластера (брокеры, топики, лидеров партиций, конфиги, ACL) и выбирал контроллер.

  4. 04

    Почему Kafka такая быстрая на обычных дисках и что дают append-only лог, page cache, батчинг и zero-copy?

    Kafka превращает любую нагрузку в крупный последовательный ввод-вывод: каждая партиция представляет собой append-only файл, чтение и запись идут через page cache ОС, записи передаются сжатыми батчами, а брокер отдаёт лог консьюмерам через sendfile, не копируя данные через JVM.

  5. 05

    Сколько Kafka хранит сообщения и чем retention по времени или размеру отличается от log compaction?

    Kafka удаляет данные по возрасту или размеру, но никогда не потому, что их прочитали: retention.ms (по умолчанию 7 дней) и retention.bytes удаляют старые сегменты целиком.

  6. 06

    Где Kafka хранит закоммиченные оффсеты consumer group, что делает auto.offset.reset и как переиграть топик с заданного момента?

    Закоммиченные оффсеты хранятся во внутреннем компактируемом топике __consumer_offsets, по одной записи на группу, топик и партицию.

    // A group reader: StartOffset applies only to partitions with no committed offset.func newBillingReader(brokers []string) *kafka.Reader {	return kafka.NewReader(kafka.ReaderConfig{		Brokers:     brokers,		GroupID:     "billing",		Topic:       "orders",…
  7. 07

    События одного заказа приходят не по порядку, а рост consumer group с 12 до 24 участников не уменьшил лаг. 12 партиций, сообщения без ключа. Почему и то и другое?

    Kafka упорядочивает сообщения только внутри партиции, а сообщения без ключа разлетаются по всем двенадцати, поэтому два события одного заказа попадают в разные партиции и обгоняют друг друга.

    // wrong: &kafka.Writer{Addr: ..., Topic: "orders"} with keyless messages;// the default balancer is RoundRobin, so one order's events scatter. // right: key by the entity whose order matters, with a balancer that hashes itvar w = &kafka.Writer{	Addr:         kafka.TCP("kafka-1:9092", "kafka-2:9092"),…
  8. 08

    Брокер упал, и часть подтверждённых сообщений пропала. Как acks, фактор репликации, min.insync.replicas и ISR решают, переживёт ли запись сбой?

    С acks=1 лидер подтверждает запись раньше, чем её скопировал хоть один фолловер, поэтому, когда этот лидер падает, у нового лидера записи нет.

    // Topic: three copies, and writes are refused while fewer than two are in sync.func createPayments(ctx context.Context, c *kafka.Client) error {	res, err := c.CreateTopics(ctx, &kafka.CreateTopicsRequest{Topics: []kafka.TopicConfig{{		Topic:             "payments",		NumPartitions:     12,		ReplicationFactor: 3,…
  9. 09

    От чего защищает enable.idempotence у продюсера Kafka, как он сохраняет порядок при ретраях и какие дубли не останавливает?

    Он не даёт собственным ретраям продюсера записать батч дважды: брокер выдаёт продюсеру id, каждый батч несёт порядковый номер (sequence number) в своей партиции, и лидер отбрасывает батч, который у него уже есть.

  10. 10

    Каждый HTTP-запрос публикует одно событие через этот kafka.Writer и выполняется чуть больше секунды. Почему так и какой компромисс задают BatchTimeout, BatchSize и Async?

    var w = &kafka.Writer{	Addr:     kafka.TCP("kafka-1:9092", "kafka-2:9092"),	Topic:    "orders",	Balancer: &kafka.Hash{},} // Called once per POST /orders, after the order is saved.func publishCreated(ctx context.Context, o Order) error {	return w.WriteMessages(ctx, kafka.Message{		Key:   []byte(o.ID),		Value: o.JSON(),	})}

    WriteMessages блокируется, пока его батч не отправлен, а writer отправляет батч, когда тот набирает BatchSize (по умолчанию 100 сообщений) или когда истекает BatchTimeout (по умолчанию 1 секунда).

  11. 11

    Какой Go-клиент Kafka вы бы выбрали (franz-go, segmentio/kafka-go, confluent-kafka-go, sarama) и чем придётся платить за каждый?

    Для нового сервиса franz-go: чистый Go, активная разработка и поддержка почти всего протокола, включая идемпотентность и транзакции.

  12. 12

    Этот консьюмер коммитит каждый оффсет, а потом отдаёт сообщение пулу воркеров. После вытеснения пода сотни событий молча пропали. Куда они делись?

    func consume(ctx context.Context, r *kafka.Reader, jobs chan<- kafka.Message) error {	for {		m, err := r.FetchMessage(ctx)		if err != nil {			return err		}		if err := r.CommitMessages(ctx, m); err != nil {			return err		}		jobs <- m // 16 goroutines read jobs and call handle(ctx, m)	}}

    Коммит до завершения работы делает консьюмер at-most-once: группа уже записала эти оффсеты как обработанные, поэтому всё, что стояло в очереди или выполнялось, умерло вместе с подом и больше не будет доставлено.

  13. 13

    После переключения брокера консьюмер платежей переигрывает минуту сообщений, и часть клиентов платит дважды. Продукт хочет exactly-once доставку. Что вы строите?

    Доставку at-least-once плюс идемпотентный консьюмер.

    func handle(ctx context.Context, db *sql.DB, r *kafka.Reader, m kafka.Message) error {	tx, err := db.BeginTx(ctx, nil)	if err != nil {		return err	}	defer tx.Rollback() // no-op after Commit…
  14. 14

    Одно битое сообщение роняет консьюмер, он ретраит его бесконечно, и вся партиция стоит. Как обрабатывать poison-сообщения?

    Отделите постоянные ошибки от временных, ретраите только временные с ограниченным backoff, а остальное паркуйте в dead-letter топик с текстом ошибки и исходным оффсетом, после чего коммитьте и идите дальше.

    func process(ctx context.Context, r *kafka.Reader, dlq *kafka.Writer, m kafka.Message) error {	var err error	for attempt := 0; attempt < 3; attempt++ {		if err = handle(ctx, m); err == nil {			return r.CommitMessages(ctx, m)		}…
  15. 15

    Что нужно, чтобы сообщение RabbitMQ пережило рестарт брокера, и что добавляют publisher confirms и quorum-очереди?

    Очередь должна быть durable, сообщение опубликовано как persistent (DeliveryMode: 2), а продюсер должен дождаться confirm, потому что только confirm означает, что брокер принял сообщение.

    // Once at startup: a replicated queue and a channel in confirm mode.func setup(ch *amqp.Channel) error {	_, err := ch.QueueDeclare("payments", true, false, false, false, amqp.Table{		amqp.QueueTypeArg: amqp.QueueTypeQuorum, // replicated with Raft, always durable	})	if err != nil {…
  16. 16

    Найдите баг: когда под воркера убивают посреди задачи, сообщения RabbitMQ, которые он обрабатывал, пропадают, хотя очередь durable, а сообщения persistent.

    func work(ctx context.Context, ch *amqp.Channel) error {	msgs, err := ch.Consume("thumbnails", "", true, false, false, false, nil)	if err != nil {		return err	}	for d := range msgs {		if err := render(ctx, d.Body); err != nil {			log.Printf("render failed: %v", err)		}	}	return nil}

    Consume вызван с autoAck, равным true, поэтому брокер считает сообщение обработанным в момент, когда записал его в сокет, и удаляет его.

  17. 17

    Несколько консьюмеров читают одну очередь RabbitMQ. Кто получает какое сообщение, что происходит с порядком и когда нужен single active consumer?

    Консьюмеры одной очереди конкурируют: каждое сообщение достаётся ровно одному из них, по очереди и в пределах prefetch каждого консьюмера.

  18. 18

    Должно ли событие OrderUpdated нести весь заказ или только его id и чем каждый вариант оборачивается для консьюмеров?

    Тонкое событие (order_id и версия) только уведомляет, поэтому каждый консьюмер обращается обратно в сервис заказов, а это связанность, нагрузка и гонки.

    type OrderUpdated struct {	OrderID    string `json:"order_id"`	Version    int64  `json:"version"` // grows with every change at the source	Status     string `json:"status"`	TotalCents int64  `json:"total_cents"`}…
  19. 19

    Продюсеры и консьюмеры события деплоятся каждый по своему графику. Как безопасно поменять схему события и что проверяет schema registry?

    Вносите только совместимые изменения и в таком порядке, чтобы работала каждая запущенная версия: добавляйте поля со значениями по умолчанию и никогда не переименовывайте, не меняйте тип и не переиспользуйте поле.