Blog Μηχανικής - Prexy: κανόνες εξαιρετικά γρήγορης αναζήτησης και αντικατάστασης

Καλώς ήρθατε στο πρώτο μας blog μηχανικής. Ίσως είναι λίγο πιο τεχνικό από ό,τι έχετε συνηθίσει από τα άλλα blog, αλλά κάναμε ό,τι μπορούσαμε για να το κάνουμε κατανοητό σε όλους. Σε αυτό το άρθρο θα μιλήσουμε για το Prexy, ένα νέο λογισμικό στο Clonable που χρησιμοποιείται για την εφαρμογή των κανόνων αντικατάστασης.

Ιστορικό

Ως χρήστης του Clonable , ίσως γνωρίζετε ήδη τη λειτουργία των κανόνων αντικατάστασης. Μπορείτε να χρησιμοποιήσετε αυτούς τους κανόνες αναζήτησης και αντικατάστασης για να αντικαταστήσετε τμήματα κειμένου ή κώδικα με μια παραλλαγή της επιλογής σας. Μπορείτε, για παράδειγμα, να το χρησιμοποιήσετε για να αντικαταστήσετε ένα κλειδί API ή ένα αναγνωριστικό analytics, ώστε να μπορείτε να πραγματοποιείτε διαφορετικές αναλύσεις για τον αρχικό σας ιστότοπο και τους μεταφρασμένους κλώνους σας. Εσωτερικά, οι ίδιοι κανόνες χρησιμοποιούνται επίσης για την αντικατάσταση του αρχικού ονόματος τομέα σας με το όνομα τομέα του κλώνα σας, καθώς και για ορισμένες άλλες λειτουργίες.

Από την πρώτη έκδοση του Clonable , αυτή η λειτουργικότητα δεν έχει τροποποιηθεί ποτέ, κάτι που δεν είναι παράξενο, καθώς έκανε άριστα τη δουλειά της. Ωστόσο, υπήρχαν περιθώρια βελτίωσης, τόσο στον τομέα της απόδοσης όσο και στον τομέα της ευχρηστίας. Κάθε τόσο, επιλέγουμε ένα τμήμα του προϊόντος μας στο Clonable , στο οποίο δεν έχει αφιερωθεί χρόνος εδώ και καιρό, για να το βελτιώσουμε. Έτσι, πέρυσι αναθεωρήσαμε ολόκληρη την υποδομή δεδομένων μας για να την κάνουμε ταχύτερη, πιο επεκτάσιμη και πιο ανθεκτική σε σφάλματα, και για τον σκοπό αυτό είχαμε ήδη ανακατασκευάσει από την αρχή τη λειτουργικότητα της μετάφρασης URL. Αυτό το τρίμηνο ήρθε η σειρά των κανόνων αντικατάστασης.

Παλαιά κατάσταση και περιορισμοί

Οι κανόνες αντικατάστασης στο Clonable εφαρμόζονται μόνο στην τελευταία φάση, ακριβώς πριν από την αποστολή της απόκρισης στον πελάτη. Στην πρώτη έκδοση του Clonable , επομένως, επιλέχθηκε η υλοποίηση αυτού του μηχανισμού εντός του διακομιστή ιστού NGINX με τη βοήθεια του replace-filter-nginx-module, ενός module που δημιουργήθηκε από την OpenResty. Το module χρησιμοποιεί το sregex ως μηχανή εκφράσεων κανονικών εκφράσεων (regex) ροής, που επίσης έχει δημιουργηθεί από την OpenResty. Η δυνατότητα ροής είναι σημαντική σε αυτή την περίπτωση, καθώς δεν θέλουμε να φορτώνουμε πλήρως κάθε απόκριση στη μνήμη. Αν το κάναμε αυτό, ορισμένες μεγάλες αποκρίσεις θα μπορούσαν να προκαλέσουν τη διακοπή λειτουργίας ή το «κρέμασμα» του NGINX λόγω ανεπαρκούς διαθέσιμης μνήμης. Ένα δεύτερο σημαντικό χαρακτηριστικό του sregex είναι ότι μπορεί να επεξεργάζεται πολλαπλούς κανόνες παράλληλα. Κάθε κλώνος διαθέτει ήδη ορισμένους τυπικούς κανόνες και, επιπλέον, μερικές φορές ορισμένους κανόνες που έχουν προστεθεί ειδικά για τον κλώνο. Εάν αυτοί δεν μπορούσαν να επεξεργαστούν ταυτόχρονα, θα έπρεπε να αποθηκεύσουμε ολόκληρη την απόκριση στο buffer, καθώς μετά τον πρώτο κανόνα θα έπρεπε να μπορέσουμε να την επανεξετάσουμε για τον δεύτερο κανόνα.

