Come ricostruire questa applicazione usando il modello di progettazione

-1

Mi sto impegnando a ridisegnare un'applicazione del server Linux c ++. L'applicazione agisce come relayer di file sotto forma di ricezione di pacchetti di file (inclusi pacchetti di controllo e pacchetti di dati) dal client A, scrivendo questi pacchetti in un file di dati locale quindi crea e aggiorna il file di indice, infine avvia molti thread per leggere i dati del file e inoltrare questi pacchetti di dati ai suoi destinatari come client B, client C, client D, ecc. Il diagramma di flusso è simile al seguente: Client A - > Server - > Client B, Client C, Client D, ...

Attualmente l'applicazione utilizza il pattern Producer-Consumer per passare pacchetti di dati tra i moduli, ecco alcuni moduli che abbiamo: SocketModule (contiene un thread per ascoltare e leggere i dati in arrivo di tutti i socket), SessionModule (per la gestione delle sessioni di tcp dell'utente) , FileRelayModule (contiene molti thread per fare lavori come l'elaborazione di pacchetti in entrata e la scrittura in file di dati locali, la lettura di file di dati locali e l'aggiornamento di file indice e l'inoltro di questi pacchetti di dati ai ricevitori tramite socket TCP).

Il problema è che le sue prestazioni sono così brutte, prendi un esempio, ci vogliono circa 15 minuti per il client A per inviare un file di dimensioni 200M al client B, ma ho fatto un file p2p che invia e riceve tra macchine diverse che serve solo 20 secondi.

Riteniamo che la causa principale delle cattive prestazioni sia la memorizzazione delle informazioni sui socket in una globale std :: map, quindi ogni thread che desidera inviare i dati (ai client) deve attendere il blocco per std :: map, quindi l'applicazione agisce come un'applicazione a thread singolo.

Poiché il design originale di questa applicazione non segue i principi del modello di progettazione e questo si traduce in molti problemi, quindi vogliamo riprogettarlo usando il pattern MVC, aggiungendo ruoli di gestore di thread e file manager. Il primo elemento che penso è di utilizzare un ThreadPool, quindi inviare ogni socket accettato nel thread che ha il minor numero di client (numero di socket). Questo è possibile o ragionevole per questa applicazione? Cosa faresti se tu fossi il progettista di questo progetto?

[update]
1. Controllerò il progetto e capirò quale parte è il vero collo di bottiglia di    prestazioni
2. Non importa quale parte è il vero collo di bottiglia, abbiamo bisogno di ricostruire il    applicazione per gestire le risorse come thread e memoria e    essere facile da mantenere

Farò cose come il seguente:
1. Crea molte classi: CSessionManager per la gestione dei dati della sessione tcp (contiene    un thread), CFileManager per l'inoltro di file e la gestione di tutti i file di dati e di indice (contiene un ThreadPool con 32 thread di cui contiene una mappa socket e un socket di ascolto), CFileServer (contiene CSessionManager e CFileManager). Qui CFileServer funge da Controller, CFileManager funge da modello.
2. CFileManager crea un pool di thread con 32 thread inizializzati all'avvio. CFileServer crea un socket di ascolto e inserisce tutti i socket accettati in CFileManager dove il nuovo socket accettato è impostato su un thread nel threadpool interrogando chi ha i socket minimi.

[Update2]
Il threadpool sarà progettato come segue (è ragionevole?):

               std::list<CThread*> workers;
               //each thread would be
               class CThread{
                   public: 
                     PostMsg(CMessage*);
                     static void Run();

               private:
                     map<int, socketinfo*> sockets;
                     std::deque<CMessage*> messageQueue;
               };

               ...
               Other objects will call PostMsg to inform message
               to this CThread and in the run function:
               void CThread::Run(){
                     ....
                     while(1){
                         ....
                         //firstly process all messages
                         //these messages includes adding socket,
                         //deleting socket, or go offline
                         CMessage* msg = messageQueue.pop_front;
                         if (msg != NULL) {
                            process(msg);
                         }

                         //after processing the messages,
                         //now read/write the sockets data
                         //set readfd and writefd
                         ret = select(max, readfd, writefd, NULL, timeout);

                         //now process readfd and writefd
                         //read data from readfd, then write to local 
                         //file read data from local file then write 
                         //to wirtefd or post a message to controller
                        // to remove fd from array.
                         ......
                     }
               }
    
posta Steve 15.06.2013 - 07:16
fonte

1 risposta

4

Se fossi il progettista di questo progetto, mi concentrerei prima sulle prestazioni e poi su tutti i modelli di design.

Il miglioramento delle prestazioni inizia con conoscere dove sono i colli di bottiglia, non dove pensi che siano. Potresti trascorrere molte ore (o giorni, settimane) per ottimizzare una parte del programma che non influenzi affatto le prestazioni complessive. Esistono strumenti per la profilazione di programmi concorrenti, ma di solito permetto ai thread di scrivere alcune informazioni di registro su dove trascorrono il loro tempo.

Prima di tutto, il tempo di riferimento. La situazione P2P con cui si confronta non è esattamente uguale alla propria applicazione. Nel caso P2P, il client mittente aveva già il file, mentre nel caso dell'applicazione, quel file deve essere caricato prima. Da quanto ho capito, anche questa fase di caricamento è un processo a thread singolo, e vale la pena controllare quanto tempo ci vuole. Una situazione paragonabile sarebbe utilizzare FTP per caricare il file e quindi P2P per distribuirlo. Inoltre, i tempi su una rete locale sarebbero molto diversi dai tempi su Internet.

Con tutto ciò, tuttavia, direi che un miglioramento del fattore 15 è un buon obiettivo.

A prima vista, vedo i seguenti potenziali colli di bottiglia:

  1. Il file viene letto nella sua interezza e quindi ulteriormente elaborato, prima dell'avvio della distribuzione. Forse questo processo può anche essere reso simultaneo, e forse l'invio di blocchi di file ai client può iniziare non appena li hai, aggiungendo le informazioni aggiuntive in seguito. Se necessario, il client può fare la propria ricostruzione del file completo (proprio come con P2P).

  2. I / O del disco. Sembra che il file sia scritto su disco e ogni thread sta eseguendo I / O su disco per leggerlo. Questo può essere velocizzato con il caching o la mappatura della memoria. Potresti anche voler controllare che l'hardware del tuo disco sia in grado di gestire l'I / O.

  3. La std :: map è una struttura di dati orribile piuttosto a sé stante, per non parlare di quando è bloccata. È un albero rosso-nero bilanciato, quindi ogni inserimento o rimozione può innescare un ribilanciamento, e ogni ricerca è O (lg n). Inoltre, l'allocazione della memoria potrebbe avere un impatto negativo sulle prestazioni. Quello che vorresti è vedere ogni socket come un'attività, e avere compiti incompleti in una semplice TaskQueue. Un elenco (doppio) collegato (eventualmente con un pool di memoria) o un vettore std :: che funge da stack è sufficiente. Ciò ridurrà il tempo di attesa a O (1) con una costante molto bassa.

  4. Socket I / O. I thread potrebbero attendere molto prima che la chiamata I / O con i client sia terminata. Impostandole come chiamate asincrone, possono gestire la scrittura su un socket, mettere il socket su TaskQueue e lasciare che un altro thread gestisca il callback.

  5. Un ThreadPool è una buona idea, ma non vorrei sovraccaricare ogni thread con più client. Consentitegli di leggere una semplice attività da TaskQueue, eseguirla rapidamente e quindi inserire nuovamente l'attività aggiornata in TaskQueue. Ogni thread fa una cosa diversa in base a qualche "comando" nell'attività.

  6. Ultimo ma non meno importante, il file è binario o compresso? Anche la riduzione della dimensione dei dati da trasferire è un'ottimizzazione valida.

Penso che ciò che stai cercando in MVC sia la reattività e la concorrenza, la "sensazione" che agisca rapidamente, come riscontrato in molte applicazioni MVC. Ciò è tuttavia dovuto, in parte, al sistema di messaggistica utilizzato da MVC e alla sua implementazione. (Le altre parti sono che diverse viste e controllori possono comportarsi in modo concorrente e che il modello non è bloccante.) MVC di per sé non garantisce guadagni in termini di prestazioni.

Quindi mi concentrerei sui pattern sottostanti, che potrebbero anche essere usati da MVC stesso: ThreadPools, TaskQueue e simultaneamente I / O asincrono e caching.

Con quello sul posto, non vedo un vero uso per MVC. MVC è un'astrazione per diverse viste e diversi controller, che mostrano e agiscono sul modello in modi diversi. Quello che hai, tuttavia, è molto simile. Quindi, invece di, ad esempio, il modello che dice a ogni thread o client che ha un file pronto, può semplicemente mettere così tante attività in coda e non preoccuparsi di quale processo gestisca esattamente cosa. Finché la coda è vuota alla fine del processo, stai bene.

Ad ogni modo, come ho detto, prima di fare qualsiasi cosa, assicurati di sapere quali sono i veri e propri colli di bottiglia.

    
risposta data 15.06.2013 - 09:30
fonte

Leggi altre domande sui tag