LinkTracker

GitHub-ի և Stack Overflow-ի թարմացումներին հետևող ծառայություն՝ Python-ով գրված երեք ասինխրոն բաղադրիչ, ծանուցումների առաքում Kafka-ով և պահեստային HTTP ուղիով, երկմակարդակ քեշ Valkey-ով և մետրիկաներ Prometheus-ում և Grafana-ում։

Ժամանակահատված
2026 թ․ փետրվար — 2026 թ․ հուլիս
Կարգավիճակ
Ավարտված
Իմ դերը
Միակ մշակողը՝ ճարտարապետությունը, ծանուցումների տրանսպորտը, արտադրողականությունը և բեռնվածության թեստավորումը, կոդը, թեստերը, ենթակառուցվածքը և դիտարկելիությունը։

Տեխնոլոգիաներ

  • Python
  • FastAPI
  • Apache Kafka
  • PostgreSQL
  • gRPC
  • asyncio
  • aiogram
  • Avro
  • Valkey
  • Prometheus
  • Grafana
  • Docker
  • Testcontainers
  • GitHub Actions

Խնդիրը

Օգտատերը ցանկանում է հետևել GitHub-ի ռեպոզիտորիաների և Stack Overflow-ի հարցերի թարմացումներին և ծանուցումներ ստանալ Telegram-ում։ Պարզունակ լուծումը բոլոր հղումները հերթով ստուգելն է նույն պրոցեսից, որը պատասխանում է օգտատիրոջը։ Այն բախվում է արտաքին API-ների սահմանափակումներին, խափանումների ժամանակ կորցնում է ծանուցումները և թույլ չի տալիս հասկանալ, թե կոնկրետ ինչ է կատարվում ներսում։

Լուծումը

Համակարգը բաժանված է երեք ասինխրոն բաղադրիչի։ Bot Service-ը պատասխանատու է օգտատիրոջ հետ երկխոսության և բաժանորդագրության հրամանների համար, Scrapper Service-ը՝ արտաքին աղբյուրների հարցման, իսկ դրանց միջև կանգնած մշակման շերտն ընտրում, առաջնահերթություն է տալիս և խմբավորում թարմացումները։ Թարմացումներն առաքվում են Kafka-ով, իսկ դրա անհասանելիության դեպքում մնում է ուղիղ HTTP ուղին։ Ծառայությունների միջև կանչերն անցնում են առանձին՝ gRPC-ով և REST-ով։

Ճարտարապետություն

LinkTracker-ի ճարտարապետությունըBot Service-ը Scrapper Service-ի հետ շփվում է REST-ով և gRPC-ով, թարմացումները հասնում են Kafka-ով, իսկ Kafka-ի անհասանելիության դեպքում՝ ուղիղ HTTP-ով։ Scrapper-ը հարցումներ է անում GitHub-ին և Stack Overflow-ին և բաժանորդագրությունները պահում է PostgreSQL-ում, իսկ հետևվող հղումների ցանկը տրվում է Valkey-ի քեշով։ Երկու ծառայություններն էլ մետրիկաներ են տալիս Prometheus-ին։REST / gRPCHTTP fallbackproduceconsumeժամանակացույցով հարցումTelegram-օգտատերBot ServicePython · aiogramKafkaupdates · DLQScrapper ServiceFastAPI · gRPC · schedulerValkey · քեշPostgreSQLGitHub API · Stack Overflow APIPrometheus · Grafana/metrics · RED
Bot Service-ը Scrapper Service-ի հետ շփվում է REST-ով և gRPC-ով, թարմացումները հասնում են Kafka-ով, իսկ Kafka-ի անհասանելիության դեպքում՝ ուղիղ HTTP-ով։ Scrapper-ը հարցումներ է անում GitHub-ին և Stack Overflow-ին և բաժանորդագրությունները պահում է PostgreSQL-ում, իսկ հետևվող հղումների ցանկը տրվում է Valkey-ի քեշով։ Երկու ծառայություններն էլ մետրիկաներ են տալիս Prometheus-ին։