Πέρυσι, ωστόσο, αντιμετωπίζαμε όλο και πιο συχνά περίεργα προβλήματα απόδοσης. Αυτά τα προβλήματα ήταν συχνά βραχύβια, αλλά σε ακραίες περιπτώσεις μπορούσαν να αυξήσουν τον χρόνο απόκρισης μιας σελίδας κατά μερικά δευτερόλεπτα. Μετά από μια τέτοια αιχμή, η σελίδα συχνά φορτωνόταν και πάλι κανονικά, γεγονός που έκανε το πρόβλημα δύσκολο να αναπαραχθεί. Για να το διερευνήσουμε περαιτέρω, προσθέσαμε την κεφαλίδα Clonable-Timings σε όλες τις απαντήσεις, κάτι που μας επέτρεψε να προσδιορίσουμε σε γενικές γραμμές ποιο στάδιο της διαδικασίας μετάφρασης απαιτούσε τόσο πολύ χρόνο. Από αυτό προέκυψε ότι, σε σχεδόν όλες τις περιπτώσεις, το upstream ανταποκρινόταν γρήγορα και ότι η μετάφραση επίσης προχωρούσε αρκετά ομαλά. Ωστόσο, υπήρχε ένα κενό μεταξύ της ολοκλήρωσης της μετάφρασης και της πλήρους ολοκλήρωσης του αιτήματος, κάτι που μας έδωσε μια ένδειξη ότι το πρόβλημα ενδέχεται να σχετίζεται με τους κανόνες υποκατάστασης.

Το μοντέλο ταυτόχρονης πρόσβασης του NGINX

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

Το NGINX διαθέτει αρχιτεκτονική «worker» με μία κύρια διεργασία (master process) και πολλαπλές διεργασίες εργασίας (worker processes). Οι συνδέσεις κατανέμονται στους workers, ώστε να μπορούν να επεξεργάζονται ταυτόχρονα πολλαπλά αιτήματα. Ωστόσο, αυτή η διάταξη έχει και ένα μειονέκτημα: όταν ένας worker είναι απασχολημένος με την επεξεργασία ενός αιτήματος, τα άλλα αιτήματα που έχουν επίσης ανατεθεί στον ίδιο worker πρέπει να περιμένουν. Αυτό μπορεί να οδηγήσει σε καθυστέρηση σε πολλά αιτήματα εξαιτίας ενός μόνο βαρύου αιτήματος, ακόμη και αν οι άλλοι workers δεν έχουν καμία εργασία. Αυτό το φαινόμενο φαίνεται καθαρά στην παρακάτω εικόνα. Κατά τη δημιουργία αυτής της εικόνας, εκτελείται μια δοκιμή φόρτωσης σε 10 διαφορετικές συνδέσεις. Στην εικόνα φαίνεται ξεκάθαρα ότι 3 εργαζόμενες διεργασίες είναι απασχολημένες, ενώ μια τέταρτη δεν κάνει σχεδόν τίποτα.

Ώρα για το Prexy

Με το εμπόδιο να είναι πλέον σαφές, καταρτίσαμε ένα σχέδιο για τη βελτίωσή του. Το έργο αυτό ονομάστηκε Prexy, μια σύνθετη λέξη από τα RegEx και Proxy. Υπήρχαν 3 βασικές απαιτήσεις για το τελικό αποτέλεσμα:

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

  2. Οι κανόνες αντικατάστασης πρέπει να εφαρμόζονται κατά τη ροή, ώστε να αποφεύγεται η υπερβολική χρήση μνήμης από τη λύση.

  3. Η νέα λύση πρέπει να είναι ταχύτερη από την τρέχουσα λύση.

