από τον Λουκά Καβακόπουλο, full stack προγραμματιστή
Στον κόσμο της PHP, η ταχύτητα είναι πάντα κορυφαία προτεραιότητα, ειδικά όταν αντιμετωπίζουμε εφαρμογές υψηλής επισκεψιμότητας που βασίζονται στο PHP-FPM (FastCGI Process Manager) και το nginx. Μπορεί να ακούγεται παράξενο, αλλά αρκετές φορές για να κάνουμε την εφαρμογή μας να τρέξει πιο “γρήγορα”, εμείς πρέπει να τη βάλουμε …να κοιμηθεί!
Ναι, αναφέρομαι στη λειτουργία sleep() της PHP. Πριν όμως μιλήσουμε για το πώς πετυχαίνει το sleep() να κάνει πιο γρήγορο ένα script, πρέπει να δούμε πώς δουλεύει η PHP-FPM.
H PHP-FPM έχει σχεδιαστεί για να χειρίζεται μεγάλο αριθμό αιτημάτων, διατηρώντας μια “ομάδα εργατών” που ονομάζει “worker pool”. Κάθε “εργάτης” χειρίζεται ένα αίτημα (πχ το άνοιγμα μιας σελίδας) και εκτελεί όλους τους υπολογισμούς και τις εργασίες που αυτό χρειάζεται. Με την παράλληλη εκτέλεση των “εργατών”, η PHP-FPM μπορεί και εξυπηρετεί πολλά αιτήματα ταυτόχρονα. Σε συνδυασμό με ένα web-server όπως το nginx, μπορεί να εξυπηρετήσει εκατοντάδες (και χιλιάδες) χρήστες ταυτόχρονα.
Είναι σημαντικό να έχουμε υπόψη μας βέβαια, ότι ο “εργάτης” τρέχει μόνος του, χωρίς καμία γνώση των άλλων “παράλληλων” εργατών, και εκτελεί σειριακά ένα php script. Μέχρι εδώ, δεν υπάρχει καμία προγραμματιστική “παραλληλία” ή ασύγχρονη εκτέλεση. Όμως, είναι πολύ εύκολο οι εργάτες να δουλεύουν για να ενημερώνουν μια “λίστα” (que) που τρέχει σε Redis ή ElasticSearch και να ενορχηστρώνονται από έναν άλλο εργάτη ή από κάποιο τρίτο script. Τα πιθανά σενάρια είναι αμέτρητα.
Ωστόσο, όταν πολλαπλοί PHP εργάτες δουλεύουν ταυτόχρονα, ανταγωνίζονται για τους πόρους των συστημάτων στα οποία τρέχουν, όπως τη CPU και τη μνήμη, και πολύ χειρότερα, ορισμένα αιτήματά τους εξαρτώνται από πολύ “αργά” συστήματα (όπως API ή βάσεις δεδομένων). Έτσι υπάρχουν στιγμές που το σύστημα “καταρρέει”, γίνεται αργό και οδηγείται σε timeouts.
Εδώ η λύση μοιάζει με τη λύση που θα δίναμε οδηγώντας στην Εθνική Οδό! Ο βιαστικός οδηγός πατάει πιο δυνατά το γκάζι, αλλά μαζί με τους άλλους βιαστικούς δημιουργεί γρήγορα ένα μποτιλιάρισμα. Δεσμεύει δηλαδή, στην περίπτωσή μας, περισσότερα resources και απαιτεί μεγαλύτερη προτεραιότητα και μικρότερο latency για τις εργασίες του, όμως το αποτέλεσμα είναι “μποτιλιάρισμα”.
Εμείς, στο δικό μας κώδικα, με μια απλή εντολή, απλά …σταματάμε για λίγο, για μερικά μικροδευτερόλεπτα συχνά στην άκρη, μέχρι το μποτιλιάρισμα των διάφορων εργατών, threads και κλήσεων, να διαλυθεί, ακριβώς επειδή του επιτρέπουμε να περάσει από δίπλα μας. Και έτσι, τελικά, συχνά φτάνουμε πολύ πιο γρήγορα στον προορισμό μας, γιατί βοηθήσαμε το δρόμο να ανοίξει!
Έτσι λοιπόν μπαίνει στο παιχνίδι η λειτουργία sleep(), η “μη αποκλειστική” (non blocking) συνάρτηση της PHP.
Ο σκοπός της συνάρτησης Sleep()
sleep(2); // περίμενε 2 δευτερόλεπτα πριν συνεχίσεις
usleep(100); // περίμενε 100 χιλιοστά του δευτερολέπτου πριν συνεχίσεις
Η συνάρτηση sleep() (και η γρηγορότερη αδελφούλα της, usleep()) στην PHP, απλώς διακόπτουν την εκτέλεση ενός σεναρίου για έναν δεδομένο αριθμό δευτερολέπτων (μικροδευτερολέπτων στην περίπτωση της usleep()). Αυτό μπορεί να φαίνεται σαν μια ασήμαντη λειτουργία, όμως αποδεικνύεται απίστευτα χρήσιμη και ωφέλιμη σε πολλά σενάρια.
Στο Linux, ειδικά, και σε κάποιες εκδόσεις των Windows (αλλά όχι σε όλες), όταν εκτελείται η PHP-FPM, οι συναρτήσεις αυτές δεν “μπλοκάρουν” την εκτέλεση σε επίπεδο συστήματος.
Αυτό σημαίνει ότι όταν το script της PHP καλεί την sleep(), το λειτουργικό σύστημα θέτει τον εργάτη που τρέχει το συγκεκριμένο iteration σε κατάσταση αναμονής, και απελευθερώνει πλήρως τη CPU. (Τονίζω εδώ ότι το script παραμένει προστατευμένο και με πλήρες state, δηλ όλα τα δεδομένα παραμένουν όπως ήταν και συνεχίζουν χωρίς να αλλάξει τίποτα μόλις η διάρκεια του sleep περάσει).
Κατά τη διάρκεια της περιόδου του sleep, ο εργάτης δεν καταναλώνει επεξεργαστική ισχύ και το PHP-FPM μπορεί να εκχωρήσει πόρους σε άλλους εργάτες, επιτρέποντάς τους να χειρίζονται νέα εισερχόμενα αιτήματα. Δεδομένου ότι η sleep() δεν μπλοκάρει ολόκληρη τη διαδικασία PHP-FPM αλλά μόνο το συγκεκριμένο νήμα, αυτό διασφαλίζει ότι ακόμη και όταν ορισμένες εργασίες τίθενται σε παύση ή καθυστερούν, το PHP-FPM συνεχίζει να εξυπηρετεί άλλα αιτήματα αποτελεσματικά. Αυτό βελτιώνει τη διαχείριση συγχρονισμού και πόρων, επιτρέποντας την παράλληλη διαχείριση πολλαπλών αιτημάτων χωρίς περιττά σημεία συμφόρησης που προκαλούνται από άλλου τύπου λειτουργίες αναμονής.
Έτσι, πετυχαίνετε τα εξής:
Περιορισμός ταχύτητας επαναλήψεων: Αν ένα εξωτερικό API επιβάλλει ένα όριο ρυθμού, η χρήση του sleep() σάς επιτρέπει να περιμένετε συγκεκριμένο χρόνο μεταξύ των κλήσεων API.
Αποτελεσματική αναμονή: Αντί να κάνουμε loop περιμένοντας να γίνει διαθέσιμος ένας πόρος ή απελευθερωθεί ένα κλείδωμα (lock), η sleep() μπορεί να χρησιμοποιηθεί για να τεθεί σε παύση το σενάριο.
Απελευθέρωση λειτουργιών: Σε ορισμένες περιπτώσεις, η καθυστέρηση της εκτέλεσης ορισμένων λειτουργιών μειώνει το φόρτο σε άλλες υπηρεσίες και επιτρέπει πιο αποτελεσματική επεξεργασία στο παρασκήνιο.
και, τελευταίο αλλά εξίσου σημαντικό:
Χρησιμοποιώντας την sleep() μπορείτε να επιταχύνετε, σωστά ακούσατε, να επιταχύνετε πολλές λειτουργίες! Ας το ελέγξουμε αυτό με περισσότερες λεπτομέρειες:
Πώς η Sleep() βελτιώνει την απόδοση στο PHP-FPM
Σκεφτείτε ένα σενάριο όπου αλληλεπιδράτε με ένα εξωτερικό API που έχει περιορισμό στις κλήσεις που μπορείτε να κάνετε ανά δευτερόλεπτο. Χωρίς το sleep(), ο “εργάτης” μπορεί να ξαναπροσπαθήσει τόσες πολλές φορές σε ένα δευτερόλεπτο, που το σύστημα να τον κλειδώσει (ban). Επίσης, ο “εργάτης” χρησιμοποεί νήματα (threads) της CPU και αυτό μειώνει τον αριθμό των διαθέσιμων πόρων του server.
Παράδειγμα: “Sleep” όταν ο σέρβερ έχει μεγάλο φόρο εργασίας
<?php
$maxRetries = 5;
$retryCount = 0;
$loadThreshold = 2.0;
while ($retryCount < $maxRetries) {
$retryCount++;
$serverLoad = sys_getloadavg();
echo "Current server load: " . $serverLoad[0] . "\n";
if ($serverLoad[0] > $loadThreshold) {
echo "Υψηλός φόρτος στο σέρβερ - σταματώ για 400 μιλισεκόντ";
usleep(400);
} else {
echo "Ο φόρτος είναι αποδεκτός, συνεχίζω τις εργασίες";
do_work();
break;
}
}
Ας πάρουμε λοιπόν έναν υπνάκο!
Αν και το sleep() μπορεί να φαίνεται σαν μια απλοϊκή και αντιφατική συνάρτηση με την πρώτη ματιά, η στρατηγική του χρήση μπορεί να βελτιώσει την αποτελεσματικότητα των διαδικασιών.
Η υπερβολική βέβαια χρήση του ή η τοποθέτησή του σε ακατάλληλα μέρη του σεναρίου μπορεί να οδηγήσει σε μείωση της συνολικής απόδοσης. Είναι σημαντικό να αναλύσετε πού είναι πραγματικά απαραίτητες οι παύσεις και να τις χρησιμοποιήσετε ώστε να επιτύχετε καλό “συγχρονισμό” στην εκτέλεση του προγράμματός σας με όλα τα άλλα “θέματα” που τρέχουν παράλληλα στο περιβάλλον των εφαρμογών σας.
Παρόλα αυτά, την επόμενη φορά που θα βελτιστοποιήσετε τη ρύθμιση του PHP-FPM, θυμηθείτε ότι μερικές φορές ο καλύτερος τρόπος για να επιταχύνετε τα πράγματα, είναι να αφήσετε τον κώδικά σας να ξεκουραστεί, παίρνοντας έναν “υπνάκο”.