Server Galore: Uso del CherryPy di Python per prototipi scalabili veloci in un ambiente magro
TL;DR
Uno dei risultati del diventare un ingegnere più anziano è che voglio semplicemente portare a termine i progetti. Passare il tempo ad hackerare una biblioteca che non funziona, è mal implementata, o mal documentata rende la codifica un lavoro di routine piuttosto che una gioia. Ecco perché sono stato molto contento di lavorare con CherryPy di recente:
- Il file Docker non è complicato
- Il contenuto statico può essere servito rapidamente
- I Python Scripts sono cittadini di prima classe
- I test sono semplici
- Scalare la mia applicazione è un unico liner
Che cos'è CherryPy?
CherryPy è un framework Python web minimalista. Non sostituirà mai un framework più grande e completo per il software aziendale, ma per far funzionare qualcosa è fantastico. Tradizionalmente, userei un SimpleHttpServer per la prototipazione, ma è una caratteristica leggera - che mi richiede di aggiornare non appena ho fatto qualcosa che ha davvero spinto il server. CherryPy permette alle mie applicazioni di scalare abbastanza per essere utili prima di dover passare a un framework più grande (se del caso). Per riferimento, Digital Ocean ha fatto un bel confronto di Pitone WSGI server.
Docker e CherryPy
Usiamo Docker per ogni cosa a ShareThis, quindi cerchiamo di farlo prima in modo da avere un ambiente di sviluppo completo con i test. Si può iniziare tutto questo semplicemente clonando il nostro repo. L'impostazione all'inizio richiede 10 minuti in più, ma si ripagherà molte volte nei primi giorni di codifica. Questo è probabilmente l'unico costo che paghiamo per scambiare CherryPy in Docker con SimpleHttpServer sulla linea di comando.
Dockerfile
FROM python
RUN pip install cherrypy coverage # cherrypy server + python 3 test coverage
ADD *.py /
ADD *.sh /
CMD python run.py
test.sh
#!/bin/bash
coverage run -a --omit=test.py /test.py
coverage report -m
run.sh
#!/bin/bash
# docker kill and rm prevent funny errors
docker kill cherrypy > /dev/null 2>&1
docker rm cherrypy > /dev/null 2>&1
# build, test, and run the image
docker build -t cherrypy .
docker run cherrypy /testing.sh
docker run -ti --name cherrypy -p 80:80 cherrypy $@
test.py
import unittest
class TestMethods(unittest.TestCase):
def test_simple(self):
self.assertTrue(True)
if __name__ == '__main__':
unittest.main()
Primo Pass: Contenuto sceneggiato
Supponiamo che vogliate iniziare un esperimento di magra per vedere se qualcuno atterrerà su una pagina. La cosa più veloce da fare è servire una pagina statica e registrare le visite. Aggiungiamo lo script run.py mancante:
run.py (V1)
import cherrypy
class Landing(object):
@cherrypy.expose
def index(self):
return scripted landing
if __name__ == "__main__":
cherrypy.quickstart(Landing(), '/')
A questo punto si dispone di un server web scalabile che può fare praticamente di tutto. Lo uso per i cruscotti, il monitoraggio e una serie di strumenti interni.
A questo punto, si vorrà iniziare a fare il mucchio con la configurazione. Per esempio, si vuole che il proprio server web sia dichiarato su 0.0.0.0.0 invece di 127.0.0.0.1 per il routing pubblico. Si vuole servire sulla porta 80 per il traffico live. Si vogliono più di 10 thread per connessioni live multiple (più su quello sotto). Si vuole che il traffico venga instradato nonostante la barra di trailing. Si vuole un una miriade di altre cose che si verifichi.
Aggiungiamo alcune configurazioni aggiungendo una chiamata a cherrypy.config.update
. Inoltre, consentiamo il caricamento di contenuti statici e di un file di configurazione modificando cherrypy.quickstart(Landing(), '/')
a cherrypy.quickstart(Landing(), '/', "prod.conf")
. Non dimenticate di aggiungere prod.conf al vostro Dockerfile!
run.py (V2)
import cherrypy
class Landing(object):
@cherrypy.expose
def index(self):
return scripted landing
# The config.update is optional, but this will prevent scaling issues in a moment
cherrypy.config.update({
'server.socket_host': '0.0.0.0', # 127.0.0.1 is default
'server.socket_port': 80, # 8080 is default
'server.thread_pool': 100, # 10 is default
'tools.trailing_slash.on': False # True is default
})
if __name__ == "__main__":
cherrypy.quickstart(Landing(), '/', "prod.conf")
Più o meno a questo punto, probabilmente avrete bisogno anche di risorse statiche. Aggiungiamo il file di configurazione con un riferimento all'immagine statica! È possibile aggiungere un numero qualsiasi di asset statici. Io preferisco un approccio dichiarativo, ma è possibile servire un elenco anche.
prod.conf
[/logo.png]
tools.staticfile.on = True
tools.staticfile.filename = "/logo.png"
Espansione rapida
Scalare un prototipo significa liberarsi rapidamente dei bloccanti. Parliamo di alcuni dei problemi più comuni che riguardano CherryPy e di come toglierli di mezzo il più velocemente possibile.
Controlli sanitari e manipolatori supplementari
A ShareThis, eseguiamo le nostre applicazioni all'interno di Kubernetes su AWS. Dobbiamo monitorare lo stato del nostro container Docker affinché i sistemi sappiano se il container è vivo e se l'equilibratore di carico è sano. Questo può essere ampiamente archiviato sotto la necessità di ulteriori operatori. Ecco un semplice esempio di scrittura di un semplice endpoint HealthCheck e di aggiunta del conduttore al nostro oggetto di atterraggio. Ora, possiamo puntare a http://localhost/healthcheck per vedere se il server è vivo.
...
class HealthCheck(object):
def index(self):
return "Yay
\n"
index.exposed = True
class Landing(object):
healthcheck = HealthCheck()
...
Un effetto collaterale del monitoraggio su Kubernetes è che la rete nasconde una certa complessità. Ogni nodo di un cluster Kubernetes ha un proxy che rimescola i dati in qualsiasi punto del container Docker sia in funzione in quel momento. L'Elastic Load Balancer (ELB) di Amazon cercherà di mantenere vive le connessioni ad ogni nodo per effettuare il controllo sanitario del cluster. Questo significa che per ogni nodo del cluster, si deve tener conto di questo nel numero di thread disponibili al server. Nel nostro cluster di 20 nodi, la nostra applicazione di test non funzionerà perché l'ELB sta accedendo a 20 thread in una sola volta. CherryPy ha un default di 10 thread, e quindi i proxy saranno continuamente cacciati dalla rotazione. Quindi, il nostro aumento a 100 threads sopra consentirà di scalare senza problemi operativi.
Aggiunta di test
La complessità ostacola la produttività quando non si è sicuri di cosa accadrà man mano che vengono effettuati i cambiamenti. L'aggiunta di semplici contratti alla vostra suite di test che dicono ai futuri sviluppatori (incluso voi stessi) cosa il codice è destinato a fare, può mantenere il vostro codice in scala senza molte spese generali.
Il modo in cui abbiamo incluso gli script e lo strumento di copertura, dovreste essere in grado di aggiungere una dichiarazione di importazione per qualsiasi script di pitone e la copertura si occuperà del resto. Potreste voler aggiungere il mock
pacchetto al vostro pip install
dichiarazione per facilitare i test.
Sommario
CherryPy è ben documentato e si scala piuttosto bene. Spesso, è tutto ciò che serve per far funzionare un'applicazione in un ambiente in scala. Iniziando con un template, si può avere immediatamente un server web che esegue il codice python in modo nativo. Per noi di ShareThis, lo abbiamo usato per l'elaborazione dei dati di backend attraverso un'interfaccia di riposo, per monitorare i feed su S3 e BQ, e per richiamare rapidamente i dashboard che devono connettersi alle API per i dati. Utilizzando Docker, diventa banale distribuire e mantenere l'applicazione in produzione.