Gestione degli errori nel sistema distribuito

8

Questa è la sequenza comune di due componenti distribuiti nella nostra applicazione 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

Questo è lo scenario ideale, assumendo che tutto funzioni. Certo, la vita reale è piena di fallimenti. Ad esempio, uno dei casi peggiori potrebbe essere se #6 fallisce semplicemente a causa della rete: il lavoro è stato eseguito correttamente, ma A non ne sa nulla.

Sto cercando un approccio leggero su come gestire gli errori in questo sistema. Tieni presente che un sacco di componenti, quindi il clustering di tutti solo a causa della gestione degli errori non ha senso. Successivamente, ho abbandonato l'utilizzo di qualsiasi memoria distribuita / repo che verrebbe di nuovo installata su ciascun componente per lo stesso motivo.

I miei pensieri vanno nella direzione di avere uno stato assoluto su un B e di non avere mai uno stato persistente su A . Ciò significa quanto segue:

  • prima di #1 contrassegno su A che l'unità di lavoro in cui cambia sta per iniziare
  • solo B potrebbe deselezionare questo stato.
  • A può recuperare informazioni su B in qualsiasi momento, per aggiornare lo stato.
  • nessuna nuova modifica sulla stessa unità può essere invocata su A .

cosa ne pensi? C'è un modo leggero per domare gli errori nel sistema di questo tipo?

    
posta igor 11.02.2016 - 09:03
fonte

2 risposte

2

L'aggiunta a un registro permanente su A dovrebbe essere sufficiente. Questo fa fronte ai riavvii e alle partizioni di rete per raggiungere un'eventuale consistenza o per segnalare la rottura che impedisce tale convergenza. Con ammortizzato gruppo di commit può richiedere meno di un scrittura singola per mantenere una voce di registro.

Hai suggerito di rendere B responsabile dello stato di non marcatura. Non sono d'accordo. Solo A diventa consapevole del nuovo lavoro e solo A dovrebbe essere responsabile del rilevamento e degli errori di segnalazione come i timeout. B invia messaggi idempotenti a A e A aggiorna lo stato, ri-interrogando a intervalli secondo necessità.

Al punto 0, A viene a conoscenza di una nuova richiesta e la registra. Ciò costituisce un obbligo A deve essere scaricato in seguito entro una scadenza: A eseguirà continuamente e ripeterà i passaggi successivi fino a quando A apprenderà che l'elaborazione della richiesta è stata completata.

Alcune richieste saranno più lunghe di altre. Le stime dei tempi di elaborazione saranno disponibili su A e su B, eventualmente rivedute man mano che l'elaborazione continua. Tali stime possono essere riconsegnate ad A, quindi raramente genererà timeout falsi positivi. Pensalo come un messaggio keep alive che dice "funziona ancora, funziona ancora".

    
risposta data 23.11.2017 - 21:31
fonte
1

Adotta un tiro invece della strategia push. Fai in modo che ogni parte estrae le modifiche dagli altri e aggiorni i propri record.

  • A registra le cose che B dovrebbe fare in una coda
  • B preleva dalla coda di A e fa il lavoro
  • B registra le cose che ha fatto in una coda
  • A tira dalla coda di B per sapere qual è stato il risultato del lavoro

(Sto usando la coda delle parole, ma puoi sostituire log o topic.)

Puoi cuocere la coda nei servizi oppure puoi avere un broker di messaggi separato. Un'implementazione inserita in un servizio può essere semplice come GET /jobrequests?from=<timestamp> (con B che tiene traccia dell'ora più recente della richiesta di lavoro elaborata).

Una parte delicata di tale architettura è decidere sulla semantica almeno una volta contro la maggior parte delle volte. Concretamente: se B estrae un articolo dalla coda e poi si blocca durante l'esecuzione, cosa dovrebbe accadere? Ci sono due possibilità e che è più appropriato dipende dal tuo caso d'uso:

  • Almeno una volta: B impegna solo il punto della coda che ha ottenuto dopo aver completato un'azione, c'è il rischio di fare due azioni. Se progettate azioni per essere idempotenti, potreste ottenere esattamente una volta il comportamento usando questo approccio. (Io uso kafka per questo scenario.)
  • Al più una volta: B consuma solo ogni elemento della coda una volta. Se si blocca durante l'esecuzione, l'oggetto non verrà mai eseguito.

Benefici di questo approccio:

  • I servizi che richiedono la coda non devono essere attivi perché si verifichi il push della coda. Ciò significa che sei libero di riavviare B mentre A sta lavorando o di riavviare A mentre B sta lavorando. L'hosting ridondante di servizi in background è necessario solo per garantire tempi di risposta generali, operazioni non affidabili.
  • Il ritmo di estrazione degli articoli in coda può essere controllato dal consumatore, che consente di bufferizzare temporaneamente i picchi di carico nella coda.
risposta data 23.01.2018 - 10:49
fonte

Leggi altre domande sui tag