În lecția trecută am ajuns la o definiție de lucru: arhitectura e setul de decizii care sunt scumpe de schimbat mai târziu. Acum trebuie să punem întrebarea evidentă următoare. Ce conduce acele decizii? Care e input-ul pe care îl folosești efectiv ca să-ți dai seama ce bază de date, ce topologie de deployment, ce stil de comunicare e potrivit pentru sistemul tău?
Răspunsul greșit, și cel pe care majoritatea echipelor îl apucă în prima săptămână a unui proiect, e „lista de funcționalități”. Product owner-ii dau echipei de inginerie o listă de lucruri pe care sistemul trebuie să le facă. Sign up. Login. Răsfoirea unui catalog. Plasarea unei comenzi. Trimiterea unui email de confirmare. Echipa citește lista, schițează un diagramă rapid, alege un stack și începe să construiască. Șase luni mai târziu descoperă că au făcut alegerea greșită pe trei dintre deciziile de nivel-an și acum plătesc pentru asta.
Motivul pentru care se întâmplă asta e că listele de funcționalități, în sine, nu conțin suficientă informație ca să proiectezi un sistem. Două sisteme cu liste de funcționalități identice pot avea nevoie de arhitecturi radical diferite. Lucrul care le separă e a doua clasă de cerințe, cea care e ignorată sau sub-specificată, și aia e subiectul lecției ăsteia.
Cerințe funcționale: ce face sistemul
Cerințele funcționale descriu comportamentul sistemului. Răspund la întrebarea „ce face?”
Un mic site de e-commerce ar putea avea cerințe funcționale precum:
- Utilizatorii pot crea un cont cu email și parolă.
- Utilizatorii pot răsfoi un catalog de produse.
- Utilizatorii pot adăuga produse într-un coș și pot face checkout.
- Utilizatorii pot plăti cu cardul de credit sau debit direct SEPA.
- Sistemul trimite un email de confirmare a comenzii după un checkout reușit.
- Adminii pot adăuga și edita produse printr-un panou intern.
- Sistemul suportă calculul TVA pentru statele membre UE.
Astea sunt punctele care ajung pe roadmap-ul product owner-ului și în user stories. Sunt esențiale, sunt modul în care echipa și business-ul cad de acord asupra a ce se construiește, și nu sunt suficiente ca să proiectezi sistemul. Fiecare dintre punctele alea ar putea fi implementat ca un singur script Python care rulează pe laptopul cuiva sau ca un sistem distribuit global care gestionează un milion de tranzacții pe minut. Punctele nu-ți spun care.
Există un exercițiu faimos pe care intervievatorii din inginerie îl iubesc: „proiectează Twitter”. Dacă citești enunțul literal, cerințele funcționale sunt aproximativ:
- Utilizatorii pot posta un mesaj scurt.
- Utilizatorii pot urmări alți utilizatori.
- Utilizatorii pot vedea un feed cu mesaje de la oamenii pe care-i urmăresc.
Acele patru puncte. Asta e toată lista de funcționalități. L-ai putea implementa într-o după-amiază ca o bază de date Postgres, o aplicație Flask și un query SELECT ... ORDER BY created_at DESC LIMIT 50. Gata.
Bineînțeles, nu asta face problema de design interesantă. Versiunea interesantă a „proiectează Twitter” apare doar când intervievatorul adaugă a doua categorie de cerințe: 500 de milioane de utilizatori, 100 de milioane de utilizatori activi zilnic, vârf de încărcare de jumătate de milion de tweets pe minut, citiri medii de feed de două miliarde pe zi, latență p99 sub 200 de milisecunde, țintă de disponibilitate 99,9%, cerințe legale de data residency în trei jurisdicții. Acum design-ul e interesant. Acum alegerea bazei de date contează, strategia de caching contează, deployment-ul geografic contează, iar întrebarea „ar trebui feed-ul calculat la citire sau pre-calculat la scriere” devine dezbaterea arhitecturală centrală.
Aceeași listă de funcționalități. Arhitectură radical diferită. Lucrul care s-a schimbat e setul de cerințe non-funcționale.
Cerințe non-funcționale: cât de bine
Cerințele non-funcționale descriu calitățile sistemului. Răspund la întrebarea „cât de bine face ce face și în ce condiții?”
Asta e categoria de cerințe care conduce arhitectura. Stabilește-le corect și alegerile arhitecturale ies în mare parte din ele. Stabilește-le greșit, sau și mai rău, ignoră-le, și vei construi sistemul potrivit pentru universul greșit.
Sunt aproximativ șapte calități non-funcționale care apar repetat când dai formă unui sistem. Diferiți autori le organizează ușor diferit, iar granițele sunt puțin neclare, dar ăsta e setul de lucru.
Latency
Latența e cât durează un singur request să se completeze, măsurată din perspectiva utilizatorului. Aproape întotdeauna e exprimată ca un percentil, nu ca o medie, pentru că mediile ascund coada distribuției.
O țintă tipică de latență arată așa: „p50 sub 80 ms, p95 sub 200 ms, p99 sub 500 ms” pentru un endpoint de API. Asta înseamnă că jumătate din toate request-urile se termină în mai puțin de 80 ms, 95% în mai puțin de 200 ms, 99% în mai puțin de 500 ms. Acel 1% peste e coada, iar coada e unde trăiesc experiențele proaste de utilizator. Media („p50” e mediana, dar media e similară pentru distribuții bine comportate) e numărul care arată bine în slide-uri. P99 e numărul pe care chiar îl simți.
Țintele de latență conduc: unde îți pui serverele (mai aproape de utilizatori înseamnă latență mai mică), dacă faci caching agresiv, dacă pre-calculezi rezultate, dacă folosești comunicare sincronă sau asincronă, și dacă ți se permite să faci un apel la baza de date pe hot path.
Throughput
Throughput e câte request-uri, evenimente sau unități de muncă procesează sistemul pe unitatea de timp. Adesea exprimat ca requests per second (RPS) pentru API-uri, transactions per second (TPS) pentru baze de date, mesaje pe secundă pentru cozi, sau evenimente pe secundă pentru sisteme de streaming.
O țintă tipică: „5.000 de requests pe secundă susținut, 20.000 pe secundă vârf, cu vârful durând până la 30 de minute în timpul unei flash sale”. Partea interesantă e raportul între susținut și vârf, și durata vârfului. Un sistem care gestionează 5.000 RPS fericit dar se topește la 8.000 RPS e ok dacă traficul tău e plat și un dezastru dacă ai spike-uri.
Țintele de throughput conduc: cum scalezi (vertical sau orizontal), dacă ai nevoie de un load balancer, cum partiționezi datele, cum dimensionezi pool-urile de workeri și dacă trebuie să absorbi burst-uri cu o coadă.
Availability
Disponibilitatea e procentul de timp în care sistemul e up și servește request-uri cu succes. Se exprimă în nouari.
- 99% disponibilitate înseamnă „doi nouari”: 3,65 zile de downtime pe an. Acceptabil pentru o unealtă internă.
- 99,9% înseamnă „trei nouari”: 8,76 ore pe an. SaaS tipic.
- 99,95% înseamnă „trei nouari și jumătate”: 4,38 ore pe an. SLO comun pentru B2B plătit.
- 99,99% înseamnă „patru nouari”: 52,6 minute pe an. Agresiv. Nu mai poți face deploy neglijent.
- 99,999% înseamnă „cinci nouari”: 5,26 minute pe an. Telecom. Ai nevoie de redundanță reală la fiecare strat.
Lucrul care îi prinde pe oameni nepregătiți e că fiecare nouar adițional costă aproximativ un ordin de mărime mai mult în efort de inginerie. Trecerea de la 99% la 99,9% e mai ales o chestiune de disciplină: nu livra bug-uri în producție, ai o poveste de rollback, monitorizează ce ai. Trecerea de la 99,9% la 99,99% e o chestiune de redundanță: multiple availability zones, failover automat, fără puncte unice de eșec. Trecerea de la 99,99% la 99,999% e o chestiune de tot: multi-region, change management formal, chaos engineering, page-uri într-o duminică dimineața.
Țintele de disponibilitate conduc: redundanță (câte copii ale fiecărui serviciu), topologie de deployment (single AZ, multi-AZ, multi-region), cum gestionezi deployment-urile (blue/green, canary, rolling) și dacă poți să închizi sistemul pentru mentenanță.
Durability
Durabilitatea e probabilitatea ca datele, odată commit-ate, să fie încă acolo când le ceri. Se exprimă în nouari, ca disponibilitatea, dar numerele sunt mai mari.
S3, exemplul canonic, e documentat ca „11 nouari de durabilitate”. Adică 99,999999999%. Interpretarea e că dacă stochezi 10.000.000 de obiecte, te-ai aștepta să pierzi unul la fiecare 10.000 de ani în medie. Asta e realizat stocând fiecare obiect în multiple facilități separate fizic și verificând continuu integritatea.
O bază de date relațională fără backup-uri are o durabilitate aproximativ egală cu durabilitatea discului pe care trăiește, ceea ce e „bun până când moare discul, apoi catastrofal”. De asta există backup-urile, replicarea și replicarea cross-region.
Țintele de durabilitate conduc: strategia de backup, topologia de replicare, cât de des faci snapshot, unde stochezi snapshot-urile și dacă faci point-in-time recovery.
Consistency
Consistența e garanția despre ce văd cititorii după ce un scriitor actualizează datele. Există o întreagă grădină zoologică de modele de consistență (vom petrece mult din modulele 3 și 8 pe asta); distincția principală pentru moment e între strong consistency și eventual consistency.
Strong consistency: după ce o scriere se completează, fiecare citire ulterioară vede valoarea nouă. Fără excepții. Asta îți oferă o singură bază de date Postgres și ce presupun naiv majoritatea dezvoltatorilor că au peste tot.
Eventual consistency: după ce o scriere se completează, citirile pot vedea valoarea veche pentru o perioadă de timp, dar în cele din urmă toate vor vedea valoarea nouă. Asta primești de la majoritatea key-value stores distribuite, de la replicarea cross-region și de la orice lucru care implică caching.
Teorema CAP (o vom acoperi în lecția 27) spune că într-un sistem distribuit, când rețeaua se partiționează, trebuie să alegi între consistency și availability. Nu poți avea ambele. Majoritatea sistemelor de producție aleg disponibilitatea și trăiesc cu eventual consistency, dar alegerea trebuie să fie conștientă pentru că consecințele se propagă prin întreaga experiență de utilizator.
Țintele de consistență conduc: alegerea bazei de date, dacă poți folosi un cache, cum gestionezi reads-after-writes și cum recuperează fluxurile orientate spre utilizator când datele nu s-au propagat încă.
Security
Securitatea e, în mare, despre cine are voie să facă ce, cum sunt stabilite și verificate identitățile, cum sunt protejate datele în repaus și în tranzit, și cum rezistă sistemul la abuz. E o cerință non-funcțională în sensul că nu schimbă ce face sistemul pentru utilizatorii legitimi; schimbă constrângerile din jurul modului în care o face.
Cerințele de securitate acoperă de obicei: autentificarea (cine ești), autorizarea (ce poți face), encripția în tranzit (TLS peste tot), encripția în repaus (encripție de disc și de bază de date), gestionarea de secrete (unde trăiesc cheile API), audit logging (cine a făcut ce și când), conformitatea (GDPR, HIPAA, PCI-DSS, SOC 2 în funcție de sector) și threat modelling (de ce atacuri ar trebui să ne îngrijorăm).
Securitatea conduce: alegerea provider-ului de auth, topologia de rețea (internet public versus VPC privat), dacă folosești un serviciu gestionat sau îți construiești propriul, unde trăiesc datele fizic (data residency) și întregul proces de deployment.
Evolvability și maintainability
Evoluabilitatea e cât de ușor e de schimbat sistemul în timp. Mentenabilitatea e verișoara operațională: cât de ușor e de menținut în funcțiune. Astea două sunt uneori despărțite și uneori puse la grămadă, și acoperă preocupările long-tail care nu apar în prima zi dar domină anii doi până la zece.
Un sistem cu evoluabilitate bună are granițe clare între module, cuplaj scăzut, teste automate la nivelul potrivit, documentație decentă și capacitatea de a absorbi noi cerințe fără rescrieri majore. Un sistem cu mentenabilitate bună are observability bună, deployment-uri previzibile, log-uri utile și o suprafață suficient de mică încât un inginer nou poate fi productiv în două săptămâni în loc de două luni.
Evoluabilitatea și mentenabilitatea sunt ușor de depriorizat pentru că nu apar pe lista de verificare a lansării. Sunt și diferența dintre un sistem care încă ajută business-ul în cinci ani și unul care e rescris pentru că nimeni nu poate să-l atingă fără să strice ceva.
Trade-off-urile sunt obligatorii, nu opționale
Capcana care prinde arhitecții fără experiență e să trateze cerințele non-funcționale ca pe o listă de dorințe în care fiecare calitate e setată la „mare” și apoi să declare victoria. Sistemele reale implică trade-off-uri. Strângerea unei calități aproape întotdeauna o slăbește pe alta.
O listă scurtă a trade-off-urilor pe care nu le poți evita:
- Disponibilitatea mai mare costă bani. Deployment multi-AZ e aproximativ de 2x costul single-AZ. Multi-region e aproximativ de 3-4x. Economiile din a fi down mai puțin trebuie să justifice cheltuiala.
- Disponibilitatea mai mare costă adesea consistență. Dacă vrei să continui să servești trafic în timpul unei partiții de rețea, trebuie să accepți că unele citiri vor fi învechite. CAP din nou.
- Latența mai mică costă adesea throughput. Un sistem reglat pentru cea mai mică latență per request face mai puțină muncă în paralel și se plafonează la un throughput agregat mai mic. Un sistem reglat pentru maximum throughput de obicei face batching și queueing, ceea ce ridică latența per request.
- Consistența mai puternică costă latență. Replicarea sincronă a unei scrieri pe trei mașini înseamnă că utilizatorul așteaptă pentru toate trei. Replicarea asincronă îți dă scrieri mai rapide și garanții mai slabe.
- Securitatea mai mare costă viteza dezvoltatorilor. Fiecare rotație de secret, fiecare politică IAM, fiecare regulă WAF e fricțiune. Merită, dar reală.
- Evoluabilitatea mai bună costă efort în avans. Trasarea unor granițe curate de modul în prima zi e mai lent decât să livrezi pur și simplu un singur fișier. Răsplata vine în anul doi.
Nu scapi de aceste trade-off-uri fiind deștept. Le navighezi alegând pentru ce optimizezi în acest sistem, dat fiind acest business, în această etapă de creștere. Arhitectura e forma acelor alegeri.
Extragerea NFR-urilor de la product owners
Majoritatea spec-urilor de produs pe care le primești vor avea cerințele funcționale în prim-plan și cerințele non-funcționale complet absente. Asta e normal și nu e răutăcios; product owner-ii gândesc în funcționalități pentru că asta îi recompensează jobul lor. Treaba ta, înainte să desenezi o singură căsuță, e să extragi cerințele non-funcționale care nu sunt în document.
Iată o listă de lucru cu întrebări pe care să le pui, ordonate după cât de des dezvăluie ceva care schimbă arhitectura.
- Câți utilizatori ne așteptăm la lansare? În șase luni? În doi ani? Ancorează țintele de throughput și informează strategia de scalare.
- Care e vârful de încărcare relativ la medie? Flash sales, ore de program, fusuri orare, spike-uri virale. Un raport peak/average de 10x e normal; un raport de 100x înseamnă că trebuie să proiectezi pentru spike.
- Care e bugetul de latență pentru cel mai folosit endpoint? „Pare instantaneu” înseamnă de obicei sub 100 ms. „Pare responsive” înseamnă de obicei sub 300 ms. „Pare lent” e orice peste o secundă.
- Ce se întâmplă dacă sistemul e down 5 minute? O oră? O zi? Asta e întrebarea care calibrează disponibilitatea. Dacă răspunsul la „down o oră” e „shrug, e o unealtă internă”, nu ai nevoie de patru nouari. Dacă răspunsul e „pierdem 2M€ și ne sună reglementatorul”, ai.
- Ce se întâmplă dacă pierdem date? Calibrează durabilitatea. Unele date sunt critice (tranzacții financiare, fotografii încărcate de utilizatori). Unele date sunt regenerabile (rezultate de căutare cache-uite, agregate calculate).
- Există cerințe stricte de latență sau locație de la reglementatori? GDPR, data residency, legea cibernetică din China etc. Astea sunt adesea decizii de nivel-an pe care nu le poți anula.
- Care e cel mai realist actor rău în cazul cel mai rău? Un adolescent plictisit, un grup criminal sofisticat, un stat național, un angajat intern. Răspunsul schimbă semnificativ postura de securitate.
- Cât de des ne așteptăm să schimbăm sistemul? În fiecare săptămână? În fiecare trimestru? O dată la cinci ani? Calibrează investiția în evoluabilitate.
Dacă pui aceste întrebări și răspunsul product owner-ului e „nu știu, ce recomanzi tu?” e de fapt ok. Înseamnă că ai cuvântul să propui ținte, să le pui pe hârtie și să te referi la ele când cineva mai târziu se plânge că sistemul e „prea scump” sau „prea lent”. NFR-urile sunt un contract cu restul business-ului în aceeași măsură în care sunt un input de inginerie.
flowchart TD
A[New feature request] --> B{What's the load?}
B -->|low, less than 100 RPS| C[Single instance is fine]
B -->|medium, 100 to 5000 RPS| D{What's the latency budget?}
B -->|high, 5000 RPS plus| E[Horizontal scaling, caching, sharding]
D -->|under 100 ms| F[Cache aggressively, colocate data]
D -->|under 1 second| G[Standard web stack]
D -->|background, async OK| H[Queue and worker pool]
C --> I{What's the availability target?}
F --> I
G --> I
H --> I
E --> I
I -->|99 percent| J[Single AZ deployment]
I -->|99.9 percent| K[Multi-AZ with health checks]
I -->|99.99 percent plus| L[Multi-region, formal SLOs]
J --> M[Architectural choices follow]
K --> M
L --> M
Diagrama e deliberat simplistă. Versiunea reală are mai multe ramuri și mai multe interacțiuni între calități, dar forma raționamentului e corectă: cobori de la cerințe la implicații arhitecturale, și o faci explicit, pe hârtie, cu product owner-ul în cameră.
Cu ce ar trebui să rămâi din lecția asta
Cerințele funcționale sunt necesare și nu suficiente. Cerințele non-funcționale sunt cele care conduc forma sistemului. Sunt aproximativ șapte: latency, throughput, availability, durability, consistency, security, evolvability. Fiecare se află în trade-off cu cel puțin una dintre celelalte. Treaba ta înainte să proiectezi orice e să fixezi ținte numerice realiste pentru cele care contează, punând întrebările potrivite oamenilor care dețin rezultatul de business.
În lecția următoare vom trece de la „ce să întrebi” la „cum să desenezi” și ne vom uita la modelul C4: o convenție de diagramare cu patru niveluri de zoom care face „hai să schițăm sistemul” efectiv productiv.
Referințe
- Bass, Clements, Kazman. Software Architecture in Practice, ediția a 4-a (2021), capitolele 4 până la 13 acoperă atributele de calitate în profunzime.
- Beyer et al. Site Reliability Engineering (2016), capitolul 4 despre Service Level Objectives.
- Werner Vogels, „Eventually Consistent” (CACM 2009), articolul fundamental despre trade-off-urile de consistency.
- ISO/IEC 25010 quality model (2011), vocabularul standardizat pentru atributele de calitate software.