Ένα εκατομμύριο αιτήσεις ανά δευτερόλεπτο με την Python

Είναι δυνατόν να πετύχετε ένα εκατομμύριο αιτήσεις ανά δευτερόλεπτο με την Python; Πιθανώς όχι μέχρι πρόσφατα.

Πολλές εταιρείες μετακινούνται μακριά από την Python και σε άλλες γλώσσες προγραμματισμού, έτσι ώστε να μπορούν να ενισχύσουν την απόδοση λειτουργίας τους και να εξοικονομήσουν τιμές διακομιστή, αλλά δεν υπάρχει ανάγκη. Η Python μπορεί να είναι το σωστό εργαλείο για τη δουλειά.

Η κοινότητα της Python κάνει πολλές επιδόσεις τελευταία. Το CPython 3.6 ενίσχυσε τη συνολική απόδοση του διερμηνέα με νέα εφαρμογή λεξικού. Το CPython 3.7 θα είναι ακόμη πιο γρήγορο, χάρη στην εισαγωγή ταχύτερων συνομιλιών κλήσεων και κρυφής μνήμης αναζήτησης λεξικού.

Για εργασίες μείωσης αριθμών, μπορείτε να χρησιμοποιήσετε το PyPy με τη σύνταξη κώδικα ακριβώς σε χρόνο. Μπορείτε επίσης να εκτελέσετε τη δοκιμαστική σουίτα του NumPy, η οποία τώρα έχει βελτιώσει τη συνολική συμβατότητα με τις επεκτάσεις C. Αργότερα φέτος το PyPy αναμένεται να φτάσει στο Python 3.5.

Όλη αυτή η σπουδαία δουλειά με ενέπνευσε να καινοτομήσω σε έναν από τους τομείς όπου η Python χρησιμοποιείται εκτενώς: ανάπτυξη ιστού και μικρο-υπηρεσιών.

Μπείτε στο Japronto!

Το Japronto είναι ένα ολοκαίνουργιο μικρο-πλαίσιο προσαρμοσμένο στις ανάγκες σας για μικρο-υπηρεσίες. Οι κύριοι στόχοι του είναι να είναι γρήγορος , επεκτάσιμος και ελαφρύς . Σας επιτρέπει να κάνετε συγχρονισμό και ασύγχρονο προγραμματισμό χάρη στο asyncio . Και είναι ξεδιάντροπα γρήγορα . Ακόμα πιο γρήγορα από το NodeJS και το Go.

Errata: Όπως επισημαίνει ο χρήστης @heppu, ο διακομιστής HTTP του stdlib του Go μπορεί να είναι 12% ταχύτερος από αυτό που δείχνει αυτό το γράφημα όταν γράφεται πιο προσεκτικά. Επίσης, υπάρχει ένας φοβερός διακομιστής fasthttp για Go που προφανώς είναι μόνο 18% πιο αργός από τον Japronto σε αυτό το συγκεκριμένο σημείο αναφοράς. Φοβερός! Για λεπτομέρειες ανατρέξτε στα //github.com/squeaky-pl/japronto/pull/12 και //github.com/squeaky-pl/japronto/pull/14.

Μπορούμε επίσης να δούμε ότι ο διακομιστής Meinheld WSGI είναι σχεδόν ισοδύναμος με τους NodeJS και Go. Παρά τον εγγενώς μπλοκαρισμένο σχεδιασμό του, είναι μια εξαιρετική απόδοση σε σύγκριση με τα προηγούμενα τέσσερα, τα οποία είναι ασύγχρονες λύσεις Python. Επομένως, μην εμπιστεύεσαι ποτέ κανέναν που λέει ότι τα ασύγχρονα συστήματα είναι πάντα πιο γρήγορα. Είναι σχεδόν πάντα πιο ταυτόχρονες, αλλά υπάρχουν πολλά περισσότερα από αυτό απλώς.

