2012-04-09 18 views
17

Quali tecniche si utilizzano le persone per utilizzare più processori/core quando si esegue un server TwistedWeb? C'è un modo consigliato di farlo?TwistedWeb su multicore/multiprocessore

Il mio servizio web basato su twisted.web è in esecuzione su istanze Amazon EC2, che spesso hanno più core CPU (8, 16), e il tipo di lavoro che il servizio sta facendo beneficia di una potenza di elaborazione extra, quindi mi piacerebbe molto piace molto usarlo.

Comprendo che è possibile utilizzare haproxy, squid o un server Web, configurato come proxy inverso, di fronte a più istanze di Twisted. In effetti, stiamo attualmente utilizzando tale impostazione, con nginx che funge da proxy inverso per diversi servizi twisted.web upstream in esecuzione sullo stesso host, ma ciascuno su una porta diversa.

Questo funziona, ma quello che mi interessa davvero è una soluzione in cui non esiste un server "frontale", ma tutti i processi di twistd in qualche modo si collegano allo stesso socket e accettano le richieste. È possibile anche ... o sono pazzo? Il sistema operativo è Linux (CentOS).

Grazie.

Anton.

risposta

39

Esistono diversi modi per supportare l'operazione multiprocesso per un'applicazione Twisted. Una domanda importante a cui rispondere all'inizio, tuttavia, è ciò che si aspetta che il modello di concorrenza sia, e in che modo la tua applicazione si occupa dello stato condiviso.

In un'applicazione a processo singolo Twisted, la concorrenza è del tutto cooperativa (con l'aiuto delle API I/O asincrone di Twisted) e lo stato condiviso può essere conservato ovunque si trovi un oggetto Python. Il tuo codice di applicazione funziona sapendo che, fino a quando non ha il controllo, nient'altro funzionerà. Inoltre, qualsiasi parte della tua applicazione che vuole accedere ad un pezzo di stato condiviso può probabilmente farlo abbastanza facilmente, dal momento che quello stato è probabilmente conservato in un noioso vecchio oggetto Python che è di facile accesso.

Quando si dispone di più processi, anche se tutti eseguono applicazioni basate su Twisted, esistono due forme di concorrenza. Uno è lo stesso del caso precedente: all'interno di un particolare processo, la concorrenza è cooperativa. Tuttavia, hai un nuovo tipo, in cui sono in esecuzione più processi. Lo scheduler dei processi della tua piattaforma potrebbe cambiare l'esecuzione tra questi processi in qualsiasi momento, e tu hai pochissimo controllo su questo (e anche poca visibilità su quando accade). Potrebbe persino programmare l'esecuzione simultanea di due processi su diversi core (probabilmente è anche quello che speri). Ciò significa che si perdono alcune garanzie sulla coerenza, poiché un processo non sa quando un secondo processo potrebbe arrivare e provare a operare su uno stato condiviso. Questo porta all'altra area importante di considerazione, in cui condividerai effettivamente lo stato tra i processi.

A differenza del singolo modello di processo, non si dispone più di luoghi convenienti e facilmente accessibili per memorizzare lo stato in cui tutto il codice può raggiungerlo. Se lo inserisci in un unico processo, tutto il codice in quel processo può accedervi facilmente come un normale oggetto Python, ma qualsiasi codice in esecuzione in uno qualsiasi degli altri tuoi processi non ha più un facile accesso ad esso. Potrebbe essere necessario trovare un sistema RPC per consentire ai processi di comunicare tra loro.In alternativa, è possibile progettare la divisione del processo in modo che ogni processo riceva solo richieste che richiedono lo stato memorizzato in tale processo. Un esempio di questo potrebbe essere un sito Web con sessioni, in cui tutti gli stati relativi a un utente sono memorizzati nella loro sessione e le loro sessioni sono identificate dai cookie. Un processo front-end può ricevere richieste web, ispezionare il cookie, cercare quale processo back-end è responsabile per quella sessione e quindi inoltrare la richiesta a tale processo back-end. Questo schema significa che il back-end in genere non ha bisogno di comunicare (purché la tua applicazione web sia sufficientemente semplice - cioè, purché gli utenti non interagiscano tra loro, o operino su dati condivisi).

Si noti che in questo esempio, un modello pre-forking non è appropriato. Il processo front-end deve possedere esclusivamente la porta di ascolto in modo che possa ispezionare tutte le richieste in arrivo prima che vengano gestite da un processo di back-end.

