Բոլոր դասերը
ՃարտարապետությունԱռաջադեմ14 րոպե ընթերցումԻմ ստեկում

Մասշտաբավորում և load balancing

Ուղղահայաց ընդդեմ հորիզոնական մասշտաբավորման, load balancer-ներ, քեշավորման շերտեր, ռեպլիկաներ, pooling և sharding։

  • Vertical / horizontal
  • Load balancers
  • CDN
  • Read replicas
  • Sharding
Քայլ 1/4
Մեկ սերվեր, շատ օգտատերեր։ CPU-ն 100% է, բոլորը սպասում են։ Լավ չէ։

30 վայրկյանում

Մասշտաբավորումը նշանակում է համակարգը պահել արագ և հասանելի, երբ տրաֆիկն աճում է։ Կարող ես գնել ավելի մեծ սերվեր (ուղղահայաց) կամ ավելացնել սերվերներ (հորիզոնական), ինչը պահանջում է stateless հավելվածներ և առջևում load balancer։ Իրական «խցանումների» մեծ մասը տվյալների բազայում է, ուստի քեշավորումը, read replica-ները, connection pooling-ը և հերթերը սովորաբար գալիս են sharding-ից առաջ։ Նախ չափիր, հետո մասշտաբավորիր այն մասը, որն իրականում դանդաղ է։

Ինչու եմ օգտագործում

  • Օգտագործում եմ, որովհետև տրաֆիկը հազվադեպ է հարթ․ ակցիաները, push արշավները և ճաշի ժամի պատվերները ստեղծում են պիկեր, որոնք մեկ պրոցեսը չի կարող կլանել։
  • Հավելվածները սկզբից stateless նախագծելը սերվերներ ավելացնելը դարձնում է կոնֆիգուրացիայի փոփոխություն, ոչ թե վերագրում։
  • Իմ շտկած արտադրողականության խնդիրների մեծ մասը տվյալների բազայի և քեշավորման խնդիրներ են, ուստի շերտերի իմացությունը գումար է խնայում․ քեշը կամ ինդեքսը հաճախ ավելի արդյունավետ են, քան նոր սարքավորումը։
  • Capacity planning-ը թույլ է տալիս հաճախորդին ասել, թե ինչ կարող է դիմանալ իր համակարգը և ինչ կարժենա հաջորդ քայլը՝ նախքան դրա կարիքն առաջանալը։

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

01Ուղղահայաց ընդդեմ հորիզոնականի, և ինչու է stateless-ը կարևոր

Ուղղահայաց մասշտաբավորումը նշանակում է ավելի մեծ մեքենա՝ ավելի շատ CPU, RAM, ավելի արագ դիսկեր։ Այն պարզ է, կոդի փոփոխություն չի պահանջում և հաճախ ճիշտ առաջին քայլն է, բայց ունի առաստաղ, թանկանում է, և մեկ մեքենան դեռ ձախողման մեկ կետ է։ Հորիզոնական մասշտաբավորումը նշանակում է ավելի շատ մեքենաներ, որոնք load balancer-ի հետևում գործարկում են հավելվածի նույնական պատճեններ։ Այն մասշտաբավորվում է ավելի հեռու և գոյատևում է հանգույցի կորուստը, բայց միայն եթե ցանկացած ինստանս կարող է սպասարկել ցանկացած հարցում։ Դրա համար պետք է stateless հավելված․ սեսիաները Redis-ում կամ ստորագրված թոքեններում, upload-ները object storage-ում՝ լոկալ դիսկի փոխարեն, քեշը ընդհանուր պահոցում, պլանավորված job-երը գործարկվում են մեկ անգամ, ոչ թե ամեն հանգույցում, և ոչ մի կարևոր վիճակ հիշողության մեջ։ Հավելվածները stateless դարձրու վաղ, նույնիսկ մեկ սերվերի վրա, և հորիզոնական մասշտաբավորումը կդառնա պարզ։

  • Սեսիաներ և քեշ՝ Redis, ոչ թե պրոցեսի հիշողություն կամ լոկալ ֆայլեր։
  • Ֆայլեր՝ S3-համատեղելի object storage՝ մատուցվող CDN-ով։
  • Cron՝ մեկ scheduler կամ բաշխված lock, որպեսզի job-երը N անգամ չգործարկվեն։

02Load balancer-ներ՝ L4 ընդդեմ L7, ալգորիթմներ, health check-եր

Load balancer-ը հարցումները բաշխում է ինստանսների միջև։ Layer 4 balancer-ները աշխատում են TCP/UDP կապերի հետ․ շատ արագ են, անկախ պրոտոկոլից, բայց «կույր» են URL-ների և header-ների նկատմամբ։ Layer 7 balancer-ները հասկանում են HTTP․ նրանք ավարտում են TLS-ը, ուղղորդում ըստ host-ի կամ path-ի, ավելացնում header-ներ, կրկնում ձախողված հարցումները, սեղմում և rate limit անում։ nginx-ը, HAProxy-ն, Envoy-ն, Traefik-ը և ամպային application load balancer-ները տարածված L7 տարբերակներ են։ Ալգորիթմներից են round robin (պարզ), least connections (ավելի լավ է, երբ հարցումների տևողությունը տարբեր է), weighted (խառը սարքավորում) և hashing (նույն կլիենտը՝ նույն սերվերին)։ Health check-երը հեռացնում են անսարք ինստանսները․ պասիվ ստուգումները հետևում են իրական տրաֆիկի սխալներին, ակտիվները պարբերաբար հարցում են անում health endpoint-ին։ Sticky session-ները օգտատիրոջը կապում են մեկ ինստանսի․ դրանք թաքցնում են stateful լինելը և անհավասարակշռում բեռը, ուստի նախընտրիր ընդհանուր սեսիաների պահոց, իսկ «կպչունությունը» պահիր այնպիսի դեպքերի համար, ինչպես WebSocket fallback-երը։

