
Përshëndetje, Habr!
Punoj në ekipin Tinkoff, i cili merret me zhvillimin e qendrës sonë të njoftimit. Kryesisht zhvilloj në Java duke përdorur Spring Boot dhe zgjidh probleme teknike të ndryshme që shfaqen në projekt.
Shumica e mikroshërbimeve tona ndërveprojnë asinkronisht përmes një brokeri mesazhi. Më parë, përdorëm IBM MQ si broker, i cili nuk arriti të përballojë ngarkesën, por kishte garanci të ulta për dërgesat.
Si zëvendësim, na u propozua Apache Kafka, e cila ka potencial të lartë për shkallëzim, por, fatkeqësisht, kërkon një qasje pothuajse të personalizuar për konfigurimin për skenarë të ndryshëm. Për më tepër, mekanizmi i dërgesës së paktën një herë, i cili funksionon në Kafka si parazgjedhje, nuk lejonte mbajtjen e nivelit të nevojshëm të konsistencës nga kutia. Më poshtë do të ndaj përvojën tonë të konfigurimit të Kafka, duke treguar veçanërisht se si të konfigurojmë dhe të jetojmë me dërgesën saktësisht një herë.
Dërgesa e garantuar dhe jo vetëm
Parametrat, për të cilët do të flasim më poshtë, do të ndihmojnë në parandalimin e një sërë problemeve me konfigurimin e parazgjedhur. Por fillimisht dëshirojmë të kushtojmë vëmendje një parametri që do ta lehtësojë potencialin e proçesit të debagut.
Këtu do të ndihmojë client.id për Producuesin dhe Konsumatorin. Në shikim të parë, si vlerë mund të përdorim emrin e aplikacionit, dhe në shumicën e rasteve do të funksionojë. Megjithatë, situata kur në aplikacion përdoren disa Konsumatorë dhe u jepni atyre të njëjtin client.id, çon në paralajmërimin e mëposhtëm:
org.apache.kafka.common.utils.AppInfoParser â Gabim te regjistrimi i AppInfo mbean javax.management.InstanceAlreadyExistsException: kafka.consumer:type=app-info,id=kafka.test-0NĂ«se dĂ«shironi tĂ« pĂ«rdorni JMX nĂ« njĂ« aplikacion me Kafka, kjo mund tĂ« jetĂ« njĂ« problem. PĂ«r kĂ«tĂ« rast, mĂ« e mira Ă«shtĂ« qĂ« si vlerĂ« client.id tĂ« pĂ«rdoret njĂ« kombinim i emrit tĂ« aplikacionit dhe, pĂ«r shembull, emrit tĂ« temĂ«s. Rezultatin e konfigurimit tonĂ« mund ta shikoni nĂ« daljen e komandĂ«s kafka-consumer-groups nga utilitarĂ«t e Confluent:

Tani le të shqyrtojmë skenarin e dërgesës së garantuar të mesazhit. Producuesi i Kafka ka parametrin acks, i cili lejon të konfiguroni se pas sa pranimeve lideri i klasit duhet të marrë këtë mesazh si të shkruar me sukses. Ky parametr mund të marrë vlerat e mëposhtme:
- 0 â pranimet nuk do tĂ« merren parasysh.
- 1 â parametri i parazgjedhur, Ă«shtĂ« e nevojshme pranimi vetĂ«m nga 1 replikĂ«.
- â1 â kĂ«rkohet njohja nga tĂ« gjitha replikat e sinkronizuara (konfigurimi i klasterit min.insync.replicas).
Nga vlerat e pĂ«rmendura, Ă«shtĂ« e qartĂ« se acks qĂ« Ă«shtĂ« â1 ofron garancitĂ« mĂ« tĂ« forta qĂ« mesazhi nuk do tĂ« humbasĂ«.
Siç e dimë të gjithë, sistemet e shpërndara janë të pasigurta. Për t'u mbrojtur nga defektet përkohësore, Kafka Producer ofron parametrin retries, i cili lejon caktimin e numrit të përpjekjeve për dërgim brenda delivery.timeout.ms. Duke qenë se parametri retries ka vlerën e paracaktuar Integer.MAX_VALUE (2147483647), numri i përsëritjeve të dërgesës së mesazhit mund të rregullohet duke ndryshuar vetëm delivery.timeout.ms.
Le të kalojmë te dërgimi me saktësi të vetme
Caktimet e përmendura lejojnë që Produsi ynë të dërgojë mesazhe me një garantim të lartë. Tani le të flasim se si të garantojmë regjistrimin e një kopjeje të vetme të mesazhit në Kafka-topic? Në rastin më të thjeshtë, për këtë Produsi duhet të vendosë parametrin enable.idempotence në vlerën true. Idempotenca garanton regjistrimin e vetëm një mesazhi në një parti të caktuar të një topiku. Parakusht për aktivizimin e idempotencës janë vlerat acks = all, retry > 0, max.in.flight.requests.per.connection †5. Nëse këto parametra nuk janë caktuar nga zhvilluesi, do të vendosen automatikisht vlerat e përmendura më sipër.
Kur idempotenca Ă«shtĂ« e konfiguruar, duhet tĂ« sigurohemi qĂ« mesazhet e njĂ«jta tĂ« shkojnĂ« gjithmonĂ« nĂ« tĂ« njĂ«jtat parti. Kjo mund tĂ« bĂ«het duke rregulluar çelĂ«sin dhe parametrin partitioner.class nĂ« Produs. Le tĂ« fillojmĂ« me çelĂ«sin. PĂ«r çdo dĂ«rgesĂ« ai duhet tĂ« jetĂ« i njĂ«jtĂ«. Kjo Ă«shtĂ« e lehtĂ« pĂ«r t'u arritur duke pĂ«rdorur njĂ« identifikues biznesi nga mesazhi origjinal. Parametri partitioner.class ka vlerĂ«n e paracaktuar â . Me kĂ«tĂ« strategji parashikimi tĂ« paracaktuar veprojmĂ« kĂ«shtu:
- Nëse partia është e specifikuar qartë gjatë dërgimit të mesazhit, atëherë e përdorim atë.
- NĂ«se partia nuk Ă«shtĂ« e specifikuar, por çelĂ«si Ă«shtĂ« i caktuar â zgjedhim partinĂ« sipas hashes nga çelĂ«si.
- NĂ«se as partia as çelĂ«si nuk janĂ« caktuar â zgjedhim partitĂ« me radhĂ« (round-robin).
PĂ«r mĂ« tepĂ«r, pĂ«rdorimi i çelĂ«sit dhe dĂ«rgimi idempotent me parametrin max.in.flight.requests.per.connection = 1 ju jep procesim tĂ« renditur tĂ« mesazheve nĂ« Consumer. ĂshtĂ« mirĂ« tĂ« mbani mend se, nĂ«se nĂ« klasterin tuaj Ă«shtĂ« e aktivizuar menaxhimi i qasjes, do t'ju nevojiten tĂ« drejtat pĂ«r regjistrimin idempotent nĂ« temĂ«.
Nëse ndonjëherë ju mungojnë mundësitë e dërgimit idempotent sipas çelësit ose logjika në anën e Producer kërkon ruajtjen e konsistencës së të dhënave midis particioneve të ndryshme, atëherë transaksionet do t'ju vijnë në ndihmë. Për më tepër, me anë të transaksionit të ndërlikuar mund të sinkronizoni kushtimisht regjistrimin në Kafka, për shembull, me regjistrimin në DB. Për të aktivizuar dërgimin transaksional në Producer, është necesare që ai të ketë idempotencë dhe të vendosni transactional.id. Nëse klasteri juaj Kafka ka menaxhim qasje, atëherë për regjistrimin transaksional, ashtu si për atë idempotent, do t'ju nevojiten të drejtat për regjistrim, të cilat mund të jepen nëpërmjet maske të përdorur me vlerën që ruhet në transactional.id.
Formalisht si identifikues transaksioni mund të përdoret çdo varg, për shembull emri i aplikacionit. Por nëse jeni duke bërë ekzekutim të disa instancave të njëjtit aplikacion me të njëjtin transactional.id, atëherë instanca e parë e nisur do të ndalet me gabim, pasi Kafka do ta konsiderojë atë si proces zombi.
org.apache.kafka.common.errors.ProducerFencedException: Prodhuesi përpiqet të kryejë një operacion me një epokë të vjeter. Ose ka një prodhues më të ri me të njëjtin transactionalId, ose transaksioni i prodhuesit ka skaduar nga brokeri.Për të zgjidhur këtë problem, ne shtojmë në emrin e aplikacionit një sufix në formën e emrit të hostit, të cilin e marrim nga variablat e mjedisit.
Prodhuesi është i konfiguruar, por transaksionet në Kafka menaxhojnë vetëm fushën e dukshmërisë së mesazhit. Pavarësisht nga statusi i transaksionit, mesazhi menjëherë hyn në temë, por disponon disa atribute shtesë sistematike.
Që këto mesazhe të mos lexohen përpara kohe nga Consumer, ai duhet të vendosë parametrin isolation.level në vlerën read_committed. Këtë Consumer do të mund të lexojë mesazhet e pa-transaksionuara si më parë, dhe ato transaksionale vetëm pas angazhimit.
Nëse keni vendosur të gjitha konfigurimet e përmendura më parë, atëherë keni konfiguruar dorëzim exactly once. Urime!
Por ka një nuancë tjetër. Transactional.id, i cili u konfiguruar më lart, në të vërtetë është një prefiks i transaksionit. Në menaxherin e transaksioneve, atij i shtohet një numër radhor. Identifikuesi i marrë jepet në transactional.id.expiration.ms, i cili konfigurohet në klasterin Kafka dhe ka një vlerë të paracaktuar "7 ditë". Nëse gjatë kësaj kohe aplikacioni nuk ka pranuar asnjë mesazh, atëherë gjatë përpjekjes për të dërguar transaksionin e ardhshëm do të merrni InvalidPidMappingException. Pas kësaj, koordinatori i transaksioneve do të japë një numër rendor të ri për transaksionin e ardhshëm. Në këtë rast, mesazhi mund të humbasë, nëse InvalidPidMappingException nuk trajtohet siç duhet.
Në vend të rezultateve
Siç mund të vëreni, nuk mjafton thjesht të dërgoni mesazhe në Kafka. Duhet të zgjidhni kombinimin e parametrave dhe të jeni të gatshëm për të bërë ndryshime të shpejta. Në këtë artikull, kam përpjekur të tregoj në detaje konfigurimin e dërgimit 'exactly once' dhe kam përshkruar disa probleme me konfigurimet e client.id dhe transactional.id, me të cilat u përballëm. Më poshtë janë përmbledhje të konfigurimeve të Prodhueseve dhe Konsumatorëve.
Producenti:
- acks = all
- retries > 0
- enable.idempotence = true
- max.in.flight.requests.per.connection †5 (1 â pĂ«r dĂ«rgesĂ« tĂ« renditur)
- transactional.id = ${application-name}-${hostname}
Konsumatori:
- isolation.level = read_committed
Për të minimizuar gabimet në aplikacionet e ardhshme, ne krijuam një mbështjellës mbi konfigurimin e spring-ut, ku tashmë janë caktuar vlerat për disa nga parametrat e përmendur.
Dhe këtu janë disa materiale për studim të pavarur:
Burimi: habr.com