Έκανα αυτό το μικρο benchmark χρησιμοποιώντας ένα "Γεια σου κόσμος!" εφαρμογή, αλλά καταδεικνύει σαφώς τα γενικά έξοδα διακομιστή-πλαισίου για μια σειρά λύσεων.

Αυτά τα αποτελέσματα αποκτήθηκαν σε μια παρουσία AWS c4.2xlarge που είχε 8 VCPU, που ξεκίνησαν στην περιοχή του Σάο Πάολο με προεπιλεγμένη κοινόχρηστη μίσθωση και εικονικοποίηση HVM και μαγνητική αποθήκευση. Το μηχάνημα έτρεχε το Ubuntu 16.04.1 LTS (Xenial Xerus) με το Linux 4.4.0-53-γενικό x86_64 πυρήνα. Το λειτουργικό σύστημα ανέφερε τον επεξεργαστή Xeon® CPU E5–2666 v3 @ 2.90GHz. Χρησιμοποίησα το Python 3.6, το οποίο συνέταξα πρόσφατα από τον πηγαίο κώδικα.

Για να είμαστε δίκαιοι, όλοι οι διαγωνιζόμενοι (συμπεριλαμβανομένης της Go) έτρεχαν μια διαδικασία μεμονωμένου εργαζομένου. Οι διακομιστές δοκιμάστηκαν φορτίο χρησιμοποιώντας wrk με 1 νήμα, 100 συνδέσεις και 24 ταυτόχρονες (σωληνώσεις) αιτήσεις ανά σύνδεση (αθροιστικός παραλληλισμός 2400 αιτήσεων)

Το HTTP pipelining είναι πολύ σημαντικό εδώ, καθώς είναι μια από τις βελτιστοποιήσεις που λαμβάνει υπόψη το Japronto κατά την εκτέλεση αιτημάτων.

Οι περισσότεροι από τους διακομιστές εκτελούν αιτήματα από πελάτες μέσω pipelining με τον ίδιο τρόπο όπως και από πελάτες χωρίς pipelining. Δεν προσπαθούν να το βελτιστοποιήσουν. (Στην πραγματικότητα, οι Sanic και Meinheld θα απορρίψουν σιωπηλά αιτήματα από πελάτες μέσω pipelining, κάτι που παραβιάζει το πρωτόκολλο HTTP 1.1.)

Με απλά λόγια, το pipelining είναι μια τεχνική στην οποία ο πελάτης δεν χρειάζεται να περιμένει την απάντηση πριν στείλει επακόλουθα αιτήματα μέσω της ίδιας σύνδεσης TCP. Για να διασφαλιστεί η ακεραιότητα της επικοινωνίας, ο διακομιστής στέλνει πίσω πολλές απαντήσεις με την ίδια παραγγελία που λαμβάνονται αιτήματα.

Οι φοβερές λεπτομέρειες των βελτιστοποιήσεων

Όταν πολλά μικρά αιτήματα GET συνδέονται μεταξύ τους από τον πελάτη, υπάρχει μεγάλη πιθανότητα να φτάσουν σε ένα πακέτο TCP (χάρη στον αλγόριθμο Nagle) από την πλευρά του διακομιστή και, στη συνέχεια, να διαβαστεί ξανά με μία κλήση συστήματος .

Η πραγματοποίηση κλήσης συστήματος και η μεταφορά δεδομένων από χώρο στον πυρήνα στον χώρο χρήστη είναι μια πολύ δαπανηρή λειτουργία σε σύγκριση με, ας πούμε, τη μετακίνηση της μνήμης μέσα στο χώρο της διαδικασίας. Γι 'αυτό, είναι σημαντικό να εκτελείτε όσο το δυνατόν λιγότερες κλήσεις συστήματος (αλλά όχι λιγότερο).

