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

Մոնոլիտ ընդդեմ միկրոծառայությունների

Ինչպես բաժանել համակարգը, երբ բաժանել և ինչու թիմերի մեծ մասը պետք է սկսի մոդուլային մոնոլիտից։

  • Monolith
  • Modular monolith
  • Microservices
  • Boundaries
Քայլ 1/4
Մոնոլիտը մեկ մեծ հավելված է, որն անում է ամեն ինչ՝ պատվերներ, օգտատերեր, վճարումներ։ Պարզ է կառուցել և աշխատեցնել։

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

Մոնոլիտը մեկ հավելված է, որը կառուցում ու դեպլոյ ես որպես մեկ ամբողջություն։ Միկրոծառայությունները շատ փոքր հավելվածներ են, որոնցից յուրաքանչյուրն ունի իր տվյալները և մյուսների հետ խոսում է ցանցով։ Մոդուլային մոնոլիտը դրանց միջև է՝ մեկ դեպլոյվող միավոր, բայց խիստ ներքին սահմաններով։ Ճիշտ ընտրությունը շատ ավելի շատ կախված է թիմի չափից և դոմեյնի պարզությունից, քան տրաֆիկից։

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

  • Լռելյայն ընտրում եմ մոդուլային մոնոլիտ, որովհետև այն տալիս է հստակ սահմաններ՝ առանց առաջին իսկ օրից վճարելու միկրոծառայությունների ցանցի, դեպլոյի և observability-ի «հարկը»։
  • Երիտասարդ պրոդուկտներում իմ տեսած ցավի մեծ մասը խճճված կոդից է, ոչ թե ծառայությունների պակասից։ Լավ մոդուլային սահմանները դա շտկում են, իսկ ծառայությունների բաժանելը սովորաբար՝ ոչ։
  • Ճարտարապետության ընտրությունը նաև բիզնես որոշում է․ միկրոծառայությունները բազմապատկում են հոսթինգի, մոնիթորինգի և on-call-ի ծախսերը, և հաճախորդը պետք է դա լսի նախքան դրա համար վճարելը։
  • Երբ բաժանումն իսկապես պետք է, լավ գծված մոդուլային սահմանները այն դարձնում են մեխանիկական առանձնացում, ոչ թե վերագրում զրոյից։

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

01Երեք ձև՝ մոնոլիտ, մոդուլային մոնոլիտ, միկրոծառայություններ

Պատկերացրու ռեստորան։ Մոնոլիտը մեկ խոհանոց է, որտեղ ցանկացած խոհարար կարող է ցանկացած դարակից վերցնել ցանկացած բաղադրիչ՝ սկզբում արագ է, հետո՝ քաոս։ Մոդուլային մոնոլիտը նույն խոհանոցն է՝ բաժանված կայանների (գրիլ, հրուշակեղեն, աղցաններ), որոնցից յուրաքանչյուրն ունի իր դարակները և վաճառասեղան, որտեղ պատվերները փոխանցվում են։ Միկրոծառայությունները առանձին ռեստորաններ են, որոնք միմյանց պատվերներ են ուղարկում առաքիչով․ յուրաքանչյուրը կարող է անկախ բացվել, փակվել կամ վերանորոգվել, բայց ամեն փոխանցում հիմա ժամանակ է խլում և կարող է կորչել։ Ծրագրային լեզվով՝ մոնոլիտը մեկ կոդաբազա է, պրոցեսի մեկ տեսակ և սովորաբար մեկ տվյալների բազա։ Մոդուլային մոնոլիտը պահում է մեկ դեպլոյվող միավոր, բայց կոդը բաժանում է հանրային ինտերֆեյսներով մոդուլների, և յուրաքանչյուր մոդուլ տիրապետում է իր աղյուսակներին։ Միկրոծառայությունները անկախ դեպլոյվող պրոցեսներ են, յուրաքանչյուրն իր տվյալների պահոցով, որոնք հաղորդակցվում են HTTP-ով, gRPC-ով կամ հաղորդագրություններով։

  • Մոնոլիտ՝ ամենապարզն է կառուցելու, թեստավորելու, դեպլոյելու և դեբագելու համար. ռիսկը «big ball of mud»-ն է՝ ցեխի մեծ գունդ։
  • Մոդուլային մոնոլիտ՝ մեկ դեպլոյ, պարտադրված սահմաններ, պրոցեսի ներսում կանչեր, մեկ տրանզակցիա, երբ պետք է։
  • Միկրոծառայություններ՝ անկախ դեպլոյներ և մասշտաբավորում ըստ ծառայության. գինը բաշխված համակարգերի բարդությունն է։

02Միկրոծառայությունների իրական գինը

