MQ: kako osigurati da poruke 100% ne nestanu?
MQ: kako osigurati da poruke 100% ne nestanu?
Kada intervjuer intervuiše kandidata i u CV-u primeti da je u projektu koristio MQ tehnologiju (kao što su Kafka, RabbitMQ, RocketMQ), gotovo uvek će postaviti jedno pitanje: pri korišćenju MQ-a, kako osigurati da poruke 100% ne nestanu?
Ovo pitanje je veoma često u praksi — njime se proverava koliko kandidat dobro vlada MQ middleware tehnologijom, a i dobro razdvaja nivoe sposobnosti među kandidatima. U nastavku ćemo iz ovog pitanja krenuti i obraditi osnovna znanja koja treba da savladaš, način razmišljanja pri odgovoru, kao i proširene ispitne tačke.
Pozadina slučaja
Kao primer uzmimo sistem kompanije JD. Kada korisnik kupuje proizvod, obično bira da deo iznosa kompenzuje JD bodovima (JingDou). Tokom tog procesa, trgovinski servis i servis JD bodova komuniciraju preko MQ reda poruka. Pri poručivanju, trgovinski servis šalje poruku „oduzmi sa naloga X 100 JD bodova" ka MQ redu poruka, a servis JD bodova na strani potrošača tu komandu potroši i izvrši stvarno umanjenje.

Pa u kom sve problemu možeš da se nađeš tokom tog procesa?
Analiza slučaja
Treba znati da je u internet intervjuima najdirektnija svrha uvođenja MQ middleware-a: dekoplingacija sistema i kontrola protoka. U korenu, to i dalje služi rešavanju problema visoke dostupnosti i visokih performansi internet sistema.
- Dekoplingacija sistema: Korišćenjem MQ reda poruka mogu se izolovati nestabilnosti koje donose promene u uzvodnom i nizvodnom okruženju. Na primer, ma kako se menjali sistemski zahtevi servisa JD bodova, trgovinski servis ne mora ništa da menja; čak i kada servis JD bodova otkaže, glavni tok transakcija može degradirati taj servis, čime se ostvaruje dekoplingacija trgovinskog servisa i servisa JD bodova i postiže visoka dostupnost sistema.
- Kontrola protoka: U scenarijima poput sekundnih rasprodaja (seckill), gde dođe do naglog skoka protoka, MQ može odigrati ulogu „ravnanja vrhova i popunjavanja dolina" i automatski regulisati protok prema procesnoj moći nizvodnog sistema. Međutim, iako uvođenje MQ-a ostvaruje dekoplingaciju sistema i kontrolu protoka, donosi i druge probleme.
Uvođenje MQ middleware-a radi dekoplingacije sistema uticaće na konzistentnost prenosa podataka između sistema. U distribuiranom sistemu, ako između dva čvora postoji sinhronizacija podataka, javlja se problem konzistentnosti podataka. Isto tako, u ovoj lekciji rešavaš upravo to: problem konzistentnosti podataka poruka između proizvođača i potrošača poruka (odnosno kako osigurati da poruke ne nestanu).
Sa druge strane, uvođenje MQ middleware-a radi kontrole protoka može dovesti do toga da procesna moć potrošača bude nedovoljna, što izaziva zagušenje poruka — i to je problem koji takođe moraš da rešiš.
Zato možeš primetiti da su problemi međusobno isprepleteni, a intervjuer će iskoristiti priliku da proveri doslednost tvog razmišljanja i koliko dobro vladaš celokupnim sistemom znanja.
Kada se suočiš sa pitanjem „pri korišćenju MQ reda poruka, kako osigurati da poruke ne nestanu", kako treba da odgovoriš? Pre svega, analiziraj koliko ispitnih tačaka se krije u njemu, na primer:
- Kako uopšte znamo da je poruka nestala?
- U kojim fazama poruka može nestati?
- Kako osigurati da poruka ne nestane?
Kandidat pri odgovoru treba prvo da pokaže intervjueru svoj način analize, a zatim tek da ponudi rešenje. Prenos podataka u mreži nije pouzdan — da bi se rešilo pitanje kako poruke ne bi nestale, prvo treba znati u kojim fazama poruka može nestati i kako da utvrdimo da li je nestala, a tek na kraju dolazi rešenje (umesto da se odmah iznese svoje rešenje). Kao kod „arhitektonskog dizajna" — „arhitektura" odražava proces razmišljanja arhitekte, dok je „dizajn" konačno rešenje; oboje su neophodni i nedostatak bilo kog narušava celinu.
Odgovor na slučaj
Hajde prvo da vidimo u kojim fazama poruka može nestati. Životni ciklus jedne poruke, od nastanka do završetka potrošnje, može se podeliti u tri faze: faza proizvodnje poruke, faza skladištenja poruke i faza potrošnje poruke.

