Patrons arquitectònics convenients

Hola Habr!

A la vista dels esdeveniments actuals a causa del coronavirus, una sèrie de serveis d'Internet han començat a rebre un augment de càrrega. Per exemple, Una de les cadenes minoristes del Regne Unit simplement va aturar el seu lloc de comandes en línia., perquè no hi havia prou capacitat. I no sempre és possible accelerar un servidor simplement afegint equips més potents, però les sol·licituds dels clients s'han de processar (o aniran als competidors).

En aquest article parlaré breument de pràctiques populars que us permetran crear un servei ràpid i tolerant a errors. Tanmateix, dels possibles esquemes de desenvolupament, només vaig seleccionar els que hi són actualment fàcil d'usar. Per a cada element, teniu biblioteques ja fetes o teniu l'oportunitat de resoldre el problema mitjançant una plataforma al núvol.

Escalat horitzontal

El punt més senzill i conegut. Convencionalment, els dos esquemes de distribució de càrrega més comuns són l'escalat horitzontal i vertical. En el primer cas permeteu que els serveis funcionin en paral·lel, distribuint així la càrrega entre ells. En el segon demaneu servidors més potents o optimitzeu el codi.

Per exemple, agafaré l'emmagatzematge de fitxers al núvol abstractes, és a dir, algun anàleg d'OwnCloud, OneDrive, etc.

A continuació es mostra una imatge estàndard d'aquest circuit, però només demostra la complexitat del sistema. Després de tot, hem de sincronitzar d'alguna manera els serveis. Què passa si l'usuari desa un fitxer de la tauleta i després vol veure'l des del telèfon?

Patrons arquitectònics convenients
La diferència entre els enfocaments: en l'escala vertical, estem preparats per augmentar la potència dels nodes, i en l'escala horitzontal, estem preparats per afegir nous nodes per distribuir la càrrega.

CQRS

Segregació de responsabilitats de consulta d'ordres Un patró força important, ja que permet a diferents clients no només connectar-se a diferents serveis, sinó també rebre els mateixos fluxos d'esdeveniments. Els seus beneficis no són tan evidents per a una aplicació senzilla, però és extremadament important (i senzill) per a un servei ocupat. La seva essència: els fluxos de dades entrants i sortints no s'han de creuar. És a dir, no podeu enviar una sol·licitud i esperar una resposta; en canvi, envieu una sol·licitud al servei A, però rebeu una resposta del servei B.

El primer avantatge d'aquest enfocament és la capacitat de trencar la connexió (en el sentit ampli de la paraula) mentre s'executa una sol·licitud llarga. Per exemple, prenem una seqüència més o menys estàndard:

  1. El client va enviar una sol·licitud al servidor.
  2. El servidor ha començat un llarg temps de processament.
  3. El servidor va respondre al client amb el resultat.

Imaginem que al punt 2 es va trencar la connexió (o es va tornar a connectar la xarxa, o l'usuari va anar a una altra pàgina, trencant la connexió). En aquest cas, serà difícil que el servidor enviï una resposta a l'usuari amb informació sobre què s'ha processat exactament. Utilitzant CQRS, la seqüència serà lleugerament diferent:

  1. El client s'ha subscrit a les actualitzacions.
  2. El client va enviar una sol·licitud al servidor.
  3. El servidor va respondre "sol·licitud acceptada".
  4. El servidor va respondre amb el resultat a través del canal des del punt "1".

Patrons arquitectònics convenients

Com podeu veure, l'esquema és una mica més complex. A més, aquí falta l'enfocament intuïtiu de sol·licitud-resposta. Tanmateix, com podeu veure, una interrupció de connexió mentre es processa una sol·licitud no comportarà cap error. A més, si de fet l'usuari està connectat al servei des de diversos dispositius (per exemple, des d'un telèfon mòbil i des d'una tauleta), podeu assegurar-vos que la resposta arriba als dos dispositius.

Curiosament, el codi per processar els missatges entrants passa a ser el mateix (no al 100%) tant per als esdeveniments que van ser influenciats pel propi client com per a altres esdeveniments, inclosos els d'altres clients.

Tanmateix, en realitat obtenim un avantatge addicional a causa del fet que el flux unidireccional es pot gestionar amb un estil funcional (utilitzant RX i similars). I això ja és un avantatge seriós, ja que en essència l'aplicació es pot fer completament reactiva, i també utilitzant un enfocament funcional. Per als programes de greix, això pot estalviar significativament recursos de desenvolupament i suport.

Si combinem aquest enfocament amb l'escala horitzontal, com a avantatge tindrem la possibilitat d'enviar sol·licituds a un servidor i rebre respostes d'un altre. Així, el client pot triar el servei que li convingui, i el sistema interior encara podrà processar els esdeveniments correctament.

Provisió d'esdeveniments

Com sabeu, una de les característiques principals d'un sistema distribuït és l'absència d'un temps comú, una secció crítica comuna. Per a un procés, podeu fer una sincronització (en els mateixos mutex), dins de la qual esteu segurs que ningú més està executant aquest codi. No obstant això, això és perillós per a un sistema distribuït, ja que requerirà una sobrecàrrega i també matarà tota la bellesa de l'escala: tots els components encara n'esperaran.

D'aquí obtenim un fet important: un sistema distribuït ràpid no es pot sincronitzar, perquè així reduirem el rendiment. D'altra banda, sovint necessitem una certa coherència entre components. I per a això podeu utilitzar l'enfocament amb consistència eventual, on es garanteix que si no hi ha canvis de dades durant algun període de temps després de l'última actualització ("eventualment"), totes les consultes retornaran el darrer valor actualitzat.

És important entendre que per a les bases de dades clàssiques s'utilitza amb força freqüència consistència forta, on cada node té la mateixa informació (això sovint s'aconsegueix en el cas en què la transacció es considera establerta només després que el segon servidor respongui). Aquí hi ha algunes relaxacions a causa dels nivells d'aïllament, però la idea general segueix sent la mateixa: es pot viure en un món completament harmonitzat.