nginx
upstream app {
    least_conn;
    server 10.0.0.11:3000 max_fails=3 fail_timeout=10s;
    server 10.0.0.12:3000 max_fails=3 fail_timeout=10s;
    keepalive 32;
}
 
server {
    listen 443 ssl;
    server_name example.com;
    # ssl_certificate / ssl_certificate_key lines go here
    location / {
        proxy_pass http://app;
        proxy_http_version 1.1;
        proxy_set_header Connection "";
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_next_upstream error timeout http_502 http_503;
    }
}

03Քեշավորման շերտեր՝ CDN, reverse proxy, հավելված, տվյալների բազա

Ամենաէժան հարցումն այն է, որը երբեք չի հասնում քո հավելվածին։ CDN-ը քեշավորում է ստատիկ ռեսուրսները և քեշավորվող էջերը օգտատերերին մոտ edge կետերում, ինչպես նաև կլանում է պարզ DDoS տրաֆիկը։ Reverse-proxy քեշը (nginx, Varnish) կարող է մի քանի վայրկյան մատուցել կրկնվող պատասխանները, ինչը հարթեցնում է պիկերը «տաք» էջերում։ Հավելվածում Redis քեշը cache-aside պատերնով պահում է թանկ հարցումների արդյունքները TTL-ով։ Տվյալների բազան ունի իր buffer cache-ը, ուստի լավ ինդեքսները «տաք» տվյալները պահում են հիշողության մեջ։ Յուրաքանչյուր շերտի պետք է invalidation-ի հստակ պատմություն․ ռեսուրսների ֆայլանուններում տարբերակ դիր, էջերի համար օգտագործիր կարճ TTL կամ purge արա փոփոխության ժամանակ, և ջնջիր կամ թարմացրու key-երը, երբ տվյալները փոխվում են։ Պաշտպանվիր stampede-ից, երբ սիրված key-ի ժամկետը լրանում է՝ lock-երով, request coalescing-ով կամ թեթևակի պատահական TTL-ներով։

04Տվյալների բազայի մասշտաբավորում՝ pooling, ռեպլիկաներ, partitioning, sharding

Տվյալների բազաները սովորական «խցանումն» են, որովհետև պահում են վիճակ և դժվար է դրանք բազմապատկել։ Աշխատիր ըստ գնի հերթականության։ Առաջինը՝ ինդեքսներ և հարցումների շտկում, որոնք գտնում ես slow-query լոգերով և EXPLAIN-ով։ Երկրորդը՝ connection pooling․ PostgreSQL-ը յուրաքանչյուր կապի համար օգտագործում է առանձին պրոցես, ուստի հավելվածի տասնյակ ինստանսներ կարող են սպառել այն. PgBouncer-ը transaction ռեժիմում թույլ է տալիս հազարավոր կլիենտային կապերին կիսել փոքր pool։ Երրորդը՝ read replica-ներ կարդալով ծանրաբեռնված տրաֆիկի համար՝ հիշելով, որ ռեպլիկաները ուշանում են, ուստի read-your-own-writes հոսքերը պետք է գնան primary։ Չորրորդը՝ մեծ աղյուսակների partitioning (ըստ ամսաթվի կամ tenant-ի) մեկ տվյալների բազայի ներսում, որպեսզի ինդեքսները փոքր մնան և հին տվյալները էժան ջնջվեն։ Վերջինը՝ sharding․ տվյալների բաժանում մի քանի տվյալների բազաների միջև ըստ shard key-ի, օրինակ՝ tenant ID-ի։ Sharding-ը մասշտաբավորում է գրառումները, բայց դժվարացնում է միջ-shard հարցումները, տրանզակցիաները և վերահավասարակշռումը, ուստի հետաձգիր այն, մինչև չափած լինես կարիքը։

ini
; /etc/pgbouncer/pgbouncer.ini
[databases]
app = host=127.0.0.1 port=5432 dbname=app
 
[pgbouncer]
listen_addr = 127.0.0.1
listen_port = 6432
auth_type = scram-sha-256
auth_file = /etc/pgbouncer/userlist.txt
pool_mode = transaction
max_client_conn = 1000
default_pool_size = 20

05Հերթեր, back-pressure և capacity planning