Faza proizvodnje poruke: od trenutka kada poruka nastane do trenutka kada se preda MQ-u, sve dok se uredno primi ack (potvrda) odgovor MQ Broker-a, smatra se da je slanje uspelo. Zato je, uz dobro obrađivanje povratne vrednosti i izuzetaka, u ovoj fazi gubitak poruke nemoguć.
Faza skladištenja poruke: ovu fazu obično prepustimo MQ middleware-u da je garantuje, ali ti treba da razumeš i njegov princip — na primer, Broker pravi replike i garantuje da se poruka sinhronizuje bar na dva čvora pre nego što vrati ack.
Faza potrošnje poruke: potrošač povlači poruku sa Broker-a; sve dok potrošač, pošto primi poruku, ne pošalje odmah potvrdu o potrošnji Broker-u, već sačeka da izvrši poslovnu logiku pa tek onda pošalje potvrdu, i na ovaj način se može osigurati da poruka ne nestane.
Rešenje deluje nepogrešivo — svaka faza može da garantuje da poruka neće nestati. Ali u distribuiranom sistemu kvar je neizbežan: kao proizvođač poruka ne možeš garantovati da MQ nije izgubio tvoju poruku, niti da li ju je potrošač već potrošio. Zato, u skladu sa principom dizajna Design for Failure, i dalje ti je potreban mehanizam kojim ćeš proveriti (Check) da li je poruka zaista nestala.
Odmah zatim možeš intervjueru objasniti i kako sprovesti detekciju poruka. Ukupna ideja rešenja je: na strani proizvođača svakoj poslanoj poruci dodeli globalno jedinstveni ID, ili joj priloži neprekidno rastući broj verzije, a zatim na strani potrošača izvrši odgovarajuću proveru verzije.
Kako to konkretno sprovesti? Možeš iskoristiti mehanizam interseptora. Pre nego što proizvođač pošalje poruku, interseptor u poruku ubaci broj verzije (koji može biti generisan kao neprekidno rastući ID, ili preko distribuiranog generatora globalno jedinstvenih ID-jeva). Zatim, kada potrošač primi poruku, interseptor proverava neprekidnost verzije ili status potrošnje. Prednost ovakvog pristupa je u tome što kôd za detekciju ne ulazi (ne zagađuje) poslovni kôd — kroz poseban zadatak možeš locirati izgubljenu poruku i izvršiti dalju istragu.
Ovde treba obratiti pažnju: ako istovremeno postoji više proizvođača i više potrošača poruka, pristup sa rastućim brojem verzije je teško sprovesti, jer se ne može garantovati jedinstvenost broja verzije. Tada detekciju poruka možeš sprovesti samo preko globalno jedinstvenog ID-ja, pri čemu je princip implementacije isti kao i kod varijante sa rastućim brojem verzije.
Sada već znaš u kojim fazama (faza skladištenja poruke, faza potrošnje poruke) može doći do problema, i imaš plan kako da otkriješ gubitak poruke. Preostaje ti da izneseš i plan dizajna koji sprečava gubitak poruka.
Nakon što odgovoriš na pitanje „kako osigurati da poruke ne nestanu?", intervjuer obično postavlja i pitanje: „kako rešiti problem da poruka bude potrošena više puta?"
Na primer: tokom potrošnje poruke, ako dođe do neuspeha, mehanizam kompenzacije će pokrenuti ponovni pokušaj pošiljaoca, a u tom ponovnom pokušaju može nastati duplikat poruke — kako onda rešiti taj problem?
Ovo pitanje se zapravo može preformulisati ovako: kako rešiti problem idempotentnosti na strani potrošača (idempotentnost znači da jedna komanda, ma koliko puta se izvršila, uvek proizvodi isti efekat kao i jedno jedino izvršenje). Sve dok je potrošač idempotentan, rešen je i problem potrošnje poruke više puta.
Hajde da se ponovo vratimo na primer sa umanjenjem JD bodova: sa naloga X se umanjuje 100 JD bodova. U ovom primeru možemo prepraviti poslovnu logiku tako da postane idempotentna.

