PyFes 2012.11 Präsentationsmaterial.
Einführung in die Meinheld-Architektur mit Schwerpunkt auf Systemaufrufen.
Ich schreibe eine Beispielimplementierung in Pure Python, um die Architektur zu erklären, aber sie funktioniert einwandfrei und ich unterbreche das Parsen von HTTP-Anforderungen, sodass ich über 10000 req / s erhalte. Wenn der Ablauf mit ereignisgesteuertem Code schwer zu verstehen ist, empfiehlt es sich, ihn während der Ablaufverfolgung auszuführen, z. B. "python -mtrace -t --ignore-module socket webserver1.py".
Heute werde ich über das Verfolgen von Anforderungen / Sek. Unter der Bedingung sprechen, dass nur eine einfache Antwort zurückgegeben wird. Zum Beispiel gibt das Nginx-Lua-Modul nur "Hallo" zurück. Wir müssen über etwas anderes nachdenken, beispielsweise über einen Server, der statische Dateien liefert.
Siehe rfc2616 für Details
Das HTTP-Parsen mit Python ist kein Systemaufruf, sondern wird zu einem Engpass. In diesem Artikel wird das HTTP-Parsen daher nicht ordnungsgemäß durchgeführt.
Die HTTP-Anfrage sieht folgendermaßen aus.
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
Die erste Zeile ist die Anforderungszeile in Form der Methode URI HTTP-Version. Der URI kann ein absoluter URI sein, der den Host enthält, oder ein absoluter Pfad, der den Host nicht enthält, aber der absolute Pfad ist häufiger.
Der Anforderungsheader ist von der zweiten Zeile bis zur leeren Zeile. Jede Zeile hat die Form Feldname: Feldwert
. Bei dem Feldnamen wird die Groß- und Kleinschreibung nicht berücksichtigt.
Von der Anforderungszeile zum Anforderungsheader, gefolgt von einer Leerzeile, sind die Zeilenumbrüche CR LF. Es ist ein Zeilenvorschubcode, den Sie häufig unter Windows sehen.
Wenn die Methode POST usw. ist, wird der Nachrichtentext nach der Leerzeile hinzugefügt. Der Datentyp, für den der Nachrichtenkörper angegeben ist, wird im Header "Content-Type" angegeben, und seine Größe wird im Header "Content-Length" angegeben. Sie können "Content-Length" weglassen, aber darüber werden wir später sprechen.
Der Server verwendet möglicherweise VirtualHost. Wenn die Anforderungszeile kein absoluter URI ist, fügen Sie einen "Host" -Header hinzu, um den Hostnamen anzugeben.
Die HTTP-Antwort sieht folgendermaßen aus.
HTTP/1.1 200 OK
Content-Type: text/plain
Content-Length: 5
Hello
Es ist fast dasselbe wie eine HTTP-Anfrage, außer dass die erste Zeile die Statuszeile ist. Die Statuszeile hat die Form "http-Version Statuscode Grundsatz". Es ist wie "200 OK" oder "404 nicht gefunden".
Ein Webserver ist ein TCP-Server, der HTTP-Anforderungen empfängt und HTTP-Antworten zurückgibt.
Sie können read, readv, recvmsg anstelle von recv in 3 verwenden. Sie können write, writev, writemsg verwenden, anstatt 5 einzusenden.
Dieses Mal werden wir uns mit req / sec befassen, also ignoriere 4. Eigentlich muss ich Keep-Alive unterstützen, aber diesmal werde ich das auch ignorieren.
Das Schreiben bis zu diesem Punkt in Python sieht so aus. (Anfrage wird nicht analysiert)
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()
Der obige Beispielcode kann jeweils nur mit einem Client kommunizieren. Es gibt verschiedene grundlegende Möglichkeiten, mehrere Verbindungen parallel zu verarbeiten, und unzählige Möglichkeiten, sie zu kombinieren. Die grundlegende Methode ist wie folgt.
Es wird ein Arbeitermodell genannt. Der Vorteil besteht darin, dass die Anzahl der Arbeitsthreads / -prozesse einfach dynamisch angepasst werden kann, jedoch die Verbindung vom akzeptierten Thread / Prozess zum Arbeitsthread / -prozess übergeben werden muss. Es belastet auch den Kontextwechsel.
Es wird ein Prefork-Modell genannt. Der Prozess von accept () bis close () bleibt einfach, sodass Sie hoffentlich maximale Leistung erzielen. Wenn die Anzahl der Threads / Prozesse jedoch gering ist, können keine paralleleren Zahlen verarbeitet werden. Wenn sie groß ist, wird die Belastung des Kontextschalters erhöht.
Es wird als ereignisgesteuertes Modell bezeichnet. Akzeptieren Sie, wann Sie akzeptieren können, recv, wenn Sie recv können, und senden Sie, wenn Sie senden können. Es ist kein Kontextwechsel erforderlich, aber jeder erfordert einen eigenen Systemaufruf, der diesen Overhead verursacht.
Für einen Server, der wirklich nur Hallo zurückgibt, ist es am besten, so viele Prozesse zu erstellen, wie Kerne mit einem einfachen Prefork-Modell vorhanden sind.
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()
Der Gunicorn-Synchronisationsarbeiter sollte diese Architektur haben.
Der zuvor erwähnte Prefork-Server ist der schnellste, wenn es nur Hallo ist. In der Praxis besteht das Problem jedoch darin, dass der Prozess die nächste Anfrage nicht verarbeiten kann und die CPU abgespielt wird, wenn das Empfangen einer Anfrage oder das Senden einer Antwort lange dauert. Es soll enden.
Wenn Sie den Sync-Worker von gunicorn verwenden, wird daher empfohlen, nginx in den Vordergrund zu stellen und dort statische Dateien zu liefern oder Anforderungen und Antworten zu puffern.
Wenn Sie jedoch eine zweistufige Konfiguration verwenden, wird die Geschwindigkeit halbiert. Daher verwendet jeder Prozess ein ereignisgesteuertes Modell wie Epoll, um eine zeitaufwändige Sende- / Empfangsverarbeitung zu ermöglichen.
Wenn Sie es wie im folgenden Code ereignisgesteuert machen, wird der Systemaufruf für ereignisgesteuert an alle Akzeptieren, Lesen und Schreiben angehängt, was den Overhead erhöht und das Hallo verlangsamt.
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()
Um den Overhead zu reduzieren, werden wir nach Stellen suchen, an denen wait_read und wait_write reduziert werden können.
Zunächst startet das Betriebssystem automatisch TCP-Verbindungen bis zum Backlog (der im Listener-Argument angegebenen Nummer), ohne accept () aufzurufen. (Gibt ACK / SYN für SYN zurück.) Wenn Sie also () von der App akzeptieren, wurde die TCP-Verbindung möglicherweise tatsächlich gestartet und die Anforderung vom Client wurde möglicherweise empfangen. Wenn Sie also () akzeptieren, führen Sie recv () sofort aus, ohne auf wait_read () zu warten.
Wenn read () beendet ist, wird die Antwort gesendet, dies kann jedoch auch sofort gesendet werden, da der Socket-Puffer zunächst leer sein sollte. Hören wir auf zu warten_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()
Diese Methode funktioniert auch hervorragend mit Prefork.
Nach dem Akzeptieren akzeptiert es das nächste nicht, bis es alles tut, was es kann, sodass Sie das Akzeptieren an einen anderen Prozess übergeben können, der wirklich keinen Job hat.
Es kann auch donnernde Probleme lindern. Das donnernde Problem besteht darin, dass nur ein Client eine Verbindung herstellt und alle Prozesse aktiv sind und nur einer von ihnen erfolgreich ist, wenn mehrere Prozesse auf vorgefertigte Weise accept () aufrufen. Ein Prozess, bei dem accept () fehlschlägt, geht vollständig verloren. Es ist ein störender Schlaf. Wenn Sie dies mit einem Server mit 100 Prozessen auf einem 1-Core-Computer tun könnten, hätte sich dies nicht angesammelt.
Was accept () betrifft, wurde das donnernde Problem vollständig gelöst, da modernes Linux jetzt nur noch für einen Prozess accept zurückgibt, wenn eine Verbindung eingeht. Wenn Sie jedoch () auswählen und dann () akzeptieren, tritt dieses Problem erneut auf.
Wenn Sie die Anzahl der Prozesse auf die Anzahl der CPU-Kerne beschränken und die Auswahl vor dem Akzeptieren nur dann durchführen, wenn Sie wirklich frei sind, tritt das Phänomen "ausgewählt, aber nicht akzeptieren" nur dann auf, wenn die CPU wirklich frei ist. Ich kann es schaffen
Dies erfordert nur mehr Systemaufrufe für accept () zum Schließen () als Prefork mit setblocking (0). Übrigens gibt es in neuerem Linux einen Systemaufruf namens accept4, und Sie können gleichzeitig mit accept auch setblocking (0) ausführen.
Die bisherige Geschichte ist die schnellste und stärkste im Benutzerbereich. Sobald der Webserver im Kernelraum implementiert ist, müssen keine Systemaufrufe mehr ausgegeben werden.
https://github.com/KLab/recaro
Recommended Posts