Ιστολόγιο

Ειδοποιήσεις που χτυπούν μία φορά, όχι μία φορά ανά διακομιστή

Μία από τις λειτουργικές μας ειδοποιήσεις, η «χαμηλά credits» για τη δημοσιευμένη εφαρμογή ενός builder, προορίζεται να φτάνει το πολύ μία φορά την ημέρα. Ένα απόγευμα έφτασε πέντε φορές. Η αιτία: κάθε αντίγραφο του API θυμόταν στη δική του μνήμη ποιες ειδοποιήσεις είχε ήδη στείλει, οπότε δύο διακομιστές την έστελναν δύο φορές, και κάθε deploy σβήνει αυτή τη μνήμη και την άφηνε να ξανασταλεί. Το ίδιο μοτίβο είχε και μια εργασία φόντου που έκανε τη δουλειά της δύο φορές. Τώρα και τα δύο συντονίζονται μέσω κοινής αποθήκευσης, και και τα δύο αποτυγχάνουν με ασφαλή τρόπο αν η αποθήκευση δεν είναι διαθέσιμη.

Το σύμπτωμα

Το Mythex στέλνει ειδοποιήσεις στην ομάδα όταν κάτι χρειάζεται προσοχή: μια υπηρεσία είναι εκτός λειτουργίας, μια εργασία κόλλησε, ή το υπόλοιπο credits ενός builder πέφτει ενώ η εφαρμογή του είναι δημοσιευμένη. Κάθε ειδοποίηση έχει ένα παράθυρο dedupe, δηλαδή μια περίοδο κατά την οποία η ίδια ειδοποίηση στέλνεται μόνο μία φορά. Για την ειδοποίηση χαμηλών credits, το παράθυρο είναι 24 ώρες.

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

Γιατί συνέβη

Το πακέτο ειδοποιήσεων κρατούσε το dedupe στη μνήμη: έναν πίνακα «κλειδί ειδοποίησης → πότε στάλθηκε τελευταία φορά», μέσα στη διεργασία που τρέχει. Αυτό δουλεύει όταν υπάρχει μία διεργασία που δεν επανεκκινείται ποτέ. Η παραγωγή δεν έχει ούτε το ένα ούτε το άλλο:

  • Δύο αντίγραφα του API. Η παραγωγή τρέχει δύο αντίγραφα του API. Κάθε ένα είχε τον δικό του πίνακα, οπότε κάθε ένα έστελνε την ειδοποίηση μία φορά. Αυτά είναι τα ζεύγη.
  • Deploys. Κάθε deploy αντικαθιστά και τα δύο αντίγραφα με νέες διεργασίες και άδειους πίνακες. Μετά από ένα deploy, η ειδοποίηση έφευγε ξανά, σαν να μην είχε σταλεί ποτέ.

Το κενό των ~11 λεπτών ανάμεσα στις δύο ειδοποιήσεις κάθε ζεύγους προερχόταν από την εργασία που δημιουργεί αυτή την ειδοποίηση. Τρέχει και στα δύο αντίγραφα και παίρνει ένα lock, οπότε τα αντίγραφα το κερδίζουν εκ περιτροπής, και καθένα δημιουργούσε την ειδοποίηση όταν ερχόταν η σειρά του.

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

Η διόρθωση

Ένα κοινό αρχείο ανά ειδοποίηση

Πριν το API στείλει μια ειδοποίηση, τώρα διεκδικεί το κλειδί dedupe της στο Redis, μια κοινή αποθήκευση στη μνήμη που μιλούν και τα δύο αντίγραφα και που δεν επανεκκινείται όταν επανεκκινείται το API. Η διεκδίκηση είναι μία λειτουργία «όρισε αυτό το κλειδί μόνο αν δεν υπάρχει, και λήξε το μετά το παράθυρο». Όποιο αντίγραφο το διεκδικήσει πρώτο στέλνει την ειδοποίηση· το άλλο βρίσκει το κλειδί πιασμένο και σιωπά. Μια νέα διεργασία μετά από deploy βρίσκει το κλειδί ακόμη εκεί.

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

Αποτυχία ανοιχτή, όχι κλειστή

Η πιο σημαντική απόφαση ήταν τι γίνεται όταν δεν μπορεί να φτάσει το Redis. Επιλέξαμε να σταλεί η ειδοποίηση ούτως ή άλλως. Αν το Redis είναι εκτός λειτουργίας ή δεν απαντά μέσα σε 1,5 δευτερόλεπτο, η ειδοποίηση φεύγει. Μια διπλή ειδοποίηση είναι θόρυβος. Μια χαμένη ειδοποίηση σημαίνει ότι κάποιος δεν ενημερώθηκε.

Οι πιο σοβαρές ειδοποιήσεις, οι P0, δεν κρατιούνται ποτέ πίσω από το dedupe.

Κάθε καταχώριση κρατά το δικό της παράθυρο

Το dedupe στη μνήμη του πακέτου ειδοποιήσεων αποθηκεύει τώρα πότε τελειώνει το παράθυρο κάθε καταχώρισης, και ο καθαρισμός αφαιρεί μόνο καταχωρίσεις των οποίων το παράθυρο έχει τελειώσει. Ένα κλειδί 24 ωρών μένει για 24 ώρες, όσες και να είναι οι άλλες ειδοποιήσεις που φτάνουν.

Το ίδιο σφάλμα, αλλού

