Ευρετήριο ημερολογίουDockup / σημείωση πεδίου
Note / health-check-failing-deployment

Αποτυγχάνει το Health Check, αλλά η εφαρμογή λειτουργεί

Όταν ένα health check αποτυγχάνει στο deployment ενώ η εφαρμογή λειτουργεί κανονικά τοπικά, συνήθως υπάρχουν πέντε αιτίες. Ελέγξτε τη σύνδεση, τη διαδρομή, τη θύρα, τον χρόνο εκκίνησης και τις dependencies με τη σειρά που οδηγεί πιο γρήγορα στη λύση.

Υπάρχει ένα ιδιαίτερο είδος αδιεξόδου, όπου ένα health check που αποτυγχάνει στο deployment μπλοκάρει κάθε release, ενώ η εφαρμογή είναι, με κάθε μέτρο που μπορείτε να ελέγξετε, απολύτως εντάξει. Λειτουργεί τοπικά. Λειτουργεί σε Docker τοπικά. Τα logs δείχνουν ότι ακούει. Και η πλατφόρμα αναφέρει αποτυχία ξανά και ξανά, μερικές φορές είκοσι φορές στη σειρά, χωρίς να εμφανίζεται ποτέ κάποιο request στο access log.

Αυτή η τελευταία λεπτομέρεια είναι η σημαντική και περιορίζει αμέσως τις πιθανότητες. Αν η εφαρμογή σας δεν κατέγραψε ποτέ το request, ο έλεγχος δεν έφτασε στην εφαρμογή σας — επομένως τίποτα στον κώδικα της εφαρμογής δεν πρόκειται να το εξηγήσει.

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

1. Έχετε κάνει bind στο localhost

Αυτή είναι μακράν η συχνότερη αιτία και εξηγεί ακριβώς το σύμπτωμα «δεν φτάνει ποτέ traffic».

Μέσα σε ένα container, το 127.0.0.1 σημαίνει το loopback του ίδιου του container. Ένα health check που έρχεται από έξω δεν μπορεί να το προσεγγίσει. Η διεργασία ακούει, τα logs σας το επιβεβαιώνουν, αλλά το socket δεν είναι προσβάσιμο από κανένα σημείο που έχει σημασία.

// Unreachable from outside the container
app.listen(3000, '127.0.0.1')

// Correct
app.listen(3000, '0.0.0.0')

Τα frameworks διαφέρουν ως προς τα defaults τους και αρκετά από αυτά άλλαξαν το default μεταξύ major versions. Ελέγξτε σε ποια διεύθυνση κάνει πράγματι bind το framework σας, αντί να βασίζεστε σε αυτό που θυμάστε.

# Confirm from inside the running container
dockup exec "ss -ltn || netstat -ltn" my-project/my-api

Αν η διεύθυνση στην οποία ακούει είναι 127.0.0.1:3000 αντί για 0.0.0.0:3000, το βρήκατε και τίποτα άλλο σε αυτή τη λίστα δεν έχει σημασία.

2. Η θύρα που ελέγχει η πλατφόρμα δεν είναι η θύρα στην οποία σερβίρετε

Εμπλέκονται δύο θύρες και είναι εύκολο να τις συγχέετε: η θύρα στην οποία ακούει η διεργασία μέσα στο container και η θύρα στην οποία δρομολογεί η πλατφόρμα. Αν η εφαρμογή σας διαβάζει το PORT από το περιβάλλον και έχετε κάνει hardcode το 3000 κάπου σε ένα Dockerfile, οι δύο τιμές μπορεί να διαφέρουν σιωπηλά.

Το αξιόπιστο μοτίβο είναι να αφήσετε την πλατφόρμα να σας ενημερώσει:

const port = process.env.PORT || 3000
app.listen(port, '0.0.0.0')

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

3. Η διαδρομή επιστρέφει κάτι διαφορετικό από επιτυχία

Μια διαδρομή health check γίνεται match ακριβώς και ένας απρόσμενα μεγάλος αριθμός αποτυχιών οφείλεται σε redirect. Αν η εφαρμογή σας κάνει redirect από το /healthz στο /healthz/ ή επιβάλλει HTTPS με 301, ένας checker που θεωρεί επιτυχία μόνο τα 2xx θα αποτυγχάνει κάθε φορά, ενώ ο browser ακολουθεί το redirect και σας εμφανίζει μια σελίδα που λειτουργεί.

Τρεις συγκεκριμένες παγίδες:

  • Redirect λόγω τελικής κάθετου. Το /healthz/healthz/ είναι 301.
  • Υποχρεωτικό HTTPS. Ο εσωτερικός έλεγχος συνήθως έρχεται μέσω απλού HTTP στο loopback. Ένα unconditional HTTPS redirect τον αποτυγχάνει.
  • Auth middleware. Ένα global authentication guard που εκτελείται πριν από το routing θα επιστρέψει 401 και για τη διαδρομή health.

Εξαιρέστε ρητά τη διαδρομή health από το auth και την επιβολή HTTPS. Είναι το ένα route που πρέπει να είναι απολύτως απλό.

4. Ο έλεγχος ολοκληρώνεται γρηγορότερα από το cold start

Αν ο έλεγχος αποτυγχάνει μερικές φορές και μετά περνάει ή αποτυγχάνει στο deploy και περνάει όταν κάνετε retry, τότε πρόκειται για θέμα χρόνου και όχι ρύθμισης.