Ամեն ինչ չէ, որ պետք է տեղի ունենա հարցման ներսում։ Email-ների ուղարկում, հաշվետվությունների գեներացում, նկարների չափափոխում, ERP-ի հետ համաժամեցում՝ ուղարկիր դրանք հերթ և թող worker-ները մշակեն կայուն տեմպով։ Հերթերը կլանում են պիկերը, մեկուսացնում են դանդաղ երրորդ կողմերին և թույլ են տալիս worker-ները մասշտաբավորել վեբ սերվերներից առանձին։ Ավելացրու նաև back-pressure․ rate limit-եր ամեն կլիենտի համար, timeout-ներ ամեն արտաքին կանչի վրա, circuit breaker-ներ ձախողվող կախվածությունների համար և նրբանկատ դեգրադացիա (մատուցիր քեշավորված տվյալներ, թաքցրու ոչ էական վիջեթները) գերբեռնվածության ժամանակ։ Capacity planning-ը կապում է այս ամենը։ Չափիր ընթացիկ թողունակությունը, latency-ի պերցենտիլները (p95, p99) և ռեսուրսների օգտագործումը, load test արա k6-ի պես գործիքներով՝ կոտրման կետը գտնելու համար, պարզիր, թե որ ռեսուրսն է առաջինը հագենում, հետո պահիր պաշար (հաճախ 30-ից 50 տոկոս) պիկերի և հանգույցների ձախողման համար։ Կրկնիր խոշոր ֆիչերից հետո, որովհետև «խցանումները» տեղափոխվում են։

  • Հետևիր p95/p99 latency-ին, ոչ թե միջինին. միջինը թաքցնում է դանդաղ օգտատերերին։
  • Load test արա staging պատճենի վրա՝ իրատեսական տվյալների ծավալներով։
  • Պլանավորիր պիկի ժամին մեկ հանգույց կորցնելը (N+1 capacity)։

Օգտագործեք, երբ

  • Տրաֆիկի կամ տվյալների աճը CPU-ն, հիշողությունը, տվյալների բազայի կապերը կամ latency-ն մոտեցնում է իրենց սահմաններին։
  • Քեզ պետք է բարձր հասանելիություն․ մեկ ինստանսի ձախողումը չպետք է վայր գցի կայքը։
  • Կանխատեսելի պիկեր են սպասվում (արշավներ, մեկնարկներ, սեզոնային պիկեր)։
  • Դանդաղ ֆոնային աշխատանքը արգելափակում է օգտատիրոջն ուղղված հարցումները։

Մի՛ օգտագործեք, երբ

  • Մինչև չափելը․ դանդաղության մեծ մասը բացակայող ինդեքս կամ N+1 հարցում է, ոչ թե սերվերների պակաս։
  • Sharding անել այն տվյալների բազան, որին ավելի մեծ ինստանսը, ռեպլիկաները և քեշավորումը դեռ տարիներ կդիմանային։
  • Ավելացնել այն հավելվածի ինստանսներ, որը դեռ սեսիաները կամ ֆայլերը պահում է լոկալ դիսկում։

Այլընտրանքներ

  • Նախ ավելի մեծ սերվեր

    Ուղղահայաց մասշտաբավորումը էժան է ինժեներական ժամանակով և հաճախ տալիս է մեկ տարի կամ ավելի։

  • Managed պլատֆորմներ

    PaaS կամ serverless պլատֆորմները autoscale են անում քո փոխարեն՝ զիջելով վերահսկողությունն ու ծախսերի կանխատեսելիությունը։

  • Edge քեշավորում

    CDN-ից ավելի շատ էջեր մատուցելը կարող է վերացնել origin-ի բեռի մեծ մասը կոնտենտով հարուստ կայքերի համար։

  • Kubernetes autoscaling

    Horizontal pod autoscaling շատ ծառայությունների համար. ծանր է մեկ-երկու հավելվածի համար։

Լավագույն պրակտիկաներ

  1. Հավելվածները պահիր stateless․ սեսիաները, քեշը և ֆայլերը՝ ընդհանուր ծառայություններում։
  2. Յուրաքանչյուր հավելվածում դիր health-check endpoint և թող load balancer-ն օգտագործի այն։
  3. Ամեն ցանցային կանչի վրա սահմանիր timeout-ներ և կրկնակի փորձեր backoff-ով։
  4. Օգտագործիր connection pooling՝ նախքան մեկ տվյալների բազա կիսող հավելվածի ինստանսներ ավելացնելը։
  5. Դանդաղ կամ արտաքին աշխատանքը տեղափոխիր հերթեր՝ կրկնակի փորձերով և dead-letter մշակմամբ։
  6. Load test արա մեծ մեկնարկներից առաջ և պահիր պաշար պիկի ժամին հանգույցի ձախողման համար։

Տարածված սխալներ

  • Մասշտաբավորել հավելվածի սերվերները, մինչդեռ իրական «խցանումը» միակ տվյալների բազան է։
  • Ապավինել sticky session-ներին՝ լոկալ վիճակը թաքցնելու համար, հետո կորցնել օգտատերերին, երբ հանգույցն ընկնում է։
  • Գրառումից անմիջապես հետո կարդալ ուշացող ռեպլիկայից և ցույց տալ հնացած տվյալներ։
  • Քեշավորել առանց invalidation-ի պլանի, հետո մատուցել սխալ գներ կամ պաշար։
  • Scale out-ից հետո cron job-եր գործարկել ամեն ինստանսում, այնպես որ դրանք կատարվում են մի քանի անգամ։

Խորը վերլուծություն

06Հերթերի տեսություն, որը կարող ես կիրառել․ Little-ի օրենք և ծանրաբեռնվածություն

