paint-brush
Mensaxería fiable en sistemas distribuídospor@fairday
37,190 lecturas
37,190 lecturas

Mensaxería fiable en sistemas distribuídos

por Aleksei8m2024/03/18
Read on Terminal Reader
Read this story w/o Javascript

Demasiado longo; Ler

Construír un sistema distribuído fiable, altamente dispoñible e escalable require o cumprimento de técnicas, principios e patróns específicos.
featured image - Mensaxería fiable en sistemas distribuídos
Aleksei HackerNoon profile picture

Problema de escritura dual

Construír un sistema distribuído fiable, altamente dispoñible e escalable require o cumprimento de técnicas, principios e patróns específicos. O deseño destes sistemas implica abordar unha infinidade de desafíos. Entre as cuestións máis prevalentes e fundamentais está o problema de escritura dual .


O "problema de escritura dual" é un desafío que xorde nos sistemas distribuídos, principalmente cando se trata de varias fontes de datos ou bases de datos que deben manterse sincronizadas. Refírese á dificultade de garantir que os cambios de datos se escriben de forma consistente en varios almacéns de datos, como bases de datos ou cachés, sen introducir problemas como incoherencias de datos, conflitos ou pescozos de botella de rendemento.


A arquitectura de microservizos e a base de datos de patróns por servizo ofréceche moitos beneficios, como a implantación e escalado independentes, fallos illados e un potencial aumento da velocidade de desenvolvemento. Non obstante, as operacións requiren cambios entre varios microservizos, o que o obriga a pensar nunha solución fiable para abordar este problema.

Case un exemplo real

Consideremos un escenario no que o noso dominio implica aceptar solicitudes de préstamo, avalialas e despois enviar alertas de notificación aos clientes.


No espírito do principio de responsabilidade única, a lei de Conway e o enfoque de deseño dirixido por dominios, despois de varias sesións de asalto de eventos, todo o dominio dividiuse en tres subdominios con contextos delimitados definidos con límites claros, modelos de dominio e linguaxe ubicua.


O primeiro encárgase de incorporar e compilar novas solicitudes de préstamo. O segundo sistema avalía estas solicitudes e toma decisións en función dos datos proporcionados. Este proceso de avaliación, incluíndo KYC/KYB, antifraude e comprobacións de risco de crédito, pode levar moito tempo, o que require a capacidade de xestionar miles de aplicacións simultaneamente. En consecuencia, esta funcionalidade delegouse a un microservizo dedicado coa súa propia base de datos, o que permite un escalado independente.

Ademais, estes subsistemas son xestionados por dous equipos diferentes, cada un cos seus propios ciclos de lanzamento, acordos de nivel de servizo (SLA) e requisitos de escalabilidade.


Por último , existe un servizo de notificación especializado para enviar alertas aos clientes.



Aquí tes unha descrición refinada do caso de uso principal do sistema:

  1. Un cliente presenta unha solicitude de préstamo.
  2. O Servizo de Solicitude de Préstamo rexistra a nova solicitude co estado "Pendente" e inicia o proceso de avaliación remitindo a solicitude ao Servizo de Avaliación.
  3. O Servizo de Avaliación avalía a solicitude de préstamo entrante e, posteriormente, comunica a decisión ao Servizo de Solicitude de Préstamo.
  4. Tras recibir a decisión, o Servizo de Solicitude de Préstamo actualiza o estado da solicitude de préstamo en consecuencia e activa o Servizo de Notificacións para informar ao cliente do resultado.
  5. O Servizo de Notificacións procesa esta solicitude e envía notificacións ao cliente por correo electrónico, SMS ou outros métodos de comunicación preferidos, segundo a configuración do cliente.


É un sistema bastante sinxelo e primitivo a primeira vista, pero imos mergullarse en como procesa o servizo de solicitude de préstamo o comando de envío de solicitude de préstamo.