Najjednostavnije rešenje je u bazi podataka napraviti tabelu dnevnika poruka (message log) sa dve kolone: ID poruke i status izvršenja poruke. Tada naša logika potrošnje poruke može postati: u tabelu dnevnika poruka ubaci jedan zapis poruke, a zatim na osnovu tog zapisa asinhrono ažuriraj korisnikov saldo JD bodova.
Pošto svaki put pre umetanja proveravamo da li poruka već postoji, ne može se desiti da jedna poruka bude izvršena više puta — i time je realizovana idempotentna operacija. Naravno, na osnovu ove ideje nije obavezna samo relaciona baza; Redis može zameniti bazu u realizaciji jedinstvenog ograničenja.
Ovde ću dodati još jednu reč: ako želiš da rešiš probleme „gubitka poruke" i „potrošnje poruke više puta", preduslov je da izvedeš tehničko rešenje za generisanje globalno jedinstvenog ID-ja. I to je pitanje koje intervjuer rado postavlja, pa ga moraš savladati.
U distribuiranim sistemima, metode za realizaciju globalno jedinstvenog ID-ja obuhvataju samorastući primarni ključ baze, UUID, Redis i Twitter-Snowflake algoritam. Sumirao sam karakteristike tih nekoliko pristupa, pa možeš pogledati.