Little-ի օրենքը կապում է երեք թիվ․ ընթացքի մեջ գտնվող հարցումներ = ժամանման արագություն x համակարգում անցկացրած միջին ժամանակ։ Վայրկյանում 200 հարցման և մեկ հարցման համար 250 ms-ի դեպքում ցանկացած պահի մոտ 50 հարցում ընթացքի մեջ է, ինչը ցույց է տալիս, թե քանի worker, thread կամ տվյալների բազայի միացում է քեզ պետք։ Երկրորդ դասը ոչ գծային է․ երբ ռեսուրսը մոտենում է լիարժեք ծանրաբեռնվածությանը, սպասման ժամանակը ոչ թե հավասարաչափ աճում է, այլ պայթում։ 50 տոկոս զբաղված սերվերն ունի կարճ հերթեր․ 90 տոկոսի դեպքում փոքր պոռթկումներն արդեն երկար հերթեր են ստեղծում, և p99 latency-ն ցատկում է։ Ահա թե ինչու է պաշարը կարևոր, և ինչու են միջինները մոլորեցնում։ Սա նաև բացատրում է, թե ինչու գերբեռնված տվյալների բազային միացումներ ավելացնելը այն դանդաղեցնում է․ ավելի շատ զուգահեռ աշխատանքը նշանակում է ավելի շատ մրցակցություն, ոչ թե ավելի շատ throughput։ Pool-երի չափը որոշիր Little-ի օրենքով՝ պաշարով, և սահմանափակիր զուգահեռությունը, որպեսզի ավելցուկային բեռը սպասի քո վերահսկած հերթում։

  • Ընթացքի մեջ = արագություն x latency․ օգտագործիր pool-երի և worker-ների չափը որոշելու համար։
  • Պլանավորիր այնպես, որ պիկի ժամին հիմնական ռեսուրսները աշխատեն հագեցումից զգալիորեն ցածր։
  • Սահմանափակիր զուգահեռությունը եզրում՝ փոխանակ թույլ տալու, որ այն կուտակվի տվյալների բազայում։

07Retry-ների փոթորիկներ, timeout-ների բյուջեներ և load shedding

Retry-ները օգնում են կարճատև խափանումների ժամանակ և վնասում գերբեռնվածության ժամանակ։ Եթե երեք շերտերից յուրաքանչյուրը երեք անգամ retry է անում, մեկ օգտատիրական հարցումը կարող է դառնալ 27 կանչ ամենաներքևի ծառայությանը՝ հենց այն պահին, երբ այն դժվարանում է։ Retry արա միայն մեկ շերտում, միայն իդեմպոտենտ գործողությունները, exponential backoff-ով և full jitter-ով, և retry-ները սահմանափակիր բյուջեով (օրինակ՝ retry-ները կարող են ավելացնել առավելագույնը 10 տոկոս լրացուցիչ բեռ)։ Յուրաքանչյուր հարցման տուր deadline և մնացած ժամանակը փոխանցիր downstream, որպեսզի ներքին կանչերը երբեք չգերազանցեն օգտատիրոջ համբերությունը։ Գերբեռնվածության ժամանակ մերժիր վաղ՝ 429 կամ 503 և Retry-After-ով, փոխանակ անվերջ հերթում պահելու․ արագ ձախողումն ավելի էժան է, քան դանդաղը։ Առաջնահերթություն տուր տրաֆիկին․ analytics-ը և recommendations-ը կտրիր checkout-ից առաջ։ Circuit breaker-ները դադարեցնում են անընդհատ ձախողվող կախվածության կանչերը և պարբերաբար ստուգում այն՝ ժամանակ տալով վերականգնվելու։

nginx
# nginx: per-IP rate limit with a small burst, reject the rest fast
limit_req_zone $binary_remote_addr zone=api:10m rate=20r/s;
limit_req_status 429;
 
server {
    location /api/ {
        limit_req zone=api burst=40 nodelay;
        proxy_connect_timeout 2s;
        proxy_read_timeout 10s;
        proxy_pass http://app;
    }
}

08Consistent hashing և քեշի մասշտաբավորում

Երբ key-երը բաշխում ես N քեշի կամ shard հանգույցների միջև hash(key) mod N-ով, մեկ հանգույց ավելացնելը վերաքարտեզագրում է գրեթե բոլոր key-երը, քեշը դատարկվում է, և տվյալների բազան միանգամից ստանում է ամբողջ բեռը։ Consistent hashing-ը հանգույցներն ու key-երը տեղադրում է օղակի վրա․ յուրաքանչյուր key պատկանում է ժամացույցի սլաքի ուղղությամբ հաջորդ հանգույցին, ուստի հանգույց ավելացնելը կամ հեռացնելը տեղափոխում է key-երի միայն մոտ 1/N-ը։ Վիրտուալ հանգույցները (շատ կետեր մեկ սերվերի համար) հարթեցնում են բաշխումը։ Redis Cluster-ն օգտագործում է հարակից գաղափար․ 16,384 hash slot՝ բաշխված հանգույցներին, որոնք resharding-ի ժամանակ տեղափոխվում են կտորներով, և hash tag-եր, օրինակ՝ {tenant:42}, որպեսզի կապված key-երը մնան մեկ slot-ում։ Load balancer-ներում nginx-ի hash դիրեկտիվը consistent պարամետրով նույնն է անում upstream-ի ընտրության համար։ Տաք key-երի համար, որոնք մեկ հանգույցը չի կարող սպասարկել, ընդհանուր քեշի առջև ավելացրու փոքր in-process քեշ՝ կարճ TTL-ով։