Միկրոծառայությունները բարդությունը կոդից տեղափոխում են ցանց։ Ֆունկցիայի կանչը, որը երբեք չէր ձախողվում, դառնում է հարցում, որը կարող է timeout տալ, երկու անգամ հասնել կամ հաջողվել՝ առանց որ դու դրա մասին իմանաս։ Տվյալների բազայի մեկ տրանզակցիան դառնում է բաշխված workflow, որին պետք են saga-ներ, outbox-ներ և իդեմպոտենտություն։ Տվյալների սեփականությունը դառնում է խիստ․ orders ծառայությունը չի կարող կարդալ customers աղյուսակը, ուստի պետք են API-ներ, իրադարձություններ կամ լոկալ պատճեններ, և այդ պատճենները կարող են հնացած լինել։ Դեբագինգին պետք են distributed tracing, իրար կապված լոգեր և ամեն ծառայության համար դաշբորդներ, այլապես մեկ դանդաղ checkout-ը դառնում է դետեկտիվ պատմություն։ Դեպլոյին պետք են CI pipeline-ներ, տարբերակավորված պայմանագրեր, service discovery և սովորաբար օրկեստրատոր։ Յուրաքանչյուր ծառայության պետք են նաև իր մոնիթորինգը, ալերտները, գաղտնիքները և թարմացումները։ Սրանցից ոչինչ անհնար չէ, բայց սա մշտական հարկ է, որը վճարում ես ամեն շաբաթ։

  • Բաշխված տրանզակցիաներ՝ ծառայությունների միջև ընդհանուր ACID տրանզակցիա չկա։
  • Ցանցային ձախողումներ՝ timeout-ներ, կրկնակի փորձեր, մասնակի ձախողումներ, կասկադային խափանումներ։
  • Տվյալների սեփականություն՝ ծառայությունների միջև join-ներ չկան. կրկնօրինակված, eventually consistent տվյալներ։
  • Observability և ծախս՝ tracing, ավելի շատ ենթակառուցվածք, on-call-ի ավելի մեծ մակերես։

03Սահմանների գտնելը bounded context-ների օգնությամբ

Ցանկացած բաժանման ամենադժվար մասը որոշելն է, թե որտեղից կտրել։ Domain-Driven Design-ը տալիս է գործնական գործիք՝ bounded context, բիզնեսի մի տիրույթ, որտեղ բառերն ունեն մեկ ճշգրիտ իմաստ։ «Ապրանքը» կատալոգում նշանակում է նկարագրություն լուսանկարներով, պահեստում՝ պաշար պահեստում, բիլինգում՝ գնի տող։ Յուրաքանչյուր կոնտեքստ ստանում է իր մոդելը և իր մոդուլը։ Լավ սահմաններն ունեն բարձր cohesion ներսում (այն, ինչ փոխվում է միասին, ապրում է միասին) և ցածր coupling դրսում (նեղ, կայուն ինտերֆեյս)։ Օգտակար թեստ․ կարո՞ղ է թիմը մեկ շաբաթ փոխել այս մոդուլը՝ ոչ մեկի հետ չհամաձայնեցնելով։ Եթե ոչ, սահմանը սխալ է։ Կտրիր ըստ բիզնես կարողության (պատվերներ, վճարումներ, լոյալություն), երբեք՝ ըստ տեխնիկական շերտի («database service» կամ «validation service»), որը միայն ստեղծում է շատախոս, փխրուն կախվածություններ։

ts
// eslint.config.mjs: modules may import each other only through a public index
export default [
  {
    files: ["src/modules/**/*.ts"],
    rules: {
      "no-restricted-imports": ["error", {
        patterns: [{
          group: ["@/modules/*/internal/*", "@/modules/*/db/*"],
          message: "Use the other module's public index.ts, not its internals.",
        }],
      }],
    },
  },
];

04Մոդուլային մոնոլիտ, որը մնում է մոդուլային

Սահմանները, որոնք ապրում են միայն դիագրամում, մի քանի ամսում քայքայվում են։ Պարտադրիր դրանք կոդում։ Յուրաքանչյուր մոդուլի տուր թղթապանակ՝ հանրային մուտքի կետով (ծառայություններ, DTO-ներ, իրադարձություններ), իսկ մնացած ամեն ինչ պահիր ներքին։ Թող յուրաքանչյուր մոդուլ տիրապետի իր աղյուսակներին, և արգելիր մյուս մոդուլներին դրանց ուղղակի հարցումներ անել․ միջմոդուլային կարդալը գնում է տեր մոդուլի ծառայության միջոցով։ Կողմնակի էֆեկտների համար օգտագործիր պրոցեսի ներսի դոմեյն իրադարձություններ («պատվերը վճարված է»-ն գործարկում է լոյալության միավորներ), որպեսզի մոդուլները միմյանց երկար շղթաներով չկանչեն։ Ավելացրու lint կանոններ կամ ճարտարապետական թեստեր, որոնք կոտրում են build-ը արգելված import-ների դեպքում։ Մոդուլային համակարգ ունեցող ֆրեյմվորքերում, ինչպես NestJS-ը, export արա միայն այն, ինչ մյուս մոդուլները կարող են օգտագործել։ Արդյունքը․ երբ մի մոդուլ իսկապես պետք է առանձին մասշտաբավորվի կամ դեպլոյվի, դու արդեն ունես նրա ինտերֆեյսը, տվյալները և իրադարձությունները, և առանձնացումը դառնում է շաբաթների նախագիծ, ոչ թե վերագրում։

05Միգրացիա strangler fig պատերնով