Podsećam te: ma koji metod izabrao, ako želiš istovremeno da zadovoljiš jednostavnost, visoku dostupnost i visoke performanse, moraš praviti kompromise. Zato, iz ugla stvarnog biznisa, objasni koja je tačka ravnoteže koju si razmatrao pri izboru. Lično u biznisu više volim Snowflake algoritam; u projektu sam ga izvesno prilagodio, uglavnom tako što sam pravila generisanja ID-ja učinio usklađenijim sa osobinama biznisa, i optimizovao probleme poput vraćanja sata unazad (clock rollback).
Naravno, pored pitanja „kako rešiti problem potrošnje poruke više puta?", intervjuer može pitati i o „zagušenju poruka". Razlog je taj što zagušenje poruka odražava problem performansi, a rešavanje tog problema pokazuje da kandidat ume da obradi pitanje procesne moći potrošnje u uslovima visoke konkurentnosti.
Pri odgovoru na ovo pitanje i dalje treba intervjueru da predočiš sledeći proces razmišljanja: ako je došlo do zagušenja, to je sigurno problem performansi; da bi se rešio problem performansi na celom putu od proizvodnje do potrošnje poruka, prvo treba znati u kojim fazama može doći do zagušenja poruka, a zatim razmotriti kako da se reši.
Pošto se problem zagušenja javlja tek nakon što je poruka poslata, nema veze sa proizvođačem poruka. A pošto gotovo svaki red poruka već na jednom čvoru može obraditi i po desetine hiljada poruka u sekundi, u poređenju sa poslovnom logikom, problem performansi se neće pojaviti u middleware skladištu poruka. Bez sumnje, problem je sigurno u fazi potrošnje poruke. Kako onda odgovoriti krećući se od potrošača?
Ako je akutni problem na produkciji, uradi privremeno skaliranje — povećaj broj potrošača, i u isto vreme degradiraj neke neključne biznise. Preuzimanjem prometa skaliranjem i degradacijom pokazuješ sposobnost obrade hitnih problema.
Zatim dolazi red na istragu i rešavanje izuzetaka: preko nadzora (monitoringa), logova i drugih sredstava analiziraj da li je poslovna logika potrošača naišla na problem, pa optimizuj poslovnu logiku potrošača.
Konačno, ako je procesna moć potrošača nedovoljna, horizontalnim skaliranjem možeš povećati procesnu moć potrošnje. Ali ovde postoji jedna ispitna tačka kojoj treba posebno obratiti pažnju: prilikom skaliranja broja instanci potrošača, moraš sinhrono skalirati i broj particija (Partition) teme (Topic), tako da broj instanci potrošača bude jednak broju particija. Ako broj instanci potrošača premaši broj particija, s obzirom na to da se particija troši jednonitno, takvo skaliranje neće dati nikakav efekat.
Na primer, u Kafka-i jedan Topic može biti konfigurisan sa više Partition-a (particija), i podaci se upisuju u više particija; ali pri potrošnji Kafka usvaja pravilo da jednu particiju može trošiti samo jedan potrošač. Broj particija teme određuje moć potrošnje, pa se povećavanjem broja particija može podići procesna moć potrošača.
Rezime
Time smo obradili rešenja popularnih pitanja vezanih za MQ redove poruka. Bilo da si juniorski, srednji ili senior programer, sadržina ovog članka je ono što moraš da savladaš, a iz svake od ovih tačaka možeš krenuti u prijatan razgovor sa intervjuerom. Da sumiram ključne sadržaje današnje lekcije.
Kako osigurati da poruke ne nestanu? Treba da znaš, u svakoj fazi od slanja do potrošnje poruke, da li može doći do gubitka poruke i kako da nadgledaš da li je poruka nestala; tek na kraju dolazi rešenje, koje se može zasnovati na pristupu „pouzdane isporuke poruka MQ-a".
Kako osigurati da poruka ne bude potrošena više puta? Pri kompenzaciji poruka sigurno će se pojaviti i duplikati; kako onda realizovati idempotentnost potrošača — to je ispitna tačka ovog pitanja.
Kako obraditi problem zagušenja poruka? Ispitna tačka ovog pitanja je kako preko MQ-a ostvariti stvarno visoke performanse. Način odgovora: rešavanje produkcije izuzetaka je najviši prioritet, zatim preko nadzora i logova sprovedi istragu i optimizuj poslovnu logiku, a konačno skaliraj broj potrošača i broj particija.
Pri odgovaranju na pitanja posebno vodi računa o tome da intervjuer razume tvoj proces razmišljanja — ta sposobnost rešavanja problema intervjuer ceni više, i vrednija je od toga da direktno odgovoriš na jedno intervju pitanje.
Pored toga, ako aplikiraš za odeljenje osnovne arhitekture (infrastructure), pored glavnih znanja o čestim problemima iz ove lekcije, moraš savladati i ostale oblasti znanja o middleware-u za poruke, kao što su:
- Kako odabrati middleware za poruke?
- Koja je razlika između modela reda i modela objavljivanje-pretplata (publish-subscribe) u middleware-u za poruke?
- Zašto red poruka može ostvariti visoku propusnost?
- Serijalizacija, protokoli prenosa, kao i upravljanje memorijom itd.
- …
Originalni link: http://t.hk.uy/Ue6