09Tail latency և fan-out

Երբ մեկ հարցումը բաշխվում է շատ backend-ների, պատասխանի ժամանակը որոշում է ամենադանդաղը։ Եթե յուրաքանչյուր backend-ի դանդաղ պատասխանի հավանականությունը 1 տոկոս է, դրանցից 100-ին դիպչող հարցումը դանդաղ է ժամանակի մեծ մասում։ Tail latency-ն առաջանում է garbage collection-ի դադարներից, սառը քեշերից, աղմկոտ հարևաններից, lock-երի մրցակցությունից և հերթերից։ Պաշտպանություններ․ կրճատիր fan-out-ը նախապես հաշվարկելով կամ ապանորմալացնելով, դիր timeout-ներ յուրաքանչյուր կանչի համար և վերադարձրու մասնակի արդյունքներ, երբ ոչ էական մասերը ուշանում են, օգտագործիր hedged request-ներ՝ ուղարկելով երկրորդ պատճենը մեկ այլ ռեպլիկայի, երբ կանչը գերազանցում է p95-ը, միայն իդեմպոտենտ ընթերցումների համար, և ինստանսների չափերն ու runtime կարգավորումները պահիր միատեսակ, որպեսզի մեկ թույլ հանգույցը չքաշի ամբողջ pool-ը։ Հիմնական endpoint-ների համար միշտ ցույց տուր p99-ը և p99.9-ը p50-ի կողքին և ուսումնասիրիր ամենադանդաղ trace-երը, ոչ միայն միջինները։

Իրական իրավիճակներ

01

Autoscaling-ից հետո տվյալների բազայի միացումները սպառվել են

Խնդիր

Արշավի ընթացքում հավելվածը 4 ինստանսից մասշտաբավորվեց 16-ի։ Մի քանի րոպե անց PostgreSQL սերվերը սկսեց մերժել միացումները «too many clients» սխալով, և ամբողջ կայքն ընկավ։

Ինչպես լուծել

Յուրաքանչյուր ինստանս ուներ 25 միացման pool, այնպես որ 16 ինստանսը պահանջեց 400 միացում՝ մոտ 200-ի սահմանի դեպքում։ PostgreSQL-ի առջև դիր PgBouncer transaction ռեժիմում և բոլոր ինստանսներն ուղղիր դեպի այն՝ սերվերային pool-ով, որի չափը որոշված է Little-ի օրենքով (թերևս 20-ից 40 իրական միացում)։ Իջեցրու յուրաքանչյուր ինստանսի pool-ի չափը, դիր statement timeout և autoscaling-ը սահմանափակիր այն թվով, որը տվյալների բազայի շերտը կարող է դիմանալ։ Ստուգիր prepared statement-ների աջակցությունը․ PgBouncer 1.21-ը և ավելի նորերը կարող են հետևել պրոտոկոլի մակարդակի prepared statement-ներին max_prepared_statements-ով։ Հաջորդ արշավից առաջ load test արա մասշտաբավորված կոնֆիգուրացիան։

02

Քեշի ժամկետի ավարտը գցում է կայքը

Խնդիր

Գլխավոր էջի ապրանքների ցանկը քեշավորվում է 10 րոպեով։ Ամեն 10 րոպեն մեկ latency-ն ցատկում է, և տվյալների բազայի CPU-ն մի քանի վայրկյանով հասնում է 100 տոկոսի։

Ինչպես լուծել

Սա cache stampede է․ երբ key-ի ժամկետն ավարտվում է, հարյուրավոր հարցումներ միաժամանակ miss են անում, և բոլորը վերակառուցում են այն։ Օգտագործիր lock, որպեսզի միայն մեկ հարցում վերակառուցի, իսկ մյուսները սպասարկեն հին արժեքը (stale-while-revalidate), կամ key-ը թարմացրու ֆոնում՝ ժամկետի ավարտից քիչ առաջ։ TTL-ներին ավելացրու պատահական jitter, որպեսզի կապված key-երի ժամկետները միասին չավարտվեն։ Proxy-ի մակարդակում nginx-ի proxy_cache_lock-ը և proxy_cache_use_stale updating-ը նույնն են անում ամբողջական էջերի համար։ Վերջապես, ստուգիր հիմքում ընկած հարցումը EXPLAIN-ով․ վայրկյաններ տևող վերակառուցմանը սովորաբար ինդեքս է պետք։

03

Մեկ դանդաղ կախվածությունը ներքև է քաշում բոլոր էջերը

Խնդիր

Երրորդ կողմի recommendations API-ն երբեմն 8 վայրկյան է տևում։ Քանի դեռ այն դանդաղ է, հավելվածի բոլոր worker-ները զբաղված են սպասելով, և նույնիսկ այն էջերը, որոնք այն չեն օգտագործում, timeout են ստանում։

Ինչպես լուծել

