![Search icon](https://hackernoon.imgix.net/search-new.png?w=19&h=19)
La resiliència en el programari es refereix a la capacitat d'una aplicació per continuar funcionant sense problemes i de manera fiable, fins i tot davant de problemes o errors inesperats. En els projectes Fintech, la resiliència té una importància especialment elevada per diverses raons. En primer lloc, les empreses estan obligades a complir els requisits reglamentaris i els reguladors financers posen l'accent en la resiliència operativa per mantenir l'estabilitat dins del sistema. A més, la proliferació d'eines digitals i la dependència de proveïdors de serveis de tercers exposa les empreses Fintech a amenaces de seguretat augmentades. La resiliència també ajuda a mitigar els riscos d'interrupcions causades per diversos factors, com ara ciberamenaces, pandèmies o esdeveniments geopolítics, salvaguardant les operacions empresarials bàsiques i els actius crítics.
Per patrons de resiliència, entenem un conjunt de millors pràctiques i estratègies dissenyades per garantir que el programari pugui suportar les interrupcions i mantenir les seves operacions. Aquests patrons actuen com a xarxes de seguretat, proporcionant mecanismes per gestionar errors, gestionar la càrrega i recuperar-se dels errors, garantint així que les aplicacions segueixen sent robustes i fiables en condicions adverses.
Les estratègies de resiliència més habituals inclouen mampara, memòria cau, alternativa, reintentar i interruptor de circuit. En aquest article, els parlaré amb més detall, amb exemples de problemes que poden ajudar a resoldre.
Fem una ullada a la configuració anterior. Tenim una aplicació molt normal amb diversos backends darrere nostre per obtenir algunes dades. Hi ha diversos clients HTTP connectats a aquests backends. Resulta que tots comparteixen el mateix grup de connexions! I també altres recursos com CPU i RAM.
Què passarà, si un dels backend experimenta algun tipus de problemes que resulta en una latència de sol·licitud elevada? A causa de l'alt temps de resposta, tot el grup de connexions estarà completament ocupat per les sol·licituds que esperen respostes del backend1. Com a resultat, les sol·licituds destinades als backend2 i backend3 saludables no podran continuar perquè el grup s'ha esgotat. Això vol dir que una fallada en un dels nostres backends pot provocar una fallada a tota l'aplicació. Idealment, volem que només la funcionalitat associada amb el backend que falla experimenti una degradació, mentre que la resta de l'aplicació continua funcionant amb normalitat.
Què és el patró de mampares?
El terme, Bulkhead pattern, deriva de la construcció naval, implica la creació de diversos compartiments aïllats dins d'un vaixell. Si es produeix una fuita en un compartiment, s'omple d'aigua, però els altres compartiments no es veuen afectats. Aquest aïllament evita que tot el vaixell s'enfonsi per una única bretxa.
El patró Bulkhead es pot utilitzar per aïllar diversos tipus de recursos dins d'una aplicació, evitant que una fallada en una part afecti tot el sistema. A continuació s'explica com podem aplicar-ho al nostre problema:
Suposem que els nostres sistemes de fons tenen una baixa probabilitat de trobar errors individualment. Tanmateix, quan una operació implica consultar tots aquests backends en paral·lel, cadascun pot retornar un error de manera independent. Com que aquests errors es produeixen de manera independent, la probabilitat global d'un error a la nostra aplicació és superior a la probabilitat d'error de qualsevol backend únic. La probabilitat d'error acumulada es pot calcular mitjançant la fórmula P_total=1−(1−p)^n, on n és el nombre de sistemes backend.
Per exemple, si tenim deu backends, cadascun amb una probabilitat d'error de p=0,001 (corresponent a un SLA del 99,9%), la probabilitat d'error resultant és:
P_total=1−(1−0,001)^10=0,009955
Això significa que el nostre SLA combinat baixa a aproximadament el 99%, cosa que il·lustra com la fiabilitat general disminueix quan es consulten diversos backends en paral·lel. Per mitigar aquest problema, podem implementar una memòria cau a la memòria.
Una memòria cau a la memòria serveix com a memòria intermèdia de dades d'alta velocitat, emmagatzema les dades a les quals s'accedeix amb freqüència i elimina la necessitat d'obtenir-les de fonts potencialment lentes cada vegada. Com que les memòries cau emmagatzemades a la memòria tenen un 0% de possibilitats d'error en comparació amb l'obtenció de dades a la xarxa, augmenten significativament la fiabilitat de la nostra aplicació. A més, la memòria cau redueix el trànsit de la xarxa, reduint encara més la possibilitat d'errors. En conseqüència, utilitzant una memòria cau a la memòria, podem aconseguir una taxa d'error encara més baixa a la nostra aplicació en comparació amb els nostres sistemes de fons. A més, les memòria cau a la memòria ofereixen una recuperació de dades més ràpida que la recuperació basada en xarxa, reduint així la latència de l'aplicació, un avantatge notable.
Per a dades personalitzades, com ara perfils d'usuari o recomanacions, l'ús de memòria cau a la memòria també pot ser molt eficaç. Però hem d'assegurar-nos que totes les sol·licituds d'un usuari van constantment a la mateixa instància de l'aplicació per utilitzar les dades emmagatzemades a la memòria cau per a ells, cosa que requereix sessions enganxades. Implementar sessions enganxades pot ser un repte, però per a aquest escenari, no necessitem mecanismes complexos. El reequilibri de trànsit menor és acceptable, de manera que n'hi haurà prou amb un algorisme d'equilibri de càrrega estable, com ara un hashing coherent.
A més, en cas de fallada d'un node, el hashing coherent garanteix que només els usuaris associats amb el node fallit se sotmetin a un reequilibri, minimitzant la interrupció del sistema. Aquest enfocament simplifica la gestió de la memòria cau personalitzada i millora l'estabilitat i el rendiment generals de la nostra aplicació.
Si les dades que volem emmagatzemar a la memòria cau són crítiques i s'utilitzen en totes les sol·licituds que gestiona el nostre sistema, com ara polítiques d'accés, plans de subscripció o altres entitats vitals del nostre domini, la font d'aquestes dades podria suposar un punt de fallada important al nostre sistema. Per abordar aquest repte, un enfocament és replicar completament aquestes dades directament a la memòria de la nostra aplicació.
En aquest escenari, si el volum de dades a la font és manejable, podem iniciar el procés descarregant una instantània d'aquestes dades a l'inici de la nostra aplicació. Posteriorment, podem rebre actualitzacions d'esdeveniments per assegurar-nos que les dades de la memòria cau es mantenen sincronitzades amb la font. En adoptar aquest mètode, millorem la fiabilitat d'accedir a aquestes dades crucials, ja que cada recuperació es produeix directament des de la memòria amb una probabilitat d'error del 0%. A més, la recuperació de dades de la memòria és excepcionalment ràpida, optimitzant així el rendiment de la nostra aplicació. Aquesta estratègia mitiga eficaçment el risc associat a la confiança en una font de dades externa, garantint un accés coherent i fiable a la informació crítica per al funcionament de la nostra aplicació.
Tanmateix, la necessitat de descarregar dades a l'inici de l'aplicació, retardant així el procés d'inici, viola un dels principis de l'"aplicació de 12 factors" que defensa l'inici ràpid de l'aplicació. Però, no volem perdre els avantatges d'utilitzar la memòria cau. Per abordar aquest dilema, explorem possibles solucions.
L'inici ràpid és crucial, especialment per a plataformes com Kubernetes, que depenen de la migració ràpida d'aplicacions a diferents nodes físics. Afortunadament, Kubernetes pot gestionar aplicacions d'inici lent mitjançant funcions com ara sondes d'inici.
Un altre repte que podem trobar és actualitzar les configuracions mentre l'aplicació s'està executant. Sovint, és necessari ajustar els temps de memòria cau o els temps d'espera de les sol·licituds per resoldre problemes de producció. Fins i tot si podem implementar ràpidament fitxers de configuració actualitzats a la nostra aplicació, l'aplicació d'aquests canvis normalment requereix un reinici. Amb el temps d'inici estès de cada aplicació, un reinici continuat pot retardar significativament la implementació de solucions als nostres usuaris.
Per fer-ho, una solució és emmagatzemar configuracions en una variable concurrent i tenir un fil de fons que l'actualitzi periòdicament. Tanmateix, alguns paràmetres, com ara els temps d'espera de les sol·licituds HTTP, poden requerir la reinicialització dels clients HTTP o de base de dades quan canvia la configuració corresponent, cosa que suposa un repte potencial. Tanmateix, alguns clients, com el controlador Cassandra per a Java, admeten la recàrrega automàtica de configuracions, simplificant aquest procés.
La implementació de configuracions recarregables pot mitigar l'impacte negatiu dels llargs temps d'inici de l'aplicació i oferir avantatges addicionals, com ara facilitar la implementació de marques de funcions. Aquest enfocament ens permet mantenir la fiabilitat i la capacitat de resposta de les aplicacions alhora que gestionem de manera eficient les actualitzacions de configuració.
Ara fem una ullada a un altre problema: al nostre sistema, quan es rep i es processa una sol·licitud d'un usuari enviant una consulta a un backend o una base de dades, de tant en tant, es rep una resposta d'error en lloc de les dades esperades. Posteriorment, el nostre sistema respon a l'usuari amb un "error".
Tanmateix, en molts escenaris, pot ser més preferible mostrar dades lleugerament obsoletes juntament amb un missatge que indiqui que hi ha un retard en l'actualització de dades, en lloc de deixar l'usuari amb un gran missatge d'error vermell.
Per solucionar aquest problema i millorar el comportament del nostre sistema, podem implementar el patró Fallback. El concepte darrere d'aquest patró implica tenir una font de dades secundària, que pot contenir dades de menor qualitat o frescor en comparació amb la font primària. Si la font de dades principal no està disponible o retorna un error, el sistema pot tornar a recuperar dades d'aquesta font secundària, assegurant-se que es presenta algun tipus d'informació a l'usuari en lloc de mostrar un missatge d'error.
Si mireu la imatge de dalt, notareu una similitud entre el problema que estem enfrontant ara i el que hem trobat amb l'exemple de la memòria cau.
Per solucionar-ho, podem considerar la implementació d'un patró conegut com a reintentar. En lloc de confiar en la memòria cau, el sistema es pot dissenyar per tornar automàticament a enviar la sol·licitud en cas d'error. Aquest patró de reintent ofereix una alternativa més senzilla i pot reduir efectivament la probabilitat d'errors a la nostra aplicació. A diferència de la memòria cau, que sovint requereix complexos mecanismes d'invalidació de la memòria cau per gestionar els canvis de dades, tornar a intentar les sol·licituds fallides és relativament senzill d'implementar. Com que la invalidació de la memòria cau es considera àmpliament com una de les tasques més difícils de l'enginyeria de programari, l'adopció d'una estratègia de reintent pot agilitzar la gestió d'errors i millorar la resistència del sistema.
Tanmateix, adoptar una estratègia de reintent sense tenir en compte les possibles conseqüències pot comportar més complicacions.
Imaginem que un dels nostres backend experimenta un fracàs. En aquest escenari, iniciar reintents al backend fallit podria provocar un augment significatiu del volum de trànsit. Aquest augment sobtat del trànsit pot desbordar el backend, agreujar la fallada i provocar un efecte en cascada a tot el sistema.
Per fer front a aquest repte, és important complementar el patró de reintent amb el patró del disjuntor. L'interruptor serveix com a mecanisme de salvaguarda que controla la taxa d'error dels serveis aigües avall. Quan la taxa d'error supera un llindar predefinit, l'interruptor interromp les sol·licituds al servei afectat durant una durada especificada. Durant aquest període, el sistema s'absté d'enviar sol·licituds addicionals per permetre que es recuperi el temps de servei fallit. Després de l'interval designat, l'interruptor automàtic permet que passin un nombre limitat de peticions, verificant si el servei s'ha estabilitzat. Si el servei s'ha recuperat, el trànsit normal es recupera progressivament; en cas contrari, el circuit roman obert, continuant bloquejant les peticions fins que el servei reprengui el funcionament normal. Mitjançant la integració del patró del disjuntor juntament amb la lògica de reintent, podem gestionar eficaçment les situacions d'error i prevenir la sobrecàrrega del sistema durant les fallades del backend.
En conclusió, amb la implementació d'aquests patrons de resiliència, podem reforçar les nostres aplicacions contra emergències, mantenir una alta disponibilitat i oferir una experiència perfecta als usuaris. A més, m'agradaria subratllar que la telemetria és una altra eina que no s'ha de passar per alt a l'hora de proporcionar resiliència al projecte. Els bons registres i mètriques poden millorar significativament la qualitat dels serveis i proporcionar informació valuosa sobre el seu rendiment, ajudant a prendre decisions informades per millorar-los encara més.