A resiliencia no software refírese á capacidade dunha aplicación para seguir funcionando de forma fluida e fiable, mesmo ante problemas ou fallos inesperados. Nos proxectos Fintech a resiliencia é de especial importancia debido a varias razóns. En primeiro lugar, as empresas están obrigadas a cumprir os requisitos regulamentarios e os reguladores financeiros fan fincapé na resistencia operativa para manter a estabilidade dentro do sistema. Ademais, a proliferación de ferramentas dixitais e a dependencia de provedores de servizos de terceiros expón ás empresas Fintech a maiores ameazas de seguridade. A resiliencia tamén axuda a mitigar os riscos de interrupcións causadas por varios factores, como ameazas cibernéticas, pandemias ou eventos xeopolíticos, salvagardando as operacións comerciais principais e os activos críticos.
Por patróns de resiliencia, entendemos un conxunto de mellores prácticas e estratexias deseñadas para garantir que o software poida soportar interrupcións e manter as súas operacións. Estes patróns actúan como redes de seguridade, proporcionando mecanismos para xestionar erros, xestionar a carga e recuperarse de fallos, garantindo así que as aplicacións sigan sendo robustas e fiables en condicións adversas.
As estratexias de resistencia máis comúns inclúen bulkhead, caché, fallback, reintento e interruptor de circuito. Neste artigo, comentareinos con máis detalle, con exemplos de problemas que poden axudar a resolver.
Vexamos a configuración anterior. Temos unha aplicación moi común con varios backends detrás de nós para obter algúns datos. Hai varios clientes HTTP conectados a estes backends. Resulta que todos comparten o mesmo grupo de conexións! E tamén outros recursos como CPU e RAM.
Que pasará se un dos backends experimenta algún tipo de problemas que provocan unha alta latencia de solicitude? Debido ao alto tempo de resposta, todo o grupo de conexións estará totalmente ocupado por solicitudes en espera de respostas do backend1. Como resultado, as solicitudes destinadas ao backend2 e backend3 saudables non poderán continuar porque o grupo está esgotado. Isto significa que un fallo nun dos nosos backends pode causar un fallo en toda a aplicación. Idealmente, queremos que só a funcionalidade asociada ao back-end fallido experimente degradación, mentres que o resto da aplicación segue funcionando normalmente.
Que é o patrón Bulkhead?
O termo, Bulkhead pattern, deriva da construción naval, implica a creación de varios compartimentos illados dentro dunha nave. Se se produce unha fuga nun compartimento, énchese de auga, pero os outros compartimentos non se ven afectados. Este illamento evita que toda a embarcación se afunda por unha única brecha.
O patrón Bulkhead pódese usar para illar varios tipos de recursos dentro dunha aplicación, evitando que un fallo nunha parte afecte a todo o sistema. Así é como podemos aplicalo ao noso problema:
Supoñamos que os nosos sistemas de backend teñen unha baixa probabilidade de atopar erros individualmente. Non obstante, cando unha operación implica consultar todos estes backends en paralelo, cada un pode devolver un erro de forma independente. Debido a que estes erros ocorren de forma independente, a probabilidade global dun erro na nosa aplicación é maior que a probabilidade de erro dun único backend. A probabilidade de erro acumulada pódese calcular mediante a fórmula P_total=1−(1−p)^n, onde n é o número de sistemas backend.
Por exemplo, se temos dez backends, cada un cunha probabilidade de erro de p=0,001 (correspondente a un SLA do 99,9%), a probabilidade de erro resultante é:
P_total=1−(1−0,001)^10=0,009955
Isto significa que o noso SLA combinado cae a aproximadamente o 99 %, o que ilustra como a fiabilidade xeral diminúe cando se consultan varios backends en paralelo. Para mitigar este problema, podemos implementar unha caché na memoria.
Unha caché na memoria serve como un búfer de datos de alta velocidade, almacenando os datos de acceso frecuente e eliminando a necesidade de buscalos de fontes potencialmente lentas cada vez. Dado que as cachés almacenadas na memoria teñen un 0 % de posibilidades de erro en comparación coa obtención de datos pola rede, aumentan significativamente a fiabilidade da nosa aplicación. Ademais, o caché reduce o tráfico de rede, reducindo aínda máis a posibilidade de erros. En consecuencia, ao utilizar unha caché na memoria, podemos conseguir unha taxa de erro aínda menor na nosa aplicación en comparación cos nosos sistemas de backend. Ademais, as cachés en memoria ofrecen unha recuperación de datos máis rápida que a obtención baseada en rede, polo que reduce a latencia das aplicacións, unha vantaxe notable.
Para datos personalizados, como perfís de usuario ou recomendacións, usar cachés na memoria tamén pode ser moi eficaz. Pero debemos asegurarnos de que todas as solicitudes dun usuario van constantemente á mesma instancia da aplicación para utilizar os datos almacenados na caché para eles, o que require sesións persistentes. Implementar sesións persistentes pode ser un reto, pero para este escenario, non necesitamos mecanismos complexos. O reequilibrio de tráfico menor é aceptable, polo que un algoritmo de equilibrio de carga estable como o hash consistente será suficiente.
Ademais, en caso de fallo dun nodo, o hash consistente garante que só os usuarios asociados co nodo fallado se sometan ao reequilibrio, minimizando a interrupción do sistema. Este enfoque simplifica a xestión de cachés personalizados e mellora a estabilidade xeral e o rendemento da nosa aplicación.
Se os datos que pretendemos almacenar na caché son críticos e utilízanse en todas as solicitudes que manexa o noso sistema, como políticas de acceso, plans de subscrición ou outras entidades vitais do noso dominio, a fonte destes datos podería supoñer un punto de falla importante no noso sistema. Para abordar este desafío, un enfoque é replicar completamente estes datos directamente na memoria da nosa aplicación.
Neste escenario, se o volume de datos na fonte é manexable, podemos iniciar o proceso descargando unha instantánea destes datos ao inicio da nosa aplicación. Posteriormente, podemos recibir actualizacións de eventos para garantir que os datos almacenados en caché permanecen sincronizados coa fonte. Ao adoptar este método, melloramos a fiabilidade de acceder a estes datos cruciais, xa que cada recuperación prodúcese directamente desde a memoria cunha probabilidade de erro do 0%. Ademais, a recuperación de datos da memoria é excepcionalmente rápida, optimizando así o rendemento da nosa aplicación. Esta estratexia mitiga eficazmente o risco asociado a depender dunha fonte de datos externa, garantindo un acceso consistente e fiable á información crítica para o funcionamento da nosa aplicación.
Non obstante, a necesidade de descargar datos sobre o inicio da aplicación, atrasando así o proceso de inicio, viola un dos principios da "aplicación de 12 factores" que defende o inicio rápido da aplicación. Pero, non queremos perder os beneficios do uso da caché. Para resolver este dilema, exploremos posibles solucións.
O inicio rápido é fundamental, especialmente para plataformas como Kubernetes, que dependen da migración rápida de aplicacións a diferentes nodos físicos. Afortunadamente, Kubernetes pode xestionar aplicacións de inicio lento usando funcións como sondas de inicio.
Outro reto ao que nos podemos enfrontar é actualizar as configuracións mentres a aplicación está en execución. Moitas veces, é necesario axustar os tempos da caché ou os tempos de espera das solicitudes para resolver os problemas de produción. Aínda que poidamos implementar rapidamente ficheiros de configuración actualizados na nosa aplicación, a aplicación destes cambios normalmente require un reinicio. Co tempo de inicio prolongado de cada aplicación, un reinicio continuo pode atrasar significativamente a implantación de correccións aos nosos usuarios.
Para solucionar isto, unha solución é almacenar configuracións nunha variable concorrente e ter un fío de fondo que a actualice periodicamente. Non obstante, certos parámetros, como os tempos de espera das solicitudes HTTP, poden requirir reiniciar os clientes HTTP ou de bases de datos cando a configuración correspondente cambia, o que supón un desafío potencial. Non obstante, algúns clientes, como o controlador Cassandra para Java, admiten a recarga automática de configuracións, simplificando este proceso.
A implementación de configuracións recargables pode mitigar o impacto negativo dos longos tempos de inicio das aplicacións e ofrecer vantaxes adicionais, como facilitar a implementación de marcas de funcións. Este enfoque permítenos manter a fiabilidade e a capacidade de resposta das aplicacións mentres xestionamos de forma eficiente as actualizacións de configuración.
Agora vexamos outro problema: no noso sistema, cando se recibe e procesa unha solicitude de usuario enviando unha consulta a un backend ou unha base de datos, en ocasións recíbese unha resposta de erro en lugar dos datos esperados. Posteriormente, o noso sistema responde ao usuario cun "erro".
Non obstante, en moitos escenarios, pode ser máis preferible mostrar datos lixeiramente desactualizados xunto cunha mensaxe que indica que hai un atraso de actualización de datos, en lugar de deixar ao usuario cunha gran mensaxe de erro vermella.
Para solucionar este problema e mellorar o comportamento do noso sistema, podemos implementar o patrón de reserva. O concepto detrás deste patrón implica ter unha fonte de datos secundaria, que pode conter datos de menor calidade ou frescura en comparación coa fonte primaria. Se a fonte de datos primaria non está dispoñible ou devolve un erro, o sistema pode volver recuperar datos desta fonte secundaria, garantindo que se presente algún tipo de información ao usuario en lugar de mostrar unha mensaxe de erro.
Se miras a imaxe de arriba, notarás unha semellanza entre o problema ao que nos enfrontamos agora e o que atopamos co exemplo da caché.
Para solucionalo, podemos considerar a implementación dun patrón coñecido como reintento. En lugar de confiar en cachés, o sistema pódese deseñar para reenviar automaticamente a solicitude en caso de erro. Este patrón de reintento ofrece unha alternativa máis sinxela e pode reducir eficazmente a probabilidade de erros na nosa aplicación. A diferenza do almacenamento na caché, que moitas veces require complexos mecanismos de invalidación da caché para xestionar os cambios de datos, tentar de novo as solicitudes fallidas é relativamente sinxelo de implementar. Dado que a invalidación da caché é amplamente considerada como unha das tarefas máis desafiantes na enxeñaría de software, adoptar unha estratexia de reintento pode axilizar o manexo de erros e mellorar a resistencia do sistema.
Non obstante, adoptar unha estratexia de reintento sen ter en conta as posibles consecuencias pode provocar máis complicacións.
Imaxinemos que un dos nosos backends experimenta un fallo. Neste caso, iniciar reintentos ao backend que falla pode producir un aumento significativo do volume de tráfico. Este repentino aumento do tráfico pode desbordar o backend, exacerbando o fallo e causando potencialmente un efecto en cascada en todo o sistema.
Para facer fronte a este desafío, é importante complementar o patrón de reintento co patrón de interruptor. O interruptor de circuito serve como mecanismo de salvagarda que supervisa a taxa de erro dos servizos posteriores. Cando a taxa de erro supera un limiar predefinido, o interruptor automático interrompe as solicitudes ao servizo afectado durante un período de tempo especificado. Durante este período, o sistema absténse de enviar solicitudes adicionais para permitir que se recupere o tempo do servizo fallido. Despois do intervalo designado, o interruptor automático permite que pasen cautelosamente un número limitado de solicitudes, verificando se o servizo se estabilizou. Se o servizo se recuperou, o tráfico normal vaise restablecendo gradualmente; en caso contrario, o circuíto permanece aberto, continuando bloqueando as solicitudes ata que o servizo retome o funcionamento normal. Ao integrar o patrón de interruptores xunto coa lóxica de reintento, podemos xestionar de forma eficaz as situacións de erro e evitar a sobrecarga do sistema durante os fallos do back-end.
En conclusión, ao implementar estes patróns de resistencia, podemos reforzar as nosas aplicacións fronte ás emerxencias, manter unha alta dispoñibilidade e ofrecer unha experiencia perfecta aos usuarios. Ademais, gustaríame enfatizar que a telemetría é outra ferramenta que non debe pasarse por alto ao proporcionar resistencia ao proxecto. Os bos rexistros e métricas poden mellorar significativamente a calidade dos servizos e proporcionar información valiosa sobre o seu rendemento, axudando a tomar decisións fundamentadas para melloralos aínda máis.