Arhitectura datelor și a sistemelor, de la zero Lecția 5 / 80

Trade-off-urile sunt totul

Latency vs throughput, consistency vs availability, simplu vs flexibil. Catalogul de trade-off-uri cu nume și de ce 'le vrem pe toate' e cea mai scumpă cerere din cameră.

Dacă nu ții minte nimic altceva din cursul ăsta, ține minte lecția asta. E singura propoziție în jurul căreia orbitează celelalte 80 de lecții.

Fiecare decizie arhitecturală e un trade-off. Nu există o arhitectură „cea mai bună”, doar „cea mai potrivită pentru setul ăsta de constrângeri.” Un sistem care e rapid pe date mici e de obicei lent pe date mari. Un sistem care scalează orizontal la o mie de mașini de obicei face asta renunțând la altceva, deseori la consistency, deseori la simplitatea operațională. Un sistem care e simplu de operat e de obicei unul căruia nu i se cere prea mult încă. În clipa în care îi ceri să facă mai mult, simplitatea începe să se cheltuiască singură.

Când un stakeholder intră într-o cameră și spune „vrem un sistem care e rapid, scalabil, consistent, ieftin, ușor de operat și ușor de extins”, ce spune de fapt e că nu știe încă la care dintre astea ține cel mai mult. Treaba ta, ca arhitect, e să traduci „le vrem pe toate” în „uite la ce vom fi excelenți, uite la ce vom fi doar adecvați și uite ce vom sacrifica deliberat.” Lista din urmă e cea pe care nimeni nu vrea s-o scrie. A o scrie e treaba.

Lecția asta e indexul de trade-off-uri cu nume care apar cel mai des în sisteme reale. Nu intrăm în profunzime în niciunul dintre ele încă. Restul cursului e, în mare parte, deep dives în trade-off-uri individuale și în cum le navighează sistemele reale. Astăzi e harta.

Teza

Fiecare calitate a unui sistem are un cost într-o altă calitate. Nu există prânz gratis în sistemele distribuite, în baze de date, în formate de stocare sau în modele de deployment. Cel mai aproape vei ajunge de „le avem pe toate” e „suntem suficient de buni la majoritatea, și slabi la cele câteva care se dovedesc a nu conta.” A recunoaște în avans care calități nu contează, pentru sistemul ăsta, cu baza asta de utilizatori, pe bugetul ăsta, e cea mai mare parte din arhitectură.

Fraza „suficient de bun la majoritatea, slab la câteva” nu e defetistă. E adevărul structural al construcției a orice există în lumea reală. O mașină care merge cu 300 km/h nu e și o familială economică la combustibil. O bază de date care scalează liniar la 1.000 de noduri nu e și cel mai ușor lucru de debug-uit la 3 dimineața. O aplicație monolitică Rails pe care o poți deploy-ui în 90 de secunde nu e și substratul pe care vei rula o organizație de 200 de ingineri. Fiecare e un produs bun. Fiecare e bun pentru că cineva a decis ce contează.

Trade-off-urile cu nume

Ce urmează e catalogul. Nu e exhaustiv: am lăsat afară pe cele mici. Astea sunt cele șapte care apar în aproape orice conversație arhitecturală reală în care am fost.

1. Latency vs throughput

Latency e cât durează un request. Throughput e câte request-uri gestionează sistemul pe secundă. Nu sunt același lucru, iar optimizarea uneia adesea o doare pe cealaltă.

Un sistem grupat în batch-uri de 1.000 de request-uri, procesat la fiecare 100 ms, are throughput excelent (10.000 requests/sec) și latency teribil (100 ms minim, deseori mai mult). Un sistem care procesează fiecare request pe măsură ce sosește are latency excelent (sub-milisecundă, uneori) și throughput mai prost, pentru că batching-ul amortizează overhead-ul, iar munca nedebatch-uită nu apucă.

Într-un warehouse de analiză optimizezi de obicei throughput-ul; utilizatorul care rulează un GROUP BY country peste 4 TB nu-i pasă dacă durează 8 sau 12 secunde, dar îi pasă că cluster-ul poate servi 50 de astfel de query-uri concurent. Într-un sistem de payment-authorisation, latency câștigă; terminalul comerciantului are nevoie de un răspuns sub 200 ms, de fiecare dată, chiar dacă asta înseamnă că throughput-ul per nod e o fracțiune din ce ar putea fi.