Naturalmente ci sono molti tipi di applicazioni, con molti altri modelli per la gestione dello stato. La selezione del modello giusto per la multielaborazione richiede innanzitutto di capire quale tipo di concorrenza ha senso per l'applicazione e come è possibile gestire lo stato dell'applicazione.

Detto questo, con molto di nuove versioni e ritorto (inediti a partire da questo punto), è abbastanza facile per condividere una porta TCP di ascolto tra più processi. Ecco un frammento di codice che dimostra in un modo si potrebbe utilizzare alcune nuove API per raggiungere questo obiettivo:

from os import environ 
from sys import argv, executable 
from socket import AF_INET 

from twisted.internet import reactor 
from twisted.web.server import Site 
from twisted.web.static import File 

def main(fd=None): 
    root = File("/var/www") 
    factory = Site(root) 

    if fd is None: 
     # Create a new listening port and several other processes to help out.                  
     port = reactor.listenTCP(8080, factory) 
     for i in range(3): 
      reactor.spawnProcess(
        None, executable, [executable, __file__, str(port.fileno())], 
       childFDs={0: 0, 1: 1, 2: 2, port.fileno(): port.fileno()}, 
       env=environ) 
    else: 
     # Another process created the port, just start listening on it.                    
     port = reactor.adoptStreamPort(fd, AF_INET, factory) 

    reactor.run() 


if __name__ == '__main__': 
    if len(argv) == 1: 
     main() 
    else: 
     main(int(argv[1])) 

Con le versioni più vecchie, a volte si può ottenere via con l'utilizzo di fork per condividere la porta. Tuttavia, questo è piuttosto soggetto ad errori, non riesce in alcune piattaforme, e non è un metodo supportato per utilizzare twisted:

from os import fork 

from twisted.internet import reactor 
from twisted.web.server import Site 
from twisted.web.static import File 

def main(): 
    root = File("/var/www") 
    factory = Site(root) 

    # Create a new listening port 
    port = reactor.listenTCP(8080, factory) 

    # Create a few more processes to also service that port 
    for i in range(3): 
     if fork() == 0: 
      # Proceed immediately onward in the children. 
      # The parent will continue the for loop. 
      break 

    reactor.run() 


if __name__ == '__main__': 
    main() 

Questo funziona a causa del normale comportamento di forcella, dove il processo appena creato (il bambino) eredita tutta la memoria e i descrittori di file dal processo originale (il genitore). Poiché i processi sono altrimenti isolati, i due processi non interferiscono tra loro, almeno per quanto riguarda il codice Python che stanno eseguendo. Poiché i descrittori di file sono ereditati, il genitore o uno dei bambini possono accettare connessioni sulla porta.

Da inoltrare richieste HTTP è un compito così facile, dubito si noterà molto di un miglioramento delle prestazioni utilizzando una di queste tecniche. Il primo è un po 'più bello del proxy, perché semplifica la distribuzione e funziona più facilmente per applicazioni non HTTP. Quest'ultimo è probabilmente più di una responsabilità che vale la pena accettare.

+0

Jean-Paul, prima di tutto, grazie per questa risposta dettagliata. Molto apprezzato! La mia applicazione è piuttosto semplice, non esiste uno stato condiviso tra i processi, nessuna sessione da gestire, nessuna necessità di comunicazione tra i processi, ecc. Il mio compito è adatto per il calcolo parallelo e (in teoria) davvero non lo faccio t attenzione se i miei processi girano tutti sullo stesso server, o ogni processo viene eseguito sul proprio computer. Il motivo per cui ho bisogno di utilizzare server più grandi con CPU multi-core è perché Amazon offre prestazioni I/O migliori su quelle. –

+0

ha sfogliato un po 'l'origine del tronco Twisted (posixbase.py, tcp.py), e sembra che le modifiche per riutilizzare il socket preesistente siano davvero nuove. Cercherò di tenere d'occhio queste nuove funzionalità e sperando di vederle nella versione 12.1 :) –

+0

Ho fatto alcuni test e ci sono 2 domande che mi inculcano: 1) non solo i lavoratori, ma anche il master accetterà le connessioni in entrata - come posso far accettare solo i lavoratori? 2) è corretto che l'impostazione di 'backlog' sulla chiamata' reactor.listenTCP' si applicherà al socket in totale - questa è la profondità della coda nel kernel, e quindi per tutti i lavoratori combinati? – oberstet