Όταν το Japronto λαμβάνει δεδομένα και αναλύει με επιτυχία πολλά αιτήματα από αυτό, προσπαθεί να εκτελέσει όλα τα αιτήματα όσο το δυνατόν γρηγορότερα, να κολλήσει τις απαντήσεις με τη σωστή σειρά και, στη συνέχεια, να γράψει ξανά σε μία κλήση συστήματος . Στην πραγματικότητα, ο πυρήνας μπορεί να βοηθήσει στο κόλλημα, χάρη στη διασπορά / συγκέντρωση κλήσεων συστήματος IO, τις οποίες το Japronto δεν χρησιμοποιεί ακόμα.

Λάβετε υπόψη ότι αυτό δεν είναι πάντα δυνατό, δεδομένου ότι ορισμένα από τα αιτήματα μπορεί να διαρκέσουν πολύ και η αναμονή τους θα αύξανε χωρίς καθυστέρηση.

Προσέξτε όταν συντονίζετε ευρετικά και λάβετε υπόψη το κόστος των κλήσεων συστήματος και τον αναμενόμενο χρόνο ολοκλήρωσης του αιτήματος.

Εκτός από την καθυστέρηση των εγγραφών για πελάτες μέσω pipelined, υπάρχουν πολλές άλλες τεχνικές που χρησιμοποιεί ο κώδικας.

Το Japronto γράφεται σχεδόν εξ ολοκλήρου στα C. Το πρόγραμμα ανάλυσης, το πρωτόκολλο, ο θεριστής σύνδεσης, ο δρομολογητής, το αίτημα και τα αντικείμενα απόκρισης γράφονται ως επεκτάσεις C.

Το Japronto προσπαθεί σκληρά να καθυστερήσει τη δημιουργία ομολόγων Python των εσωτερικών του δομών έως ότου ρωτηθεί ρητά. Για παράδειγμα, δεν θα δημιουργηθεί λεξικό κεφαλίδων έως ότου ζητηθεί σε προβολή. Όλα τα όρια των διακριτικών έχουν ήδη σημειωθεί πριν, αλλά η ομαλοποίηση των πλήκτρων κεφαλίδας και η δημιουργία πολλών αντικειμένων str γίνεται όταν αυτά έχουν πρόσβαση για πρώτη φορά.

Το Japronto βασίζεται στην εξαιρετική βιβλιοθήκη picohttpparser C για ανάλυση γραμμής κατάστασης, κεφαλίδες και ένα τεμάχιο σώμα μηνυμάτων HTTP. Το Picohttpparser χρησιμοποιεί απευθείας οδηγίες επεξεργασίας κειμένου που βρίσκονται σε σύγχρονες CPU με επεκτάσεις SSE4.2 (σχεδόν κάθε 10χρονη CPU x86_64 το έχει) για γρήγορη αντιστοίχιση των ορίων των διακριτικών HTTP. Το I / O διαχειρίζεται το εξαιρετικά φοβερό uvloop, το οποίο είναι το ίδιο περιτύλιγμα γύρω από το libuv. Στο χαμηλότερο επίπεδο, αυτή είναι μια γέφυρα για την κλήση συστήματος epoll που παρέχει ασύγχρονες ειδοποιήσεις σχετικά με την ετοιμότητα ανάγνωσης-εγγραφής.

Η Python είναι μια γλώσσα συλλογής απορριμμάτων, οπότε πρέπει να ληφθεί μέριμνα κατά το σχεδιασμό συστημάτων υψηλής απόδοσης, ώστε να μην αυξάνεται άσκοπα η πίεση στον συλλέκτη απορριμμάτων. Ο εσωτερικός σχεδιασμός του Japronto προσπαθεί να αποφύγει τους κύκλους αναφοράς και να κάνει όσο λιγότερες κατανομές / αφαιρέσεις είναι απαραίτητο. Αυτό το επιτυγχάνει με προκατανομή ορισμένων αντικειμένων σε λεγόμενες αρένες. Προσπαθεί επίσης να επαναχρησιμοποιήσει αντικείμενα Python για μελλοντικά αιτήματα εάν δεν αναφέρονται πλέον, αντί να τα πετάξετε.