Πρώτα απ’ όλα, έπρεπε να αναζητήσουμε ένα ισχυρό περιβάλλον εκτέλεσης πολλαπλών νημάτων (multi-threaded runtime) που να μπορεί να επεξεργάζεται τα αιτήματα με αποτελεσματικό τρόπο. Αφού εξετάσαμε διάφορες επιλογές, η επιλογή μας έπεσε στο περιβάλλον εκτέλεσης Tokio για τη γλώσσα προγραμματισμού Rust. Η Rust είναι γνωστή για το ότι επιτρέπει τη συγγραφή γρήγορων προγραμμάτων, χωρίς τους κινδύνους ασφάλειας που ενέχουν άλλες γλώσσες χαμηλού επιπέδου, όπως η C++. Το Tokio runtime είναι ένα ευέλικτο ασύγχρονο runtime που έχει δημιουργηθεί για εφαρμογές δικτύου. Μία από τις σημαντικότερες λειτουργίες για εμάς είναι το γεγονός ότι υποστηρίζει «work-stealing». Αυτό σημαίνει ότι, σε αντίθεση με το NGINX, ένα αίτημα δεν δεσμεύεται σε έναν μόνο worker, αλλά ένας worker που δεν έχει δουλειά μπορεί να «κλέψει» εργασία από έναν άλλο worker. Με αυτόν τον τρόπο, οι πόροι των διακομιστών μας αξιοποιούνται καλύτερα.

Πρώτο πρωτότυπο

Αφού επιλέξαμε τις υποκείμενες τεχνολογίες, αποφασίσαμε να δημιουργήσουμε ένα πρώτο πρωτότυπο για να εκτιμήσουμε πόση αύξηση ταχύτητας περίπου θα μας προσφέρει αυτό το έργο και αν αξίζει τον κόπο. Ως πρώτη υλοποίηση για τον κινητήρα regex (το τμήμα που επεξεργάζεται και εφαρμόζει πραγματικά τους κανόνες), χρησιμοποιήσαμε την τυπική βιβλιοθήκη regex (ή «crate», όπως ονομάζεται στην ορολογία της Rust). Αυτός ο μηχανισμός, όπως και το sregex, είναι «non-backtracking», πράγμα που σημαίνει ότι υπάρχει μικρότερος κίνδυνος εμφάνισης προβλήματος ReDoS. Σε περίπτωση ReDoS, μια κανονική έκφραση αντιμετωπίζει μια τέτοια είσοδο, ώστε ο χρόνος που απαιτείται για την αξιολόγησή της να αυξάνεται εκθετικά. Αυτό μπορεί να είναι καταστροφικό για την απόδοση και προκάλεσε μια σημαντική διακοπή λειτουργίας στην Cloudflare. Γι’ αυτό είναι σημαντικό για εμάς ο μηχανισμός που χρησιμοποιούμε να είναι χωρίς backtracking.

Με αυτόν τον μηχανισμό regex δημιουργήσαμε ένα πρώτο πρωτότυπο. Αυτό το πρωτότυπο χρησιμοποιούσε κανόνες που είχαν προγραμματιστεί σταθερά (hard-coded) και ο μηχανισμός regex αποθήκευε ολόκληρη την απόκριση στο buffer αντί να την μεταδίδει σε ροή, αλλά αυτό ήταν αρκετό για μια πρώτη δοκιμή απόδοσης. 

Με αυτόν τον μηχανισμό regex δημιουργήσαμε ένα πρώτο πρωτότυπο. Αυτό το πρωτότυπο χρησιμοποιούσε κανόνες που είχαν προγραμματιστεί σταθερά (hard-coded) και ο μηχανισμός regex αποθήκευε ολόκληρη την απόκριση στο buffer αντί να την μεταδίδει συνεχώς, αλλά αυτό ήταν αρκετό για μια πρώτη δοκιμή απόδοσης. Η δοκιμαστική μας εγκατάσταση αποτελούνταν από δύο εικονικές μηχανές (VM). Σε μία VM εκτελούταν μια εγκατάσταση στην οποία μπορούσαν να λειτουργούν τόσο η παλιά όσο και η νέα λύση. Με αυτόν τον τρόπο μπορούσαμε εύκολα να κάνουμε συγκρίσεις μεταξύ των δύο μεθόδων. Στον άλλο διακομιστή εκτελούταν το NGINX, το οποίο λειτουργούσε ως διακομιστής προέλευσης και εξυπηρετούσε ένα αρχείο δοκιμής. Στον ίδιο διακομιστή εκτελούταν επίσης το εργαλείο wrk, με το οποίο δημιουργούσαμε το φορτίο. Αυτό δεν είναι απολύτως βέλτιστο, καθώς για καθαρά δεδομένα είναι προτιμότερο να εκτελείται ο γεννήτρια φορτίου σε ξεχωριστή VM, αλλά αυτή η VM διέθετε αρκετούς πόρους ώστε το NGINX και το wrk να μην παρεμποδίζουν το ένα το άλλο.

