
Ahojte. V tomto článku vysvetlím, prečo sme vo firme Avito pred deviatimi mesiacmi zvolili Kafka a čo to vlastne je. Podelím sa o jeden z prípadov použitia - správca správ. A nakoniec sa porozprávame o výhodách, ktoré sme získali z prístupu Kafka as a Service.
Problém

Na začiatok trochu kontextu. Pred nejakým časom sme začali prechádzať od monolitickej architektúry a teraz má Avito už niekoľko stoviek rôznych služieb. Majú svoje úložiská, svoj technologický stack a zodpovedajú za svoju časť obchodnej logiky.
Jedným z problémov s veľkým počtom služieb sú komunikácie. Služba A často chce získať informácie, ktoré má služba B. V tomto prípade sa služba A obracia na službu B cez synchronné API. Služba C chce vedieť, čo sa deje v službách G a D, a tie zase majú záujem o služby A a B. Keď sa takýchto „zvedavých“ služieb stáva veľa, vzťahy medzi nimi sa menia na zamotanú sieť.
Pri tom môže byť služba A kedykoľvek nedostupná. A čo má robiť služba B a všetky ďalšie na ňu naviazané služby v takom prípade? A ak je potrebné na vykonanie obchodnej operácie vykonať reťazec sériových synchronných volaní, pravdepodobnosť zlyhania celej operácie sa ešte zvyšuje (a je vyššia, čím dlhší je tento reťazec).
Výber technológie

OK, problémy sú jasné. Môžeme ich odstrániť vytvorením centralizovaného systému výmeny správ medzi službami. Teraz každá zo služieb potrebuje poznať iba tento systém výmeny správ. Navyše, samotný systém by mal byť odolný voči zlyhaniu a horizontálne škálovateľný, a v prípade havárie uchovávať vo svojom úložisku vyrovnávací čakanie na následné spracovanie.
Teraz si vyberme technológiu, na ktorej bude realizovaná dodávka správ. Na to si najprv uvedomíme, čo od nej očakávame:
- správy medzi službami by nemali byť stratené;
- správy môžu byť duplicitné;
- správy môžeme uchovávať a čítať v hĺbke niekoľkých dní (perzistentná vyrovnávacia pamäť);
- služby sa môžu prihlásiť na údaje, ktoré ich zaujímajú;
- niekoľko služieb môže čítať rovnaké údaje;
- správy môžu obsahovať podrobný, objemný payload (event-carried state transfer);
- niekedy potrebujeme zaručiť poradí správ.
Rovnako bolo pre nás kriticky dôležité zvoliť čo najviac škálovateľný a spoľahlivý systém s vysokou priepustnosťou (minimálne 100k správ s niekoľkými kilobajtmi za sekundu).
V tejto fáze sme sa rozlúčili s RabbitMQ (ťažko sa udržuje stabilné pri vysokých rps), PGQ od SkyTools (nedostatočne rýchly a zle škálovateľný) a NSQ (nepersistujúci). Všetky tieto technológie sa v našej spoločnosti používajú, ale pre danú úlohu sa nehodili.
Potom sme začali sledovať nové technológie pre nás — Apache Kafka, Apache Pulsar a NATS Streaming.
Prvým sme zamietli Pulsar. Rozhodli sme sa, že Kafka a Pulsar sú pomerne podobné riešenia. A aj keď je Pulsar overený veľkými spoločnosťami, novší a teoreticky ponúka nižšiu latenciu, rozhodli sme sa zo zdvoch týchto dvoch ponechať Kafku ako de facto štandard pre tieto úlohy. Pravdepodobne sa v budúcnosti vrátime k Apache Pulsar.
A tak zostali dvaja kandidáti: NATS Streaming a Apache Kafka. Obidve riešenia sme podrobne preskúmali a obidva vyhovovali úlohe. Nakoniec sme sa však obávali relatívnej mladosti NATS Streaming (a toho, že jeden z hlavných vývojárov, Tyler Treat, sa rozhodol opustiť projekt a založiť si svoj vlastný — Liftbridge). Okrem toho režim Clustering NATS Streaming neposkytoval silné horizontálne škálovanie (pravdepodobne to už nie je problém po pridaní režimu partitioning v roku 2017).
Napriek tomu je NATS Streaming skvelá technológia napísaná v Go a má podporu Cloud Native Computing Foundation. Na rozdiel od Apache Kafka, nepotrebuje Zookeeper na fungovanie (možno, ), pretože vnútri implementuje RAFT. NATS Streaming je tiež jednoduchšie na administráciu. Nevyhadzujeme, že sa v budúcnosti ešte k tejto technológii vrátime.
A predsa, našim víťazom sa stala Apache Kafka. Na našich testoch sa preukázala ako dostatočne rýchla (viac ako milión správ za sekundu pri čítaní a zápise s objemom správ 1 kilobajt), dostatočne spoľahlivá, dobre škálovateľná a overená skúsenosťou vo veľkých spoločnostiach. Okrem toho Kafka podporuje aspoň niekoľko veľkých komerčných spoločností (my napríklad používame Confluent verziu) a Kafka má vyvinutý ekosystém.
Prehľad Kafky
Predtým, ako začnete, hneď odporúčam skvelú knihu — „Kafka: Definitívny sprievodca“ (existuje aj v ruskom preklade, ale odborné výrazy sú trochu zložité). Obsahuje informácie potrebné na základné pochopenie Kafka a dokonca aj niečo viac. Samotná dokumentácia od Apache a blog od Confluent sú tiež výborne napísané a ľahko sa čítajú.
Poďme sa teda pozrieť na to, ako je Kafka usporiadaná z vtáčej perspektívy. Základná topológia Kafka pozostáva z producer, consumer, broker a zookeeper.
Broker