Worker-ների սպառում․ արգելափակված կանչերը զբաղեցնում են բոլոր slot-երը։ Այդ կլիենտի վրա դիր կարճ timeout (օրինակ՝ 800 ms), փաթաթիր այն circuit breaker-ով և էջը ցուցադրիր առանց recommendations-ի, երբ այն բաց է։ Մեկուսացրու կախվածությունը bulkhead-ով․ առանձին, փոքր զուգահեռության սահման այդ API-ի համար, որպեսզի այն երբեք չզբաղեցնի մի քանի worker-ից ավելին։ Ավելի լավ է՝ recommendations-ը բեռնիր ասինխրոն բրաուզերից կամ նախապես հաշվարկիր background job-ում։ Կախվածության latency-ի վրա առանձին alert դիր, որպեսզի տեսնես պատճառը, ոչ միայն ախտանիշը։

Արագ ուղեցույց

Pool-ի չափի որոշում
Զուգահեռություն = հարցում/վ x վայրկյան մեկ հարցման համար (+ պաշար)
Least-connections upstream
upstream app { least_conn; server 10.0.0.11:3000; }
Consistent hashing nginx-ում
hash $request_uri consistent;
Rate limit ըստ IP-ի
limit_req_zone $binary_remote_addr zone=api:10m rate=20r/s;
Հին արժեքի սպասարկում թարմացման ժամանակ
proxy_cache_use_stale updating error timeout; proxy_cache_lock on;
Շատ ինստանսների միացումների pool
PgBouncer pool_mode = transaction
Գտնել դանդաղ հարցումները
EXPLAIN (ANALYZE, BUFFERS) SELECT ...;
Load test-ի աստիճանական բարձրացում
k6 run --vus 200 --duration 10m script.js
Retry-ի քաղաքականություն
Միայն իդեմպոտենտ, exponential backoff + full jitter, առավելագույնը 3, մեկ շերտում

Իմ իրական նախագծերում

  • POS SaaS-ը աշխատում է Docker Compose-ով՝ nginx reverse proxy-ի և CDN-ի հետևում, PostgreSQL-ով, Redis-ով և առանձին BullMQ worker-ներով, որոնք սպասարկում են 20 հերթ։
  • Նրա real-time շերտը օգտագործում է socket.io room-եր ըստ ընկերության, մասնաճյուղի, տերմինալի և օգտատիրոջ, իսկ Redis pub/sub-ը իրադարձությունները տարածում է ինստանսների միջև։
  • Կայքերի առջև դնում եմ Cloudflare՝ DNS-ի և DDoS-ից հիմնական պաշտպանության համար, և upload-ները տեղափոխում եմ S3-համատեղելի object storage, որպեսզի հավելվածի սերվերները մնան stateless։
  • Կենդանիների ապրանքների հարթակում որոնումն աշխատում է Algolia-ով՝ rate limiting-ով, ինչը ծանր որոնողական տրաֆիկը հեռու է պահում հիմնական տվյալների բազայից։

Հարցեր և պատասխաններ

Q1Ինչպե՞ս կմասշտաբավորեիր դանդաղող հավելվածը։

Չափում եմ՝ նախքան որևէ բան փոխելը։ Մետրիկաներն ու trace-երը ցույց են տալիս, թե ժամանակը գնում է հավելվածի՞ն, տվյալների բազայի՞ն, թե՞ արտաքին կանչի։ Ամենից հաճախ դա տվյալների բազան է՝ բացակայող ինդեքսներ, N+1 հարցումներ կամ չափազանց շատ կապեր։ Շտկում եմ հարցումները, ավելացնում քեշավորում «տաք» կարդալների համար, ավելացնում connection pooling և դանդաղ աշխատանքը տեղափոխում հերթեր։ Եթե հավելվածի շերտն ինքն է հագեցած, համոզվում եմ, որ այն stateless է, և ավելացնում ինստանսներ load balancer-ի հետևում։ Հաջորդը read replica-ներն են կարդալով ծանրաբեռնված բեռների համար։ Sharding-ը վերջին միջոցն է։ Ամեն փոփոխությունից հետո load test եմ անում՝ հաստատելու, որ «խցանումն» իրականում տեղափոխվել է։

Q2Ո՞րն է տարբերությունը L4 և L7 load balancing-ի միջև։

L4 balancer-ն աշխատում է տրանսպորտային շերտում․ այն փոխանցում է TCP կամ UDP կապերը ըստ IP-ի և պորտի՝ ներս չնայելով։ Այն շատ արագ է, էժան և աշխատում է ցանկացած պրոտոկոլի հետ, ներառյալ տվյալների բազաները և raw socket-ները։ L7 balancer-ը հասկանում է HTTP․ ավարտում է TLS-ը, ուղղորդում ըստ hostname-ի կամ path-ի, ստուգում header-ներն ու cookie-ները, կրկնում idempotent հարցումները, կիրառում rate limit-եր և կարող է քեշավորել։ Դա այն դարձնում է իդեալական վեբ հավելվածների և API-ների համար՝ ամեն հարցման վրա ավելի շատ մշակման գնով։ Շատ համակարգեր համադրում են երկուսը՝ L4 balancer պարզ բաշխման համար և նրա հետևում L7 proxy-ներ routing-ի համար։

Q3Ինչու՞ են sticky session-ները համարվում «վատ հոտ»։