Capcana e să tratezi „rapid” ca pe o singură calitate. Nu este. De fiecare dată când cineva spune că vrea un sistem „rapid”, întrebarea ta de follow-up e: rapid la ce, pentru cine, sub ce încărcare? Răspunsul e rareori ambele axe.

2. Consistency vs availability

Ăsta e trade-off-ul teoremei CAP, și primește o lecție întreagă pentru el (lecția 10), dar titlul e: într-un sistem distribuit, când rețeaua se partiționează, trebuie să alegi. Poți refuza să servești request-uri până se vindecă partiția (consistency, cu costul availability) sau poți servi date potențial stale pe ambele părți până se reconciliază (availability, cu costul consistency).

Serviciul de account-balance al băncii tale alege consistency. Mai degrabă îți arată o pagină de eroare în timpul unei partiții de rețea decât să-ți spună că ai 100€ într-un cont care e de fapt în descoperire de zece minute. Like-counter-ul rețelei tale sociale alege availability. Mai degrabă îți arată un număr ușor stale, sau chiar un număr care scade pentru scurt timp înainte să crească, decât să refuze să încarce pagina cu totul.

Niciun răspuns nu e „corect.” Sunt produse diferite cu toleranțe diferite la a fi greșite. Conversația DDB-vs-Spanner-vs-Postgres e, în esență, acest trade-off în trei forme diferite. Vom petrece mult timp aici.

3. Read-optimized vs write-optimized

Forma engine-ului tău de stocare determină la ce e rapid. Engine-urile bazate pe B-tree (Postgres, MySQL cu InnoDB, SQL Server) sunt excelente la point reads și range scans pe coloane indexate; calea de scriere e mai scumpă pentru că fiecare insert poate avea nevoie să rebalanseze pagini pe disc. Engine-urile bazate pe LSM-tree (RocksDB, Cassandra, BigTable, ScyllaDB, părți din Mongo) sunt excelente la scrieri de volum mare; calea de citire e mai scumpă pentru că o cheie poate trăi în oricare dintre mai multe SSTables stratificate, iar engine-ul trebuie să facă merge la rezultate.

Dacă construiești un sistem care în principal citește (un CMS, un dashboard intern, o aplicație web tipică), alege o bază de date în formă de B-tree. Dacă construiești un sistem care în principal scrie (un backend de metrici, un event log, un store de istoric chat), alege o bază de date în formă de LSM. Dacă construiești un sistem care face ambele, alege-l pe cel a cărui parte slabă o poți compensa cu caching, batching sau un read store separat.

Trade-off-ul ăsta face muncă reală pe system design de treizeci de ani și nu dă semne să dispară. Revenim la el în modulul 4 (data systems).

4. Simplu vs flexibil

Un fișier de config YAML cu două setări e mai simplu decât un sistem de pluginuri. Un sistem de pluginuri e mai flexibil. Ambele își au momentul. A alege greșit pentru momentul în care ești e una dintre cele mai comune greșeli arhitecturale.

Devreme în viața unui proiect, simplu câștigă. Cele două cazuri pe care le ai astăzi sunt cele două cazuri pe care le ai astăzi, iar un config care le gestionează în 30 de linii e mai rapid de scris, mai rapid de citit, mai rapid de debug-uit și mai rapid de șters când se schimbă cerințele. Flexibilitatea pe care ai fi construit-o într-un sistem de pluginuri plătește dobândă la un împrumut pe care nu l-ai luat niciodată.

Mai târziu, când ai 50 de cazuri în loc de două, simplu pierde. Config-ul de 30 de linii a devenit un config de 3.000 de linii; fiecare intrare e un caz special; nimeni nu mai poate prezice ce schimbare va strica ce. Atunci sistemul de pluginuri pe care nu l-ai construit acum cinci ani începe să arate ca un chilipir.

Abilitatea e să observi pe care parte a punctului de inflexiune ești. Default-ul cinstit e: construiește simplu, urmărește cum crește, refactorizează spre flexibil când cazurile încetează să mai poată fi adăugate curat, nu când sunt doar numeroase. „Premature flexibility” a costat mai multe proiecte decât „missing flexibility.”

5. Decentralizat vs coordonat

Scala vrea decentralizare. Corectitudinea vrea coordonare. Aceste două trag în direcții opuse pe aproape orice întrebare de sisteme distribuite.

Un cache distribuit global care lasă fiecare nod să decidă ce să evicteze e decentralizat; scalează liniar și supraviețuiește morții oricărui nod individual, dar două noduri pot ține stare inconsistentă pentru o vreme. Un cache care folosește un coordinator central pentru a aloca chei nodurilor e coordonat; starea e mereu consistentă, dar coordinator-ul e acum un bottleneck și un single point of failure.