Za uloženie vašich dát zodpovedá broker. Všetky dáta sú uložené v binárnej podobe a broker má len málo informácií o tom, čo tieto dáta predstavujú a aká je ich štruktúra.
Každý logický typ udalosti sa zvyčajne nachádza v samostatnej topike. Napríklad, udalosť vytvorenia inzerátu môže ísť do topiky item.created, zatiaľ čo udalosť jej zmeny — do item.changed. Topiky môžeme považovať za klasifikátory udalostí. Na úrovni topiky môžeme nastaviť také konfiguračné parametre, ako sú:
- objem uložených dát a/alebo ich vek (retention.bytes, retention.ms);
- faktor redundancie dát (replication factor);
- maximálna veľkosť jednej správy (max.message.bytes);
- minimálny počet súhlasných replik, pri ktorých sa dá do topiky zapisovať (min.insync.replicas);
- možnosť vykonať failover na nesynchronizovanú oneskorenú repliku s potenciálnou stratou dát (unclean.leader.election.enable);
- a ešte mnohé ďalšie ().
Na druhej strane, každá topika je rozdelená na jednu alebo viac partícií. Práve do partície sa nakoniec dostávajú udalosti. Ak je v klastri viac ako jeden broker, tak sa partície rozdelia medzi všetkých brokerov rovnomerne (čo najviac to pôjde), čo umožní škálovať zaťaženie na zápis a čítanie do jednej topiky na viac brokerov naraz.
Na disku sú dáta pre každú partíciu uchovávané vo forme segmentových súborov, predvolene s veľkosťou jedného gigabajtu (kontrolované prostredníctvom log.segment.bytes). Dôležitou vlastnosťou je, že odstránenie dát z partícií (pri nastavení retention) prebieha práve podľa segmentov (nie je možné odstrániť jednu udalosť z partície, môže sa odstrániť len celý segment, a to len neaktívny).
Zookeeper
Zookeeper zohráva úlohu úložiska metadát a koordinátora. Práve on dokáže povedať, či sú brokery živé (na to sa môže pozrieť pomocou zookeeper-shell príkazu ls /brokers/ids), ktorý z brokerov je kontrolórom (get /controller), sú partície v synchronizovanom stave so svojimi replikami (get /brokers/topics/topic_name/partitions/partition_number/state). Rovnako tak pošlú producer a consumer najprv do zookeepera, aby zistili, na ktorom brokeri sú aké témy a partície uložené. V prípadoch, keď je pre tému nastavený replication factor väčší ako 1, zookeeper ukáže, ktoré partície sú lídrami (do nich sa bude zapisovať a z nich sa bude čítať). V prípade zlyhania brokera bude v zookeeperi zapísaná informácia o nových líder-partíciách (od verzie 1.1.0 asynchrónne, ).
V starších verziách Kafka zookeeper zodpovedal aj za uchovávanie offsetov, ale teraz sú uložené v špeciálnej téme __consumer_offsets na brokeri (aj keď ich môžete stále používať aj na tieto účely so zookeeperom).
Najjednoduchší spôsob, ako premeniť vaše dáta na tekvicu, je práve strata informácií so zookeeperom. V takom scenári bude veľmi ťažké pochopiť, čo a odkiaľ potrebujete čítať.
Producer
Producer je najčastejšie služba, ktorá priamo zapisuje dáta do Apache Kafka. Producer si vyberie tému, v ktorej budú uložené jeho tematické správy, a začne do nej zapisovať informácie. Napríklad, producerom môže byť služba na inzerovanie. V takom prípade bude posielať do tematických tém také udalosti, ako je „inzerát vytvorený“, „inzerát aktualizovaný“, „inzerát odstránený“ atď. Každá udalosť pritom predstavuje pár kľúč-hodnota.
Predvolene sú všetky udalosti rozdelené medzi partície témy round-robin, ak nie je zadaný kľúč (stratou poradia), a cez MurmurHash (kľúč), ak je prítomný kľúč (poradie v rámci jednej partície).
Je dôležité poznamenať, že Kafka zaručuje poradie udalostí len v rámci jednej partície. Ale v skutočnosti to často nie je problém. Napríklad je možné zaručene pridať všetky zmeny jednej a tej istej inzerátu do jednej partície (čím sa zachová poradie týchto zmien v rámci inzerátu). Rovnako je možné odoslať poradové číslo v jednom z polí udalosti.
Consumer

