Ευρετήριο ημερολογίουDockup / σημείωση πεδίου
Note / deploy-succeeded-but-site-is-down

Η ανάπτυξη ολοκληρώθηκε, αλλά ο ιστότοπος είναι εκτός λειτουργίας

Ο πίνακας ελέγχου δείχνει ότι η εφαρμογή εκτελείται, ενώ οι χρήστες βλέπουν σφάλμα. Μάθετε γιατί η επιτυχής ανάπτυξη και η υγεία της εφαρμογής είναι διαφορετικές ενδείξεις και πώς να κάνετε μια πράσινη ανάπτυξη να σημαίνει ότι η εφαρμογή απαντά πραγματικά.

Υπάρχει ένα συγκεκριμένο είδος κακού πρωινού που ξεκινά με ένα πράσινο σημάδι επιλογής. Η ανάπτυξη ολοκληρώθηκε αλλά ο ιστότοπος είναι εκτός λειτουργίας, ο πίνακας ελέγχου δείχνει running και κάποιος σας στέλνει screenshot με σφάλμα 502.

Αυτό δεν είναι σπάνια ακραία περίπτωση. Είναι το προβλέψιμο αποτέλεσμα μιας πλατφόρμας που αναφέρει ένα πράγμα και μετρά κάτι άλλο — και αξίζει να το κατανοήσετε με ακρίβεια, γιατί η λύση δεν είναι να «ελέγχετε πιο σχολαστικά», αλλά να αλλάξετε το τι επιτρέπεται να σημαίνει η λέξη running.

Τρεις διαφορετικές ερωτήσεις, μία ένδειξη κατάστασης

Όταν μια πλατφόρμα λέει ότι μια υπηρεσία εκτελείται, μπορεί να απαντά σε οποιαδήποτε από τις παρακάτω ερωτήσεις:

  1. Ξεκίνησε το container; Η διεργασία υπάρχει και δεν έχει τερματιστεί.
  2. Είναι ανοιχτή η θύρα; Κάτι ακούει στη διεύθυνση όπου περιμένει η πλατφόρμα.
  3. Απαντά σωστά η εφαρμογή; Ένα request λαμβάνει απόκριση που σημαίνει ότι η εφαρμογή είναι έτοιμη να λειτουργήσει.

Αυτές είναι εντελώς διαφορετικές εγγυήσεις και τα περισσότερα incidents αυτού του τύπου προκύπτουν επειδή ο πίνακας ελέγχου απαντά στην ερώτηση 1, ενώ εσείς υποθέτετε ότι απαντά στην ερώτηση 3.

Μια διεργασία Node που ξεκινά, αποτυγχάνει να συνδεθεί στη βάση δεδομένων και παραμένει σε loop επαναπροσπαθειών ικανοποιεί για πάντα την ερώτηση 1. Δεν έχει καταρρεύσει. Δεν θα εξυπηρετήσει ποτέ request. Το container είναι «σε λειτουργία» με κάθε έννοια που ενδιαφέρει τον orchestrator.

Το κενό όπου βρίσκεται η διακοπή λειτουργίας

Το επικίνδυνο διάστημα βρίσκεται ανάμεσα στη στιγμή που «ξεκίνησε η νέα έκδοση» και στη στιγμή που «η νέα έκδοση μπορεί να λειτουργήσει». Σε αυτό το διάστημα, μια απλοϊκή πλατφόρμα έχει ήδη μεταφέρει την κίνηση, επειδή το μόνο που μέτρησε ήταν η εκκίνηση.

Αυτό είναι χειρότερο από ένα απλό crash λόγω του τρόπου λειτουργίας του rollback. Ένα crash loop είναι θορυβώδες: το container τερματίζεται, επανεκκινείται, τερματίζεται ξανά και η πλατφόρμα το αντιλαμβάνεται τελικά. Μια εφαρμογή που ξεκινά και κολλάει είναι αθόρυβη. Δεν γίνεται επανεκκίνηση, δεν ενεργοποιείται καμία ειδοποίηση και η προηγούμενη λειτουργική έκδοση συνήθως έχει ήδη απομακρυνθεί.