Podemos considerar dous enfoques para as interaccións de servizos:

  1. First-Local-Commit-Then-Publish: Neste enfoque, o servizo actualiza a súa base de datos local (commits) e despois publica un evento ou mensaxe para outros servizos.

  2. First-Publish-Then-Local-Commit: pola contra, este método implica publicar un evento ou mensaxe antes de confirmar os cambios na base de datos local.


Ambos métodos teñen os seus inconvenientes e son só parcialmente seguros para a comunicación en sistemas distribuídos.


Este é un diagrama de secuencia de aplicación do primeiro enfoque.


Primeiro-Local-Commit-Despois-Publica


Neste escenario, o servizo de solicitude de préstamo emprega o enfoque First-Local-Commit-Then-Publish , onde primeiro comete unha transacción e despois tenta enviar unha notificación a outro sistema. Non obstante, este proceso é susceptible de fallar se, por exemplo, hai problemas de rede, o servizo de avaliación non está dispoñible ou o servizo de solicitude de préstamo atopa un erro de memoria sen memoria (OOM) e falla. Nestes casos, a mensaxe perderíase, deixando a Avaliación sen previo aviso da nova solicitude de préstamo, a non ser que se implementen medidas adicionais.


E o segundo.

Primeiro-Publicar-Despois-Local-Commit
No escenario First-Publish-Then-Local-Commit , o Servizo de Solicitude de Préstamo enfróntase a riscos máis importantes. Pode informar ao Servizo de Avaliación sobre unha nova aplicación, pero non pode gardar esta actualización localmente debido a problemas como problemas de base de datos, erros de memoria ou erros de código. Este enfoque pode dar lugar a inconsistencias importantes nos datos, o que pode causar serios problemas, dependendo de como o Servizo de Revisión de Préstamos xestione as solicitudes entrantes.


Polo tanto, debemos identificar unha solución que ofreza un mecanismo robusto para publicar eventos a consumidores externos. Pero, antes de afondar en posibles solucións, primeiro deberiamos aclarar os tipos de garantías de entrega de mensaxes alcanzables nos sistemas distribuídos.

Garantías de entrega de mensaxes

Hai catro tipos de garantías que poderiamos conseguir.

  1. Sen garantías
    Non hai garantía de que a mensaxe sexa entregada ao destino. O enfoque First-Local-Commit-Then-Publish trata precisamente diso. Os consumidores poden recibir mensaxes unha vez, varias veces ou nunca.

  2. Como máximo unha vez entrega
    Como máximo unha entrega significa que a mensaxe será entregada ao destino como máximo 1 vez. O enfoque First-Local-Commit-Then-Publish pódese implementar deste xeito tamén coa política de reintentos de intentos co valor un.

  3. Polo menos unha entrega\Os consumidores recibirán e procesarán todas as mensaxes, pero poden recibir a mesma mensaxe máis dunha vez.

  4. Exactamente unha vez entrega\Exactamente unha vez entrega significa que o consumidor recibirá a mensaxe de forma efectiva unha vez.
    Tecnicamente, é posible conseguir con Kafka transaccións e implementación idempotente específica de produtor e consumidor.


Na maioría dos casos, as garantías de entrega "polo menos unha vez" abordan moitos problemas garantindo que as mensaxes se entreguen polo menos unha vez, pero os consumidores deben ser idempotentes. Non obstante, ante os inevitables fallos da rede, toda a lóxica do consumidor debe ser idempotente para evitar o procesamento de mensaxes duplicadas, independentemente das garantías do produtor. Polo tanto, este requisito non é tanto un inconveniente como reflicte a realidade.

Solucións

Hai moitas solucións a este problema, que teñen as súas vantaxes e desvantaxes.

Compromiso en dúas fases