Հիմնական տեխնիկական որոշումները

  • Bot Service-ի և Scrapper Service-ի բաժանումը մեկ պրոցեսի փոխարեն. արտաքին API-ների հարցումը չի արգելափակում օգտատիրոջն ուղղված պատասխանները։
  • Ծանուցումների տրանսպորտը սահմանվում է կոնֆիգուրացիայով. լռելյայն օգտագործվում է Kafka-ն, ընդ որում երկու տրանսպորտներից յուրաքանչյուրը կարող է պահեստային լինել մյուսի համար։ Կոմպոզիտ ուղարկիչը փոխում է ուղին միայն առաքման սխալի դեպքում, իսկ հենց կոդի սխալները դուրս է նետում, այլ ոչ թե քողարկում ուղու փոխարկմամբ։
  • Kafka-ի հաղորդագրությունները՝ JSON կամ Avro՝ Schema Registry-ով. հաղորդագրության սխեման ֆիքսվում է, այլ ոչ թե ենթադրվում։
  • Այն հաղորդագրությունները, որոնք չհաջողվեց մշակել, գնում են dead letter queue, այլ ոչ թե լուռ կորչում։
  • Timeout, կրկնափորձեր էքսպոնենցիալ հետաձգմամբ և circuit breaker՝ արտաքին API-ներին ուղղված բոլոր հարցումների վրա, որպեսզի GitHub-ի կամ Stack Overflow-ի դեգրադացիան շարքից չհանի ամբողջ ծառայությունը։
  • Երկմակարդակ քեշ սեփական GET /links-ի համար. երեք հանգույցից Valkey-կլաստեր, իսկ դրա առջև՝ ոչ պարտադիր ներպրոցեսային LRU+TTL։ Բանալիները հեշավորվում են ըստ զրույցի նույնացուցիչի, ուստի մեկ զրույցի բոլոր էջերն ընկնում են կլաստերի նույն slot-ը, և գրառման ժամանակ չեղարկումը մնում է մեկ անցումով։
  • REST և gRPC ծառայությունների միջև. gRPC՝ ներքին տրաֆիկի համար, REST՝ որպես հասկանալի արտաքին սահման։
  • Ընտրությունը, առաջնահերթությունները և խմբավորումը տեղափոխված են ծառայությունների միջև առանձին շերտ, այլ ոչ թե Scrapper. բոտը ստանում է արդեն ֆիլտրված և խմբավորված թարմացումներ։

Ինչպես է աշխատում

Օգտատերն ավելացնում է հղում GitHub-ի ռեպոզիտորիային կամ Stack Overflow-ի հարցին՝ Telegram-բոտի միջոցով։ Bot Service-ն ընդունում է օգտատիրոջ հրամանները և բաժանորդագրությունները փոխանցում Scrapper Service-ին REST-ով և gRPC-ով։ Scrapper-ը ժամանակացույցով հարցումներ է անում արտաքին API-ներին, համեմատում վիճակը և գտնված թարմացումները հրապարակում Kafka-ում։ Այնտեղից դրանք վերցնում է մշակման շերտը, ընտրում է անհրաժեշտը և խմբավորում, իսկ Bot Service-ը կարդում է արդեն պատրաստը և ուղարկում օգտատիրոջը։ Scrapper-ը հղումները մշակում է խմբաքանակներով՝ մի քանի զուգահեռ առաջադրանքով։ Մեկ հղման սխալը չի կանգնեցնում մնացածը և ներառվում է ամփոփիչ հաշվետվության մեջ։

Եթե Kafka-ն անհասանելի է, Scrapper-ը թարմացումն ուղարկում է ուղիղ HTTP-ով՝ ուղին այլ է, արդյունքն օգտատիրոջ համար՝ նույնը։ Այն հաղորդագրությունները, որոնք չհաջողվեց մշակել, գնում են dead letter queue, այլ ոչ թե անհետանում։ Հրապարակային REST-ը սահմանափակված է հարցումների հաճախականությամբ՝ ըստ IP-ի, յուրաքանչյուր endpoint-ի համար առանձին bucket-ով։

Կայունություն արտաքին խափանումների նկատմամբ

Արտաքին API-ներին ուղղված բոլոր հարցումներն ունեն timeout, կրկնափորձեր և circuit breaker։ Երբ GitHub-ը կամ Stack Overflow-ը պատասխանում են դանդաղ կամ ընդհանրապես չեն պատասխանում, հարցումներն ընդհատվում են timeout-ով և կրկնվում հետաձգմամբ, իսկ կայուն սխալների դեպքում շղթան բացվում է։ Ծառայությունը դադարում է դիմել չաշխատող աղբյուրին և շարունակում սպասարկել մնացածը։

Թարմացումների ընտրություն և խմբավորում

Scrapper-ի և բոտի միջև կանգնած է մշակման առանձին շերտ՝ նույնպես Kafka-ի սպառող։ Այն կարդում է հում թարմացումները մեկ topic-ից, դեն է նետում ֆիլտրման կանոններին համապատասխանողները, մնացածին տալիս է առաջնահերթություն ըստ հիմնաբառերի, կրճատում է չափից երկար տեքստերը, կարճ ժամանակահատվածում խմբավորում է նույն զրույցին ուղղվածը և արդյունքը հրապարակում մեկ այլ topic-ում։ Բոտը կարդում է արդեն պատրաստը։

