PyFes 2012.11 Matériel de présentation.
Présentation de l'architecture meinheld avec un accent sur les appels système.
J'écris un exemple d'implémentation en Pure Python pour expliquer l'architecture, mais cela fonctionne bien et je romps l'analyse des requêtes HTTP, donc j'obtiens plus de 10000req / sec.
Si le flux est difficile à comprendre avec du code événementiel, c'est une bonne idée de l'exécuter pendant le traçage, comme python -mtrace -t --ignore-module socket webserver1.py
.
Aujourd'hui, je vais parler de la poursuite de req / sec à la condition qu'elle ne renvoie qu'une simple réponse. Par exemple, le module nginx lua renvoie simplement "hello". Nous devons penser à autre chose, comme un serveur qui fournit des fichiers statiques.
Voir rfc2616 pour plus de détails
L'analyse HTTP avec Python n'est pas un appel système, cela devient un goulot d'étranglement, donc cet article ne fait pas correctement l'analyse HTTP.
La requête HTTP ressemble à ceci.
GET / HTTP/1.1
Host: localhost
POST /post HTTP/1.1
Host: localhost
Content-Type: application/x-www-form-urlencoded
Content-Length: 7
foo=bar
La première ligne est request-line, sous la forme de `` method URI HTTP-version ''. L'URI peut être un URI absolu qui inclut l'hôte ou un chemin absolu qui n'inclut pas l'hôte, mais le chemin absolu est plus courant.
L'en-tête de la demande va de la deuxième ligne à la ligne vide. Chaque ligne a la forme `` field-name: field-value ''. field-name est insensible à la casse.
De la ligne de demande à l'en-tête de demande suivi d'une ligne vide, les sauts de ligne sont CR LF. C'est un code de saut de ligne que vous voyez souvent sur Windows.
Lorsque la méthode est POST, etc., le corps du message est ajouté après la ligne vide. Le type de données qui correspond au corps du message est spécifié dans l'en-tête "Content-Type" et sa taille est spécifiée dans l'en-tête "Content-Length". Vous pouvez omettre Content-Length
, mais nous en reparlerons plus tard.
Le serveur utilise peut-être VirtualHost, donc si la ligne de demande n'est pas un URI absolu, ajoutez un en-tête "Host" pour spécifier le nom d'hôte.
La réponse HTTP ressemble à ceci.
HTTP/1.1 200 OK
Content-Type: text/plain
Content-Length: 5
Hello
C'est presque la même chose qu'une requête HTTP, sauf que la première ligne est status-line. La ligne d'état se présente sous la forme «phrase-raison du code d'état de la version http». C'est comme "200 OK" ou "404 Not Found".
Un serveur Web est un serveur TCP qui reçoit des requêtes HTTP et renvoie des réponses HTTP.
Vous pouvez utiliser read, readv, recvmsg au lieu de recv dans 3. Vous pouvez utiliser write, writev, writemsg au lieu de send in 5.
Cette fois, nous allons creuser dans req / sec, alors ignorez 4. En fait, je dois soutenir Keep-Alive, mais je l'ignorerai également cette fois.
L'écriture jusqu'à ce point en Python ressemble à ceci. (La demande n'est pas analysée)
webserver1.py
import socket
def server():
# 1: bind & listen
server = socket.socket()
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server.bind(('', 8000))
server.listen(100)
while 1:
# 2: accept new connection
con, _ = server.accept()
# 3: read request
con.recv(32*1024)
# 4: process request and make response
# 5: send response
con.sendall(b"""HTTP/1.1 200 OK\r
Content-Type: text/plain\r
Content-Length: 5\r
\r
hello""")
# 6: close connection
con.close()
if __name__ == '__main__':
server()
L'exemple de code ci-dessus ne peut communiquer qu'avec un client à la fois. Il existe plusieurs méthodes de base pour gérer plusieurs connexions en parallèle et une myriade de façons de les combiner. La méthode de base est la suivante.
C'est ce qu'on appelle un modèle de travailleur. L'avantage est qu'il est facile d'ajuster dynamiquement le nombre de threads / processus de travail, mais cela nécessite le processus de transmission de la connexion du thread / processus accepté au thread / processus de travail. Cela met également une charge sur le changement de contexte.
C'est ce qu'on appelle un modèle préfork. Le processus d'acceptation () à close () reste simple, donc j'espère que vous obtiendrez des performances maximales. Cependant, si le nombre de threads / processus est petit, il ne peut pas gérer plus de nombres parallèles, et s'il est grand, la charge sur le changement de contexte sera augmentée.
C'est ce qu'on appelle un modèle événementiel. Acceptez quand vous pouvez accepter, recevez quand vous pouvez recevoir et envoyez quand vous pouvez envoyer. Aucun changement de contexte n'est requis, mais chacun nécessite son propre appel d'appel système, ce qui ajoute à la surcharge.
Pour un serveur qui ne renvoie vraiment que bonjour, il est préférable de créer autant de processus qu'il y a de cœurs avec un simple modèle préfork.
webserver2.py
import multiprocessing
import socket
import time
def worker(sock):
while 1:
con, _ = sock.accept()
con.recv(32 * 1024)
con.sendall(b"""HTTP/1.1 200 OK\r
Content-Type: text/plain\r
Content-Length: 5\r
\r
hello""")
con.close()
def server():
sock = socket.socket()
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sock.bind(('', 8000))
sock.listen(100)
ncpu = multiprocessing.cpu_count()
procs = []
for i in range(ncpu):
proc = multiprocessing.Process(target=worker, args=(sock,))
proc.start()
procs.append(proc)
while 1:
time.sleep(0.5)
for proc in procs:
proc.terminate()
proc.join(1)
if __name__ == '__main__':
server()
L'ouvrier de synchronisation gunicorn devrait avoir cette architecture.
Le serveur prefork que j'ai mentionné plus tôt est le plus rapide s'il s'agit simplement de bonjour, mais le problème dans la pratique est que s'il faut beaucoup de temps pour recevoir une demande ou envoyer une réponse, le processus ne pourra pas traiter la demande suivante et le processeur jouera. C'est pour finir.
Par conséquent, lors de l'utilisation de l'outil de synchronisation de gunicorn, il est recommandé de placer nginx au premier plan et d'y livrer des fichiers statiques, ou de tamponner les demandes et les réponses.
Cependant, si vous utilisez une configuration en deux étapes, la vitesse sera réduite de moitié. Par conséquent, chaque processus utilise un modèle événementiel tel que epoll pour permettre un traitement de transmission / réception chronophage.
Si vous le faites en fonction des événements comme le code suivant, l'appel système pour les événements sera attaché à tous les types d'acceptation, de lecture et d'écriture, ce qui augmentera la surcharge et ralentira bonjour.
webserver4.py
import socket
import select
read_waits = {}
write_waits = {}
def wait_read(con, callback):
read_waits[con.fileno()] = callback
def wait_write(con, callback):
write_waits[con.fileno()] = callback
def evloop():
while 1:
rs, ws, xs = select.select(read_waits.keys(), write_waits.keys(), [])
for rfd in rs:
read_waits.pop(rfd)()
for wfd in ws:
write_waits.pop(wfd)()
class Server(object):
def __init__(self, con):
self.con = con
def start(self):
wait_read(self.con, self.on_acceptable)
def on_acceptable(self):
con, _ = self.con.accept()
con.setblocking(0)
Client(con)
wait_read(self.con, self.on_acceptable)
class Client(object):
def __init__(self, con):
self.con = con
wait_read(con, self.on_readable)
def on_readable(self):
data = self.con.recv(32 * 1024)
self.buf = b"""HTTP/1.1 200 OK\r
Content-Type: text/plain\r
Content-Length: 6\r
\r
hello
"""
wait_write(self.con, self.on_writable)
def on_writable(self):
wrote = self.con.send(self.buf)
self.buf = self.buf[wrote:]
if self.buf:
wait_write(self.con, self.on_writable)
else:
self.con.close()
def serve():
sock = socket.socket()
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sock.bind(('', 8000))
sock.listen(128)
server = Server(sock)
server.start()
evloop()
if __name__ == '__main__':
serve()
Afin de réduire la surcharge, nous chercherons des endroits où wait_read et wait_write peuvent être réduits.
Tout d'abord, le système d'exploitation démarre automatiquement les connexions TCP jusqu'au backlog (le nombre spécifié dans l'argument d'écoute) sans appeler accept (). (Renvoie ACK / SYN pour SYN) Ainsi, lorsque vous acceptez () de l'application, la connexion TCP peut avoir effectivement démarré et la demande du client peut avoir été reçue. Donc, si vous acceptez (), faites immédiatement recv () sans wait_read ().
Lorsque read () est terminé, la réponse est envoyée, mais cela peut également être envoyé immédiatement car le tampon de socket doit être vide au début. Arrêtons de wait_write ().
webserver5.py
import socket
import select
read_waits = {}
write_waits = {}
def wait_read(con, callback):
read_waits[con.fileno()] = callback
def wait_write(con, callback):
write_waits[con.fileno()] = callback
def evloop():
while 1:
rs, ws, xs = select.select(read_waits.keys(), write_waits.keys(), [])
for rfd in rs:
read_waits.pop(rfd)()
for wfd in ws:
write_waits.pop(wfd)()
class Server(object):
def __init__(self, con):
self.con = con
def start(self):
wait_read(self.con, self.on_acceptable)
def on_acceptable(self):
try:
while 1:
con, _ = self.con.accept()
con.setblocking(0)
Client(con)
except IOError:
wait_read(self.con, self.on_acceptable)
class Client(object):
def __init__(self, con):
self.con = con
self.on_readable()
def on_readable(self):
data = self.con.recv(32 * 1024)
if not data:
wait_read(self.con, self.on_readable)
return
self.buf = b"""HTTP/1.1 200 OK\r
Content-Type: text/plain\r
Content-Length: 6\r
\r
hello
"""
self.on_writable()
def on_writable(self):
wrote = self.con.send(self.buf)
self.buf = self.buf[wrote:]
if self.buf:
wait_write(self.con, self.on_writable)
else:
self.con.close()
def serve():
sock = socket.socket()
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sock.setblocking(0)
sock.bind(('', 8000))
sock.listen(128)
server = Server(sock)
server.start()
evloop()
if __name__ == '__main__':
serve()
Cette méthode fonctionne également très bien avec prefork.
Après avoir accepté, il n'accepte pas le suivant tant qu'il n'a pas fait tout ce qu'il peut, vous pouvez donc passer l'acceptation à un autre processus qui n'a pas vraiment de travail.
Cela peut également atténuer les problèmes difficiles et tonitruants. Le problème épineux est que lorsque plusieurs processus appellent accept () de manière préfork, un seul client se connecte et tous les processus sont actifs et un seul d'entre eux réussit. Un processus dans lequel accept () échoue est complètement perdu. C'est un sommeil inquiétant. Si vous pouviez faire cela avec un serveur de 100 processus sur une machine à 1 cœur, cela ne se serait pas accumulé.
En ce qui concerne accept (), le problème difficile et tonitruant a été complètement résolu, car Linux moderne ne retourne désormais accepter que pour un processus lorsqu'une connexion arrive. Toutefois, si vous sélectionnez () puis acceptez (), ce problème se reproduit.
En limitant le nombre de processus au nombre de cœurs de processeur et en effectuant la sélection avant d'accepter uniquement lorsque vous êtes vraiment libre, le phénomène de «sélectionné mais ne peut pas accepter» ne se produira que lorsque le processeur est réellement libre. Je peux le faire.
Cela ne nécessite que plus d'appels système pour accept () pour fermer () que pour prefork avec setblocking (0). À propos, dans Linux récent, il existe un appel système appelé accept4, et vous pouvez également faire setblocking (0) en même temps que accept.
Jusqu'à présent, l'histoire est la plus rapide et la plus forte de l'espace utilisateur. Une fois que le serveur Web est implémenté dans l'espace noyau, il n'est pas nécessaire d'émettre des appels système.
https://github.com/KLab/recaro
Recommended Posts