Sticky session-ները յուրաքանչյուր օգտատիրոջ կապում են մեկ ինստանսի, սովորաբար cookie-ով։ Նրանք ստիպում են stateful հավելվածին թվալ աշխատող load balancer-ի հետևում, բայց խնդիրները մնում են․ երբ այդ ինստանսը վերագործարկվում կամ հեռացվում է, օգտատերը կորցնում է իր սեսիան, բեռը դառնում է անհավասար, որովհետև ծանր օգտատերերը մնում են մեկ հանգույցի վրա, իսկ դեպլոյներն ու autoscaling-ը դժվարանում են։ Ավելի լավ լուծումը վիճակը ինստանսից հեռացնելն է՝ սեսիաները պահելով Redis-ում կամ օգտագործելով ստորագրված թոքեններ, իսկ ֆայլերը՝ object storage-ում։ «Կպչունությունը» դեռ օգտակար է նեղ դեպքերում, օրինակ՝ WebSocket-ների long-polling fallback-երի համար։

Q4Ե՞րբ են պետք read replica-ները և որո՞նք են թակարդները։

Երբ կարդալը գերակշռում է, և primary-ն զբաղված է դրանով․ հաշվետվություններ, կատալոգի դիտում, անալիտիկա։ Ռեպլիկաները պատճենում են տվյալները primary-ից, սովորաբար ասինխրոն, և սպասարկում են read-only հարցումներ։ Գլխավոր թակարդը replication lag-ն է․ օգտատերը պահպանում է իր պրոֆիլը, հաջորդ էջը կարդում է ռեպլիկայից, որը մեկ վայրկյան հետ է, և փոփոխությունը կարծես կորել է։ Այն կարդալները, որոնք պետք է տեսնեն վերջին գրառումները, ուղղիր primary, կամ գրառումից հետո կարճ ժամանակով օգտատիրոջը կապիր primary-ին։ Նաև մոնիթորինգ արա lag-ը, failover-ը դարձրու թեստավորված ընթացակարգ և հիշիր, որ ռեպլիկաները չեն օգնում գրառումների բեռին։

Q5Ի՞նչ է connection pooling-ը և ինչու՞ է այն կարևոր։

Տվյալների բազայի կապ բացելը թանկ է, և PostgreSQL-ը յուրաքանչյուր կապի համար ստեղծում է պրոցես, ուստի մի քանի հարյուր կապը կարող է շատ հիշողություն օգտագործել և դանդաղեցնել ամեն ինչ։ Երբ հավելվածը մասշտաբավորում ես շատ ինստանսների, որոնցից յուրաքանչյուրն ունի իր pool-ը, ընդհանուր քանակը հեշտությամբ գերազանցում է այն, ինչ տվյալների բազան լավ է կրում։ PgBouncer-ի պես pooler-ը կանգնում է նրանց միջև և թույլ է տալիս շատ կլիենտային կապերի կիսել իրական կապերի փոքր քանակ։ Transaction ռեժիմում սերվերային կապը փոխառվում է միայն տրանզակցիայի տևողությամբ։ Սեսիայի մակարդակի հնարավորությունները, ինչպես SET-ը կամ advisory lock-երը, այդ ռեժիմում զգուշություն են պահանջում։

Q6Ինչպե՞ս ես մոտենում capacity planning-ին մեկնարկի համար։

Սկսում եմ սպասվող բիզնես թվերից, օրինակ՝ րոպեում պատվերների պիկային քանակից, և դրանք թարգմանում եմ վայրկյանում հարցումների հիմնական endpoint-ների վրա։ Հետո load test եմ անում staging միջավայրը իրատեսական տվյալներով՝ k6-ի պես գործիքով, աստիճանաբար բարձրացնելով բեռը, մինչև latency-ի պերցենտիլները կամ սխալների հաճախականությունը խախտեն համաձայնեցված նպատակները, և նշում եմ, թե որ ռեսուրսն է առաջինը հագենում։ Production համակարգը չափում եմ սպասվող պիկի և պաշարի համար, սովորաբար այնքան, որ պիկի ժամին մեկ հանգույց կորցնելու դեպքում էլ դիմանա։ Նաև նախապես պատրաստում եմ լծակներ՝ քեշի կանոններ, ֆիչերի անջատիչներ ծանր վիջեթներն անջատելու համար և հերթի worker-ներ, որոնք կարող եմ արագ մասշտաբավորել։

Սենիոր մակարդակի հարցեր

S1Ե՞րբ կանեիր sharding, և ինչպե՞ս կընտրեիր shard key-ը։

Միայն այն բանից հետո, երբ չափել ես, որ մեկ primary-ն, նույնիսկ մեծը՝ ռեպլիկաներով, քեշավորմամբ և partitioned աղյուսակներով, չի կարող դիմանալ գրառումների ծավալին կամ տվյալների չափին պլանավորված հորիզոնում։ Shard key-ը պետք է համապատասխանի գերիշխող access pattern-ին, որպեսզի հարցումների մեծ մասը դիպչի մեկ shard-ի․ B2B SaaS-ում tenant ID-ն բնական է, որովհետև գրեթե ամեն հարցում սահմանափակված է մեկ tenant-ով։ Այն պետք է բեռը բաշխի հավասարաչափ, ուստի հետևիր շատ մեծ tenant-ներին, որոնց սեփական shard է պետք։ Խուսափիր hotspot ստեղծող key-երից, օրինակ՝ timestamp-ներից։ Պահիր directory, որը key-երը քարտեզագրում է shard-երին, որպեսզի tenant-ները կարելի լինի տեղափոխել առանց ամեն ինչ վերա-hash անելու։ Ծախսերն իրական են․ cross-shard հարցումներին պետք է fan-out կամ առանձին analytics պահոց, տրանզակցիաները մնում են մեկ shard-ի ներսում, իսկ միգրացիաները գործարկվում են յուրաքանչյուր shard-ի համար առանձին։