Το ίδιο μοτίβο είχε διπλασιάσει και ένα κομμάτι δουλειάς φόντου: μια εργασία συμφιλίωσης που κρατά την καταγραφή της κατάστασης κάθε χώρου εργασίας ευθυγραμμισμένη με τον πάροχο sandbox. Κάθε πέρασμα απαριθμεί κάθε sandbox του λογαριασμού, χιλιάδες στην παραγωγή, για περίπου πέντε δευτερόλεπτα, συν κάθε έργο που τρέχει ή είναι σε παύση. Και τα δύο αντίγραφα το έτρεχαν κάθε δύο λεπτά.

Οι εγγραφές του είναι idempotent, δηλαδή η εκτέλεσή τους δύο φορές δίνει το ίδιο αποτέλεσμα με μία, οπότε τίποτα δεν χάλασε. Απλώς ήταν διπλάσια δουλειά. Τώρα κάθε παράθυρο δύο λεπτών διεκδικείται στο Redis με τον ίδιο τρόπο που ήδη διεκδικούσαν τα μετρητές χρέωσης, και μόνο το αντίγραφο που το κερδίζει εκτελεί το πέρασμα. Αν το Redis δεν είναι προσβάσιμο, το πέρασμα τρέχει ούτως ή άλλως: να γίνεται η δουλειά δύο φορές είναι αβλαβές, το να μην γίνεται καθόλου δεν είναι.

Πώς το δοκιμάσαμε

Τα τεστ μιμούνται το πραγματικό σύστημα. Φορτώνουν το πακέτο ειδοποιήσεων δύο φορές, ως δύο ξεχωριστά «αντίγραφα», δείχνουν και τα δύο στην ίδια κοινή αποθήκευση, δημιουργούν την ίδια ειδοποίηση από καθένα και μετά φορτώνουν ένα τρίτο, νέο αντίγραφο για να μιμηθούν το «μετά από deploy», και τη δημιουργούν ξανά. Στέλνεται ακριβώς μία ειδοποίηση. Άλλα τεστ ελέγχουν ότι η ειδοποίηση ενός άλλου builder εξακολουθεί να φεύγει, ότι μια P0 στέλνεται και από τα δύο αντίγραφα, και ότι η ειδοποίηση στέλνεται ακόμη και όταν η αποθήκευση είναι εκτός λειτουργίας ή κολλάει πέρα από το timeout. Η διόρθωση του καθαρισμού έχει δικό της τεστ: ένα κλειδί 24 ωρών επιβιώνει από 600 άλλες ειδοποιήσεις που φτάνουν 20 λεπτά αργότερα.

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

Τι κρατήσαμε από όλο αυτό

  • Η κατάσταση στη μνήμη είναι ανά διεργασία. Ό,τι πρέπει να ισχύει σε όλους τους διακομιστές («στάλθηκε μία φορά», «έτρεξε μία φορά») πρέπει να ζει κάπου που μοιράζονται.
  • Τα deploys είναι επανεκκινήσεις. Κατάσταση που επιβιώνει μόνο μέχρι την επόμενη επανεκκίνηση δεν επιβιώνει ούτε μια κανονική εργάσιμη μέρα.
  • Αποφασίστε προς ποια κατεύθυνση θα αποτύχετε. Για ειδοποιήσεις, μια αποθήκευση εκτός λειτουργίας πρέπει να σημαίνει περισσότερες ειδοποιήσεις, όχι λιγότερες.
  • Ψάξτε το μοτίβο, όχι μόνο το περιστατικό. Η ειδοποίηση ήταν η δυνατή περίπτωση· η διπλασιασμένη εργασία συμφιλίωσης ήταν το ίδιο σφάλμα, που κόστιζε δουλειά αθόρυβα.

Τίποτα από αυτά δεν φαίνεται από μέσα στο Mythex. Για περισσότερα για το πώς κρατάμε σταθερή την πλατφόρμα, δείτε πώς φτάσαμε τα deploys στο μηδέν σφάλματα 502 και το αρχείο swap που έλειπε. Αν δεν έχετε ξανασκεφτεί την ιδέα να τρέχουν πολλά αντίγραφα μιας υπηρεσίας, ο οδηγός μας για τι είναι ένας διακομιστής καλύπτει τα βασικά.

Mythex · Pricing · Χτίστε οτιδήποτε με AI · Πρότυπα ιστοσελίδων και εφαρμογών · Το Mythex σε σύγκριση · Γλωσσάρι δημιουργίας εφαρμογών με AI · Δωρεάν εργαλεία δημιουργίας εφαρμογών με AI · English

English አማርኛ العربية Български বাংলা Bosanski Català Čeština Dansk Deutsch Español Español (España) Eesti فارسی Suomi Français Français (Canada) ગુજરાતી हिन्दी Hrvatski Magyar Հայերեն Bahasa Indonesia Íslenska Italiano 日本語 ქართული Қазақ тілі ಕನ್ನಡ 한국어 Lietuvių Latviešu Македонски മലയാളം Монгол मराठी Bahasa Melayu မြန်မာ Norsk bokmål Nederlands ਪੰਜਾਬੀ Polski Português (Brasil) Português (Portugal) Română Русский Slovenčina Slovenščina Soomaali Shqip Srpski Svenska Kiswahili தமிழ் తెలుగు ไทย Tagalog Türkçe Українська اردو Tiếng Việt 简体中文 繁體中文(香港) 繁體中文(台灣)