Consumer je zodpovedný za získavanie údajov z Apache Kafka. Ak sa vrátim k vyššie uvedenému príkladu, consumer môže byť moderovací servis. Tento servis sa prihlási na tému servisu inzerátov a pri objavení sa nového inzerátu ho získa a analyzuje na súlad s určitými stanovenými politikami.
Apache Kafka si pamätá, aké posledné udalosti obdržal consumer (na tento účel sa používa servisná téma __consumer__offsets), čím sa zaručuje, že pri úspešnom čítaní consumer nedostane tú istú správu dvakrát. Napriek tomu, ak použijete možnosť enable.auto.commit = true a úplne odovzdáte prácu sledovania pozície consumeru Kafke, môžete . V produkčnom kóde je najčastejšie pozícia consumeru kontrolovaná ručne (vývojár riadi moment, kedy musí určite dôjsť k commitu prečítanej udalosti).
V prípadoch, keď jeden consumer nestačí (napríklad, ak je prúd nových udalostí veľmi veľký), môžete pridať ďalších consumerov a spojiť ich do consumer group. Consumer group logicky predstavuje presne takého istého consumer, ale s rozdelením údajov medzi účastníkmi skupiny. To umožňuje každému z účastníkov vziať si svoj podiel správ, čím sa zvyšuje rýchlosť čítania.
Výsledky testovania