Երբ գոյություն ունեցող մոնոլիտը պետք է բաժանել, երբեք մի փորձիր «big bang» վերագրում․ այն ամիսներով սառեցնում է նոր ֆիչերները և սովորաբար ձախողվում է։ Օգտագործիր strangler fig պատերնը, որն անվանվել է մի բույսի պատվին, որը դանդաղ աճում է ծառի շուրջը։ Մոնոլիտի առջև դիր routing շերտ (reverse proxy կամ API gateway)։ Ընտրիր մեկ լավ սահմանազատված կարողություն, կառուցիր այն որպես նոր ծառայություն և միայն նրա endpoint-ները ուղղիր այնտեղ, մինչ մնացած ամեն ինչ դեռ գնում է մոնոլիտ։ Անցման ընթացքում տվյալները համաժամեցրու իրադարձություններով կամ change data capture-ով, հետո տվյալների սեփականությունը փոխանցիր նոր ծառայությանը։ Կրկնիր՝ մեկ կարողություն ամեն անգամ, և յուրաքանչյուր քայլից հետո ջնջիր հին կոդը։ Ամեն քայլ փոքր է, հետադարձելի և թողարկելի, ուստի բիզնեսը շարունակում է առաջ գնալ, մինչ ճարտարապետությունը փոխվում է տակից։

  • Սկսիր այնպիսի կարողությունից, որն ունի տվյալների հստակ սեփականություն և քիչ մուտքային կախվածություններ։
  • Գործարկիր հին և նոր ուղիները զուգահեռ և համեմատիր արդյունքները՝ նախքան տրաֆիկը փոխելը։
  • Չափիր․ եթե նոր ծառայությունը չի հեշտացնում դեպլոյը կամ մասշտաբավորումը, կանգ առ։

06Ընտրություն՝ թիմի չափ, դոմեյն և մասշտաբավորման կարիքներ

Կոնվեյի օրենքն ասում է, որ համակարգերը կրկնում են դրանք կառուցող թիմերի հաղորդակցության կառուցվածքը։ Մեկ կամ երկու փոքր թիմ միկրոծառայություններից գրեթե ոչինչ չի շահում, որովհետև նրանք առանց այդ էլ հեշտ են համաձայնեցնում, մինչդեռ վճարում են ամբողջ օպերացիոն գինը։ Միկրոծառայությունները սկսում են իրենց արդարացնել, երբ մի քանի ինքնավար թիմեր միմյանց արգելափակում են ընդհանուր թողարկման վրա, երբ համակարգի մասերն ունեն շատ տարբեր մասշտաբավորման կամ հուսալիության կարիքներ (վիդեո էնկոդեր ընդդեմ ադմին պանելի), կամ երբ compliance-ը պահանջում է կոշտ մեկուսացում։ Միայն տրաֆիկը հազվադեպ է պատճառ․ լավ կառուցված մոնոլիտը load balancer-ի հետևում հորիզոնական մասշտաբավորվում է մինչև շատ մեծ բեռներ։ Ընկերությունների մեծ մասի համար խելամիտ լռելյայն ընտրությունն է՝ սկսել մոդուլային մոնոլիտից, ներդրում անել սահմանների, թեստերի և observability-ի մեջ, և ծառայություն առանձնացնել միայն այն ժամանակ, երբ կոնկրետ, չափված ցավը դա արդարացնում է։

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

  • Մոդուլային մոնոլիտ՝ նոր պրոդուկտներ, մինչև մի քանի տասնյակ ինժեներից բաղկացած թիմեր կամ դոմեյններ, որոնք դեռ բացահայտվում են։
  • Միկրոծառայություններ՝ մի քանի անկախ թիմեր, որոնց արգելափակում է ընդհանուր թողարկման ցիկլը։
  • Միկրոծառայություններ՝ կոմպոնենտներ շատ տարբեր մասշտաբավորման, runtime-ի կամ հուսալիության կարիքներով։
  • Մեկ ծառայության առանձնացում՝ «տաք» ուղի (որոնում, մեդիայի մշակում), որին պետք է առանձին մասշտաբավորում։

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

  • Մի ընտրիր միկրոծառայություններ, որ ժամանակակից երևաս, կամ որովհետև մեծ տեխնոլոգիական ընկերությունն է դրանք օգտագործում։
  • Մի բաժանիր, երբ սահմանները դեռ պարզ չեն. սխալ սահմանները ցանցով շատ ավելի թանկ են նստում։
  • Փոքր թիմի պրոդուկտը մի բաժանիր ավելի շատ ծառայությունների, քան ինժեներներ կան։

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

  • Սովորական մոնոլիտ

    Լավ է փոքր հավելվածների և պրոտոտիպների համար. ավելացրու մոդուլային սահմաններ, երբ կոդաբազան մեծանա։

  • Մոդուլային մոնոլիտ

    Մեկ դեպլոյվող միավոր՝ պարտադրված ներքին սահմաններով. լավագույն լռելյայն ընտրությունը թիմերի մեծ մասի համար։

  • Service-based ճարտարապետություն

    Մի քանի խոշոր ծառայություն (օր.՝ core app, search, media)՝ տասնյակ մանր ծառայությունների փոխարեն։

  • Serverless ֆունկցիաներ

    Լավ են ալիքաձև, իրադարձությամբ գործարկվող առաջադրանքների համար. ավելի դժվար են երկար workflow-ների և լոկալ դեբագինգի համար։

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

  1. Սահմանները կտրիր ըստ բիզնես կարողության՝ bounded context-ներով, ոչ թե ըստ տեխնիկական շերտի։
  2. Յուրաքանչյուր մոդուլի կամ ծառայության տուր իր տվյալների բացառիկ սեփականությունը։
  3. Սահմանները պարտադրիր ավտոմատ՝ lint կանոններով կամ ճարտարապետական թեստերով։
  4. Կողմնակի էֆեկտները հաղորդիր իրադարձություններով, ոչ թե սինխրոն կանչերի երկար շղթաներով։
  5. Ներդրում արա լոգերի, մետրիկաների և tracing-ի մեջ՝ նախքան որևէ բան բաժանելը։
  6. Ծառայությունները առանձնացրու մեկ առ մեկ՝ strangler fig պատերնով, և յուրաքանչյուր քայլ պահիր հետադարձելի։

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

  • «Բաշխված մոնոլիտ» կառուցել՝ ծառայություններ, որոնք պետք է միասին դեպլոյվեն և կիսում են մեկ տվյալների բազա։
  • Չափազանց վաղ բաժանել, նախքան դոմեյնը հասկանալը, հետո վճարել ծառայությունները հետ միավորելու համար։
  • Սինխրոն կանչերի շղթաներ սարքել (A-ն կանչում է B-ին, B-ն՝ C-ին), այնպես որ մեկ դանդաղ ծառայությունը վայր է գցում ամեն ինչ։
  • Մոռանալ, որ յուրաքանչյուր ծառայություն բազմապատկում է CI-ի, մոնիթորինգի, գաղտնիքների և on-call-ի աշխատանքը։

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