DNS-ul e decentralizat. Scalează la întreaga planetă pentru că niciun nod nu trebuie să se coordoneze cu altul. Costul e că propagarea durează minute până la ore; nu poți folosi DNS-ul ca mecanism de consistency în timp real. O bază de date relațională tradițională e coordonată. Fiecare scriere trece printr-un singur primary, motiv pentru care ACID e simplu și de ce scalarea scrierilor peste o singură mașină e grea.

Majoritatea sistemelor reale aleg un mijloc: coordonate într-o rază mică de explozie (un serviciu, o regiune, o partiție) și decentralizate între raze de explozie. A ști care axă merită care cost de coordonare e jumătate din system design.

6. Cost of build vs cost of buy

Multe decizii arhitecturale sunt deghizate ca întrebări tehnice, dar sunt de fapt întrebări de prețuri. „Ar trebui să folosim Kafka sau Kinesis?” e rareori despre meritele inginerești ale celor două sisteme. E despre dacă ai prefera să plătești AWS aproximativ 0,08$ per GB pentru un produs gestionat fără muncă de operare, sau să-i plătești pe propriii ingineri să ruleze un cluster Kafka care costă mai puțin per GB, dar mănâncă rotația de on-call a doi ingineri.

Aceeași întrebare reapare peste tot: managed Postgres vs self-hosted, Auth0 vs propriul tău serviciu de auth, Stripe vs propriul tău procesor de plăți, Snowflake vs un lakehouse Spark+Iceberg self-hosted. Răspunsul aproape întotdeauna se schimbă cu mărimea echipei și stadiul de creștere. Un startup de două persoane care cumpără tot de la vendori și livrează produs ia decizia corectă. O companie de 200 de ingineri care plătește 4 milioane $/an pentru Auth0 a trecut probabil linia unde a o construi ar fi fost mai ieftin. O companie de 2.000 de ingineri care își construiește singură totul e înapoi pe partea greșită.

Capcana e că răspunsul nu e stabil. Decizia corectă la 5 ingineri e greșită la 50, iar decizia corectă la 50 e greșită la 500. Arhitectura trebuie să revadă cost-of-build vs cost-of-buy pentru fiecare piesă majoră, la fiecare doi ani. Majoritatea echipelor n-o fac și ajung să plătească opt cifre pe an pentru ceva ce ar putea acum să ruleze singure, sau să bage o echipă în mentenanța unui sistem homegrown care e acum strict mai prost decât SaaS-ul pe care l-ar putea cumpăra.

7. Time to market vs cost pe termen lung

Ultimul trade-off, și posibil meta-trade-off-ul care le conține pe toate celelalte. Fiecare scurtătură pe care o iei astăzi are un cost viitor. Fiecare investiție pe care o faci în arhitectură curată pe termen lung are un cost de astăzi.

Dacă ești la un startup cu 18 luni de runway, decizia corectă e aproape întotdeauna să livrezi versiunea cu duct-tape. Valori hardcodate, fără teste pe căile plictisitoare, fără documentație, fără abstracțiuni, toate funcționalitățile în spatele feature flags ca să le poți scoate ieftin. Te lupți cu runway-ul, iar majoritatea duct-tape-ului va fi oricum înlocuit pentru că produsul pe care îl livrezi aproape sigur nu e produsul pe care-l vrea piața. A investi în arhitectură curată pentru o funcționalitate care va fi tăiată în trei luni e cea mai scumpă formă de muncă.

Dacă ești la o companie de 10 ani cu venituri puternice, decizia corectă se inversează. Duct-tape-ul pe care-l scrii astăzi va fi încă acolo în 2034, va fi operat de oameni care nu s-au alăturat încă companiei și va acumula dobândă compusă în costul de mentenanță. A petrece o săptămână în plus să-l faci curat înseamnă a achita o datorie de 40 de săptămâni pe care altcineva ar fi moștenit-o.

Instinctele majorității inginerilor sunt calibrate la un capăt al acestui trade-off, în funcție de unde și-au petrecut anii formativi. Abilitatea e să observi în ce mediu ești acum și să te ajustezi corespunzător. Un inginer de startup care scrie arhitectură la scară Google în prima săptămână nu livrează nimic. Un inginer de enterprise care duct-tapează fluxul de credit-card livrează un incident de reglementare.