3

Il modo consigliato per utilizzare IMO haproxy (o un altro servizio di bilanciamento del carico) come già fatto, il collo di bottiglia non deve essere il bilanciamento del carico se configurato correttamente. Inoltre, ti consigliamo di avere un metodo fallover che fornisce haproxy nel caso in cui uno dei tuoi processi si interrompa.

Non è possibile associare più processi allo stesso socket TCP, ma è possibile con UDP.

+0

Ian, grazie per la risposta. Hai ragione, il bilanciamento del carico non è il collo di bottiglia, la mia preoccupazione principale era principalmente la sicurezza e bug di tipo overflow del buffer, il che significa che se vado con nginx o haproxy, ho bisogno di tenere traccia delle vulnerabilità e aggiornare la versione di quel software regolarmente. L'intera danza avrebbe potuto essere spostata lateralmente se ci fosse solo Twisted.Web –

+0

stava pensando forse qualcosa di simile a ciò che Apache fa con il pre-fork sarebbe possibile ... –

+3

Qualcosa di simile a ciò che fa Apache è sicuramente possibile. È vero che i socket non possono * essere * vincolati * allo stesso indirizzo, ma un singolo socket associa alcuni indirizzi * può * essere * condiviso *. Inoltre, non penso che "il modo consigliato di usare haproxy" sia un'ottima affermazione. Sono sicuro che qualcuno lo consiglierebbe, ma molte persone consiglierebbero qualcos'altro. –

1

Se si desidera servire i contenuti web tramite HTTPS così, questo è ciò che sarà necessario fare in cima al frammento di @ Jean-Paul.

from twisted.internet.ssl import PrivateCertificate 
from twisted.protocols.tls import TLSMemoryBIOFactory 

''' 
Original snippet goes here 
.......... 
............... 
''' 

privateCert = PrivateCertificate.loadPEM(open('./server.cer').read() + open('./server.key').read()) 
tlsFactory = TLSMemoryBIOFactory(privateCert.options(), False, factory) 
reactor.adoptStreamPort(fd, AF_INET, tlsFactory) 

Utilizzando fd, servirete HTTP o HTTPS, ma non entrambi. Se si desidera avere entrambi, listenSSL sul processo padre e includere lo ssl fd si ottiene dalla porta SSL come secondo argomento quando la deposizione delle uova il processo figlio.

snipper completo è qui:

from os import environ 
from sys import argv, executable 
from socket import AF_INET 

from twisted.internet import reactor 
from twisted.web.server import Site 
from twisted.web.static import File 

from twisted.internet import reactor, ssl 
from twisted.internet.ssl import PrivateCertificate 
from twisted.protocols.tls import TLSMemoryBIOFactory 

def main(fd=None, fd_ssl=None): 
    root = File("/var/www") 
    factory = Site(root) 

    spawned = [] 
    if fd is None: 
     # Create a new listening port and several other processes to help out.                  
     port = reactor.listenTCP(8080, factory) 
     port_ssl = reactor.listenSSL(8443, factory, ssl.DefaultOpenSSLContextFactory('./server.key', './server.cer')) 
     for i in range(3): 
      child = reactor.spawnProcess(
       None, executable, [executable, __file__, str(port.fileno()), str(port_ssl.fileno())], 
       childFDs={0: 0, 1: 1, 2: 2, port.fileno(): port.fileno(), port_ssl.fileno(): port_ssl.fileno()}, 
       env=environ) 
      spawned.append(child) 
    else: 
     # Another process created the port, just start listening on it.                    
     port = reactor.adoptStreamPort(fd, AF_INET, factory) 
     cer = open('./server.cer') 
     key = open('./server.key') 
     pem_data = cer.read() + key.read() 
     cer.close() 
     pem.close() 
     privateCert = PrivateCertificate.loadPEM(pem_data) 
     tlsFactory = TLSMemoryBIOFactory(privateCert.options(), False, factory) 
     reactor.adoptStreamPort(fd_ssl, AF_INET, tlsFactory) 

    reactor.run() 

    for p in spawned: 
     p.signalProcess('INT') 


if __name__ == '__main__': 
    if len(argv) == 1: 
     main() 
    else: 
     main(int(argv[1:]))