Manejo de errores en sistema distribuido

8

Esta es la secuencia común de dos componentes distribuidos en nuestra aplicación Java:

1  A sends request to B
2      B starts some job J in parallel thread
3      B returns response to A
4  A accepts response
5      Job finishes after some time
6      Job sends information to A
7  A receives response from a Job and updates

Este es el escenario ideal, suponiendo que todo funcione. Por supuesto, la vida real está llena de fracasos. Por ejemplo, uno de los peores casos puede ser si #6falla simplemente debido a la red: el trabajo se ha ejecutado correctamente, pero Ano sabe nada al respecto.

Estoy buscando un enfoque liviano sobre cómo administrar los errores en este sistema. Tenga en cuenta que tenemos muchos componentes, por lo que agruparlos todos solo por el manejo de errores no tiene sentido. Luego, abandoné el uso de cualquier memoria distribuida / repositorio que se instalaría nuevamente en cada componente por la misma razón.

Mis pensamientos van en la dirección de tener un estado absoluto en una B y nunca tener un estado persistente en una A. Esto significa lo siguiente:

  • antes de #1marcar en Aque la unidad de trabajo, es decir, el cambio está a punto de comenzar
  • solo Bpuede desmarcar este estado.
  • Apuede obtener información sobre Bcualquier momento para actualizar el estado.
  • no se puede invocar ningún cambio nuevo en la misma unidad A.

¿Qué piensas? ¿Hay alguna forma ligera de domar los errores en un sistema de este tipo?

igor
fuente
Esta es una vieja pregunta. ¿Encontraste una buena solución? ... Si es así, ¿puedes compartirlo?
svidgen

Respuestas:

2

Agregar a un registro persistente en A debería ser suficiente. Esto hace frente a reinicios y particiones de red para lograr una consistencia eventual, o para romper la señal que impide dicha convergencia. Con el compromiso de grupo amortizado , puede tomar menos de una sola escritura para persistir una entrada de registro.

Usted sugirió que B sea responsable del estado de desmarcado. Estoy en desacuerdo. Solo A se da cuenta del nuevo trabajo, y solo A debe ser responsable de rastrearlo y reportar errores como tiempos de espera. B envía mensajes idempotentes a A, y A actualiza el estado, volviendo a consultar a intervalos según sea necesario.

En el paso 0, A se da cuenta de una nueva solicitud y la registra. Eso constituye una obligación que A debe cumplir más adelante en un plazo determinado: A realizará continuamente y repetirá los pasos posteriores hasta que A sepa que el procesamiento de la solicitud se ha completado.

Algunas solicitudes serán más largas que otras. Las estimaciones del tiempo de procesamiento estarán disponibles en A y en B, quizás revisadas a medida que el procesamiento continúe. Dichas estimaciones pueden retroalimentarse a A, por lo que rara vez producirá tiempos de espera falsos positivos. Piense en ello como un mensaje para mantener vivo que dice "sigue trabajando, sigue trabajando".

J H
fuente
1

Adopte una estrategia de atracción en lugar de empujar. Haga que cada parte extraiga los cambios de las otras y actualice sus propios registros.

  • A registra las cosas que B debe hacer en una cola
  • B se retira de la cola de A y hace el trabajo
  • B registra las cosas que ha hecho en una cola
  • A se retira de la cola de B para saber cuál fue el resultado del trabajo

(Estoy usando la palabra cola, pero puede sustituir el registro o el tema).

Puede hornear la cola en los servicios o puede tener un agente de mensajes separado. Una implementación integrada en un servicio puede ser tan simple como GET /jobrequests?from=<timestamp>(con B haciendo un seguimiento de la última marca de tiempo de la solicitud de trabajo procesada).

Una parte complicada de dicha arquitectura es decidir sobre la semántica al menos una vez frente a la mayoría. Concretamente: si B saca un elemento de la cola y luego se bloquea mientras lo realiza, ¿qué debería pasar? Hay dos posibilidades, y la más adecuada depende de su caso de uso:

  • Al menos una vez: B solo confirma qué punto de la cola llegó después de completar una acción, existe el riesgo de realizar acciones dos veces. Si diseña acciones para ser idempotentes, puede lograr un comportamiento exacto una vez usando este enfoque. (Yo uso kafka para este escenario).
  • A lo sumo una vez: B solo consume todos los elementos de la cola una vez. Si se bloquea al ejecutarlo, el elemento nunca se ejecutará.

Beneficios de este enfoque:

  • Los servicios que consumen colas no necesitan estar listos para que se produzca la inserción de colas. Esto significa que puede reiniciar B mientras A está trabajando o reiniciar A mientras B está trabajando. El alojamiento redundante de servicios en segundo plano solo es necesario para garantizar un tiempo de respuesta general, no una operación confiable.
  • El consumidor puede controlar el ritmo de extracción de los elementos de la cola, lo que le permite amortiguar temporalmente los picos de carga en la cola.
Joeri Sebrechts
fuente