07Coupling, որը չես տեսնում․ ժամանակային, տվյալների և դեպլոյի

Import-ի կանոնները բռնում են կոդի coupling-ը, բայց երեք ավելի «լուռ» տեսակ են որոշում՝ բաժանումը կաշխատի՞, թե՞ ոչ։ Ժամանակային coupling․ A ծառայությունը կարող է ավարտել իր հարցումը միայն այն ժամանակ, երբ B-ն աշխատում է, որովհետև այն սինխրոն կանչում է B-ին։ Այդ դեպքում availability-ն բազմապատկվում է․ երեք ծառայություն՝ յուրաքանչյուրը 99.9 տոկոս, շղթայով միացված, ամբողջ հոսքի համար տալիս են մոտ 99.7 տոկոս։ Տվյալների coupling․ երկու մոդուլ կարդում կամ գրում են նույն աղյուսակները, այնպես որ մեկի սխեմայի փոփոխությունը կոտրում է մյուսին՝ առանց նրանց միջև որևէ import-ի։ Դեպլոյի coupling․ փոփոխությունը պահանջում է համաձայնեցված թողարկումներ, որովհետև պայմանագիրը փոխվել է ոչ backward-compatible ձևով։ Չափիր սրանք մինչև բաժանելը։ Նայիր, թե որ աղյուսակներին է դիպչում յուրաքանչյուր մոդուլ, tracing-ից ստացված կանչերի գրաֆին և թե որ թղթապանակներն են փոխվում նույն commit-ներում մի քանի ամսվա ընթացքում (change coupling՝ Git-ի պատմությունից)։ Մոդուլները, որոնք անընդհատ փոխվում են միասին, պատկանում են միասին՝ ինչ էլ որ ասի դիագրամը։

  • Շղթայի availability․ բազմապատկիր յուրաքանչյուր սինխրոն քայլի SLO-ները։
  • Ընդհանուր աղյուսակները սահմանի խախտում են՝ նույնիսկ մաքուր import-ների դեպքում։
  • Git-ում միասին փոփոխվող ֆայլերի վերլուծությունը էժան ձևով ցույց է տալիս իրական coupling-ը։

08Ծառայությունների միջև պայմանագրեր․ տարբերակավորում և consumer-driven թեստեր

Երբ սահմանը դառնում է ցանցային սահման, նրա ինտերֆեյսը հրապարակված պայմանագիր է, և անկախ դեպլոյը կախված է դրա կայունությունից։ Հետևիր tolerant reader կանոնին․ consumer-ները անտեսում են իրենց անծանոթ դաշտերը, իսկ producer-ները միայն ավելացնում են ոչ պարտադիր դաշտեր։ Կոտրող փոփոխություններն անցնում են expand and contract ճանապարհով․ ավելացրու նոր դաշտը կամ endpoint-ը, միգրացրու consumer-ներին, հետո հեռացրու հինը, երբ դրա օգտագործումը իջնի զրոյի։ Consumer-driven contract թեստերը (օրինակ՝ Pact գործիքով) թույլ են տալիս յուրաքանչյուր consumer-ի գրանցել այն հարցումներն ու պատասխանի դաշտերը, որոնց վրա հենվում է․ provider-ի CI-ը վերարտադրում է այդ սպասումները և ձախողվում մինչև կոտրող փոփոխության թողարկումը՝ առանց ամբողջ համակարգը բարձրացնելու։ Սա փոխարինում է դանդաղ և անկայուն end-to-end միջավայրերին՝ որպես անվտանգության հիմնական ցանց։ Իրադարձությունները տարբերակավորիր նույն ձևով և յուրաքանչյուր API-ի համար պահիր գրավոր նշված տեր, որպեսզի հարցերն ունենան հասցե։

09Առանձնացման մեխանիկան․ branch by abstraction և anti-corruption layer

Մոնոլիտի ներսում branch by abstraction-ը մոդուլը պատրաստում է առանձնացման՝ առանց երկարակյաց branch-ի։ Ներմուծիր ինտերֆեյս այդ կարողության համար, բոլոր կանչողներին անցկացրու դրա միջով, հետո ավելացրու երկրորդ իրականացում, որը կանչում է նոր ծառայությունը և միացվում է feature flag-ով՝ ըստ tenant-ի կամ տոկոսի։ Առաջ գնա աստիճանաբար և in-process իրականացումը պահիր որպես fallback, մինչև հեռավորը չապացուցի իրեն։ Եթե նոր ծառայությունն ունի ավելի մաքուր մոդել, քան legacy կոդը, նրանց միջև դիր anti-corruption layer՝ թարգմանիչ, որը հին հասկացությունները քարտեզագրում է նորերին, որպեսզի legacy անվանումներն ու տարօրինակությունները չթափանցեն նոր դիզայն։ Տվյալները տեղափոխվում են փուլերով․ նոր ծառայությունը նախ կարդում է ռեպլիկացված պատճենը, հետո դառնում է գրողը, իսկ մոնոլիտը կարդում է նրանից, և վերջում հին աղյուսակները ջնջվում են։