Tu nebudem písať veľa vysvetľujúceho textu, len sa podelím o získané výsledky. Testovanie bolo vykonané na 3 fyzických strojoch (12 CPU, 384GB RAM, 15k SAS DISK, 10GBit/s Net), brokery a zookeeper boli rozmiestnení v lxc.
Testovanie výkonu
Počas testovania boli získané nasledujúce výsledky.
- Rýchlosť zápisu správ s veľkosťou 1KB súčasne 9 producentmi — 1300000 udalostí za sekundu.
- Rýchlosť čítania správ s veľkosťou 1KB súčasne 9 consumermi — 1500000 udalostí za sekundu.
Testovanie odolnosti
Počas testovania boli získané nasledujúce výsledky (3 brokery, 3 zookeeperi).
- Nehodné ukončenie jedného z brokerov nevedie k zastaveniu alebo nedostupnosti klastra. Práca pokračuje normálne, ale na zostávajúce brokery je väčšia záťaž.
- Nehodové ukončenie dvoch brokerov v prípade clustra z troch brokerov a min.isr = 2 vedie k nedostupnosti clustra na zápis, ale dostupnosti na čítanie. V prípade, ak je min.isr = 1, cluster zostáva dostupný na čítanie aj zápis. Tento režim však odporuje požiadavke na vysokú spoľahlivosť údajov.
- Nehodové ukončenie jedného zo serverov Zookeeper nevedie k zastaveniu alebo nedostupnosti clustra. Práca pokračuje v normálnom režime.
- Nehodové ukončenie dvoch serverov Zookeeper vedie k nedostupnosti clustra, kým sa neobnoví činnosť aspoň jedného zo serverov Zookeeper. Toto tvrdenie platí pre cluster Zookeeper zo 3 serverov. Na základe týchto zistení sme sa rozhodli zvýšiť cluster Zookeeper na 5 serverov na zvýšenie odolnosti voči chybám.
Kafka ako služba

Uistili sme sa, že Kafka je vynikajúca technológia, ktorá nám pomohla vyriešiť zadanú úlohu (implementáciu správy správ). Napriek tomu sme sa rozhodli zakázať službám priamy prístup k Kafke a uzavreli sme ju nad službou data-bus. Prečo sme to urobili? V skutočnosti je razlogov niekoľko.
Data-bus prevzal všetky úlohy spojené s integráciou s Kafkou (implementácia a nastavenie consumerov a producerov, monitorovanie, alerting, logovanie, škálovanie atď.). Integrácia s brokerom správ prebieha maximálne jednoducho.
Data-bus umožnil abstrahovať od konkrétneho jazyka alebo knižnice na prácu s Kafkou.
Data-bus umožnil iným službám abstrahovať od vrstvy ukladania. Možno, že v niektorom momente vymeníme Kafku za Pulsar a nikto si toho ani nevšimne (všetky služby poznajú iba API data-bus).
Data-bus prevzal validáciu schém udalostí.
S pomocou data-bus bola implementovaná autentifikácia.
Pod záštitou data-bus môžeme bez prestojov a nenápadne aktualizovať verzie Kafky, centralizovane spravovať konfigurácie producerov, consumerov, brokerov atď.
Data-bus umožnil pridávať potrebné funkcie, ktoré Kafka nemá (ako audit tém, kontrola anomálií v clustri, vytváranie DLQ atď.).
Data-bus umožňuje realizovať failover centralizovane pre všetky služby.
V súčasnosti stačí pripojiť malú knižnicu do kódu svojho servisu a môžete začať odosielať udalosti do správy brokera. A to je všetko. Získate možnosť písať, čítať a škálovať jedným riadkom kódu. Celá implementácia je pred vami skrytá, navonok sa objaví len niekoľko ovládacích prvkov, ako je veľkosť dávky. Pod kapotou služba data-bus spúšťa v Kubernetes požadovaný počet inštancií producerov a consumerov a dodáva im potrebnú konfiguráciu, ale všetko toto je transparentné pre váš servis.
Samozrejme, neexistuje strieborná guľka a tento prístup má svoje obmedzenia.
- Data-bus si treba udržiavať svojpomocne, na rozdiel od externých knižníc.
- Data-bus zvyšuje počet interakcií medzi službami a brokerom správ, čo vedie k zníženiu výkonu v porovnaní s čistou Kafkou.
- Nie všetko je možné tak jednoducho skryť pred službami, duplicovať funkčnosť KSQL alebo Kafka Streams v data-bus sa nám nepáči, preto niekedy je nutné povoliť službám, aby sa pripojili priamo.
V našom prípade prevažovali výhody nad nevýhodami a rozhodnutie skryť broker správ pod samostatnú službu sa osvedčilo. Po roku prevádzky sme nemali žiadne vážne havárie ani problémy.
P.S. Ďakujem svojej priateľke, Ekaterine Obaljajevovej, za úžasné obrázky k tomuto článku. Ak sa vám páčili, nájdete ešte viac ilustrácií.
Zdroj: habr.com