Τα πρώτα αποτελέσματα των δοκιμών ήταν απολύτως σαφή: το Prexy ήταν περίπου 22 φορές ταχύτερο στην επεξεργασία ενός αιτήματος (βλ. την εικόνα παρακάτω). Αυτό έδωσε οριστικά το πράσινο φως στο έργο, καθώς με μια τόσο μεγάλη διαφορά υπήρχε αρκετός περιθώριος για να αντιμετωπιστούν τυχόν μειώσεις στην απόδοση που θα μπορούσαν να προκύψουν κατά την ολοκλήρωση της λειτουργικότητας.

Μετά την πρώτη δοκιμή, πραγματοποιήσαμε ορισμένες προφανείς βελτιστοποιήσεις, όπως η αποθήκευση σε cache των μεταγλωττισμένων κανονικών εκφράσεων. Επιπλέον, εφαρμόσαμε έναν πιο αποδοτικό τρόπο επαναχρησιμοποίησης των συνδέσεων προς το upstream. Με αυτόν τον τρόπο, επιτύχαμε αύξηση της ταχύτητας κατά περίπου 20%. Στη συνέχεια, δοκιμάσαμε και τις δύο λύσεις υπό συνθήκες αιχμής, στέλνοντας όσο το δυνατόν περισσότερες αιτήσεις στον διακομιστή. Και σε αυτή την περίπτωση, η διαφορά ήταν σαφής, με το Prexy να επιτυγχάνει ρυθμό διακίνησης περίπου 25 φορές υψηλότερο από την παλιά λύση.

Μηχανή εκτέλεσης εκφράσεων κανονικών εκφράσεων (regex)

Μία από τις απαιτήσεις αυτού του έργου ήταν η μηχανή regex που χρησιμοποιείται να είναι τύπου streaming. Η μηχανή regex του πρωτοτύπου δεν ήταν τέτοια, οπότε σε αυτό το επίπεδο χρειάστηκε επιπλέον εργασία. Ωστόσο, η μηχανή regex αποδείχθηκε πολύ βελτιστοποιημένη, οπότε αποφασίσαμε να χρησιμοποιήσουμε αυτή την υλοποίηση ως βάση για τη δική μας μηχανή streaming. Όταν μεταδίδετε δεδομένα μέσω μιας μηχανής regex, υπάρχουν ορισμένα σημαντικά ζητήματα που πρέπει να έχετε υπόψιν. Πρώτον, δεν γνωρίζετε ποια δεδομένα θα ακολουθήσουν. Επομένως, πρέπει να εργαστείτε με μερικές αντιστοιχίσεις: αντιστοιχίσεις που δεν είναι ακόμη πλήρεις, αλλά έχουν ήδη αντιστοιχίσει 1 ή περισσότερους χαρακτήρες. Όταν εντοπίζεται μια πλήρης αντιστοιχία, πρέπει στη συνέχεια να ελέγχεται ότι δεν υπάρχουν επικαλυπτόμενες μερικές αντιστοιχίσεις, καθώς αυτές ενδέχεται τελικά να αποτελέσουν αντιστοιχίες (και μια μακρύτερη αντιστοιχία υπερισχύει μιας μικρότερης σε περίπτωση επικάλυψης). Επομένως, μπορείτε να επεξεργαστείτε μια αντιστοίχιση μόνο όταν όλες οι μερικές αντιστοιχίσεις που υπάρχουν εκείνη τη στιγμή αποδειχθούν ότι δεν αποτελούν πλήρη αντιστοίχιση.

Περαιτέρω βελτιστοποιήσεις

Η αναδιαμόρφωση του μηχανισμού regex είχε, όπως ήταν αναμενόμενο, αρνητική επίδραση στις επιδόσεις του Prexy. Ωστόσο, υπήρχε ακόμα αρκετό περιθώριο σε σύγκριση με την παλιά λύση και, με την προσθήκη περαιτέρω βελτιστοποιήσεων, καταλήξαμε τελικά να έχουμε ακόμη καλύτερες επιδόσεις από ό,τι πριν την αναδιαμόρφωση του μηχανισμού regex. Μία από τις βελτιστοποιήσεις που εφαρμόσαμε ήταν η απομνημόνευση του αν μια ρύθμιση πρέπει να εφαρμοστεί σε ένα συγκεκριμένο αρχείο. Τα στατικά στοιχεία, όπως τα αρχεία CSS και JS, σχεδόν ποτέ δεν αλλάζουν και, ως εκ τούτου, είναι άσκοπο να εκτελείται κάθε φορά ένας κανόνας, ενώ αυτός δεν ταιριάζει ποτέ με το συγκεκριμένο αρχείο. Μια εναλλακτική λύση ήταν η αποθήκευση του αποτελέσματος των αντικαταστάσεων στην κρυφή μνήμη, αλλά το μειονέκτημα αυτής της στρατηγικής είναι ότι απαιτείται μεγάλη μνήμη για την αποθήκευση όλων των αρχείων. Απομνημονεύοντας ποιες κανόνες πρέπει να εφαρμοστούν, χρειαζόμαστε μόνο λίγα byte ανά αρχείο, αποθηκεύοντας τις πληροφορίες ως bitmap.