Αυτό το τελευταίο είναι η πραγματική ζημιά. Η παλιά έκδοση λειτουργούσε κανονικά. Αφαιρέθηκε επειδή ξεκίνησε μια νέα έκδοση και η εκκίνηση θεωρήθηκε λανθασμένα ένδειξη λειτουργίας.

Τι κάνει ένας πραγματικός έλεγχος υγείας πριν από τη μεταφορά κίνησης

Η λύση είναι δομική, όχι διαδικαστική. Η κίνηση δεν πρέπει να μεταφέρεται μέχρι η νέα έκδοση να έχει απαντήσει σε ένα request.

Στο Dockup, ένα release λειτουργεί ως εξής: η νέα έκδοση γίνεται build απομονωμένα, ξεκινά παράλληλα με την έκδοση που εξυπηρετεί αυτή τη στιγμή και στη συνέχεια δέχεται μια ερώτηση. Μόνο όταν απαντήσει, το domain δείχνει προς αυτήν. Αν δεν απαντήσει ποτέ, το release σταματά εκεί και η προηγούμενη έκδοση συνεχίζει να εξυπηρετεί — κανείς εκτός του πίνακα ελέγχου σας δεν μαθαίνει ποτέ ότι επιχειρήθηκε ανάπτυξη.

Γι’ αυτό μια αποτυχημένη ανάπτυξη στο Dockup δεν αποτελεί διακοπή λειτουργίας. Το παλιό container δεν αφαιρέθηκε ποτέ με την υπόθεση ότι το νέο θα λειτουργούσε σωστά.

# The health gate is per-service configuration, not a platform default you inherit
dockup info my-project/my-api --json

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

Ρυθμίστε τον έλεγχο ώστε να απαντά στην ερώτηση 3

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

Ένα χρήσιμο readiness endpoint επαληθεύει τα πράγματα χωρίς τα οποία η εφαρμογή δεν μπορεί να λειτουργήσει:

// Not this — it proves only that the process is alive
app.get('/healthz', (req, res) => res.send('ok'))

// This — it proves the app can actually serve a request
app.get('/healthz', async (req, res) => {
  try {
    await db.query('select 1')       // the dependency that is usually the problem
    if (!cacheReady) throw new Error('cache warming')
    res.status(200).json({ ok: true })
  } catch (err) {
    res.status(503).json({ ok: false, reason: err.message })
  }
})

Δύο κανόνες κάνουν αυτή την προσέγγιση να λειτουργεί στην πράξη:

Ελέγξτε τις εξαρτήσεις χωρίς τις οποίες δεν μπορείτε να εξυπηρετήσετε requests, και τίποτα άλλο. Αν η εφαρμογή σας μπορεί να λειτουργήσει υποβαθμισμένα όταν το search index δεν είναι διαθέσιμο, μην κάνετε το readiness να εξαρτάται από το search index — θα μπλοκάρετε τις αναπτύξεις για κάτι που δεν αποτελεί διακοπή λειτουργίας.

Κρατήστε τον έλεγχο οικονομικό. Το endpoint καλείται επανειλημμένα σε κάθε release. Ένας readiness έλεγχος που εκτελεί ένα ακριβό query δημιουργεί πρόβλημα φόρτου με δική σας ευθύνη.

Δώστε του αρκετό χρόνο, αλλά όχι απεριόριστο

Δύο ρυθμίσεις καθορίζουν αν ο έλεγχος βοηθά ή βλάπτει:

  • Το timeout ανά προσπάθεια πρέπει να είναι μεγαλύτερο από τον πιο αργό νόμιμο cold start. Μια εφαρμογή που συνδέεται σε βάση δεδομένων και θερμαίνει μια cache σε οκτώ δευτερόλεπτα θα αποτυγχάνει κάθε φορά σε έναν έλεγχο τριών δευτερολέπτων και θα «διορθώσετε» το πρόβλημα απενεργοποιώντας τον έλεγχο — επιστρέφοντας έτσι στο σημείο από όπου ξεκινήσατε.
  • Οι επαναπροσπάθειες πρέπει να καλύπτουν τον συνολικό χρόνο εκκίνησης και όχι μία μόνο προσπάθεια. Το διάστημα × οι επαναπροσπάθειες είναι ο πραγματικός διαθέσιμος χρόνος.