Segundo Wikipedia, o Two-Phase Commit (2PC) é un protocolo de transaccións distribuídas que se usa en sistemas de xestión de bases de datos e informática para garantir a coherencia e fiabilidade das transaccións distribuídas. Está deseñado para situacións nas que varios recursos (por exemplo, bases de datos) necesitan participar nunha soa transacción, e garante que ou todos cometen a transacción ou que todos a aborten, mantendo así a coherencia dos datos. Parece exactamente o que necesitamos, pero Two-Phase Commit ten varios inconvenientes:

  • Se un recurso participante non responde ou experimenta un fallo, pódese bloquear todo o proceso ata que se resolva o problema. Isto pode provocar problemas potenciais de rendemento e dispoñibilidade.
  • Two-Phase Commit non proporciona mecanismos de tolerancia a fallos integrados. Depende de mecanismos externos ou intervención manual para xestionar fallos.
  • Non todas as bases de datos modernas admiten Two-Phase Commit.

Base de datos compartida

A solución máis aparente para a arquitectura de microservizos é aplicar un patrón (ou ás veces antipatrón): unha base de datos compartida. Este enfoque é moi intuitivo se necesitas coherencia transaccional en varias táboas en diferentes bases de datos, só tes que usar unha base de datos compartida para estes microservizos.


Os inconvenientes deste enfoque inclúen a introdución dun único punto de fallo, a inhibición do escalado independente da base de datos e a limitación da capacidade de usar diferentes solucións de bases de datos máis adecuadas para requisitos específicos e casos de uso. Ademais, serían necesarias modificacións nas bases de código dos microservizos para admitir tal forma de transacción distribuída.

Caixa de saída transaccional

A " caixa de saída transaccional " é un patrón de deseño que se usa nos sistemas distribuídos para garantir unha propagación fiable das mensaxes, mesmo fronte a sistemas de mensaxería pouco fiables. Implica almacenar eventos nunha táboa "OutboxEvents" designada dentro da mesma transacción que a propia operación. Este enfoque aliña ben coas propiedades ACID das bases de datos relacionais. Pola contra, moitas bases de datos No-SQL non admiten totalmente as propiedades ACID, optando no seu lugar polos principios do teorema CAP e da filosofía BASE, que priorizan a dispoñibilidade e a eventual consistencia fronte á estrita coherencia.


Unha caixa de saída transaccional ofrece polo menos unha garantía e pódese implementar con varios enfoques:

  1. Rexistro de rexistro de transaccións

  2. Editora de enquisas


O enfoque de seguimento do rexistro de transaccións implica o uso de solucións específicas de bases de datos como CDC (Change Data Capture). Os principais inconvenientes deste enfoque son:

  • Solucións específicas de bases de datos

  • Aumento da latencia debido ás especificacións das implementacións de CDC


Outro método é o Polling Publisher , que facilita a descarga da caixa de saída consultando a táboa da caixa de saída. O principal inconveniente deste enfoque é o potencial de aumento da carga da base de datos, o que pode levar a custos máis elevados. Ademais, non todas as bases de datos No-SQL admiten consultas eficientes para segmentos de documentos específicos. A extracción de documentos enteiros pode, polo tanto, producir unha degradación do rendemento.


Aquí tes un pequeno diagrama de secuencia que explica como funciona.


Escoitate a ti mesmo

O principal desafío co patrón Transactional Outbox reside na súa dependencia das propiedades ACID da base de datos. Pode ser sinxelo nas bases de datos OLTP típicas, pero presenta desafíos no ámbito NoSQL. Para solucionar isto, unha solución potencial é aproveitar o rexistro de anexos (por exemplo, Kafka) desde o inicio do procesamento da solicitude.


En lugar de procesar directamente o comando "enviar solicitude de préstamo", enviámolo inmediatamente a un tema interno de Kafka e despois devolvemos un resultado "aceptado" ao cliente. Non obstante, dado que é moi probable que o comando aínda teña que ser procesado, non podemos informar inmediatamente ao cliente do resultado. Para xestionar esta eventual coherencia, podemos empregar técnicas como sondaxes longas, sondaxes iniciadas polo cliente, actualizacións optimistas da interface de usuario ou usar WebSockets ou eventos enviados polo servidor para as notificacións. Non obstante, este é un tema completamente distinto, así que volvamos ao noso tema inicial.


