
Hei, Habr!
Eg jobbar i Tinkoff-teamet som utviklar vÄrt eige varslingssenter. For det meste utviklar eg i Java med Spring Boot og lÞser ulike tekniske problem som oppstÄr i prosjektet.
De fleste av vÄre mikrotjenester kommuniserer asynkront med kvarandre via ein meldingsbroker. Tidlegare brukte vi IBM MQ som broker, men den klarte ikkje Ä takle lasta lenger, sjÞlv om den hadde hÞge leveringsgarantiar.
Som erstatning fekk vi anbefalt Apache Kafka, som har hÞg skaleringspotensial, men som dessverre krev nÊrare ein individuell tilnÊrming til konfigurasjon for ulike scenario. I tillegg tillot mekanismen for minst ein gong levering, som fungerer i Kafka som standard, ikkje Ä oppretthalde det nÞdvendige nivÄet av konsistens ut av boksen. Seinare vil eg dele vÄr erfaring med konfigurering av Kafka, spesielt fortelje korleis ein kan sette opp og leve med nÞyaktig ein gong levering.
Garantert levering og meir
Parameters som vil bli omtala vidare, vil hjelpe til med Ä unngÄ ei rekkje problem med standard tilkoplingsinnstillingane. Men fÞrst er det eit punkt eg gjerne vil fokusere pÄ, som vil lette mogleg debugging.
Dette vil hjelpast av client.id for Producer og Consumer. Ved fÞrste augekast kan ein bruke applikasjonsnamnet som verdi, og i dei fleste tilfeller vil det fungere. Men situasjonen der applikasjonen har fleire Consumerar og de fÄr same client.id, fÞrer til fÞlgjande advarsel:
org.apache.kafka.common.utils.AppInfoParser â Error registering AppInfo mbean javax.management.InstanceAlreadyExistsException: kafka.consumer:type=app-info,id=kafka.test-0Dersom du vil bruke JMX i ein applikasjon med Kafka, kan dette bli eit problem. For dette tilfellet er det best Ă„ bruke ein kombinasjon av applikasjonsnamn og til dĂžmes topiknamnet som verdi for client.id. Resultatet av vĂ„r konfigurasjon kan sjĂ„ast i utdataene frĂ„ kommandoen kafka-consumer-groups frĂ„ verktĂžya frĂ„ Confluent:

No skal vi sjÄ pÄ scenarioet for garantert levering av meldingar. Kafka Producer har parameteren acks, som lar deg konfigurere etter kor mange acknowledge leiarclusteret treng Ä rekne med at meldinga er skriven vel. Denne parameteren kan ta fÞlgjande verdiar:
- 0 â acknowledge vil ikkje bli rekna.
- 1 â standardparameter, krev acknowledge berre frĂ„ 1 replika.
- â1 â bekreftelser fra alle synkroniserte replikater er nĂždvendig (klusterinnstilling) min.insync.replicas).
Av de oppfĂžrte verdiene er det klart at acks lik â1 gir de sterkeste garantiene for at meldingen ikke gĂ„r tapt.
Som vi alle vet, er distribuerte systemer upÄlitelige. For Ä beskytte seg mot midlertidige feil, gir Kafka Producer parameteren retries, som lar deg spesifisere antall forsÞk pÄ Ä sende pÄ nytt i lÞpet av delivery.timeout.ms. Siden parameteren retries har standardverdi Integer.MAX_VALUE (2147483647), kan antall gjenutsendte meldinger justeres ved kun Ä endre delivery.timeout.ms.
Vi beveger oss mot exactly once delivery
De nevnte innstillingene lar vÄr Producer levere meldinger med hÞy sikkerhet. La oss nÄ snakke om hvordan man kan garantere at bare én kopi av meldingen skrives til Kafka-topikken? I det enkleste tilfellet mÄ Produceren sette parameteren enable.idempotence til true. Idempotens garanterer at bare én melding skrives til en bestemt partisjon av en topikk. Forutsetningen for Ä aktivere idempotens er verdiene acks = all, retry > 0, max.in.flight.requests.per.connection †5. Hvis disse alternativene ikke er spesifisert av utvikleren, vil de automatisk bli satt til de nevnte verdiene.
NĂ„r idempotens er konfigurert, mĂ„ man sikre at identiske meldinger alltid havner i de samme partisjonene. Dette kan gjĂžres ved Ă„ konfigurere nĂžkkelen og parameteren partitioner.class pĂ„ Producer. La oss begynne med nĂžkkelen. Den mĂ„ vĂŠre lik for hver sending. Dette er lett Ă„ oppnĂ„ ved Ă„ bruke en forretningsidentifikator fra den originale meldingen. Parameteren partitioner.class har standardverdi â . Med denne standard partisjoneringsstrategien fungerer vi slik:
- Hvis partisjonen er spesifisert ved sending av meldingen, bruker vi den.
- Hvis partisjonen ikke er spesifisert, men nĂžkkelen er angitt â velger vi partisjonen basert pĂ„ hash av nĂžkkelen.
- Hvis verken partisjonen eller nĂžkkelen er spesifisert â velger vi partisjonene pĂ„ rundgang (round-robin).
I tillegg, bruk av nÞkkel og idempotent sending med parameteren max.in.flight.requests.per.connection = 1 gir deg en organisert behandling av meldinger pÄ Consumer. Det er ogsÄ viktig Ä huske at hvis clusteret ditt har tilgangskontroll aktivert, vil du trenge rettigheter for idempotent skriving til topic.
Hvis du mangler muligheten til idempotent sending etter nÞkkel, eller logikken pÄ produsentsiden krever at du opprettholder konsistens i data mellom forskjellige partisjoner, kan transaksjoner hjelpe. I tillegg kan du med kjedetransaksjoner betinget synkronisere skriving til Kafka, for eksempel med skriving til DB. For Ä aktivere transaksjonell sending pÄ produsenten mÄ den vÊre idempotent, og du mÄ ogsÄ angi transactional.id. Hvis Kafka-clusteret ditt har tilgangskontroll aktivert, trenger du som med idempotent skriving, skrivetilgang som kan gis via en maske ved Ä bruke verdien lagret i transactional.id.
Formelt kan du bruke hvilken som helst streng som transaksjonsidentifikator, for eksempel navnet pÄ applikasjonen. Men hvis du kjÞrer flere instanser av den samme applikasjonen med samme transactional.id, vil den fÞrste kjÞrende instansen bli stoppet med en feil, da Kafka vil anse den som en zombieprosess.
org.apache.kafka.common.errors.ProducerFencedException: Produsenten forsÞkte en operasjon med en gammel epoke. Enten er det en nyere produsent med samme transactionalId, eller produsentens transaksjon har utlÞpt pÄ broker.For Ä lÞse dette problemet, legger vi til et suffiks til applikasjonsnavnet i form av vertsnavn, som vi fÄr fra miljÞvariablene.
Produsenten er konfigurert, men transaksjoner i Kafka styrer kun synligheten av meldingen. Uansett status pÄ transaksjonen, blir meldingen umiddelbart plassert i topic, men har ekstra systemattributter.
For at slike meldinger ikke skal bli lest av Consumer for tidlig, mÄ den stille inn parameteren isolation.level til verdien read_committed. En slik Consumer kan lese ikke-transaksjonelle meldinger som fÞr, men transaksjonelle meldinger kun etter commit.
Hvis du har satt opp alle de fÞrnevnte innstillingene, sÄ har du konfigurert exactly once delivery. Gratulerer!
Men det er en annen nyanse. Transactional.id, som vi konfigurerte tidligere, er faktisk et prefiks for transaksjonen. PÄ transaksjonslederen legges det til et sekvensnummer. Den resulterende identifikatoren utstedes ved transactional.id.expiration.ms, som er konfigurert pÄ Kafka-klyngen og har en standardverdi pÄ «7 dager». Hvis applikasjonen ikke har mottatt noen meldinger i lÞpet av denne tiden, vil du under det neste transaksjonsforsÞket fÄ InvalidPidMappingException. Etter dette vil transaksjonskoordinatoren gi et nytt sekvensnummer for neste transaksjon. Meldingen kan bli tapt hvis ikke InvalidPidMappingException blir riktig hÄndtert.
I stedet for oppsummeringer
Som man kan se, er det ikke nok bare Ä sende meldinger til Kafka. Man mÄ velge en kombinasjon av parametere og vÊre klar til Ä gjÞre raske endringer. I denne artikkelen har jeg prÞvd Ä vise i detalj oppsettet for exactly once delivery, og jeg har beskrevet noen konfigurasjonsproblemer med client.id og transactional.id som vi har mÞtt. Nedenfor er innstillingene for Produsent og Konsument oppsummert kort.
Produsent:
- acks = all
- retries > 0
- enable.idempotence = true
- max.in.flight.requests.per.connection †5 (1 â for ordnet sending)
- transactional.id = ${application-name}-${hostname}
Konsument:
- isolation.level = read_committed
For Ă„ minimere feil i fremtidige applikasjoner har vi laget et lag rundt spring-konfigurasjonen, hvor verdiene for noen av de nevnte parameterne allerede er satt.
Her er et par ressurser for selvstudium:
Kilde: habr.com
