Πώς φτάσαμε σε μηδέν σφάλματα 502 στις αναπτύξεις
Κάθε φορά που κάναμε ανάπτυξη του backend του Mythex, λίγα αιτήματα αποτύγχαναν με 502 Bad Gateway. Δεν ήταν πολλά, όμως ένα 502 κατά την ανάπτυξη σημαίνει ότι το κλικ κάποιου δεν έκανε τίποτα, ή ότι ένα build που παρακολουθούσε στάθηκε για λίγο. Το ανιχνεύσαμε σε δύο ξεχωριστά προβλήματα: ένα αυτοθεραπευόμενο watchdog που μάχονταν τις αναπτύξεις μας, και ένα κενό ανάμεσα στη στιγμή που ένας διακομιστής σταματά να ακούει και τη στιγμή που ο load balancer σταματά να του στέλνει κίνηση. Στις τρεις συνεχόμενες αναπτύξεις παραγωγής μετά τις διορθώσεις, ο αριθμός πήγε από 10 σε 6 και μετά σε 0.
Η διάταξη
Το API και ο ενορχηστρωτής μας τρέχουν ως containers σε μια μικρή ομάδα διακομιστών πίσω από ένα AWS Application Load Balancer (ALB). Μια ανάπτυξη ενημερώνει έναν διακομιστή τη φορά: αποσύρει τα παλιά containers, ξεκινά καινούργια, και προχωρά. Ένα watchdog σε κάθε διακομιστή ελέγχει ότι τα containers τρέχουν και επανεκκινεί όσα δεν τρέχουν — υπάρχει ώστε μια διεργασία που κρασάρει στις 3 το πρωί να επιστρέφει χωρίς να ξυπνήσει κανείς.
Και τα δύο είναι λογικά από μόνα τους. Μαζί δεν ήταν.
Πρόβλημα 1: το watchdog συναγωνιζόταν την ανάπτυξη
Το watchdog έτρεχε μία φορά τον λεπτό και «θεράπευε» σε έναν αποτυχημένο έλεγχο υγείας. Μια ανάπτυξη αποσύρει σκόπιμα τις υπηρεσίες για έως 150 δευτερόλεπτα — και σε αυτό το διάστημα ένας έλεγχος υγείας υποτίθεται ότι αποτυγχάνει. Άρα το watchdog ενεργοποιούνταν σε κάθε ανάπτυξη.
Τις περισσότερες φορές έχανε τον αγώνα χωρίς ζημιά. Μία φορά, τον κέρδισε: ξαναέγραψε το αρχείο compose (την περιγραφή των containers που πρέπει να τρέχουν) στη μέση μιας ανάπτυξης, και άφησε έναν διακομιστή χωρίς καθόλου container gateway — ενώ η εργασία ανάπτυξης ανέφερε επιτυχία.
Οι διορθώσεις:
- Ένα κλείδωμα. Η ανάπτυξη παίρνει πλέον το ίδιο κλείδωμα αρχείου με το σενάριο αρχικοποίησης του διακομιστή, ώστε το watchdog και η ανάπτυξη να μην τρέχουν ποτέ ταυτόχρονα.
- Τέσσερα χτυπήματα, όχι ένα. Το watchdog χρειάζεται τέσσερις διαδοχικούς αποτυχημένους ελέγχους πριν ενεργήσει. Μια υπηρεσία που αποσύρεται δεν μοιάζει πια με νεκρή.
- Ένας ορισμός. Το αρχείο compose έχει πλέον μία μόνο πηγή, δημοσιευμένη στους διακομιστές, αντί για αντίγραφα που μπορούσαν να αποκλίνουν.
- Επαλήθευση, όχι εμπιστοσύνη. Η εργασία ανάπτυξης δεν περνά πια επειδή μια εντολή επέστρεψε. Ελέγχει το API, τον ενορχηστρωτή, το gateway και τον εργάτη δημοσίευσης κάθε διακομιστή πριν μπορέσει να ολοκληρωθεί.
Πρόβλημα 2: ο διακομιστής έφευγε πριν φύγει η κίνηση
Το δεύτερο πρόβλημα ήταν πιο διακριτικό. Όταν μια ανάπτυξη αποσύρει διακομιστή, ο διακομιστής αρχίζει να απαντά στο endpoint υγείας του με 503, ώστε ο load balancer να τον βγάλει από την εξυπηρέτηση. Το δικό μας το έκανε για 5 δευτερόλεπτα, και μετά έκλεινε.
Όμως το ALB ελέγχει την υγεία μόνο κατά διαστήματα, και χρειάζεται αρκετούς αποτυχημένους ελέγχους στη σειρά πριν σταματήσει να στέλνει σε έναν στόχο — έως περίπου 60 δευτερόλεπτα συνολικά. Άρα για περίπου 55 δευτερόλεπτα, ο load balancer συνέχιζε να στέλνει πραγματικά αιτήματα σε διακομιστή που είχε ήδη σταματήσει να ακούει. Αυτά ήταν τα δικά μας 502.
Η διόρθωση ήταν να συμφωνήσουν τα δύο χρονοδιαγράμματα:
- Η ομάδα στόχων ελέγχει πλέον κάθε 5 δευτερόλεπτα και σημαίνει έναν στόχο ως μη υγιή μετά από 2 αποτυχίες — περίπου 10 δευτερόλεπτα για να το καταλάβει.
- Η απόσυρση εξακολουθεί να εξυπηρετεί πραγματική κίνηση για 15 δευτερόλεπτα αφού αρχίσει να αποτυγχάνει στους ελέγχους υγείας, ώστε να υπάρχει πάντα επικάλυψη.
- Αυτές οι ρυθμίσεις ζουν στο σενάριο που παρέχει την υποδομή, όχι σε κονσόλα που κάποιος κλικάρισε μία φορά.
Να κρατούν οι αριθμοί ο ένας τον άλλον
Τα χρονικά όρια αυτού του είδους εξαρτώνται το ένα από το άλλο. Η απόσυρση πρέπει να διαρκεί περισσότερο από όσο χρειάζεται ο balancer για να το καταλάβει· η απόσυρση συν τον τερματισμό πρέπει να τελειώσει πριν σκοτωθεί βίαια το container. Εύκολα κάποιος αλλάζει έναν αριθμό σε έξι μήνες και ξαναφέρνει αθόρυβα το σφάλμα.
Γι' αυτό γράψαμε αυτές τις σχέσεις ως τεστ: η απόσυρση πρέπει να είναι μεγαλύτερη από το διάστημα ελέγχου επί το όριο αποτυχιών· ο χρόνος για να ξαναθεωρηθεί ως υγιής πρέπει να μένει μικρός· ολόκληρη η απόσυρση πρέπει να χωράει μέσα στη χάρη τερματισμού του container. Αλλάξτε έναν αριθμό χωρίς τους άλλους και το CI αποτυγχάνει. Ελέγξαμε και τα ίδια τα τεστ με mutation testing — αλλάξαμε κάθε αριθμό σε λάθος τιμή και ελέγξαμε ότι κάποιο τεστ το πιάνει.
Η μέτρηση μιας ανάπτυξης είναι εύκολο να γίνει λάθος
Η πρώτη μέτρησή μας έλεγε ότι μια καθαρή ανάπτυξη ήταν περίπου 3% χαλασμένη. Δεν ήταν. Μετρούσαμε κάθε απάντηση που δεν ήταν 200, και ένα 503 από endpoint υγείας κατά τη διάρκεια απόσυρσης δεν είναι αποτυχία — είναι η απόσυρση που κάνει τη δουλειά της, λέγοντας στον balancer να μην επιλέγει αυτόν τον διακομιστή ενώ συνεχίζει να εξυπηρετεί πραγματικά αιτήματα.
Αυτό που νιώθει ο χρήστης είναι ένα 502: το αίτημα δεν έφτασε σε τίποτα. Μετρώντας μόνο αυτά βγήκε η πραγματική εικόνα — 10, μετά 6, μετά 0 σε τρεις αναπτύξεις παραγωγής.
Τι κρατήσαμε από όλο αυτό
- Η αυτοθεραπεία χρειάζεται να ξέρει για τα προγραμματισμένα διαστήματα μη διαθεσιμότητας. Ένα watchdog που δεν μπορεί να ξεχωρίσει μια ανάπτυξη από μια κατάρρευση θα μαλώσει κάποια στιγμή με την ανάπτυξη.
- Η απόσυρση είναι χειραψία. Ο διακομιστής πρέπει να συνεχίζει να εξυπηρετεί μέχρι ο load balancer να έχει αποδείξει ότι σταμάτησε να στέλνει κίνηση, και αυτή η συγχρονισμένη χρονική στιγμή ορίζεται στον balancer, όχι στον διακομιστή.
- Κωδικοποιήστε τις αναλλοίωτες ως τεστ. Οι λειτουργικοί αριθμοί παρεκκλίνουν. Τα τεστ που τους συσχετίζουν μεταξύ τους δεν παρεκκλίνουν.
- Αποφασίστε τι μετράτε πριν μετρήσετε. Τα «σφάλματα» ήταν λάθος μέτρο· τα «αιτήματα που δεν έφτασαν πουθενά» ήταν το σωστό.
Αν δημοσιεύετε μια εφαρμογή στο Mythex, τίποτα από όλα αυτά δεν φαίνεται σε εσάς — που είναι και το ζητούμενο. Σχετικά: πώς η δημοσιευμένη εφαρμογή σας μένει online όσο την ξαναδημοσιεύετε.