ts
// Branch by abstraction: callers depend on the interface, not the location
export interface PricingPort {
  quote(cartId: string): Promise<Quote>;
}
 
export function pricingFor(tenantId: string, deps: Deps): PricingPort {
  return deps.flags.isOn("pricing-service", tenantId)
    ? new RemotePricing(deps.http)      // new service, behind timeouts and retries
    : new InProcessPricing(deps.db);    // existing module, kept as fallback
}

10Թիմերի տոպոլոգիա և inverse Conway manoeuvre

Եթե համակարգերը արտացոլում են թիմերի հաղորդակցությունը, կարող ես կազմակերպությունը նախագծել այնպես, որ ստանաս քո ուզած ճարտարապետությունը․ սա inverse Conway manoeuvre-ն է։ Յուրաքանչյուր stream-aligned թիմի տուր մեկ կամ մի քանի bounded context-ի ամբողջական սեփականություն՝ ներառյալ տվյալները, դեպլոյները և on-call-ը։ Պլատֆորմային թիմը տրամադրում է «հարթ ճանապարհներ» (CI ձևանմուշներ, observability, ծառայությունների scaffolding), որպեսզի ամեն նոր ծառայություն դրանք նորից չհորինի։ Հետևիր ճանաչողական բեռին․ թիմը, որին պատկանում է ավելի շատ ծառայություն, քան այն կարող է հասկանալ, բոլորի վրա էլ «անկյուններ կկտրի»։ Կոպիտ ստուգում է դեպլոյվող միավորների և ինժեներների հարաբերակցությունը․ երբ այն անցնում է մեկը, օպերացիոն աշխատանքը սկսում է գերիշխել ֆիչերի վրա։ Սահմանները վերագծիր, երբ թիմերի կառուցվածքը փոխվում է, որովհետև երեք թիմի պատկանող ծառայությունը դառնում է համակարգման խցան, իսկ անտեր ծառայությունը կամաց փտում է։

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

01

Հինգ հոգանոց թիմն ուզում է ամեն ինչ վերագրել միկրոծառայություններով

Խնդիր

Չորս տարվա ընթացքում կառուցված պրոդուկտը դժվար է փոխել դարձել․ ամեն ֆիչ դիպչում է շատ ֆայլերի, և թողարկումները հաճախ կոտրում են անկապ էկրաններ։ Թիմն առաջարկում է այն վերագրել որպես տասներկու միկրոծառայություն։

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

Դեմ արտահայտվիր վերագրմանը և նախ շտկիր coupling-ը։ Թիմի հետ կարճ workshop-ում քարտեզագրիր bounded context-ները, հետո կոդը վերակազմակերպիր մոդուլների՝ հանրային մուտքի կետերով և lint կանոններով, որոնք արգելում են խորը import-ները։ Աղյուսակների սեփականությունը բաշխիր ըստ մոդուլների և միջմոդուլային հարցումները փոխարինիր սերվիսային կանչերով կամ իրադարձություններով։ Ավելացրու tracing և characterisation թեստեր ամենառիսկային հոսքերի վրա։ Մի քանի ամիս անց չափիր․ դեպլոյի հաճախականությունը, փոփոխությունների ձախողման տոկոսը և քանի մոդուլի է դիպչում տիպիկ ֆիչը։ Միայն եթե որևէ մոդուլի դեռ պետք է առանձին մասշտաբավորում կամ թողարկման ռիթմ, առանձնացրու այդ մեկ մոդուլը strangler fig պատերնով։

02

Բաժանումից հետո checkout-ի latency-ն կրկնապատկվում է

Խնդիր

Pricing-ը, inventory-ն և promotions-ը ծառայությունների առանձնացնելուց հետո checkout-ի p95 latency-ն 300 ms-ից դարձել է 700 ms, և ձախողումները կտրուկ աճում են ամեն անգամ, երբ inventory-ն դեպլոյ է լինում։

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

Tracing արա մեկ checkout ամբողջությամբ, և սովորաբար կգտնես հաջորդական սինխրոն կանչեր՝ յուրաքանչյուրն իր retry-ներով։ Անկախ կանչերն արա զուգահեռ, դիր խիստ timeout-ներ՝ կանչողի սեփական բյուջեից ցածր, և retry արա միայն իդեմպոտենտ կանչերը՝ jitter-ով backoff-ով։ Դանդաղ փոփոխվող տվյալների (գներ, ակցիաների կանոններ) համար պահիր լոկալ read model՝ թարմացվող իրադարձություններով, փոխանակ ամեն հարցման ժամանակ կանչելու։ Ավելացրու circuit breaker, որպեսզի inventory-ի դեպլոյը checkout-ը չձախողի, այլ նվազեցնի մինչև «առկայությունը կհաստատվի ավելի ուշ»։ Օգտագործիր rolling deploy-ներ՝ readiness ստուգումներով։ Եթե երկու ծառայություն միշտ կանչվում են միասին, միավորիր դրանք հետ։