S2Ինչպե՞ս ես որոշում, թե ինչի հիման վրա autoscale անել։

Մասշտաբավորիր այն ազդանշանով, որը լավագույնս է կանխատեսում մասշտաբավորվողի հագեցումը։ CPU-bound վեբ շերտերի համար CPU-ի ծանրաբեռնվածությունն աշխատում է․ I/O-bound հավելվածների համար զուգահեռ հարցումները կամ latency-ն ավելի լավն են, որովհետև CPU-ն ցածր է մնում, մինչ worker-ները սպասում են։ Հերթի worker-ների համար մասշտաբավորիր ըստ backlog-ի՝ բաժանած մշակման արագության վրա, ինչը գնահատում է դատարկվելու ժամանակը։ Ավելացրու cool-down ժամանակահատվածներ և scale-in-ի ուշացումներ՝ «թրթռոցից» խուսափելու համար, և նախապես մասշտաբավորիր հայտնի իրադարձություններից առաջ, որովհետև նոր ինստանսներին ժամանակ է պետք բեռնվելու և քեշերը տաքացնելու համար։ Ամենակարևորը՝ ստուգիր, թե ինչի վրա է ճնշում autoscaling-ը․ ավելի շատ վեբ ինստանսներ նշանակում են ավելի շատ տվյալների բազայի միացումներ և ավելի շատ ճնշում ընդհանուր կախվածությունների վրա։ Լավ autoscaling քաղաքականությունն ունի առաստաղ, որը որոշվում է ամենաթույլ downstream կոմպոնենտով։

S3Ինչպե՞ս պահել համակարգը արձագանքող, երբ պահանջարկը գերազանցում է հզորությունը։

Ընդունիր, որ որոշ աշխատանք չի կատարվի, և ընտրիր՝ որը։ Նախ պաշտպանիր միջուկը․ admission control-ը եզրում ավելցուկային հարցումները արագ մերժում է 429 կամ 503-ով՝ փոխանակ թույլ տալու, որ հերթերը աճեն, մինչև ամեն ինչ timeout ստանա։ Առաջնահերթություն տուր ըստ բիզնես արժեքի՝ պահելով checkout-ը և login-ը և կտրելով recommendations-ը, analytics beacon-ները և ծանր export-ները։ Ֆիչերը նվազեցրու նրբորեն՝ սպասարկելով քեշավորված կամ պարզեցված էջեր։ Հետաձգելի աշխատանքը տար սահմանափակ չափով հերթեր։ Ներսում սահմանափակիր զուգահեռությունը յուրաքանչյուր կախվածության համար և օգտագործիր deadline-ներ, որպեսզի ոչ մի աշխատանք չշարունակվի այն բանից հետո, երբ կլիենտն արդեն հրաժարվել է։ Փոխզիջումը բացահայտ է․ որոշ օգտատերեր տեսնում են սխալ կամ կրճատված էջ, բայց համակարգը մնում է աշխատող մեծամասնության համար՝ փոխանակ բոլորի համար փլուզվելու։

Բառարան

Հորիզոնական մասշտաբավորում
Load balancer-ի հետևում ավելի շատ նույնական ինստանսների ավելացում՝ ավելի մեծ մեքենայի փոխարեն։
Little-ի օրենք
Համակարգում գտնվող տարրերի քանակը հավասար է ժամանման արագության և դրանում անցկացրած միջին ժամանակի արտադրյալին։
Tail latency
Ամենադանդաղ պատասխանները, օրինակ՝ p99, որոնք շատ օգտատերեր այնուամենայնիվ զգում են։
Back-pressure
Ազդանշաններ կամ սահմանափակումներ, որոնք դանդաղեցնում են producer-ներին, երբ consumer-ները կամ սերվերները չեն հասցնում։
Load shedding
Գերբեռնվածության ժամանակ որոշ հարցումների վաղ մերժում, որպեսզի մնացածներն արագ մնան։
Circuit breaker
Փոխարկիչ, որը որոշ ժամանակով դադարեցնում է ձախողվող կախվածության կանչերը, հետո ստուգում այն։
Bulkhead
Առանձին ռեսուրսային սահմաններ յուրաքանչյուր կախվածության համար, որպեսզի մեկ ձախողումը չսպառի ամեն ինչ։
Consistent hashing
Key-երի տեղաբաշխում, որի դեպքում հանգույց ավելացնելը տեղափոխում է key-երի միայն փոքր մասը։
Cache stampede
Շատ հարցումներ, որոնք միաժամանակ վերակառուցում են քեշի նույն ժամկետանց գրառումը։

Պրոդուկտի գաղափար ունե՞ք։Եկեք ստեղծենք այն։

Պատմեք ձեր գաղափարի, պրոդուկտի կամ թիմի մասին։ Սովորաբար պատասխանում եմ մեկ աշխատանքային օրվա ընթացքում։