Շերտը միտումնավոր դետերմինիստական է. ֆիլտրումը, առաջնահերթությունները և տեքստի կրճատումը կանոններ են, ոչ թե մոդել։ Ներսում ոչ մի LLM չկա։ Ընդ որում կրճատումը դուրս է բերված ինտերֆեյսի հետևում, ուստի կանոնը մոդելով կարելի է փոխարինել՝ առանց հենց pipeline-ին դիպչելու։ Շերտի կոդը ոչինչ չի ներմուծում Scrapper-ից և գտնվում է առանձին փաթեթում։ Այն ինքնուրույն ծառայության հանելու համար ոչինչ փոխել պետք չի լինի։

Քեշը և ինչ տվեց այն

Հետևվող հղումների ցանկը տրվում է երկմակարդակ քեշով. երեք հանգույցից Valkey-կլաստեր, իսկ դրա առջև՝ ոչ պարտադիր ներպրոցեսային LRU+TTL։ Բանալիները հեշավորվում են ըստ զրույցի նույնացուցիչի, ուստի մեկ զրույցի բոլոր էջերն ընկնում են կլաստերի նույն slot-ը, և գրառումը դրանք չեղարկում է մեկ անցումով՝ առանց հարևաններին դիպչելու։

Բեռնվածության թեստն անցկացվել է երեք կոնֆիգուրացիայով անընդմեջ։ Սա էլ հենց պատասխանն է այն հարցին, թե արդյոք արժեր ջանք թափել քեշի վրա.

Կոնֆիգուրացիա RPS p50 p99
առանց քեշի 146 250 մվ 690 մվ
Valkey 710 22 մվ 250 մվ
Valkey + ներպրոցեսային 876 11 մվ 230 մվ

Հիմնական շահույթը տալիս է առաջին մակարդակը՝ գրեթե հնգապատիկ թողունակությամբ և մեկ կարգով՝ մեդիանով։ Երկրորդն ավելացնում է ևս մեկ քառորդ և կիսով չափ կրճատում մնացած հապաղումը։ Շահույթը փոքր է, քանի որ Valkey-ն առանց այդ էլ արագ է պատասխանում loopback-ով։

Գրման հարցումների արագությունն այդ ընթացքում սահմանափակված է PostgreSQL-ով, և քեշը դրանք չի արագացնում. POST-ը և DELETE-ը երկրորդ և երրորդ սցենարների միջև մնում են 15%-ի սահմաններում։ Առաջինի համեմատ դրանց տեսանելի բարելավումը տալիս է ոչ թե քեշն ինքնին, այլ այն, որ ընթերցումը դադարել է ռեսուրսներ խլել գրառումից։

Դիտարկելիություն (observability)

Երկու ծառայություններն էլ /metrics են տալիս Prometheus-ի ձևաչափով՝ RED-մետրիկաներ (հարցումների հաճախականություն, սխալների բաժին, տևողություն) գումարած կիրառական հաշվիչ links_on_track_total, որը ցույց է տալիս, թե քանի հղում է այս պահին հետևվում։ Dashboard-ները հավաքված են Grafana-ում, այնպես որ համակարգի վիճակը երևում է ոչ թե լոգերից, այլ գրաֆիկներից։

Թեստեր և բիլդ

Թեստերը բաժանված են չորս շերտի՝ unit, ինտեգրացիոն, e2e և բեռնվածության։ Ինտեգրացիոնները Testcontainers-ով գործարկում են իրական Kafka, PostgreSQL և Valkey, ուստի ստուգվում է իրական վարքը, ոչ թե mock-երի վարքը։ Բեռնվածության թեստերը տվել են այն թվերը, որոնք վերևում՝ մետրիկաներում են։ Այս ամենն անցնում է GitHub Actions-ում յուրաքանչյուր push-ի ժամանակ՝ լինտերների հետ միասին, իսկ ծառայությունները փաթեթավորված են Docker-ում։

Արդյունքներ

  • Բեռնվածության թեստը երեք կոնֆիգուրացիայով ցույց տվեց, թե ինչ տվեց քեշի յուրաքանչյուր մակարդակը. առանց քեշի՝ 146 RPS, p50 250 մվ, Valkey-ով՝ 710 RPS, p50 22 մվ, Valkey-ով և ներպրոցեսային քեշով՝ 876 RPS, p50 11 մվ և p99 230 մվ։
  • Երեք անցումներն էլ ավարտվեցին առանց մեկ խափանման. լավագույն սցենարում՝ 314 045 հարցում, զրո սխալ և զրո բացառություն։
  • Գրման հարցումների արագությունը սահմանափակված է PostgreSQL-ով, և քեշը դրանք չի արագացնում, դա երևում է թվերից։
  • Ծանուցումների առաքումը դիմանում է հիմնական տրանսպորտի խափանմանը՝ պահեստայինի հաշվին։

Մետրիկաներ

Բեռնվածության թեստ
876 RPS
Հապաղման մեդիան
11 ms
99-րդ պերցենտիլ
230 ms