03

Ծանր մեդիա մշակման մոդուլի առանձնացում

Խնդիր

Մոնոլիտում նկարների և վիդեոների մշակումը upload-ների ժամանակ «ուտում» է CPU-ն և դանդաղեցնում API-ն բոլորի համար։ Սա միակ մասն է՝ շատ տարբեր ռեսուրսային կարիքներով։

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

Սա առանձնացման լավ թեկնածու է․ նեղ ինտերֆեյս, տվյալների հստակ սեփականություն և տարբեր մասշտաբավորում։ Նախ սահմանիր պայմանագիրը․ API-ն օրիգինալը պահում է object storage-ում և հրապարակում «media uploaded» job՝ ID-ով և storage key-ով։ Առանձին worker ծառայություն, որը մասշտաբավորվում է ըստ հերթի խորության, մշակում է այն և արձակում «media processed»՝ տարբերակներով։ API-ն իր գրառումները իդեմպոտենտ թարմացնում է այդ իրադարձությունից։ Թողարկիր այն flag-ի հետևում, երկու ուղիներն էլ գործարկիր նմուշի վրա, համեմատիր արդյունքները, հետո ամբողջությամբ անցիր նորին և ջնջիր in-process կոդը։

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

Արգելել խորը import-ները մոդուլների միջև
ESLint no-restricted-imports on '@/modules/*/internal/*'
Գտնել միասին փոփոխվող մոդուլները
git log --name-only --since=6.months --format='' | sort | uniq -c | sort -rn
Սինխրոն շղթայի availability
0.999 x 0.999 x 0.999 ≈ 0.997 (ամսական մոտ 2 ժ անհասանելիություն)
Պայմանագրի անվտանգ փոփոխություն
Expand (ավելացնել optional) -> միգրացնել consumer-ներին -> contract (հեռացնել)
Consumer-driven contract թեստավորում
Pact․ consumer-ը հրապարակում է pact-ը, provider-ի CI-ը ստուգում է այն
Աստիճանական առանձնացման փոխարկիչ
Feature flag ըստ tenant-ի․ in-process կամ հեռավոր իրականացում
Strangler routing եզրում
location /api/billing/ { proxy_pass http://billing; }
Ողջամտության հարաբերակցություն
Դեպլոյվող միավորներ մեկ ինժեների հաշվով՝ 1-ից ցածր․ դրանից վեր ops-ը գերիշխում է

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

  • Multi-tenant POS SaaS-ը, որը ես համատեղ ղեկավարում եմ, pnpm + Turborepo մոնոռեպո է՝ մեկ NestJS API և մեկ worker, կազմակերպված 83 դոմեյն մոդուլներով, ինչպես նաև 6 հավելված և 10 ընդհանուր փաթեթ։
  • Այնտեղ միջմոդուլային կողմնակի էֆեկտները հոսում են transactional outbox-ի միջոցով դեպի BullMQ հերթեր և handler-ներ, այլ ոչ թե մոդուլները միմյանց ուղղակի կանչում են։
  • Ռեստորանային պատվերների հարթակում մեկ Laravel API սպասարկում է վեբը, մոբայլը և ադմինը, իսկ կոնտրոլերները բաժանված են Shared/Ui/Mobile/Admin, որպեսզի ամեն ալիք կրկին օգտագործի նույն հիմնական տրամաբանությունը։
  • Կենդանիների ապրանքների հարթակում մոտ 90 մոդել ապրում է 12 դոմեյն թղթապանակում՝ մոտ 110 սերվիս կլասով, ինչը մոնոլիտը պահում է հեշտ կողմնորոշվելի։

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

Q1Ե՞րբ կառաջարկեիր հաճախորդին միկրոծառայություններ։

Երբ կա կոնկրետ ցավ, որը լուծում է միայն անկախ դեպլոյը․ մի քանի թիմեր միմյանց արգելափակում են մեկ թողարկման գնացքի վրա, կոմպոնենտ՝ շատ տարբեր մասշտաբավորման կամ runtime-ի կարիքներով, կամ compliance-ի պահանջ կոշտ մեկուսացման համար։ Կստուգեի նաև՝ արդյոք նրանք կարող են իրենց թույլ տալ պլատֆորմային աշխատանքը՝ CI ամեն ծառայության համար, tracing, on-call, պայմանագրերի տարբերակավորում։ Եթե թիմը փոքր է կամ դոմեյնը դեռ շարժվում է, առաջարկում եմ մոդուլային մոնոլիտ՝ խիստ սահմաններով, որովհետև այն տալիս է կազմակերպչական օգուտի մեծ մասը գնի մի փոքր մասով, և հետագայում կարելի է բաժանել նույն գծերով։

Q2Ի՞նչ է բաշխված մոնոլիտը և ինչպե՞ս խուսափել դրանից։

Դա համակարգ է, որը դեպլոյված է որպես շատ ծառայություններ, բայց դեռ իրեն պահում է որպես մեկ․ ծառայությունները կիսում են տվյալների բազա, պետք է թողարկվեն միասին կամ միմյանց սինխրոն կանչում են երկար շղթաներով, այնպես որ մեկ ձախողումը կոտրում է ամեն ինչ։ Ստանում ես երկու աշխարհների ծախսերը և ոչ մեկի օգուտները։ Խուսափիր դրանից՝ յուրաքանչյուր ծառայությանը տալով տվյալների բացառիկ սեփականություն, կողմնակի էֆեկտները հաղորդելով իրադարձություններով, պայմանագրերը տարբերակավորելով, որ ծառայությունները դեպլոյվեն անկախ, և սահմանները գծելով բիզնես կարողությունների, ոչ թե տեխնիկական շերտերի շուրջ։ Պարզ ստուգում․ եթե մեկ ֆիչի փոփոխությունը պարբերաբար պահանջում է երեք ծառայության համաձայնեցված դեպլոյ, սահմանները սխալ են։

Q3Ինչպե՞ս ես որոշում ծառայությունների կամ մոդուլների սահմանները։

Սկսում եմ բիզնեսից, ոչ թե կոդից։ Domain-Driven Design-ով փնտրում եմ bounded context-ներ՝ տիրույթներ, որտեղ «պատվեր» կամ «ապրանք» տերմինն ունի մեկ ճշգրիտ իմաստ և հստակ տեր։ Հետո ստուգում եմ cohesion-ը և coupling-ը․ այն, ինչ փոխվում է միասին, պետք է ապրի միասին, իսկ տիրույթների միջև ինտերֆեյսը պետք է լինի նեղ և կայուն։ Տվյալների սեփականությունը ամենաուժեղ ազդանշանն է․ եթե երկու թեկնածու անընդհատ միմյանց աղյուսակների կարիքն ունեն, հավանաբար դրանք մեկ կոնտեքստ են։ Սահմանները նախ ստուգում եմ մոդուլային մոնոլիտի ներսում, որտեղ դրանք տեղափոխելը էժան է, նախքան դրանցից որևէ մեկը ցանցային սահման դարձնելը։

Q4Ինչպե՞ս կմիգրացնեիր legacy մոնոլիտը ծառայությունների։

Աստիճանաբար՝ strangler fig պատերնով։ Նախ մոնոլիտի շուրջ ավելացնում եմ observability և թեստեր, հետո նրա առջև դնում եմ routing շերտ։ Ընտրում եմ մեկ կարողություն՝ հստակ սահմաններով և քիչ կախվածություններով, կառուցում եմ այն որպես նոր ծառայություն, տվյալները համաժամեցնում եմ իրադարձություններով կամ change data capture-ով և փոխում եմ նրա route-ները, իդեալում՝ երկու ուղիները զուգահեռ գործարկելուց և արդյունքները համեմատելուց հետո։ Կայունանալուց հետո նոր ծառայությունը դառնում է այդ տվյալների տերը, և հին կոդը ջնջվում է։ Ամեն քայլ փոքր է և հետադարձելի։ «Big bang» վերագրումը ամիսներով կանգնեցնում է ֆիչերի աշխատանքը և հազվադեպ է ավարտվում։

Q5Ինչպե՞ս ես աշխատում երկու ծառայություն ընդգրկող տրանզակցիայի հետ։

Ընդունում ես, որ ընդհանուր ACID տրանզակցիա չկա, և նախագծում ես դրա համար։ Յուրաքանչյուր ծառայություն commit է անում իր լոկալ տրանզակցիան և հրապարակում իրադարձություն transactional outbox-ի միջոցով, այնպես որ վիճակի փոփոխությունը և հաղորդագրությունը պահպանվում են ատոմար։ Workflow-ն համակարգվում է որպես saga՝ կամ օրկեստրավորված մեկ կոմպոնենտի կողմից, կամ խորեոգրաֆված իրադարձություններով, յուրաքանչյուր քայլի համար փոխհատուցող գործողությամբ, օրինակ՝ վճարման վերադարձ, եթե պաշարի ամրագրումը ձախողվում է։ Consumer-ները պետք է իդեմպոտենտ լինեն, որովհետև հաղորդագրությունները կարող են մեկից ավելի անգամ հասնել։ Եթե workflow-ին սա անընդհատ պետք է, դա ակնարկ է, որ երկու ծառայությունները պետք է մեկը լինեն։

Q6Մոնոլիտը մասշտաբավորվո՞ւմ է։

Այո, շատ ավելի հեռու, քան մարդիկ կարծում են։ Stateless մոնոլիտը աշխատում է որպես բազմաթիվ նույնական ինստանսներ load balancer-ի հետևում՝ սեսիաները Redis-ում, ֆայլերը object storage-ում և տվյալների բազա՝ read replica-ներով ու քեշավորմամբ։ Շատ մեծ պրոդուկտներ տարիներով աշխատել են մոնոլիտների վրա։ Այն, ինչ մոնոլիտը քեզ չի տալիս, մեկ «տաք» կոմպոնենտի անկախ մասշտաբավորումն է կամ առանձին թիմերի անկախ դեպլոյը։ Եթե մի մաս, օրինակ՝ նկարների մշակումը կամ որոնումը, այլ ռեսուրսների կարիք ունի, առանձնացրու միայն այդ մասը։ Մասշտաբավորումը սովորաբար տվյալների բազայի և քեշավորման խնդիր է շատ ավելի վաղ, քան ճարտարապետական խնդիր։

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

S1Ինչպե՞ս տվյալները պահել համաձայնեցված և հարցումների համար հասանելի, երբ ամեն ծառայություն ունի իր տվյալների բազան։

Յուրաքանչյուր ծառայություն իր տվյալների միակ գրողն է, իսկ մյուսները դրանք ստանում են երեք ձևերից մեկով։ Թարմ, որոշումների համար կրիտիկական ընթերցումների համար կանչիր տիրոջ API-ն՝ timeout-ով և fallback-ով։ Դանդաղ փոփոխվող տվյալների հաճախակի ընթերցումների համար պահիր լոկալ read model՝ թարմացվող տիրոջ իրադարձություններով՝ ընդունելով մի քանի վայրկյանի հնացում։ Ծառայությունների միջև ռեպորտինգի համար փոփոխությունները stream արա առանձին analytics պահոց՝ փոխանակ կենդանի տվյալների բազաները join անելու։ Միջծառայողական գրառումները դառնում են saga-ներ՝ փոխհատուցումներով և outbox-ով հրապարակվող իրադարձություններով։ Փոխզիջումը բացահայտ է․ շահում ես անկախ դեպլոյ և մասշտաբավորում, իսկ վճարում ես eventual consistency-ով, կրկնվող տվյալներով և ավելի շատ կոդով։ Եթե ֆիչին անընդհատ պետք են ատոմար գրառումներ երկու ծառայությունում, դա ապացույց է, որ սահմանը սխալ է։

S2Ո՞ր մետրիկաները ցույց կտան, որ ճարտարապետական փոփոխությունն իսկապես օգնել է։

Որպես բազա օգտագործում եմ DORA-ի չորս մետրիկաները․ դեպլոյի հաճախականություն, փոփոխության lead time, փոփոխությունների ձախողման տոկոս և ծառայության վերականգնման ժամանակ։ Լավ բաժանումը պետք է բարձրացնի հաճախականությունը և կրճատի lead time-ը ներգրավված թիմերի համար՝ առանց փոփոխությունների ձախողումները ավելացնելու։ Ավելացնում եմ ճարտարապետությանը հատուկ ազդանշաններ․ քանի մոդուլի կամ ծառայության է դիպչում տիպիկ ֆիչը, որքան հաճախ են դեպլոյները համակարգում պահանջում, հիմնական օգտատիրական հոսքերի p95 latency-ն և error rate-ը, ինչպես նաև ինֆրաստրուկտուրայի և on-call-ի արժեքը ըստ թիմի։ Չափիր մի քանի ամիս առաջ և հետո։ Եթե դեպլոյներն ավելի անկախ չեն դարձել, իսկ ինցիդենտներն աճել են, փոփոխությունը ծախս է ավելացրել առանց օգուտի, և հետ միավորելը օրինական, պրոֆեսիոնալ որոշում է։

S3Սինխրոն կանչե՞ր, թե՞ իրադարձություններ ծառայությունների միջև․ ինչպե՞ս ես որոշում յուրաքանչյուր փոխազդեցության համար։

Հարցնում եմ՝ արդյոք կանչողին պատասխանը պետք է շարունակելու համար։ Հարցումները, որոնք որոշում են օգտատիրոջ հաջորդ էկրանը, օրինակ՝ գինը կամ թույլտվության ստուգումը, սինխրոն են՝ պաշտպանված timeout-ներով, իդեմպոտենտ գործողությունների retry-ներով և circuit breaker-ներով։ Կողմնակի էֆեկտները, որոնք կարող են տեղի ունենալ պատասխանից հետո, օրինակ՝ անդորրագրերի ուղարկումը, loyalty-ի թարմացումը կամ որոնման վերաինդեքսավորումը, իրադարձություններ են․ դրանք վերացնում են ժամանակային coupling-ը և թույլ են տալիս consumer-ներին անջատված լինել՝ առանց producer-ին վնասելու։ Կոնկրետ ծառայությանն ուղղված հրամանները, որոնք կարող են երկար տևել, անցնում են հերթով՝ կարգավիճակով, որը կլիենտը կարող է poll անել։ Իրադարձությունների գինը ավելի բարդ debugging-ն ու eventual consistency-ն է, ուստի մինչև դրանց վրա լրջորեն հենվելը պնդում եմ correlation ID-ների, հերթերի միջով tracing-ի և dead-letter մոնիթորինգի վրա։

Բառարան

Bounded context
Դոմեյնի մաս, որտեղ ամեն տերմին ունի մեկ ճշգրիտ իմաստ և մեկ մոդել։
Բաշխված մոնոլիտ
Շատ ծառայություններ, որոնք դեռ պետք է դեպլոյվեն և փոխվեն միասին՝ միավորելով երկու ոճերի ծախսերը։
Strangler fig
Legacy համակարգի կտոր առ կտոր փոխարինում՝ տրաֆիկը աստիճանաբար նոր կոմպոնենտներին ուղղելով։
Branch by abstraction
Ինտերֆեյսի ներմուծում, որպեսզի նոր իրականացումը փոխարինի հնին փոխարկիչի հետևում։
Anti-corruption layer
Թարգմանիչ, որը թույլ չի տալիս legacy մոդելի հասկացություններին թափանցել նոր դիզայն։
Conway-ի օրենք
Համակարգերը հակված են արտացոլելու դրանք կառուցող կազմակերպության հաղորդակցության կառուցվածքը։
Tolerant reader
Consumer, որն անտեսում է անծանոթ դաշտերը, այնպես որ producer-ները կարող են տվյալներ ավելացնել առանց այն կոտրելու։
Consumer-driven contract
Թեստեր, որտեղ consumer-ները հայտարարում են, թե ինչի վրա են հենվում, իսկ provider-ը դա ստուգում է CI-ում։
Ժամանակային coupling
Երբ մի կոմպոնենտ կարող է աշխատել միայն այն ժամանակ, երբ մյուսը նույն պահին հասանելի է։

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

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