Όλες οι εκχωρήσεις γίνονται ως πολλαπλάσια του 4KB. Οι εσωτερικές δομές σχεδιάζονται προσεκτικά έτσι ώστε τα δεδομένα που χρησιμοποιούνται συχνά μαζί να είναι αρκετά κοντά στη μνήμη, ελαχιστοποιώντας την πιθανότητα απώλειας προσωρινής μνήμης.

Το Japronto προσπαθεί να μην αντιγράψει μεταξύ των buffer άσκοπα και κάνει πολλές λειτουργίες στη θέση του. Για παράδειγμα, αποκωδικοποιεί το μονοπάτι πριν από την αντιστοίχιση στη διαδικασία του δρομολογητή.

Συνεισφέροντες ανοιχτού κώδικα, θα μπορούσα να χρησιμοποιήσω τη βοήθειά σας.

Δουλεύω συνεχώς στο Japronto τους τελευταίους 3 μήνες - συχνά τα σαββατοκύριακα, καθώς και τις κανονικές εργάσιμες ημέρες. Αυτό ήταν δυνατό μόνο επειδή έκανα ένα διάλειμμα από την κανονική μου δουλειά προγραμματιστή και έβαλα όλη μου την προσπάθεια σε αυτό το έργο.

Νομίζω ότι είναι καιρός να μοιραστώ τον καρπό της εργασίας μου με την κοινότητα.

Επί του παρόντος, το Japronto εφαρμόζει ένα αρκετά συμπαγές σύνολο χαρακτηριστικών:

  • Υλοποίηση HTTP 1.x με υποστήριξη για κομμένες μεταφορτώσεις
  • Πλήρης υποστήριξη για HTTP pipelining
  • Διατήρηση ζωντανών συνδέσεων με ρυθμιζόμενο θεριστή
  • Υποστήριξη για σύγχρονες και ασύγχρονες προβολές
  • Master-multiworker μοντέλο βασισμένο στο πιρούνι
  • Υποστήριξη για επαναφόρτωση κώδικα σε αλλαγές
  • Απλή δρομολόγηση

Θα ήθελα να εξετάσω τις υποδοχές Webs και τη ροή των απαντήσεων HTTP ασύγχρονα στη συνέχεια.

Πρέπει να γίνει πολλή δουλειά όσον αφορά την τεκμηρίωση και τον έλεγχο. Εάν ενδιαφέρεστε να βοηθήσετε, επικοινωνήστε μαζί μου απευθείας στο Twitter. Εδώ είναι το αποθετήριο έργων του Japronto GitHub.

Επίσης, εάν η εταιρεία σας αναζητά έναν προγραμματιστή Python που είναι φρικτός απόδοσης και επίσης κάνει το DevOps, είμαι ανοιχτός για να το ακούσω. Θα εξετάσω θέσεις παγκοσμίως.

Τελικές λέξεις

Όλες οι τεχνικές που ανέφερα εδώ δεν είναι πραγματικά συγκεκριμένες για την Python. Θα μπορούσαν πιθανώς να χρησιμοποιηθούν σε άλλες γλώσσες όπως το Ruby, το JavaScript ή ακόμα και το PHP. Θα με ενδιέφερε επίσης να κάνω τέτοια δουλειά, αλλά αυτό δυστυχώς δεν θα συμβεί εκτός αν κάποιος μπορεί να το χρηματοδοτήσει.

Θα ήθελα να ευχαριστήσω την κοινότητα της Python για τη συνεχή επένδυσή τους στη μηχανική απόδοσης. Δηλαδή Victor Stinner @VictorStinner, INADA Naoki @methane και Yury Selivanov @ 1st1 και ολόκληρη η ομάδα του PyPy.

Για την αγάπη του Πύθωνα.