Enviamos a mensaxe sobre un tema interno de Kafka. A continuación, o servizo de solicitude de préstamo consome esta mensaxe (o mesmo comando que recibiu do cliente) e comeza a procesala. En primeiro lugar, executa algunha lóxica empresarial; só despois de que esta lóxica se execute con éxito e os resultados persistan, publica novas mensaxes sobre un tema público de Kafka.


Vexamos un pouco de pseudocódigo.


 public async Task HandleAsync(SubmitLoanApplicationCommand command, ...) { //First, process business logic var loanApplication = await _loanApplicationService.HandleCommandAsync(command, ...); //Then, send new events to public Kafka topic producer.Send(new LoanApplicationSubmittedEvent(loanApplication.Id)); //Then, commit offset consumer.Commit(); }


E se falla o procesamento da lóxica empresarial? Non te preocupes, xa que a compensación aínda non se comprometeu, tentarase de novo a mensaxe.


E se falla ao enviar novos eventos a Kafka? Non te preocupes, xa que a lóxica empresarial é idempotente, non creará unha solicitude de préstamo duplicada. En vez diso, tentará reenviar mensaxes ao tema público de Kafka.


E se se envían mensaxes a Kafka, pero a commit de compensación falla? Non te preocupes, xa que a lóxica empresarial é idempotente, non creará unha solicitude de préstamo duplicada. Pola contra, volverá enviar mensaxes ao tema público de Kafka e espera que o compromiso de compensación teña éxito nesta ocasión.


Os principais inconvenientes deste enfoque inclúen a complexidade engadida asociada a un novo estilo de programación, a eventual coherencia (xa que o cliente non coñecerá inmediatamente o resultado) e a esixencia de que toda a lóxica empresarial sexa idempotente.

Abastecemento de eventos

Que é a fonte de eventos e como se podería aplicar aquí? A fonte de eventos é un patrón arquitectónico de software usado para modelar o estado dun sistema capturando todos os cambios nos seus datos como unha serie de eventos inmutables. Estes eventos representan feitos ou transicións de estado e serven como fonte única de verdade para o estado actual do sistema. Polo tanto, tecnicamente, ao implementar un sistema de aprovisionamento de eventos, xa temos todos os eventos en EventStore, e os consumidores poden usar esta EventStore como fonte única de verdade sobre o que pasou. Non hai necesidade dunha solución de base de datos específica para rastrexar todos os cambios ou preocupacións sobre o pedido, o único problema é estar no lado de lectura, xa que para poder obter o estado real da entidade é necesario reproducir todos os eventos.

Conclusión

Neste artigo, revisamos varios enfoques para crear mensaxes fiables en sistemas distribuídos. Hai varias recomendacións que podemos ter en conta ao construír sistemas con estas características

  1. Desenvolver sempre consumidores idempotentes xa que o fallo da rede é inevitable.
  2. Use coidadosamente o First-Local-Commit-Then-Publish cunha comprensión clara dos requisitos de garantía.
  3. Nunca utilice o enfoque First-Publish-Then-Local-Commit, xa que pode provocar graves inconsistencias de datos no seu sistema.
  4. Se é moi probable que a decisión de elección da base de datos existente cambie ou a estratexia técnica implica seleccionar a mellor solución de almacenamento para o problema, non cree bibliotecas compartidas vinculando a solucións de bases de datos como CDC .
  5. Use o enfoque Transactional Outbox como solución estándar para conseguir polo menos unha vez as garantías.
  6. Considere usar o enfoque Escóitase a si mesmo cando se aproveitan as bases de datos No-SQL.


A próxima vez, veremos un exemplo máis práctico de implementación dunha caixa de saída transaccional. Vexa

ti!