Իրադարձահեն համակարգեր՝ Kafka, RabbitMQ և հերթեր
Իրադարձություններ ընդդեմ հրամանների, բրոքերներ, Kafka-ի partition-ներ և consumer group-եր, և ինչպես ընտրել ճիշտ գործիքը։
30 վայրկյանում
Իրադարձահեն (event-driven) համակարգում ծառայությունները հայտարարում են փաստեր («պատվերը ձևակերպված է»), և մյուս ծառայություններն արձագանքում են՝ միմյանց ուղղակի կանչելու փոխարեն։ Message broker-ը պահում և առաքում է այդ հաղորդագրությունները։ Kafka-ն կայուն, վերարտադրելի log է՝ կառուցված բարձր թողունակությամբ հոսքերի համար, RabbitMQ-ն ճկուն հաղորդագրությունների ռաութեր է, իսկ Redis-ի վրա հիմնված հերթերը, ինչպես BullMQ-ն, պարզ լուծում են ֆոնային առաջադրանքների համար։ Ճիշտ ընտրությունը կախված է հերթականությունից, replay-ից, թողունակությունից և նրանից, թե որքան ենթակառուցվածք ես ուզում սպասարկել։
Իմ ամենօրյա stack-ում չէ․ գիտեմ հիմնական գաղափարները և մանրամասները կսովորեմ, երբ նախագիծը դա պահանջի։
Ինչու է սա կարևոր
- Իրադարձությունները producer-ներին անջատում են consumer-ներից․ checkout-ը կարիք չունի իմանալու, որ լոյալությունը, անալիտիկան և email-ը բոլորն արձագանքում են պատվերին։
- Բրոքերը կլանում է տրաֆիկի պիկերը և աշխատանքը պահում ապահով, երբ consumer-ը ընկած է, ինչը համակարգերը դարձնում է շատ ավելի դիմացկուն։
- Kafka-ն կարևոր է, երբ թիմին պետք են բարձր թողունակությամբ հոսքեր, երկար պահպանում և replay՝ անալիտիկ pipeline-ներ, audit լոգեր, change data capture, նույն տվյալների բազմաթիվ անկախ consumer-ներ։
- Յուրաքանչյուր գործիքի առաքման երաշխիքների իմացությունը կանխում է դասական բագերը՝ կորած հաղորդագրություններ, կրկնօրինակներ և սխալ հերթականությամբ մշակում։
Ինչպես է աշխատում
01Իրադարձություններ, հրամաններ և բրոքերներ
Հրամանը (command) ինչ-որ մեկին խնդրում է ինչ-որ բան անել՝ «ուղարկիր այս email-ը», «գանձիր այս քարտից»։ Այն ունի մեկ նախատեսված handler և կարող է մերժվել։ Իրադարձությունը (event) արձանագրում է արդեն տեղի ունեցած փաստ՝ «42 պատվերը վճարված է»։ Նրան չի հետաքրքրում, թե ով է լսում․ կարող է արձագանքել զրո կամ տասը consumer։ Սրանք խառնելը խնդիրներ է ստեղծում․ SendEmail անունով «իրադարձությունը» producer-ին կապում է մեկ consumer-ի։ Message broker-ը նրանց միջև փոստատունն է։ Producer-ները հաղորդագրություններ են հրապարակում նրանում, այն դրանք պահում է կայուն, և consumer-ները ստանում են դրանք իրենց տեմպով։ Եթե consumer-ը ընկած է, հաղորդագրությունները սպասում են։ Եթե տրաֆիկը կտրուկ աճում է, մեծանում է backlog-ը, այլ ոչ թե «հալվում» է տվյալների բազան։ Գինը ասինխրոնությունն է․ producer-ն այլևս չգիտի, թե երբ կամ արդյոք աշխատանքն արված է, ուստի պետք է մոնիթորինգ backlog-ների և ձախողումների համար։
- Հրաման՝ հրամայական, մեկ handler, կարող է ձախողվել («ReserveStock»)։
- Իրադարձություն՝ անցյալ ժամանակ, շատ handler-ներ, անփոփոխ փաստ («StockReserved»)։
- Բրոքեր՝ կայուն բուֆեր և ռաութեր producer-ների և consumer-ների միջև։
02Kafka՝ topic-ներ, partition-ներ, offset-ներ և consumer group-եր
Kafka-ն բաշխված append-only log է։ Հաղորդագրությունները գրվում են topic-ի մեջ, որը բաժանված է partition-ների. յուրաքանչյուր partition դասավորված հաջորդականություն է, որտեղ ամեն հաղորդագրություն ստանում է offset (իր դիրքի համարը)։ Նույն key-ով հաղորդագրությունները միշտ գնում են նույն partition, ուստի հերթականությունը երաշխավորված է ըստ key-ի (օրինակ՝ ըստ պատվերի ID-ի), երբեք՝ ամբողջ topic-ի մասշտաբով։ Consumer-ները միանում են consumer group-ի. Kafka-ն յուրաքանչյուր partition հանձնում է խմբի ճիշտ մեկ consumer-ի, այնպես որ partition-ների քանակը սահմանափակում է զուգահեռությունը։ Յուրաքանչյուր խումբ հետևում է իր commit արված offset-ներին, ինչը նշանակում է, որ շատ խմբեր (billing, analytics, search) կարդում են նույն topic-ը անկախ։ Հաղորդագրությունները կարդալիս չեն ջնջվում․ դրանք մնում են retention-ի ժամկետի կամ չափի ընթացքում, կամ compact են արվում՝ ամեն key-ի համար թողնելով վերջին արժեքը։ Kafka 4.0-ից սկսած կլաստերներն աշխատում են KRaft-ով՝ առանց ZooKeeper-ի, իսկ Kafka 4.2-ը ավելացրեց production-ready share group-եր՝ հերթի ոճով սպառման համար։
# Create a topic: 6 partitions, 3 replicas, 7-day retention
kafka-topics.sh --bootstrap-server localhost:9092 --create \
--topic orders.v1 --partitions 6 --replication-factor 3 \
--config retention.ms=604800000 --config min.insync.replicas=2
# Produce keyed messages (same key -> same partition -> ordered)
kafka-console-producer.sh --bootstrap-server localhost:9092 \
--topic orders.v1 --property parse.key=true --property key.separator=:
# Inspect a consumer group: current offset, end offset and lag per partition
kafka-consumer-groups.sh --bootstrap-server localhost:9092 \
--describe --group billing-service
03RabbitMQ և Redis հերթեր
RabbitMQ-ն հաղորդագրությունների ռաութեր է։ Producer-ները հրապարակում են exchange-ում, որը հաղորդագրությունները binding-ների միջոցով ուղղորդում է հերթեր՝ direct (ճշգրիտ routing key), topic (պատերններ, ինչպես «order.*»), fanout (ամեն կապված հերթ) կամ headers։ Consumer-ները վերցնում են հաղորդագրությունները հերթերից և հաստատում (acknowledge) դրանք. չհաստատվածները կրկին առաքվում են, իսկ մերժվածները կարող են գնալ dead-letter exchange։ Հաստատվելուց հետո հաղորդագրությունն անհետանում է, ուստի RabbitMQ-ն աշխատանք առաքելու մասին է, ոչ թե պատմություն պահելու (նրա stream queue-երը ավելացնում են log-անման ռեժիմ, երբ replay է պետք)։ Այն փայլում է ըստ հաղորդագրության routing-ում, առաջնահերթություններում և request-reply-ում։ Redis-ի վրա հիմնված հերթերը, ինչպես BullMQ-ն (Node) կամ Laravel-ի հերթերը Redis դրայվերով, ամենաթեթև տարբերակն են՝ job-եր կրկնակի փորձերով, backoff-ով, ուշացումներով, rate limit-երով, պլանավորված և կրկնվող job-երով, բոլորը այն Redis-ի վրա, որը հավանաբար արդեն ունես։ Նրանց սահմանը կայունությունն ու մասշտաբն է․ Redis-ը տվյալները պահում է հիշողության մեջ, ուստի կարգավորիր persistence-ը և հերթին վերաբերվիր որպես job runner-ի, ոչ թե մշտական event store-ի։
04Առաքման երաշխիքները գործնականում
Ամեն բրոքեր առաջարկում է երեք երաշխիքների որոշակի համադրություն։ At-most-once՝ consumer-ը commit է անում մինչև մշակելը, ուստի crash-ը կորցնում է հաղորդագրությունը, բայց երբեք չի կրկնօրինակում. ընդունելի է մետրիկաների համար, հազվադեպ՝ բիզնես տվյալների։ At-least-once՝ consumer-ը մշակում է, հետո հաստատում. այդ երկուսի միջև crash-ը նշանակում է կրկնակի առաքում, ուստի կրկնօրինակներ լինում են։ Սա իրատեսական լռելյայն տարբերակն է համակարգերի մեծ մասի համար։ Exactly-once՝ յուրաքանչյուր հաղորդագրություն ազդում է արդյունքի վրա մեկ անգամ։ Kafka-ն դա աջակցում է Kafka-ի ներսում՝ idempotent producer-ներով և տրանզակցիաներով, երբ consumer-ները կարդում են միայն commit արված տվյալները, read-process-write pipeline-ների համար Kafka topic-ների միջև։ Հենց որ կողմնակի էֆեկտը դուրս է գալիս Kafka-ից (գրառում տվյալների բազայում, email, քարտից գանձում), դու վերադառնում ես at-least-once-ին և պետք է consumer-ը դարձնես idempotent․ պահիր մշակված հաղորդագրությունների ID-ները, օգտագործիր unique constraint-ներ կամ upsert-ներ և handler-ները նախագծիր այնպես, որ երկու անգամ գործարկելը ոչինչ չփոխի։
- Producer-ի կողմ՝ acks=all և idempotence-ը միացված (ժամանակակից Kafka կլիենտներում լռելյայն է)։
- Consumer-ի կողմ՝ offset-ները commit արա հաջող մշակումից հետո, ոչ թե առաջ։
- «Թունավոր» հաղորդագրություններ՝ կրկնիր backoff-ով, հետո տեղափոխիր dead-letter topic կամ հերթ։
05Սխեմաներ, տարբերակավորում և գործիքի ընտրություն
Իրադարձությունները հանրային պայմանագիր են, որը հաճախ կարդում են թիմեր, որոնց երբեք չես հանդիպի, ուստի նրանց ձևին վերաբերվիր API-ի պես։ Սխեմաները սահմանիր Avro-ով, Protobuf-ով կամ JSON Schema-ով, պահիր schema registry-ում և պարտադրիր համատեղելիության կանոններ․ ազատ ավելացրու ոչ պարտադիր դաշտեր, երբեք մի հեռացրու և մի փոխիր այն դաշտի իմաստը, որին consumer-ները հենվում են։ Կոտրող փոփոխությունների համար հրապարակիր իրադարձության նոր տեսակ կամ topic-ի նոր տարբերակ (orders.v2) և գործարկիր երկուսն էլ, մինչև consumer-ները միգրացվեն։ Յուրաքանչյուր հաղորդագրության մեջ ներառիր իրադարձության ID, տեսակ, տարբերակ, timestamp և էնթիթիի key։ Ընտրության համար․ Kafka՝ մեծ ծավալի հոսքերի, երկար retention-ի, replay-ի և շատ consumer group-երի համար, RabbitMQ՝ ճկուն routing-ի, առաջադրանքների բաշխման և ըստ հաղորդագրության acknowledgement-ի համար, Redis հերթեր, ինչպես BullMQ-ն՝ հավելվածի ֆոնային job-երի համար՝ կրկնակի փորձերով ու պլանավորմամբ, managed ծառայություններ (SQS, SNS, Pub/Sub, managed Kafka)՝ երբ չես ուզում բրոքերներն ինքդ սպասարկել։
Օգտագործեք, երբ
- Բազմաթիվ անկախ consumer-ների պետք է փաստերի նույն հոսքը (անալիտիկա, որոնման ինդեքսավորում, ծանուցումներ)։
- Քեզ պետք է replay՝ read model-ի վերակառուցում, նոր ծառայության backfill կամ պատմության աուդիտ։
- Թողունակությունը բարձր է (հազարավորից միլիոնավոր հաղորդագրություններ վայրկյանում)՝ հերթականությամբ ըստ key-ի։
- Աշխատանքը պետք է գոյատևի consumer-ի անգործությունը և տրաֆիկի պիկերը՝ առանց հարցումներ կորցնելու։
Մի՛ օգտագործեք, երբ
- Մեկ հավելված ֆոնային job-երով․ Redis-ի կամ տվյալների բազայի վրա հիմնված հերթն ավելի պարզ ու էժան է։
- Կանչողին անհապաղ պատասխան է պետք․ օգտագործիր սինխրոն API, իսկ իրադարձությունները պահիր կողմնակի էֆեկտների համար։
- Թիմը դեռ չի կարող սպասարկել և մոնիթորինգ անել բաշխված բրոքեր․ օգտագործիր managed ծառայություն կամ ավելի պարզ հերթ։
Այլընտրանքներ
- RabbitMQ
Exchange-ներ և հերթեր՝ հարուստ routing-ով, ack-երով և dead-lettering-ով. իդեալական է առաջադրանքների բաշխման համար։
- Redis + BullMQ / Laravel queues
Թեթև job-եր՝ կրկնակի փորձերով, ուշացումներով և rate limit-երով. երկարաժամկետ event log չէ։
- Managed ամպային հերթեր
SQS/SNS, Google Pub/Sub, Azure Service Bus՝ բրոքերներ սպասարկելու կարիք չկա, վճարում ես ըստ օգտագործման։
- NATS / Redis Streams
Թեթև streaming՝ consumer group-երով, երբ Kafka-ն չափազանց ծանր է։
Լավագույն պրակտիկաներ
- Իրադարձությունները անվանիր անցյալ ժամանակով և պահիր որպես անփոփոխ փաստեր։
- Partition key-երը ընտրիր ըստ այն էնթիթիի, որին հերթականություն է պետք, և partition-ների քանակը հաշվարկիր ապագա consumer-ների համար։
- Յուրաքանչյուր consumer դարձրու idempotent. ենթադրիր, որ ամեն հաղորդագրություն կարող է երկու անգամ հասնել։
- Իրադարձությունները հրապարակիր transactional outbox-ի միջոցով, որպեսզի տվյալների բազայի փոփոխություններն ու հաղորդագրությունները երբեք չտարամիտվեն։
- Սխեմաները տարբերակավորիր registry-ով և համատեղելիության ստուգումներով։
- Մոնիթորինգ արա consumer lag-ը, dead-letter հերթերը և կրկնակի փորձերի հաճախականությունը, և ալերտ դիր դրանց աճի վրա։
Տարածված սխալներ
- Kafka-ում գլոբալ հերթականություն սպասել. այն երաշխավորված է միայն partition-ի ներսում։
- Offset-ները commit անել մինչև մշակելը, հետո crash-ի ժամանակ կորցնել հաղորդագրությունները։
- Բրոքերը օգտագործել որպես տվյալների բազա՝ ad hoc հարցումներով, պատշաճ read model-ներ կառուցելու փոխարեն։
- «Տաք» partition-ներ՝ անհավասարաչափ key-ի պատճառով (օրինակ՝ մեկ հսկայական հաճախորդ), որոնք սահմանափակում են թողունակությունը։
- Իրադարձության ձևը փոխել առանց տարբերակավորման և լուռ կոտրել consumer-ներին։
Խորը վերլուծություն
06Durability-ն Kafka-ի ներսում․ ռեպլիկաներ, ISR և leader-ի ընտրություն
Յուրաքանչյուր partition ունի մեկ leader ռեպլիկա, որն ընդունում է գրառումները, և follower-ներ, որոնք պատճենում են այն։ In-sync replica set-ը (ISR) leader-ն է՝ գումարած այն follower-ները, որոնք հետ չեն մնում replica.lag.time.max.ms-ից ավելի։ acks=all-ի դեպքում producer-ը հաջողություն ստանում է միայն այն ժամանակ, երբ ընթացիկ ISR-ի բոլոր անդամներն ունեն գրառումը․ min.insync.replicas-ը սահմանում է, թե ISR-ը որքան կարող է փոքրանալ, մինչև գրառումները մերժվեն NotEnoughReplicas-ով։ Տարածված durable կարգավորումն է replication factor 3, min.insync.replicas 2 և acks=all․ մեկ broker կարող է ընկնել՝ առանց տվյալների կորստի և առանց գրառումների դադարի։ unclean.leader.election.enable-ը թող իր լռելյայն false արժեքով․ դրա միացումը թույլ է տալիս չհամաժամեցված ռեպլիկային դառնալ leader և լուռ կորցնել արդեն հաստատված հաղորդագրությունները՝ availability-ի դիմաց։ Ռեպլիկաները բաշխիր rack-երի կամ availability zone-երի միջև broker.rack-ով, որպեսզի մեկ zone-ի խափանումը չտանի բոլոր պատճենները։
- RF 3 + min.insync.replicas 2 + acks=all․ անվտանգ վերապրում է մեկ broker-ի կորուստը։
- Producer-ի idempotence-ը և acks=all-ը կլիենտի լռելյայն արժեքներ են Kafka 3.0-ից ի վեր։
- Unclean election-ը հաստատված տվյալները փոխանակում է availability-ի հետ․ թող այն անջատված։
07Rebalancing, նոր consumer պրոտոկոլը և մշակման կանգերը
Երբ consumer-ները միանում են, դուրս գալիս կամ crash լինում, partition-ները վերաբաշխվում են rebalance-ի ընթացքում։ Դասական պրոտոկոլը կարող էր կանգնեցնել ամբողջ խումբը․ cooperative-sticky assignment-ը դա նվազեցրեց, իսկ նոր սերնդի պրոտոկոլը (KIP-848), որը Kafka 4.0-ից ընդհանուր հասանելի է, assignment-ը տեղափոխում է broker-ի կողմի group coordinator և rebalance է անում ինկրեմենտալ։ Կլիենտները միանում են group.protocol=consumer-ով, իսկ խմբերը փոխարկվում են rolling upgrade-ի ընթացքում։ Երկու timeout կարևոր է։ session.timeout.ms-ը heartbeat-ների միջոցով հայտնաբերում է «մեռած» consumer-ին։ max.poll.interval.ms-ը հայտնաբերում է «կախված» consumer-ին․ եթե handler-ը poll կանչերի միջև ավելի երկար է տևում, consumer-ը հեռացվում է, նրա partition-ները տեղափոխվում են, և չcommit-ված batch-ը նորից մշակվում է այլ տեղում։ Այսպիսով դանդաղ handler-ները առաջացնում են կրկնվող աշխատանք և rebalance-ի «փոթորիկներ»։ Իջեցրու max.poll.records-ը, երկար աշխատանքը տար առանձին հերթ կամ մշակման ընթացքում pause արա partition-ները։
08Compaction, retention և replay մեծ մասշտաբով
Retention-ը կարող է լինել ըստ ժամանակի կամ չափի (delete policy) կամ ըստ key-ի (compact policy)։ Compaction-ը պահում է առնվազն վերջին գրառումը յուրաքանչյուր key-ի համար և ֆոնային ռեժիմում հեռացնում հները, ինչը topic-ը դարձնում է ընթացիկ վիճակի durable աղյուսակ․ հաճախորդների պրոֆիլներ, ապրանքների գներ, կոնֆիգուրացիա։ null արժեքով գրառումը tombstone է․ այն key-ը նշում է ջնջման համար և ինքն էլ հեռացվում է delete.retention.ms-ից հետո, ուստի դրանից ավելի հետ մնացած consumer-ները կարող են բաց թողնել ջնջումները։ Երկու policy-ների համադրումը պահում է վերջին արժեքը յուրաքանչյուր key-ի համար և միևնույն ժամանակ ժամկետանց է դարձնում շատ հին key-երը։ Replay-ը «գերուժն» է․ նոր consumer group-ը, որը սկսում է ամենավաղ offset-ից, պատմությունից վերակառուցում է read model։ Պլանավորիր դա․ վերակառուցումները կարող են տերաբայթներ կարդալ, ուստի սահմանափակիր դրանց արագությունը, տուր նրանց առանձին խումբ և համոզվիր, որ retention-ը երկար է քո ամենավատ իրատեսական վերականգնման ժամանակից։
# A compacted topic holding the latest price per product
kafka-topics.sh --bootstrap-server localhost:9092 --create \
--topic prices.v1 --partitions 12 --replication-factor 3 \
--config cleanup.policy=compact \
--config min.compaction.lag.ms=3600000 \
--config delete.retention.ms=86400000
# Rebuild a read model: new group, start from the beginning
kafka-consumer-groups.sh --bootstrap-server localhost:9092 \
--group search-indexer-v2 --topic prices.v1 \
--reset-offsets --to-earliest --execute
09Partition-ների քանակ, key skew և throughput-ի tuning
Partition-ները զուգահեռության և հերթականության միավորն են, ուստի դրանց քանակը որոշիր այն consumer զուգահեռության համար, որը սպասում ես մեկ-երկու տարի հետո, ոչ թե այսօր․ partition-ներ ավելացնելը հետագայում վերաքարտեզագրում է key-երը և փոփոխության ընթացքում կոտրում per-key հերթականությունը։ Չափազանց շատ partition-ներն էլ արժեք ունեն․ ավելի շատ բաց ֆայլեր, ավելի երկար leader election-ներ և ավելի շատ մետատվյալներ։ Մյուսներից շատ ավելի շատ տրաֆիկով key-ը ստեղծում է «տաք» partition, որը մեկ consumer պետք է մենակ սպասարկի։ Skew-ը շտկիր ավելի մանր key ընտրելով (order ID՝ customer ID-ի փոխարեն), եթե հերթականությունը կարևոր է միայն պատվերի սահմաններում, կամ հայտնի տաք key-ը «աղելով» մի քանի ենթա-key-երի միջև և downstream-ում միավորելով։ Առանց key-ի գրառումները բաշխվում են sticky partitioner-ով, որը լցնում է մեկ batch, նոր միայն անցնում հաջորդին։ Producer-ի կողմում linger.ms-ը և batch.size-ը մի քանի միլիվայրկյան latency-ն փոխանակում են շատ ավելի բարձր throughput-ի հետ, իսկ compression-ը (lz4 կամ zstd) կրճատում է ցանցի և դիսկի օգտագործումը։
Իրական իրավիճակներ
Consumer lag-ը աճում է ամեն երեկո
Խնդիր
Analytics consumer group-ը պիկի ժամին ժամերով հետ է մնում և հասնում է միայն գիշերը։ Consumer ինստանսներ ավելացնելը չօգնեց։
Ինչպես լուծել
Գործարկիր kafka-consumer-groups.sh --describe՝ lag-ը ըստ partition-ի տեսնելու համար։ Եթե lag-ը կենտրոնացած է մեկ-երկու partition-ի վրա, պատճառը key skew-ն է․ փոխիր key-ը կամ «աղիր» տաք key-ը։ Եթե այն հավասարաչափ է բաշխված, և ինստանսներն արդեն հավասար են partition-ների քանակին, լրացուցիչ ինստանսները պարապ են մնում, ուստի ավելացրու partition-ները topic-ի նոր տարբերակում և միգրացիա արա, կամ արագացրու handler-ը՝ տվյալների բազայի գրառումները batch անելով։ Ստուգիր rebalance-ի ցիկլերը, որոնք առաջանում են max.poll.interval.ms-ը գերազանցող handler-ներից։ Հետո alert դիր lag-ի աճի արագության և time-to-drain-ի վրա, ոչ թե բացարձակ թվի։
Կրկնակի գանձումներ consumer-ի crash-ից հետո
Խնդիր
Վճարումների consumer-ը երբեմն հաճախորդից երկու անգամ է գանձում։ Լոգերը ցույց են տալիս consumer-ի restart գանձման և offset-ի commit-ի միջև։
Ինչպես լուծել
Սա at-least-once առաքումն է, որն աշխատում է ինչպես նախատեսված է․ handler-ը իդեմպոտենտ չէ։ Յուրաքանչյուր իրադարձության տուր կայուն ID և վճարային provider-ին փոխանցիր դրանից ստացված idempotency key, որպեսզի երկրորդ կանչը վերադարձնի սկզբնական արդյունքը։ Մշակված իրադարձությունների ID-ները գրանցիր նույն տվյալների բազայի տրանզակցիայում, ինչ լոկալ վիճակի փոփոխությունը, unique constraint-ով, և բաց թող արդեն տեսածները։ Auto-commit-ը պահիր անջատված և offset-ները commit արա միայն մշակումից հետո։ Ավելացրու reconciliation job, որն ամեն օր համեմատում է լոկալ վճարումները provider-ի հաշվետվության հետ՝ բռնելու այն ամենը, ինչ սայթաքել է։
Broker-ի ընտրություն միջին չափի պրոդուկտի համար
Խնդիր
Թիմը գործարկում է մեկ հավելված, որտեղ Redis-ն արդեն կա։ Նրանց պետք են պատվերի իրադարձություններ email-ի, loyalty-ի և ապագա analytics pipeline-ի համար, և հարցնում են՝ արդյոք Kafka-ն հիմա ներդնե՞լ։
Ինչպես լուծել
Սկսիր այսօրվա կարիքներից․ մի քանի consumer, համեստ ծավալ, replay-ի պահանջ դեռ չկա։ Redis-ի վրա հիմնված job queue կողմնակի էֆեկտների համար, որը սնվում է transactional outbox-ից, ծածկում է սա առանց նոր ինֆրաստրուկտուրայի։ Իրադարձությունները նախագծիր այնպես, կարծես դրանք հանրային են (ID-ներ, տարբերակներ, սխեմաներ), որպեսզի հետագայում log-ի տեղափոխելը լինի relay-ի փոփոխություն, ոչ թե վերագրում։ Երբ analytics-ին կամ մի քանի անկախ թիմերի պետք լինեն պատմություն և replay, ավելացրու managed Kafka և outbox relay-ն ուղղիր դեպի այն՝ փոխանակ առաջին օրվանից broker-ները ինքդ գործարկելու։
Արագ ուղեցույց
- Durable producer
acks=all, enable.idempotence=true (լռելյայն՝ 3.0-ից)- Durable topic
--replication-factor 3 --config min.insync.replicas=2- Consumer group-ի lag
kafka-consumer-groups.sh --bootstrap-server host:9092 --describe --group app- Replay սկզբից
kafka-consumer-groups.sh ... --group g --topic t --reset-offsets --to-earliest --execute- Նոր սերնդի rebalance պրոտոկոլ (4.0+)
group.protocol=consumer- Կարդալ միայն commit-ված տրանզակցիոն տվյալները
isolation.level=read_committed- Վերջին արժեքը յուրաքանչյուր key-ի համար
cleanup.policy=compact- Producer-ի ավելի բարձր throughput
linger.ms=10, batch.size=65536, compression.type=zstd- Կանխել handler-ների հեռացումը
Իջեցրու max.poll.records-ը կամ բարձրացրու max.poll.interval.ms-ը
Հարցեր և պատասխաններ
Q1Kafka՞, թե՞ RabbitMQ․ ինչպե՞ս ես ընտրում։
Հարցնում եմ, թե ինչի համար են հաղորդագրությունները։ Եթե դրանք փաստերի հոսք են, որը մի քանի թիմեր սպառում են անկախ, պետք է retention և replay և գալիս են մեծ ծավալով, Kafka-ն է համապատասխանում․ այն կայուն log է՝ consumer group-երով և հերթականությամբ ըստ key-ի։ Եթե դրանք առաջադրանքներ են, որոնք պետք է բաշխել worker-ների միջև՝ routing կանոններով, առաջնահերթություններով և ըստ հաղորդագրության acknowledgement-ով, RabbitMQ-ն ավելի լավ է համապատասխանում և ավելի հեշտ է հասկանալ։ Մեկ հավելվածի ներսում ֆոնային job-երի համար հաճախ բավական է Redis-ի վրա հիմնված հերթ, ինչպես BullMQ-ն։ Սպասարկման ծախսը նույնպես կարևոր է. managed տարբերակը փոքր թիմերի համար վերացնում է ռիսկի մեծ մասը։
Q2Ինչպե՞ս է Kafka-ն երաշխավորում հերթականությունը։
Միայն partition-ի ներսում։ Producer-ն ուղարկում է հաղորդագրություններ key-ով, key-ը hash է արվում partition ընտրելու համար, և այդ partition-ի ներսում հաղորդագրությունները պահպանում են իրենց հերթականությունը և ստանում աճող offset-ներ։ Այսպիսով, եթե key-ը պատվերի ID-ն է, մեկ պատվերի բոլոր իրադարձությունները մշակվում են հաջորդաբար, մինչդեռ տարբեր պատվերներ մշակվում են զուգահեռ՝ տարբեր partition-ներում։ Partition-ների միջև հերթականություն չկա։ Հերթականությունը կախված է նաև producer-ից․ միացված idempotence-ի դեպքում կրկնակի փորձերը չեն խախտում հաղորդագրությունների հերթականությունը։ Զգույշ եղիր partition-ների քանակն ավելացնելիս, որովհետև key-երը կարող են ընկնել նոր partition-ներ, և ընթացքի մեջ գտնվող key-երի հերթականությունը փոփոխության ժամանակ կարող է խախտվել։
Q3Ի՞նչ է նշանակում exactly-once Kafka-ում։
Kafka-ն առաջարկում է exactly-once սեմանտիկա read-process-write հոսքերի համար, որոնք մնում են Kafka-ի ներսում։ Idempotent producer-ները կանխում են կրկնակի փորձերից առաջացող կրկնօրինակները, իսկ տրանզակցիաները ատոմար գրում են ելքային հաղորդագրություններն ու consumer-ի offset-ները, այնպես որ read_committed-ով կարդացող consumer-ը յուրաքանչյուր արդյունք տեսնում է մեկ անգամ։ Սա հզոր է stream processing-ի համար։ Բայց հենց որ handler-դ գրում է տվյալների բազա, կանչում է վճարման API կամ ուղարկում email, Kafka-ն չի կարող այդ արտաքին էֆեկտը դարձնել exactly-once։ Գործնականում համադրում ես at-least-once առաքումը idempotent handler-ների հետ՝ unique key-երով, upsert-ներով կամ մշակված հաղորդագրությունների աղյուսակով, որպեսզի ստանաս exactly-once էֆեկտներ։
Q4Ի՞նչ է consumer lag-ը և ինչու՞ է այն կարևոր։
Lag-ը partition-ի վերջին offset-ի և consumer group-ի commit արած offset-ի տարբերությունն է՝ քանի հաղորդագրություն է սպասում։ Սա իրադարձահեն համակարգի առողջության ամենակարևոր ազդանշանն է։ Աճող lag-ը նշանակում է, որ consumer-ները producer-ներից դանդաղ են՝ բագի, դանդաղ կախվածության, կրկին ու կրկին փորձվող «թունավոր» հաղորդագրության կամ չափազանց քիչ consumer-ների պատճառով։ Քանի որ մեկ partition-ը խմբում կարդում է մեկ consumer, partition-ների քանակից ավելի consumer ավելացնելը չի օգնում։ Ես ալերտ կդնեի ժամանակի ընթացքում lag-ի աճի վրա, ոչ թե ֆիքսված թվի, և այն կզուգակցեի dead-letter և սխալների հաճախականության մետրիկաների հետ։
Q5Ինչպե՞ս զարգացնել իրադարձությունների սխեմաները՝ առանց consumer-ներին կոտրելու։
Իրադարձություններին վերաբերվիր որպես հանրային API-ի։ Սահմանիր դրանք Avro-ով, Protobuf-ով կամ JSON Schema-ով, գրանցիր schema registry-ում և պարտադրիր համատեղելիության ռեժիմ, սովորաբար backward կամ full։ Անվտանգ փոփոխություններն են ոչ պարտադիր դաշտերի ավելացումը լռելյայն արժեքներով, վտանգավորները՝ դաշտերի հեռացումը, վերանվանումը կամ իմաստի փոփոխությունը։ Կոտրող փոփոխության համար հրապարակիր իրադարձության նոր տարբերակ կամ նոր topic և գործարկիր երկուսն էլ, մինչև բոլոր consumer-ները միգրացվեն, հետո հանիր հինը։ Յուրաքանչյուր հաղորդագրություն պետք է կրի իր տեսակն ու տարբերակը, որպեսզի consumer-ները անցման ընթացքում կարողանան մշակել մի քանի տարբերակ։
Q6Ինչու՞ պարզապես ամեն ինչի համար Redis հերթ չօգտագործել։
Շատ հավելվածների համար կարող ես, և պետք է։ BullMQ-ն կամ Laravel-ի հերթերը Redis-ի վրա տալիս են կրկնակի փորձեր, backoff, ուշացումներ, rate limiting և պլանավորում՝ գրեթե առանց լրացուցիչ ենթակառուցվածքի։ Սահմանները հայտնվում են մասշտաբի և պատմության հետ․ Redis-ը տվյալները պահում է հիշողության մեջ, persistence-ը պետք է զգուշորեն կարգավորել, job-երը ջնջվում են ավարտից հետո, և ներկառուցված replay չկա նոր consumer-ի համար, որին պետք են անցյալ ամսվա իրադարձությունները։ Երբ մի քանի թիմերի պետք է նույն հոսքը անկախ, կամ քեզ պետք է երկար retention և շատ բարձր թողունակություն, Kafka-ի պես log-ը դառնում է ավելի լավ գործիք։ Ընտրիր ըստ կարիքի, ոչ թե ըստ մոդայի։
Սենիոր մակարդակի հարցեր
S1Ինչպե՞ս կերաշխավորեիր հերթականությունը յուրաքանչյուր էնտիտիի համար՝ միաժամանակ retry անելով ձախողված հաղորդագրությունները։
Հերթականությունը և retry-ները հակասում են իրար․ եթե պատվերի 5-րդ հաղորդագրությունը ձախողվում է, և դու առաջ ես գնում, 6-րդը կիրառվում է առաջինը։ Տարբերակներ՝ ըստ խստության։ Արգելափակիր partition-ը և retry արա տեղում՝ backoff-ով․ հերթականությունը պահպանվում է, բայց մեկ «թունավոր» հաղորդագրությունը կանգնեցնում է այդ partition-ի բոլոր key-երը, ուստի սահմանափակիր փորձերը և alert դիր։ Օգտագործիր retry topic՝ ամեն ուշացման մակարդակի համար, բայց նաև «կայանիր» նույն key-ի հաջորդ հաղորդագրությունները, մինչև ձախողվածը հաջողվի, ինչը պահանջում է փոքր per-key state store։ Կամ նախագծիր handler-ները այնպես, որ դիմանան խառը հերթականությանը տարբերակների միջոցով․ ամեն իրադարձություն կրում է էնտիտիի տարբերակը, իսկ consumer-ն անտեսում է այն ամենը, ինչ ավելի հին է իր պահածից։ Վերջին տարբերակը լավագույնս է մասշտաբավորվում և վերապրում replay-ները, ուստի նախընտրում եմ վիճակի փոփոխությունները դարձնել տարբերակով ստուգվող և իդեմպոտենտ՝ խիստ արգելափակումը պահելով միայն այն քիչ հոսքերի համար, որոնց դա իսկապես պետք է։
S2Ինքնուրույն կառավարվող Kafka՞, թե՞ managed ծառայություն․ ի՞նչ ես կշռադատում։
Kafka-ն լավ գործարկելը նշանակում է capacity planning broker-ների և դիսկերի համար, rolling upgrade-ներ, partition-ների վերաբաշխում, rack-aware ռեպլիկացիա, under-replicated partition-ների և controller-ի առողջության մոնիթորինգ, գումարած անվտանգություն՝ TLS-ով և ACL-ներով։ Դրա համար պետք են իրական փորձառություն և on-call ժամանակ։ Managed ծառայությունը վերացնում է սրա մեծ մասը, հաճախ ավելացնում է tiered storage և autoscaling, իսկ գինը throughput-ի միավորի ավելի բարձր հաշիվն է, գործիքների որոշակի lock-in և տարբերակների ու tuning-ի նկատմամբ ավելի քիչ վերահսկողություն։ Առանց հատուկ պլատֆորմային ինժեներների թիմի համար managed-ը գրեթե միշտ ավելի էժան է, երբ աշխատավարձերը հաշվի ես առնում։ Self-hosting-ը սկսում է իմաստ ունենալ մեծ, կայուն ծավալների, տվյալների տեղակայման խիստ կանոնների դեպքում կամ երբ պլատֆորմային թիմն արդեն գործարկում է նմանատիպ stateful համակարգեր։ Ամեն դեպքում կլիենտները պահիր ստանդարտ Kafka պրոտոկոլների վրա, որպեսզի հետագայում տեղափոխվելը հնարավոր մնա։
S3Event sourcing Kafka-ի վրա․ լա՞վ գաղափար է, թե՞ ոչ։
Kafka-ն հիանալի է իրադարձություններ բաշխելու համար, բայց թույլ հիմնական պահոց է event-sourced aggregate-ների համար։ Event sourcing-ին պետք է արագ բեռնել մեկ էնտիտիի բոլոր իրադարձությունները և ավելացնել միայն այն դեպքում, եթե էնտիտին դեռ սպասվող տարբերակում է՝ optimistic concurrency ստուգում։ Kafka-ն չունի ոչ per-entity ընթերցումներ, քանի որ topic-ները key-ով բաժանված են շատ էնտիտիների միջև, ոչ էլ պայմանական append։ Թիմերը, որոնք փորձում են սա, վերջում կառուցում են առանձին snapshot պահոց և պայքարում race condition-ների դեմ։ Ավելի լավ դիզայնը event store-ը պահում է տվյալների բազայում կամ հատուկ event store-ում՝ per-stream տարբերակավորմամբ, հետո այդ իրադարձությունները հրապարակում Kafka՝ outbox-ի կամ change data capture-ի միջոցով այլ consumer-ների համար։ Այդ դեպքում Kafka-ն անում է այն, ինչում լավ է․ fan-out, retention և replay projection-ների համար։
Բառարան
- Partition
- Topic-ի կարգավորված, միայն ավելացվող հատված․ հերթականության և զուգահեռության միավորը։
- Offset
- Գրառման դիրքի համարը partition-ի ներսում։
- Consumer group
- Consumer-ների խումբ, որոնք կիսում են topic-ի partition-ները՝ իրենց սեփական commit-ված offset-ներով։
- ISR
- In-sync replicas․ leader-ը և այն follower-ները, որոնք բավական մոտ են հասել, որ հաշվվեն acks=all-ի համար։
- Rebalance
- Partition-ների վերաբաշխում խմբի consumer-ների միջև, երբ անդամակազմը փոխվում է։
- Log compaction
- Յուրաքանչյուր key-ի համար առնվազն վերջին գրառման պահպանում և հին տարբերակների հեռացում։
- Tombstone
- null արժեքով գրառում, որը compacted topic-ում key-ը նշում է ջնջման համար։
- KRaft
- Kafka-ի ներկառուցված, Raft-ի վրա հիմնված մետատվյալների quorum․ միակ ռեժիմը, քանի որ Kafka 4.0-ը հեռացրեց ZooKeeper-ը։
- Dead-letter topic
- Վայր, ուր հաղորդագրություններն ընկնում են մշակման կրկնվող ձախողումներից հետո՝ ուսումնասիրության և replay-ի համար։
Շարունակեք սովորել
Պրոդուկտի գաղափար ունե՞ք։Եկեք ստեղծենք այն։
Պատմեք ձեր գաղափարի, պրոդուկտի կամ թիմի մասին։ Սովորաբար պատասխանում եմ մեկ աշխատանքային օրվա ընթացքում։