Skip to main content

ACKS (acknowledgemenets) summary

Oczywiście! Temat acks (od acknowledgements, czyli potwierdzenia) jest absolutnie fundamentalny dla zrozumienia gwarancji dostarczenia wiadomości w Kafce. Wyjaśnijmy to dokładnie.

Co to jest acks?​

acks to konfiguracja producenta, która określa, na jak "mocne" potwierdzenie dostarczenia wiadomości czeka producent, zanim uzna ją za wysłaną pomyślnie. Innymi słowy, definiuje ona poziom trwałości (durability) zapisu.

Pomyśl o tym jak o wysyłaniu listu poleconego. Możesz:

  • Wrzucić list do skrzynki i odejść (nie wiesz, czy dotarł).
  • Poczekać na potwierdzenie od listonosza, że odebrał list (wiesz, że jest w systemie pocztowym).
  • Poczekać na zwrotne poświadczenie odbioru podpisane przez adresata (masz pewność, że dotarł do celu).

acks w Kafce działa na podobnej zasadzie, ale w kontekście brokera (lidera partycji) i jego replik.


Możliwe wartości acks i jak działają​

Są trzy główne wartości dla acks:

1. acks=0 (Najszybszy, "Fire and Forget")​

  • Jak działa: Producent wysyła wiadomość do brokera (lidera partycji) i nie czeka na żadne potwierdzenie. Od razu po wysłaniu wiadomości do bufora sieciowego, uznaje ją za dostarczoną.
  • Dostarczanie: To jest najmniej pewny tryb. Wiadomość może zostać utracona, jeśli:
  • Broker (lider) ulegnie awarii, zanim zdąży zapisać wiadomość.
  • Wystąpi problem z siecią w trakcie wysyłania.
  • Gwarancje: "At most once" (co najwyżej raz). Wiadomości mogą być utracone, ale nie zostaną zduplikowane przez ponowienia (bo ich nie ma).
  • Zalety:
  • Ekstremalnie wysoka przepustowość (throughput).
  • Minimalne opóźnienie (latency).
  • Wady:
  • Wysokie ryzyko utraty danych.
  • Kiedy używać? Gdy utrata pojedynczych wiadomości jest akceptowalna, a liczy się przede wszystkim szybkość. Przykłady: zbieranie metryk, logowanie o niskim priorytecie, śledzenie kliknięć na stronie (gdzie utrata kilku zdarzeń nie jest krytyczna).

2. acks=1 (Domyślny, Zbalansowany)​

  • Jak działa: Producent wysyła wiadomość i czeka na potwierdzenie tylko od lidera partycji. Lider zapisuje wiadomość w swoim logu i odsyła potwierdzenie (ack) do producenta. Proces replikacji na pozostałe brokery (followers) odbywa się w tle, a producent na to nie czeka.
  • Dostarczanie: Jest to kompromis między wydajnością a bezpieczeństwem. Wiadomość może zostać utracona w jednym, konkretnym scenariuszu:
  1. Lider zapisuje wiadomość i wysyła ack.
  2. Producent otrzymuje ack i uznaje wiadomość za dostarczoną.
  3. Lider ulega awarii, zanim zdążył zreplikować wiadomość do swoich followerów.
  4. Nowym liderem zostaje jeden z followerów, który tej wiadomości nie ma. Wiadomość przepada.
  • Gwarancje: "At least once" (co najmniej raz), jeśli producent ma włączone ponowienia (retries). Jeśli wiadomość zostanie wysłana, ale ack nie dotrze do producenta (np. z powodu problemu sieciowego), producent spróbuje wysłać ją ponownie, co może prowadzić do duplikatów.
  • Zalety:
  • Dobry kompromis między opóźnieniem a trwałością.
  • Znacznie bezpieczniejszy niż acks=0.
  • Wady:
  • Możliwa (choć rzadka) utrata danych w przypadku awarii lidera.
  • Możliwe duplikaty przy ponowieniach.
  • Kiedy używać? W większości standardowych przypadków, gdzie niewielkie ryzyko utraty danych jest akceptowalne, a wydajność jest ważna.

3. acks=all (lub acks=-1) (Najbezpieczniejszy, "Zero Data Loss")​

  • Jak działa: Producent wysyła wiadomość i czeka na potwierdzenie od lidera, ale lider wyśle to potwierdzenie dopiero wtedy, gdy wiadomość zostanie pomyślnie zreplikowana na minimalną liczbę replik w synchronizacji (In-Sync Replicas - ISR).
  • Dostarczanie: To jest najbezpieczniejszy tryb, który gwarantuje, że wiadomość nie zostanie utracona, o ile co najmniej jedna z replik w synchronizacji pozostaje sprawna.
  • Gwarancje: "At least once" (z ponowieniami) lub, przy odpowiedniej konfiguracji, "Exactly once" (dokładnie raz).
  • Zalety:
  • Maksymalna trwałość i gwarancja braku utraty danych.
  • Wady:
  • Największe opóźnienie (latency), ponieważ producent musi czekać na cały cykl replikacji.
  • Niższa przepustowość.
  • Kiedy używać? W systemach krytycznych, gdzie utrata nawet jednej wiadomości jest niedopuszczalna. Przykłady: transakcje finansowe, przetwarzanie zamówień, kluczowe zdarzenia biznesowe.

Kluczowe powiązane konfiguracje​

Sam acks=all nie wystarczy. Aby osiągnąć prawdziwą trwałość, musisz skonfigurować kilka rzeczy razem:

Po stronie Producenta (w application.properties/yml lub w kodzie):​

  1. retries:
  • Liczba prób ponownego wysłania wiadomości w przypadku błędu (np. braku ack). Dla acks=1 i acks=all powinna być ustawiona na wartość większą od zera (np. Integer.MAX_VALUE), aby przetrwać przejściowe problemy z brokerem.
  1. enable.idempotence=true (Wprowadzone w Kafka 0.11):
  • TO JEST NOWOCZESNE I ZALECANE PODEJŚCIE!
  • Włączenie tej opcji automatycznie ustawia acks=all, retries na dużą wartość i max.in.flight.requests.per.connection=5 (w nowszych wersjach, wcześniej 1).
  • Gwarantuje, że ponowienia nie spowodują duplikatów. Producent przypisuje każdej wiadomości unikalny numer sekwencyjny, a broker śledzi te numery, odrzucając duplikaty. Zapewnia to gwarancję "exactly once" na poziomie partycji.
  • Zalecenie: Jeśli zależy Ci na braku utraty danych i braku duplikatów, po prostu ustaw enable.idempotence=true.

Po stronie Brokera/Topicu (konfiguracja na serwerze Kafki):​

  1. replication.factor:
  • Ile kopii (lider + followery) każdej partycji ma istnieć w klastrze. Dla środowisk produkcyjnych zaleca się wartość co najmniej 3.
  1. min.insync.replicas (min.isr):
  • To jest partner dla acks=all! Definiuje, ile replik (wliczając lidera) musi być "w synchronizacji" i potwierdzić zapis, aby lider mógł wysłać ack do producenta.
  • Przykład: replication.factor=3, min.insync.replicas=2.
  • Gdy producent z acks=all wysyła wiadomość, lider musi zapisać ją u siebie i poczekać na potwierdzenie od co najmniej jednego followera. Dopiero wtedy wysyła ack do producenta.
  • Jeśli w danym momencie dostępny jest tylko lider (np. dwie pozostałe repliki miały awarię), to liczba ISR spadnie do 1. Ponieważ 1 < min.insync.replicas, producent otrzyma błąd NotEnoughReplicasException i będzie próbował wysłać wiadomość ponownie. To chroni przed scenariuszem opisanym w acks=1.

Złota zasada trwałości: acks=all + min.insync.replicas > 1 (zazwyczaj replication.factor - 1).


Przykłady kodu i konfiguracji (Spring Boot)​

Konfiguracja w application.yml​

spring:
kafka:
producer:
# --- Konfiguracja dla MAKSYMALNEJ TRWAŁOŚCI (zalecana) ---
acks: all
# Włączenie idempotencji upraszcza sprawę. Nie trzeba już ustawiać retries i max.in.flight...
properties:
enable.idempotence: true
# Dla starszych wersji Kafki, lub gdy nie używamy idempotencji:
# retries: 5
# max.in.flight.requests.per.connection: 1 # Zapobiega zmianie kolejności przy ponowieniach

# --- Konfiguracja dla MAKSYMALNEJ WYDAJNOŚCI (ryzykowna) ---
# acks: 0

# --- Konfiguracja ZBALANSOWANA (domyślna) ---
# acks: 1
# retries: 3 # Domyślnie retries jest włączone

# Wspólne konfiguracje
bootstrap-servers: localhost:9092
key-serializer: org.apache.kafka.common.serialization.StringSerializer
value-serializer: org.springframework.kafka.support.serializer.JsonSerializer

Konfiguracja w kodzie (jak w Twoim przykładzie z RoutingKafkaTemplate)​

Jeśli chcesz dynamicznie tworzyć producentów z różnymi ustawieniami acks, robisz to dokładnie tak, jak w przykładzie z serializatorami:

@Bean
public RoutingKafkaTemplate routingTemplate(GenericApplicationContext context,
ProducerFactory<Object, Object> pf) { // pf jest domyślną fabryką

// Stwórz konfigurację dla producenta o maksymalnej trwałości
Map<String, Object> highDurabilityConfigs = new HashMap<>(pf.getConfigurationProperties());
highDurabilityConfigs.put(ProducerConfig.ACKS_CONFIG, "all");
highDurabilityConfigs.put("enable.idempotence", true); // To jest kluczowe!

DefaultKafkaProducerFactory<Object, Object> highDurabilityPF = new DefaultKafkaProducerFactory<>(highDurabilityConfigs);
context.registerBean("highDurabilityPF", DefaultKafkaProducerFactory.class, () -> highDurabilityPF);

// Stwórz konfigurację dla "szybkiego" producenta
Map<String, Object> fastConfigs = new HashMap<>(pf.getConfigurationProperties());
fastConfigs.put(ProducerConfig.ACKS_CONFIG, "0");

DefaultKafkaProducerFactory<Object, Object> fastPF = new DefaultKafkaProducerFactory<>(fastConfigs);
context.registerBean("fastPF", DefaultKafkaProducerFactory.class, () -> fastPF);

// Mapa routingowa
Map<Pattern, ProducerFactory<Object, Object>> map = new LinkedHashMap<>();

// Reguły:
map.put(Pattern.compile("orders-.*"), highDurabilityPF); // Wszystkie topici zamówień - max bezpieczeństwo
map.put(Pattern.compile("metrics-.*"), fastPF); // Wszystkie topici metryk - max szybkość
map.put(Pattern.compile(".+"), pf); // Reszta - domyślne ustawienia (np. acks=1)

return new RoutingKafkaTemplate(map);
}

W ten sposób jedna aplikacja może wysyłać wiadomości o zamówieniach z gwarancją ich nieutracenia, a jednocześnie wysyłać logi i metryki z maksymalną prędkością, używając do tego jednego, wygodnego RoutingKafkaTemplate.