Το περιθώριο που χρειάζεστε δεν είναι μία προσπάθεια — είναι interval × retries. Μια εφαρμογή που χρειάζεται δώδεκα δευτερόλεπτα για να συνδεθεί στη βάση δεδομένων και να γεμίσει ένα cache χρειάζεται συνολικό περιθώριο μεγαλύτερο από δώδεκα δευτερόλεπτα. Διαφορετικά, θα αποτυγχάνει σε κάθε release και τελικά θα απενεργοποιήσετε το gate, αφαιρώντας το μοναδικό εμπόδιο ανάμεσα σε ένα broken build και τους χρήστες σας.

dockup info my-project/my-api --json | grep -A6 healthCheck

Ορίστε το timeout πάνω από τη μεγαλύτερη νόμιμη μεμονωμένη προσπάθεια και ρυθμίστε τα retries έτσι ώστε το interval × retries να ξεπερνά με άνεση τον μεγαλύτερο νόμιμο χρόνο εκκίνησης. Μετρήστε την εκκίνηση αντί να μαντεύετε — τα logs έχουν timestamps.

5. Η εφαρμογή δεν είναι πραγματικά έτοιμη

Η τελευταία περίπτωση είναι εκείνη για την οποία υπάρχει ο έλεγχος: η εφαρμογή ξεκίνησε, δεν μπόρεσε να επικοινωνήσει με μια dependency και κάνει retry. Δεν έχει καταρρεύσει, οπότε τίποτα δεν την επανεκκινεί. Δεν μπορεί να εξυπηρετήσει requests, οπότε ο έλεγχος αποτυγχάνει. Το σύστημα λειτουργεί ακριβώς όπως σχεδιάστηκε και σας ενημερώνει ότι αυτό το release δεν πρέπει να δεχτεί traffic.

Ο τρόπος να τη διακρίνετε από τις άλλες τέσσερις περιπτώσεις είναι ότι η εφαρμογή σας κατέγραψε το request και απάντησε με non-2xx. Αν το request εμφανίζεται στα logs σας, οι αιτίες 1 έως 3 έχουν αποκλειστεί.

Η διαγνωστική σειρά που εξοικονομεί χρόνο

# 1. Did the request reach the app at all?
dockup logs my-project/my-api --follow

# 2. What is the process actually bound to?
dockup exec "ss -ltn || netstat -ltn" my-project/my-api

# 3. Does the path answer from inside the container?
dockup exec "curl -si localhost:3000/healthz" my-project/my-api

# 4. What is the gate configured to expect?
dockup info my-project/my-api --json

Το βήμα 3 επιλύει τις περισσότερες από αυτές τις περιπτώσεις. Ένα curl από μέσα στο container αφαιρεί ταυτόχρονα κάθε μεταβλητή δικτύου: αν επιστρέφει 200 εκεί και η πλατφόρμα εξακολουθεί να αποτυγχάνει, το πρόβλημα είναι η διεύθυνση ή η θύρα και όχι η εφαρμογή. Αν επιστρέφει 301 ή 401, βρήκατε την αιτία χωρίς να αγγίξετε καθόλου την πλατφόρμα.

Γιατί αξίζει να διατηρήσετε το gate

Μετά το τέταρτο αποτυχημένο deploy, είναι δελεαστικό να απενεργοποιήσετε το health check και να ολοκληρώσετε το release. Αξίζει να θυμάστε τι ακριβώς απενεργοποιείτε.

Στο Dockup, το health gate είναι αυτό που κρατά ένα broken release μακριά από τους χρήστες σας. Η νέα έκδοση γίνεται build και ξεκινά ενώ η τρέχουσα συνεχίζει να εξυπηρετεί requests· το traffic μεταφέρεται μόνο όταν η νέα έκδοση απαντήσει. Αν απενεργοποιήσετε το gate, ενεργοποιείτε ξανά το failure mode όπου ένα container που ξεκινά αλλά δεν λειτουργεί αντικαθιστά ένα container που ήταν εντάξει.

Ένας έλεγχος που αποτυγχάνει σε τέσσερα releases στη σειρά είναι ενοχλητικός. Ένας έλεγχος που περνάει χωρίς όρους είναι ένας έλεγχος που δεν θα σταματήσει το deploy που έχει πραγματικά σημασία.

Συχνές ερωτήσεις

Γιατί αποτυγχάνει το health check όταν η εφαρμογή λειτουργεί τοπικά; Σχεδόν πάντα επειδή το container κάνει bind στο 127.0.0.1 αντί για το 0.0.0.0. Τοπικά συνδέεστε μέσω του ίδιου loopback· έξω από το container, αυτή η διεύθυνση δεν είναι προσβάσιμη.

Πρέπει το health endpoint να απαιτεί authentication; Όχι. Εξαιρέστε το από το global auth middleware, διαφορετικά ο checker θα λάβει 401 και το deploy θα αποτύχει ενώ η εφαρμογή λειτουργεί κανονικά.

Τι timeout πρέπει να χρησιμοποιήσω; Μεγαλύτερο από τη μεγαλύτερη νόμιμη μεμονωμένη προσπάθεια, με retries που καλύπτουν τον μεγαλύτερο νόμιμο cold start. Διαβάστε τον χρόνο εκκίνησης από τα logs αντί να τον μαντεύετε.

Είναι ασφαλές να απενεργοποιήσω το health check για να ξεμπλοκάρω ένα release; Ξεμπλοκάρει το release και αφαιρεί την προστασία που εμποδίζει μια broken έκδοση να λάβει traffic. Διορθώστε τον έλεγχο — στις περισσότερες περιπτώσεις η αιτία είναι μια bind address ή ένα redirect και η διόρθωση διαρκεί λίγα λεπτά.