Tanmateix, tornem a la tasca original. Si part del sistema es pot construir amb consistència eventual, llavors podem construir el diagrama següent.

Patrons arquitectònics convenients

Característiques importants d'aquest enfocament:

  • Cada sol·licitud entrant es col·loca en una cua.
  • Mentre processa una sol·licitud, el servei també pot col·locar tasques en altres cues.
  • Cada esdeveniment entrant té un identificador (que és necessari per a la desduplicació).
  • La cua funciona ideològicament segons l'esquema "només adjuntar". No podeu eliminar-ne elements ni reorganitzar-los.
  • La cua funciona segons l'esquema FIFO (perdó per la tautologia). Si necessiteu fer una execució paral·lela, en un moment haureu de moure objectes a diferents cues.

Us recordo que estem considerant el cas de l'emmagatzematge d'arxius en línia. En aquest cas, el sistema es veurà com aquest:

Patrons arquitectònics convenients

És important que els serveis del diagrama no signifiquin necessàriament un servidor separat. Fins i tot el procés pot ser el mateix. Una altra cosa és important: ideològicament, aquestes coses estan separades de manera que l'escala horitzontal es pugui aplicar fàcilment.

I per a dos usuaris el diagrama tindrà aquest aspecte (els serveis destinats a diferents usuaris s'indiquen amb colors diferents):

Patrons arquitectònics convenients

Bonificacions d'aquesta combinació:

  • Els serveis de tractament de la informació estan separats. Les cues també estan separades. Si necessitem augmentar el rendiment del sistema, només hem de llançar més serveis a més servidors.
  • Quan rebem informació d'un usuari, no hem d'esperar fins que les dades estiguin completament desades. Al contrari, només hem de respondre "d'acord" i després començar a treballar a poc a poc. Al mateix temps, la cua suavitza els pics, ja que l'addició d'un nou objecte es produeix ràpidament i l'usuari no ha d'esperar una passada completa per tot el cicle.
  • Com a exemple, he afegit un servei de deduplicació que intenta combinar fitxers idèntics. Si funciona durant molt de temps en l'1% dels casos, el client gairebé no ho notarà (vegeu més amunt), la qual cosa és un gran avantatge, ja que ja no ens exigeix ​​ser XNUMX% ràpids i fiables.

Tanmateix, els inconvenients són immediatament visibles:

  • El nostre sistema ha perdut la seva coherència estricta. Això vol dir que si, per exemple, us subscriviu a diferents serveis, teòricament podeu obtenir un estat diferent (ja que és possible que un dels serveis no tingui temps de rebre una notificació de la cua interna). Com a altra conseqüència, el sistema ara no té temps comú. És a dir, és impossible, per exemple, ordenar tots els esdeveniments simplement per l'hora d'arribada, ja que els rellotges entre servidors poden no ser sincrònics (a més, la mateixa hora en dos servidors és una utopia).
  • Ara no es pot revertir cap esdeveniment (com es podria fer amb una base de dades). En lloc d'això, heu d'afegir un esdeveniment nou − esdeveniment de compensació, que canviarà l'últim estat al requerit. Com a exemple d'una àrea similar: sense reescriure l'historial (que és dolent en alguns casos), no podeu revertir una confirmació a git, però podeu fer-ne un especial. compromís de retrocés, que essencialment només retorna l'estat antic. Tanmateix, tant la confirmació errònia com la recuperació romandran a la història.
  • L'esquema de dades pot canviar d'una versió a una altra, però els esdeveniments antics ja no es podran actualitzar al nou estàndard (ja que els esdeveniments no es poden canviar en principi).

Com podeu veure, Event Sourcing funciona bé amb CQRS. A més, implementar un sistema amb cues eficients i còmodes, però sense separar els fluxos de dades, ja és difícil en si mateix, perquè caldrà afegir punts de sincronització que neutralitzin tot l'efecte positiu de les cues. Aplicant els dos enfocaments alhora, cal ajustar lleugerament el codi del programa. En el nostre cas, quan s'envia un fitxer al servidor, la resposta només arriba "d'acord", que només vol dir que "s'ha desat l'operació d'afegir el fitxer". Formalment, això no vol dir que les dades ja estiguin disponibles en altres dispositius (per exemple, el servei de deduplicació pot reconstruir l'índex). Tanmateix, al cap d'un temps, el client rebrà una notificació amb l'estil "El fitxer X s'ha desat".

Com a resultat:

  • El nombre d'estats d'enviament de fitxers augmenta: en comptes del clàssic "fitxer enviat", obtenim dos: "el fitxer s'ha afegit a la cua del servidor" i "el fitxer s'ha desat a l'emmagatzematge". Això últim significa que altres dispositius ja poden començar a rebre el fitxer (ajustat pel fet que les cues funcionen a diferents velocitats).
  • Com que la informació d'enviament ara arriba per diferents canals, hem de trobar solucions per rebre l'estat de tramitació de l'expedient. Com a conseqüència d'això: a diferència del clàssic petició-resposta, el client es pot reiniciar mentre es processa el fitxer, però l'estat d'aquest processament serà correcte. A més, aquest article funciona, essencialment, fora de la caixa. Com a conseqüència: ara som més tolerants amb els fracassos.

Esqueixades

Com s'ha descrit anteriorment, els sistemes d'abastament d'esdeveniments no tenen una coherència estricta. Això vol dir que podem utilitzar diversos emmagatzematges sense cap sincronització entre ells. Aproximant-nos al nostre problema, podem:

  • Separeu els fitxers per tipus. Per exemple, es poden descodificar imatges/vídeos i es pot seleccionar un format més eficient.
  • Separeu els comptes per país. A causa de moltes lleis, això pot ser necessari, però aquest esquema d'arquitectura ofereix aquesta oportunitat automàticament

Patrons arquitectònics convenients

Si voleu transferir dades d'un emmagatzematge a un altre, ja no n'hi ha prou amb els mitjans estàndard. Malauradament, en aquest cas, cal aturar la cua, fer la migració i després iniciar-la. En el cas general, les dades no es poden transferir "sobre la marxa", però, si la cua d'esdeveniments s'emmagatzema completament i teniu instantànies dels estats d'emmagatzematge anteriors, podem reproduir els esdeveniments de la següent manera:

  • A Font d'esdeveniments, cada esdeveniment té el seu propi identificador (idealment, no decreixent). Això vol dir que podem afegir un camp a l'emmagatzematge: l'identificador de l'últim element processat.
  • Dupliquem la cua perquè tots els esdeveniments es puguin processar per a diversos emmagatzematges independents (el primer és aquell en què ja estan emmagatzemades les dades, i el segon és nou, però encara buit). La segona cua, per descomptat, encara no s'està processant.
  • Llencem la segona cua (és a dir, comencem a reproduir esdeveniments).
  • Quan la nova cua estigui relativament buida (és a dir, la diferència de temps mitjana entre afegir un element i recuperar-lo és acceptable), podeu començar a canviar els lectors al nou emmagatzematge.

Com podeu veure, no teníem, i encara no tenim, una coherència estricta en el nostre sistema. Només hi ha constància eventual, és a dir, una garantia que els esdeveniments es processen en el mateix ordre (però possiblement amb retards diferents). I, amb això, podem transferir dades amb relativa facilitat sense aturar el sistema a l'altre costat del globus.

Així, seguint el nostre exemple sobre l'emmagatzematge en línia per a fitxers, aquesta arquitectura ja ens ofereix una sèrie de bonificacions:

  • Podem apropar objectes als usuaris d'una manera dinàmica. D'aquesta manera es pot millorar la qualitat del servei.
  • Podem emmagatzemar algunes dades dins de les empreses. Per exemple, els usuaris d'empresa sovint requereixen que les seves dades s'emmagatzemin en centres de dades controlats (per evitar fuites de dades). Mitjançant l'esquart ho podem donar suport fàcilment. I la tasca és encara més fàcil si el client té un núvol compatible (per exemple, Azure s'allotja automàticament).
  • I el més important és que no hem de fer això. Després de tot, per començar, estaríem molt contents amb un emmagatzematge per a tots els comptes (per començar a treballar ràpidament). I la característica clau d'aquest sistema és que tot i que és ampliable, en la fase inicial és força senzill. Simplement no cal escriure codi immediatament que funcioni amb un milió de cues independents, etc. Si cal, això es pot fer en el futur.

Allotjament de contingut estàtic

Aquest punt pot semblar força obvi, però encara és necessari per a una aplicació carregada més o menys estàndard. La seva essència és senzilla: tot el contingut estàtic no es distribueix des del mateix servidor on es troba l'aplicació, sinó des d'altres especials dedicats específicament a aquesta tasca. Com a resultat, aquestes operacions es realitzen més ràpidament (nginx condicional serveix fitxers més ràpidament i de manera menys costosa que un servidor Java). Més arquitectura CDN (Xarxa de lliurament de contingut) ens permet localitzar els nostres fitxers més a prop dels usuaris finals, la qual cosa té un efecte positiu en la comoditat de treballar amb el servei.

L'exemple més senzill i estàndard de contingut estàtic és un conjunt d'scripts i imatges per a un lloc web. Tot és senzill amb ells: es coneixen per endavant, després l'arxiu es puja als servidors CDN, des d'on es distribueixen als usuaris finals.

Tanmateix, en realitat, per al contingut estàtic, podeu utilitzar un enfocament una mica similar a l'arquitectura lambda. Tornem a la nostra tasca (emmagatzematge de fitxers en línia), en la qual hem de distribuir fitxers als usuaris. La solució més senzilla és crear un servei que, per a cada petició de l'usuari, faci totes les comprovacions necessàries (autorització, etc.), i després descarregui l'arxiu directament del nostre emmagatzematge. El principal desavantatge d'aquest enfocament és que el contingut estàtic (i un fitxer amb una determinada revisió és, de fet, contingut estàtic) es distribueix pel mateix servidor que conté la lògica empresarial. En canvi, podeu fer el diagrama següent:

  • El servidor proporciona una URL de descàrrega. Pot ser de la forma file_id + key, on key és una signatura minidigital que dóna dret a accedir al recurs durant les properes 24 hores.
  • El fitxer es distribueix per nginx simple amb les opcions següents:
    • Emmagatzematge en memòria cau de contingut. Com que aquest servei es pot localitzar en un servidor independent, ens hem deixat una reserva per al futur amb la possibilitat d'emmagatzemar tots els darrers fitxers descarregats al disc.
    • Comprovació de la clau en el moment de crear la connexió
  • Opcional: processament de contingut en streaming. Per exemple, si comprimim tots els fitxers del servei, podem descomprimir directament en aquest mòdul. Com a conseqüència: les operacions d'IO es fan on corresponen. Un arxivador a Java assignarà fàcilment molta memòria addicional, però reescriure un servei amb lògica empresarial en condicionals Rust/C++ també pot ser ineficaç. En el nostre cas, s'utilitzen diferents processos (o fins i tot serveis) i, per tant, podem separar amb força eficàcia la lògica empresarial i les operacions d'IO.

Patrons arquitectònics convenients

Aquest esquema no és gaire semblant a la distribució de contingut estàtic (ja que no pengem tot el paquet estàtic en algun lloc), però en realitat, aquest enfocament es preocupa precisament de la distribució de dades immutables. A més, aquest esquema es pot generalitzar a altres casos en què el contingut no és simplement estàtic, sinó que es pot representar com un conjunt de blocs immutables i no esborrables (tot i que es poden afegir).

Com a altre exemple (per reforçar): si heu treballat amb Jenkins/TeamCity, sabeu que ambdues solucions estan escrites en Java. Tots dos són un procés Java que gestiona tant l'orquestració de compilació com la gestió de continguts. En particular, tots dos tenen tasques com "transferir un fitxer/carpeta des del servidor". Com a exemple: emissió d'artefactes, transferència de codi font (quan l'agent no baixa el codi directament del repositori, però el servidor ho fa per ell), accés als registres. Totes aquestes tasques es diferencien en la seva càrrega d'IO. És a dir, resulta que el servidor responsable de la lògica empresarial complexa ha de ser capaç, alhora, d'impulsar amb eficàcia grans fluxos de dades per si mateix. I el més interessant és que aquesta operació es pot delegar al mateix nginx segons exactament el mateix esquema (excepte que la clau de dades s'ha d'afegir a la sol·licitud).

Tanmateix, si tornem al nostre sistema, obtenim un diagrama similar:

Patrons arquitectònics convenients

Com podeu veure, el sistema s'ha tornat radicalment més complex. Ara no és només un mini-procés que emmagatzema fitxers localment. Ara el que es requereix no és el suport més senzill, el control de versions de l'API, etc. Per tant, després de dibuixar tots els diagrames, el millor és avaluar amb detall si l'extensibilitat val la pena el cost. Tanmateix, si voleu poder ampliar el sistema (incloent-hi treballar amb un nombre encara més gran d'usuaris), haureu de buscar solucions similars. Però, com a resultat, el sistema està preparat arquitectònicament per augmentar la càrrega (gairebé tots els components es poden clonar per a l'escalat horitzontal). El sistema es pot actualitzar sense aturar-lo (simplement algunes operacions es ralentitzaran una mica).

Com he dit al principi, ara una sèrie de serveis d'Internet han començat a rebre una càrrega més gran. I alguns d'ells simplement van començar a deixar de funcionar correctament. De fet, els sistemes van fallar precisament en el moment en què el negoci havia de guanyar diners. És a dir, en lloc d'entrega ajornada, en lloc de suggerir als clients "planifiqueu el vostre lliurament per als propers mesos", el sistema simplement va dir "aneu als vostres competidors". De fet, aquest és el preu de la baixa productivitat: les pèrdues es produiran precisament quan els beneficis serien més alts.

Conclusió

Tots aquests enfocaments eren coneguts abans. El mateix VK fa temps que utilitza la idea de l'allotjament de contingut estàtic per mostrar imatges. Molts jocs en línia utilitzen l'esquema Sharding per dividir els jugadors en regions o per separar les ubicacions del joc (si el món en si és un). L'enfocament d'obtenció d'esdeveniments s'utilitza activament al correu electrònic. La majoria de les aplicacions comercials on es reben dades constantment es basen en un enfocament CQRS per poder filtrar les dades rebudes. Bé, l'escala horitzontal s'ha utilitzat en molts serveis durant força temps.

Tanmateix, el més important és que tots aquests patrons s'han tornat molt fàcils d'aplicar en aplicacions modernes (si són adequats, és clar). Els núvols ofereixen Sharding i escala horitzontal immediatament, cosa que és molt més fàcil que demanar diferents servidors dedicats en diferents centres de dades. CQRS s'ha tornat molt més fàcil, encara que només sigui pel desenvolupament de biblioteques com RX. Fa uns 10 anys, un lloc web rar podria donar suport a això. L'abastament d'esdeveniments també és increïblement fàcil de configurar gràcies als contenidors preparats amb Apache Kafka. Fa 10 anys això hauria estat una innovació, ara és habitual. Passa el mateix amb l'allotjament de contingut estàtic: a causa de tecnologies més convenients (incloent-hi el fet que hi ha documentació detallada i una gran base de dades de respostes), aquest enfocament s'ha tornat encara més senzill.

Com a resultat, la implementació d'una sèrie de patrons arquitectònics força complexos ara s'ha tornat molt més senzill, el que significa que és millor mirar-ho més de prop per endavant. Si en una aplicació de fa deu anys es va abandonar una de les solucions anteriors a causa de l'elevat cost d'implantació i funcionament, ara, en una nova aplicació, o després de la refactorització, es pot crear un servei que ja serà tant arquitectònicament extensible ( en termes de rendiment) i preparats per a noves peticions dels clients (per exemple, per localitzar dades personals).

I el més important: no utilitzeu aquests enfocaments si teniu una aplicació senzilla. Sí, són boniques i interessants, però per a un lloc amb una visita màxima de 100 persones, sovint us podeu sortir amb un monòlit clàssic (almenys a l'exterior, tot el que hi ha a dins es pot dividir en mòduls, etc.).

Font: www.habr.com

Afegeix comentari