2x2-ul

Iată o cale de a vizualiza patru dintre aceste trade-off-uri ca cadrane cu nume:

flowchart TD
    subgraph Q1["High consistency, high availability"]
        A1["Hard. Spanner, FoundationDB.<br/>Achievable with cost and complexity."]
    end
    subgraph Q2["High consistency, low availability"]
        A2["Traditional RDBMS in single primary.<br/>Postgres, classic SQL Server."]
    end
    subgraph Q3["Low consistency, high availability"]
        A3["Eventual consistency.<br/>DynamoDB default, Cassandra, DNS."]
    end
    subgraph Q4["Low consistency, low availability"]
        A4["Nobody chooses this on purpose.<br/>It's where misconfigured systems land."]
    end

Trei dintre acele patru cadrane sunt alegeri arhitecturale reale pentru workload-uri reale. Al patrulea, low-consistency-low-availability, e locul unde ajung sistemele când nimeni nu a făcut o alegere deliberată și default-urile au arătat toate în direcția greșită. Dacă post-mortem-ul tău spune „am pierdut date și serviciul a fost și el down”, ești în cadranul patru, iar cadranul patru e mereu un accident.

„Le vrem pe toate”

Cea mai scumpă propoziție din ședința de arhitectură e „le vrem pe toate.” Rapid, scalabil, consistent, ieftin, simplu, flexibil și gata până în Q2. Sistemele reale nu există în acest punct al spațiului de design. Nu pot. Cel mai aproape a ajuns industria e Google Spanner, care îți dă strong consistency geo-distribuită cu costul că e scump de rulat, scump de licențiat, necesită ceasuri atomice (da, reale) și e supus floor-ului de latency al replicării sincrone WAN. Spanner e aproximativ cel mai scump sistem din industrie și e cel care face cele mai puține trade-off-uri. Asta nu e o coincidență; e structura trade-off-ului.

Când cineva la masă cere totul, treaba ta nu e să te cerți cu el. Treaba ta e să traduci. „Dacă optimizăm pentru latency la p99 sub 50 ms, vom plăti pentru asta în throughput per mașină și în availability regională; uite trei arhitecturi și la ce renunță fiecare.” Propoziția aia nu îi costă nimic dacă prioritățile sunt încă flexibile, și îi costă săptămâni din data de livrare dacă nu sunt. Oricum, ai informat decizia în loc să fii surprins de ea mai târziu.

Arhitecții care fac burnout sunt cei care încearcă să livreze „totul” și descoperă peste șase luni că sistemul e mediocru pe fiecare axă pentru că trade-off-urile n-au fost niciodată făcute conștient. Arhitecții care nu fac burnout sunt cei care fac trade-off-urile devreme, le scriu într-un ADR (lecția 4) și le revizuiesc la fiecare șase luni. Sistemul mediocru e costul inevitabil al refuzului de a alege. Sistemul doar-adecvat-pe-majoritatea-lucrurilor-și-excelent-pe-lucrurile-care-contează e cel care câștigă.

Ce urmează

Restul cursului e în mare parte trade-off-uri cu nume și cum le navighează sistemele reale. Lecția 6 trece prin cea mai simplă arhitectură posibilă (un VM, o bază de date) și arată la ce renunță ca să fie atât de simplă. Lecțiile 9 până la 12 intră în profunzime pe consistency vs availability și familia CAP. Modulul 4 e lupta read-vs-write storage engines. Modulul 6 e balanța buy-vs-build, simple-vs-flexible pentru cozi, cache-uri, search și ML serving. Modulul 9 e povestea time-to-market vs long-term-cost pentru partea operațională: deployments, observability, on-call.

Fiecare dintre acele lecții pornește de la aceeași fundație: lucrul ăsta e un trade-off, uite axele, uite care dintre ele alege tehnologia asta, uite la ce renunță și uite când ai alege-o pe cealaltă.

Arhitectura nu e un set de pattern-uri de memorat. E un set de trade-off-uri de navigat. Pattern-urile sunt de ordinul al doilea; sunt soluții cristalizate la configurații specifice de trade-off-uri care apar suficient de des încât să merite un nume. Vom ajunge la multe dintre ele. Vom ajunge la fiecare dintre ele prin trade-off-ul pe care îl rezolvă.

Lecția următoare: cea mai simplă arhitectură pe care o poți livra. Un VM, un Postgres, un proces și o carieră perfect respectabilă construită pe forma asta.

Caută