Επιπλέον, αξιοποιούμε στο έπακρο τις βελτιστοποιήσεις εντός του μηχανισμού regex, για παράδειγμα, μη διατηρώντας ομάδες σύλληψης όταν αυτές δεν χρησιμοποιούνται στην αντικατάσταση. Έτσι, ο μηχανισμός regex χρειάζεται να απομνημονεύει μόνο την αρχή και το τέλος ολόκληρης της αντιστοίχισης, κάτι που εξοικονομεί εργασία. Επίσης, απενεργοποιήσαμε τις ονομασμένες ομάδες σύλληψης, καθώς αυτό ήταν αρκετά περίπλοκο στην έκδοση ροής και, δεδομένου ότι η παλιά λύση δεν το υποστήριζε, δεν ήταν απαραίτητα απαραίτητο. Όλες αυτές οι βελτιστοποιήσεις οδήγησαν τελικά στις ακόλουθες επιδόσεις:

Το τελικό αποτέλεσμα

Αφού δοκιμάσαμε διεξοδικά το Prexy για να βεβαιωθούμε ότι η λειτουργία του ήταν πράγματι ίδια με την παλιά λύση, ξεκινήσαμε τη σταδιακή εφαρμογή. Σε διάστημα περίπου μιας εβδομάδας, ενεργοποιήσαμε το Prexy για κάθε κλώνο. Στο σύστημα παρακολούθησής μας, η στιγμή της ενεργοποίησης ήταν συχνά ευδιάκριτη. Δεν ήταν σπάνιο να παρατηρούμε μείωση περίπου 50% στους χρόνους απόκρισης. Παρατηρήθηκαν επίσης μικρότερες διαφορές, της τάξης του 10% περίπου, σε άλλους πελάτες που είχαν ελάχιστους κανόνες αντικατάστασης. Ο δικός μας ιστότοπος έγινε περίπου 30% ταχύτερος (~50 ms).

Όσον αφορά τη συνολική μας υποδομή, παρατηρούμε τις ακόλουθες διαφορές μετά την εφαρμογή του Prexy.

  • 33% λιγότερη κατανάλωση μνήμης RAM

  • 20% λιγότερη κατανάλωση CPU

  • 10-50 % ταχύτερες απαντήσεις

Συνολικά, μπορούμε λοιπόν να πούμε ότι το Prexy ήταν ένα επιτυχημένο έργο. Με την αντικατάσταση ενός ξεπερασμένου module του NGINX, μπορούμε πλέον να χρησιμοποιούμε νεότερες και αποδοτικότερες τεχνικές που κάνουν μια σαφώς μετρήσιμη διαφορά για όλους τους πελάτες μας. Και στο μέλλον θα συνεχίσουμε να βελτιώνουμε το Clonable, τόσο όσον αφορά νέες λειτουργίες όσο και τη βελτίωση των υφιστάμενων λειτουργιών.


Σας ευχαριστούμε που διαβάσατε αυτό το τεχνικό blog. Ενημερώστε μας αν σας αρέσει να λαμβάνετε πιο συχνά τέτοιου είδους τεχνικές πληροφορίες για το προϊόν μας. Σας ενθουσίασε αυτό το έργο; Τότε ρίξτε μια ματιά στη σελίδα με τις θέσεις εργασίας μας :)

Ξεκινήστε σήμερα δωρεάν με το Clonable

Ανακαλύψτε πώς μπορείτε να διαχειρίζεστε διεθνείς ιστοσελίδες κεντρικά από μία μόνο πλατφόρμα.

Χρησιμοποιούμε cookies για να βελτιώσουμε την εμπειρία σας, να αναλύσουμε την επισκεψιμότητα του ιστότοπου και να βελτιώσουμε τις υπηρεσίες μας.

Διαβάστε την πολιτική μας για τα cookies