Come ridurre la dipendenza nelle comunicazioni tra i server

3

Background: il back office dei miei team back, il cui compito è ricevere input, eseguire calcoli e restituire il risultato al chiamante o ad un altro server lungo la linea per ulteriore elaborazione. Scriviamo in C # e il nostro codice funziona come servizi di Windows.

Alcuni anni fa eravamo soliti comunicare via TCP con altri server (che erano anche i servizi Windows scritti in C #). Questo approccio aveva degli svantaggi, principalmente la sua incapacità di scalare a più di un server per gestire le richieste.

Da allora il team ha iniziato a utilizzare Rabbitmq e non ci sono problemi reali che stiamo affrontando attualmente. Quando realizziamo progetti con altri team, solitamente riusciamo a convincerli a utilizzare il client Rabbitmq per comunicare con il nostro server Rabbitmq. Passiamo all'XML dicendo che cosa deve essere fatto il calcolo e quali sono gli argomenti e restituisce un XML come risultato. Gli XML hanno XSD e questo approccio ha funzionato bene finora e si adattava ai nostri bisogni.

Anche se sono generalmente contento del modo in cui sono fatte le cose, temo che tra qualche anno avremo una strong dipendenza da Rabbitmq (lo facciamo già) e l'esperienza mi dice che potremmo voler cambiare il metodo di comunicazione ( come TCP sembrava buono e che è stato cambiato ..)

Il problema è questo: qualunque cosa possiamo scegliere di usare, sembra che dipenderemo molto da quel metodo. E se provassimo molte cose (che alcuni servizi in X e altri nel metodo e nel protocollo Y) avremo una dipendenza inferiore, ma su molte cose (e il team dovrebbe sapere come usare più tecnologie), e non mi sembra che ne valga la pena.

Che cosa posso fare per ridurre questa dipendenza?

    
posta Belgi 30.03.2017 - 10:22
fonte

1 risposta

3

TL; DR Le tue applicazioni dovrebbero dipendere da interfacce che descrivono modelli di messaggi e non dovrebbero dipendere dai dettagli di implementazione relativi a RabbitMQ oa qualsiasi altro tecnologia di trasporto dei messaggi.

In generale, i diversi tipi di middleware tendono a risolvere gli stessi problemi utilizzando schemi di messaggistica comuni, ad esempio:

  • Messaggi diretti punto-punto
  • Messaggio Broadcast / Multicast
  • RPC (richiesta di risposta)
  • Consegna dei messaggi garantita affidabile
  • Separazione del traffico / segregazione
  • ecc.

Ho usato RabbitMQ in due grandi progetti C # finora - in entrambi i casi ho scritto il mio wrapper per incapsulare (nascondere) tutta la "roba" di RabbitMQ, ma modelli come request-response o pub / sub sono agnostici per qualsiasi particolare tecnologia di messaggistica.

Ecco alcuni esempi dei tipi di interfacce che uso nelle applicazioni che usano RabbitMQ - l'aspetto più importante di loro è che nessuna delle applicazioni sa qualcosa di RabbitMQ in particolare, sanno solo su modelli di messaggi e dati grezzi sulla topologia generale del sistema (ad esempio identificatori per endpoint di invio / ricezione, informazioni di routing di messaggi, identificatori di messaggi, ecc.).

In primo luogo, le interfacce usano strutture generiche che includono informazioni sul modello del messaggio insieme ai payload effettivi dei messaggi - nulla qui che necessariamente deve essere legato a RabbitMQ:

public class Message<T>
{
    // Message data
    T Payload { get; set; }

    // Identifiying "Network Name" of the target/recipient
    // This might identify an application, a physical host address, etc.
    // In RabbitMQ terms, it's a Routing Key. 
    // If null, assume broadcast/fanout.
    string TargetName { get; set; }

    // Unique identifier for RPC (Request-Reply) pattern
    // - Perhaps a GUID would be appropriate for this
    string CorrelationTerm { get; set; }

    // Should the message layer include a provision to guarantee delivery?
    bool IsGuaranteedDelivery { get; set; }

    // TODO - anything else?  stuff like durability and persistence adds complexity
    // but it can be abstracted out here.
}

L'interfaccia con il livello messaggio stesso non "perde" i nomi delle strutture specifiche di RabbitMQ, tutti i tipi di argomento e i tipi di ritorno fanno parte della libreria wrapper:

public interface IMessageLayer : IDisposable
{
    // Fire-n-forget publishing
    void Publish<T>(Message<T> message);

    // General-purpose method for consumption of received messages
    ISubscriber<T> Subscribe<T>(SubscriptionParameters<T> parameters);

    // Request-Reply pattern for RPC
    Task<Message<TReply>> Request<TReq, TReply>(Message<T> request);
}

Un esempio per un'interfaccia sottoscrittore abbastanza minimale - un evento di callback per ricevere un messaggio, un paio di semplici metodi per iniziare e interrompere la sottoscrizione.

public interface ISubscriber<T> : IDisposable
{
    event EventArgs<MessageReceivedEventArgs<T>> MessageReceived;
    void StartConsuming();
    void StopConsuming();
}

Un breve / breve esempio di come le applicazioni potrebbero usare questo:

Connessione broker

var connectionParams = new MessageLayerConfig { /* Broker details */ }
using (var myMessageEndpoint = MyMessageLayerFactory.Connect(connectionParams)) 
{
    // TODO - Use messaging patterns here
}

Pattern di sottoscrizione di messaggi ad alto livello in un'applicazione:

var mySubscriptionParams = new SubscriptionParams<T> { /* Consumer args */ }
using (var mySubscription = myMessageEndpoint.CreateSubscriber(mySubscriptionParams))
{
    var tcs = new TaskCompletionSource<FooMessage>();
    mySubscription.MessageReceived += (s, e) =>
    {
        // TODO: Handle message
        tcs.TrySetResult( /* receivedFooMessage */ );
    }
    mySubscription.StartConsuming();
    await tcs.Task;
}

Messaggio di alto livello fire'n'forget publishing in un'applicazione:

var message = new Message<FooMessage> 
{
    Payload = myLittleFooMessage,
    TargetName = "SomeHostName.SomeAppName",
    IsGuaranteedDelivery = true,
};
myMessageEndpoint.Publish(message);

Modello di richiesta di risposta ad alto livello in un'applicazione:

var message = new Message<MyRequest> 
{
    Payload = myRequestData,
    TargetName = "SomeRpcServer.SomeRpcAppName",
    IsGuaranteedDelivery = false,
};
var task = myMessageEndpoint.Request(message);

if (await Task.WhenAny(task, Task.Delay(5000)) != task)
{
    throw new TimeoutException("Request wait time exceeded 5sec");
}
var response = await task;

In generale, la chiave della flessibilità consiste nel mantenere le cose semplici e cercare il più possibile solo affidarsi ai più noti schemi di messaggistica piuttosto che cercare di trovare modi per fare un sacco di cose "intelligenti" con RabbitMQ.

vale a dire. attenersi ai tipi di modelli di messaggi che sono facili da replicare in altre tecnologie (o altre implementazioni AMQP o qualcosa come ZeroMQ), in questo modo la sostituzione del framework sottostante dovrebbe essere abbastanza semplice.

Se decidi di seguire il coniglio giù nel buco con tutti i tipi di pattern di messaggi avanzati come Last Value Cache, Shovels, Federated Exchanges, ecc. allora il lavoro coinvolto in sostituire quei pattern in il futuro sarà maggiore, perché avrai più schemi da replicare.

Infine, un approccio roll-your-own con RabbitMQ non è l'unica opzione; la gente è già andata meglio di questa con framework come MassTransit: link che funziona a un livello molto più alto di RabbitMq.Client - le sue interfacce sono molto più vicine ai tipi di modelli di messaggi a cui le vostre applicazioni dovrebbero essere interessate.

Anche se si usa anche MassTransit, c'è ancora un vantaggio nella scrittura delle proprie interfacce per il wrapper; la capacità di estrarre qualsiasi framework sottostante e sostituirlo con qualcos'altro con modifiche minimamente intrusive alle applicazioni che dipendono dai pattern dei messaggi.

    
risposta data 30.03.2017 - 22:19
fonte

Leggi altre domande sui tag