Στο Dockup, αυτές είναι οι healthCheckInterval, healthCheckTimeout και healthCheckRetries και ορίζονται ανά υπηρεσία, επειδή ένα monolith σε Rails και ένα sidecar σε Go δεν ξεκινούν με το ίδιο χρονοδιάγραμμα.

Όταν η εφαρμογή είναι ήδη εκτός λειτουργίας

Αν διαβάζετε αυτό κατά τη διάρκεια ενός incident, η σειρά ενεργειών που το επιλύει πιο γρήγορα είναι η εξής:

  1. Ελέγξτε αν η εφαρμογή απαντά απευθείας, παρακάμπτοντας το domain. Αν απαντά στη θύρα της αλλά όχι μέσω του domain, πρόκειται για πρόβλημα routing και όχι για πρόβλημα της εφαρμογής· επομένως σταματήστε να κάνετε debugging στον κώδικά σας.
  2. Διαβάστε τα runtime logs και όχι τα build logs. Το build ολοκληρώθηκε με επιτυχία — αυτή είναι η αφετηρία μας. Αυτό που θέλετε να δείτε είναι τι έκανε η διεργασία μετά την εκκίνησή της.
  3. Κάντε rollback πριν από τη διάγνωση. Η διάγνωση είναι φθηνότερη όταν κανείς δεν περιμένει.
dockup logs my-project/my-api --follow            # what the running process is saying
dockup deployments my-project/my-api              # what was live before this
dockup rollback <deployment-id> my-project/my-api # put that back

Στο Dockup, το rollback είναι αλλαγή έκδοσης και όχι rebuild, επειδή η προηγούμενη έκδοση βρίσκεται ακόμη στον δίσκο. Αυτό έχει σημασία στις 3 τα ξημερώματα: η ταχύτερη αποκατάσταση είναι εκείνη που δεν χρειάζεται να κάνει compile τίποτα.

Η ερώτηση που πρέπει να κάνετε σε μια πλατφόρμα

Όταν επιλέγετε πού θα εκτελείται το production, αξίζει να το δοκιμάσετε σκόπιμα: κάντε deploy μια εφαρμογή που ξεκινά με επιτυχία και στη συνέχεια αποτυγχάνει να συνδεθεί στη βάση δεδομένων της. Παρακολουθήστε τι εμφανίζει ο πίνακας ελέγχου.

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

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

Γιατί ο πίνακας ελέγχου μου δείχνει ότι η εφαρμογή εκτελείται ενώ ο ιστότοπος είναι εκτός λειτουργίας; Επειδή το «running» συνήθως σημαίνει ότι η διεργασία του container υπάρχει και όχι ότι η εφαρμογή μπορεί να εξυπηρετήσει ένα request. Μια διεργασία που έχει κολλήσει και επαναλαμβάνει προσπάθειες σύνδεσης στη βάση δεδομένων ικανοποιεί επ’ αόριστον αυτόν τον ορισμό.

Πρέπει ένας health check να επικοινωνεί με τη βάση δεδομένων; Ναι, αν η εφαρμογή σας δεν μπορεί να εξυπηρετήσει requests χωρίς αυτήν. Ελέγξτε τις εξαρτήσεις που χρειάζεστε πραγματικά και παραλείψτε όσες δεν είναι απαραίτητες για τη λειτουργία της εφαρμογής.

Ποια είναι η διαφορά μεταξύ liveness και readiness; Το liveness ελέγχει αν πρέπει να γίνει επανεκκίνηση της διεργασίας. Το readiness ελέγχει αν πρέπει να της αποσταλεί κίνηση. Ο έλεγχος που αποτρέπει αυτή την αστοχία είναι το readiness και πρέπει να εκτελείται πριν μεταφερθεί η κίνηση.

Πώς μπορώ να εμποδίσω μια προβληματική ανάπτυξη από το να ρίξει εξαρχής τον ιστότοπο; Μεταφέρετε την κίνηση μόνο αφού η νέα έκδοση απαντήσει σε ένα πραγματικό request και κρατήστε την προηγούμενη έκδοση μέχρι να επιβεβαιωθεί η αλλαγή. Έτσι, ένα αποτυχημένο release είναι ένα release που δεν πραγματοποιήθηκε ποτέ και όχι μια διακοπή λειτουργίας.