
Hej, Habr!
Jag arbetar i Tinkoff-teamet som utvecklar vÄrt eget notifieringscenter. Större delen av tiden utvecklar jag i Java med hjÀlp av Spring Boot och löser olika tekniska problem som uppstÄr i projektet.
De flesta av vÄra mikrotjÀnster kommunicerar asynkront med varandra via en meddelandebroker. Tidigare anvÀnde vi IBM MQ som broker, men den klarade inte belastningen lÀngre, trots att den hade höga leveransgarantier.
Som ett alternativ fick vi Apache Kafka, som har hög skalbarhet, men tyvÀrr krÀver en nÀstan skrÀddarsydd konfiguration för olika scenarier. Dessutom tillÀt mekanismen för Ätminstone en leverans, som fungerar i Kafka som standard, inte att upprÀtthÄlla den nödvÀndiga nivÄn av konsistens direkt. Jag kommer nu att dela vÄr erfarenhet av att konfigurera Kafka, sÀrskilt hur man stÀller in och lever med exakt en leverans.
Garanti för leverans och mer
Parametrarna som vi kommer att diskutera nedan kan hjÀlpa till att förebygga vissa problem med standardanslutningsinstÀllningar. Men först vill jag fokusera pÄ en parameter som underlÀttar eventuell felsökning.
Detta kommer att hjÀlpa med client.id för Producer och Consumer. Vid första anblick kan man anvÀnda applikationens namn som vÀrde, och i de flesta fall fungerar det. Dock kan situationen dÀr flera Consumers anvÀnds i applikationen och man ger dem samma client.id leda till följande varning:
org.apache.kafka.common.utils.AppInfoParser â Fel vid registrering av AppInfo mbean javax.management.InstanceAlreadyExistsException: kafka.consumer:type=app-info,id=kafka.test-0Om du vill anvĂ€nda JMX i en applikation med Kafka kan detta bli ett problem. I detta fall Ă€r det bĂ€st att anvĂ€nda en kombination av applikationens namn och till exempel Ă€mnets namn som vĂ€rde för client.id. Resultatet av vĂ„r konfiguration kan ses i utmatningen av kommandot kafka-consumer-groups frĂ„n verktygen frĂ„n Confluent:

Nu ska vi gÄ igenom scenariot för garanterad leverans av meddelanden. Kafka Producer har en parameter acks, som tillÄter konfigurationen av efter hur mÄnga acknowledgments som klusterledaren bör betrakta meddelandet som framgÄngsrikt skrivet. Denna parameter kan ta följande vÀrden:
- 0 â acknowledgments kommer inte att rĂ€knas.
- 1 â standard parameter, acknowledgement is only required from 1 replica.
- â1 â acknowledgement is required from all synchronized replicas (cluster configuration) min.insync.replicas).
From the listed values, it is clear that acks equal to â1 provide the strongest guarantees that the message will not be lost.
As we all know, distributed systems are unreliable. To protect against temporary failures, Kafka Producer provides the parameter retries, which allows you to specify the number of retry attempts during delivery.timeout.ms. Since the retries parameter defaults to Integer.MAX_VALUE (2147483647), the number of message retries can be controlled by only changing delivery.timeout.ms.
Moving towards exactly once delivery
The listed settings allow our Producer to deliver messages with a high guarantee. Now letâs talk about how to ensure that only one copy of the message is written to the Kafka topic? In the simplest case, for this the Producer needs to set the parameter enable.idempotence to true. Idempotence guarantees that only one message is written to a specific partition of one topic. A prerequisite for enabling idempotence is the values acks = all, retry > 0, max.in.flight.requests.per.connection †5. If these parameters are not set by the developer, the above values will be automatically assigned.
When idempotence is configured, it is necessary to ensure that the same messages go to the same partitions every time. This can be done by configuring the key and the partitioner.class parameter on the Producer. Letâs start with the key. It must be the same for each delivery. This is easily achieved by using some business identifier from the original message. The partitioner.class parameter defaults to . With this default partitioning strategy, we do the following:
- If the partition is explicitly specified when sending the message, use it.
- If no partition is specified but a key is provided â choose the partition based on the hash of the key.
- If neither partition nor key is specified â select partitions in a round-robin manner.
In addition, using the key and idempotent sending with the parameter max.in.flight.requests.per.connection = 1 ger en ordnad behandling av meddelanden i Consumer. Det Àr viktigt att komma ihÄg att om din kluster har instÀllningar för Ätkomstkontroll, sÄ kommer du att behöva rÀttigheter för idempotent skrivning till Àmnet.
Om du plötsligt saknar funktioner för idempotent sÀndning baserat pÄ nyckel eller logik pÄ Producer-sidan krÀver bevarande av datakonsistens mellan olika partitioner, sÄ kan transaktioner hjÀlpa. Dessutom kan du med hjÀlp av kedjetransaktioner villkorsbundet synkronisera skrivningen i Kafka, till exempel med skrivningen i en databas. För att aktivera transaktionell sÀndning pÄ Producer mÄste den ha idempotens och dessutom ange transactional.id. Om din Kafka-kluster har instÀllningar för Ätkomstkontroll, sÄ krÀvs det ocksÄ rÀttigheter för transaktionell skrivning, precis som för idempotent skrivning, som kan tillhandahÄllas med en mask med hjÀlp av vÀrdet som lagras i transactional.id.
Formellt kan vilken strÀng som helst anvÀndas som transaktionsidentifierare, till exempel programmets namn. Men om du kör flera instanser av samma program med samma transactional.id, dÄ kommer den första instansen som startas att stoppas med ett fel, eftersom Kafka kommer att betrakta den som en zombiprocess.
org.apache.kafka.common.errors.ProducerFencedException: Producenten försökte göra en operation med en gammal epok. Antingen finns det en nyare producent med samma transactionalId, eller sÄ har producentens transaktion löpt ut av broker.För att lösa det hÀr problemet lÀgger vi till ett suffix till applikationsnamnet som Àr vÀrdnamnet, vilket vi fÄr frÄn miljövariablerna.
Producenten Àr konfigurerad, men transaktioner i Kafka hanterar endast meddelandets synlighet. Oavsett transaktionens status, gÄr meddelandet direkt till Àmnet men fÄr ytterligare systemattribut.
För att sÄdana meddelanden inte ska lÀsas av Consumer innan tiden, mÄste den stÀlla in parametern isolation.level till vÀrdet read_committed. En sÄdan Consumer kan lÀsa icke-transaktionella meddelanden som tidigare, medan transaktionella endast kan lÀsas efter commit.
Om du har stÀllt in alla tidigare nÀmnda instÀllningar, sÄ har du konfigurerat exakt en gÄng-leverans. Grattis!
Men det finns en annan nuance. Transactional.id, som vi konfigurerade ovan, Àr faktiskt en prefix för transaktionen. Till transaktionshanteraren lÀggs ett sekvensnummer till. Den identifierare som erhÄlls utfÀrdas för transactional.id.expiration.ms, som konfigureras pÄ en Kafka-kluster och har ett standardvÀrde pÄ "7 dagar". Om applikationen under denna tid inte har mottagit nÄgra meddelanden kommer du vid nÀsta transaktionella sÀndning att fÄ InvalidPidMappingException. DÀrefter kommer transaktionskoordinatorn att tilldela ett nytt sekvensnummer för nÀsta transaktion. Medan detta meddelande kan gÄ förlorat om InvalidPidMappingException inte hanteras korrekt.
IstÀllet för sammanfattningar
Som man kan se Àr det inte tillrÀckligt att bara skicka meddelanden till Kafka. Man mÄste vÀlja en kombination av parametrar och vara beredd pÄ att göra snabba förÀndringar. I den hÀr artikeln har jag försökt visa i detalj hur man konfigurerar exakt engÄngsleverans och beskrivit flera konfigurationsproblem med client.id och transactional.id som vi stött pÄ. Nedan, i korthet, Àr instÀllningarna för Producer och Consumer.
Producer:
- acks = all
- retries > 0
- enable.idempotence = true
- max.in.flight.requests.per.connection †5 (1 â för ordnad sĂ€ndning)
- transactional.id = ${application-name}-${hostname}
Consumer:
- isolation.level = read_committed
För att minimera fel i framtida applikationer har vi gjort vÄrt eget wrapper över spring-konfigurationen, dÀr vÀrden redan Àr angivna för nÄgra av de angivna parametrarna.
HÀr Àr ett par resurser för sjÀlvstudier:
KĂ€lla: habr.com
