Συμβουλές και κόλπα Kubernetes: χαρακτηριστικά χαριτωμένου τερματισμού λειτουργίας σε NGINX και PHP-FPM

Μια τυπική συνθήκη κατά την εφαρμογή CI/CD στο Kubernetes: η εφαρμογή πρέπει να μπορεί να μην αποδέχεται νέα αιτήματα πελατών πριν διακόψει τελείως και, το πιο σημαντικό, να ολοκληρώσει με επιτυχία τα υπάρχοντα.

Συμβουλές και κόλπα Kubernetes: χαρακτηριστικά χαριτωμένου τερματισμού λειτουργίας σε NGINX και PHP-FPM

Η συμμόρφωση με αυτήν την προϋπόθεση σάς επιτρέπει να επιτύχετε μηδενικό χρόνο διακοπής λειτουργίας κατά την ανάπτυξη. Ωστόσο, ακόμη και όταν χρησιμοποιείτε πολύ δημοφιλή πακέτα (όπως το NGINX και το PHP-FPM), μπορείτε να αντιμετωπίσετε δυσκολίες που θα οδηγήσουν σε ένα κύμα σφαλμάτων με κάθε ανάπτυξη...

Θεωρία. Πώς ζει ο λοβός

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

Θα πρέπει επίσης να θυμάστε ότι η προεπιλεγμένη περίοδος χάριτος είναι 30 δευτερόλεπτα: μετά από αυτό, το pod θα τερματιστεί και η εφαρμογή πρέπει να έχει χρόνο να επεξεργαστεί όλα τα αιτήματα πριν από αυτήν την περίοδο. Σημείωση: αν και οποιοδήποτε αίτημα διαρκεί περισσότερα από 5-10 δευτερόλεπτα είναι ήδη προβληματικό και ο χαριτωμένος τερματισμός λειτουργίας δεν θα το βοηθήσει πλέον...

Για να κατανοήσετε καλύτερα τι συμβαίνει όταν ένα pod τερματίζεται, απλώς δείτε το παρακάτω διάγραμμα:

Συμβουλές και κόλπα Kubernetes: χαρακτηριστικά χαριτωμένου τερματισμού λειτουργίας σε NGINX και PHP-FPM

A1, B1 - Λήψη αλλαγών σχετικά με την κατάσταση της εστίας
A2 - Αναχώρηση SIGTERM
B2 - Αφαίρεση μιας ομάδας από τα τελικά σημεία
B3 - Λήψη αλλαγών (η λίστα με τα τελικά σημεία έχει αλλάξει)
B4 - Ενημερώστε τους κανόνες iptables

Σημείωση: η διαγραφή της ομάδας τέλους σημείου και η αποστολή του SIGTERM δεν γίνονται διαδοχικά, αλλά παράλληλα. Και λόγω του γεγονότος ότι το Ingress δεν λαμβάνει αμέσως την ενημερωμένη λίστα των Endpoints, νέα αιτήματα από πελάτες θα αποστέλλονται στο pod, γεγονός που θα προκαλέσει σφάλμα 500 κατά τον τερματισμό του pod (για πιο αναλυτικό υλικό σχετικά με αυτό το θέμα, εμείς μεταφρασμένο). Αυτό το πρόβλημα πρέπει να λυθεί με τους εξής τρόπους:

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

Θεωρία. Πώς το NGINX και το PHP-FPM τερματίζουν τις διαδικασίες τους

nginx

Ας ξεκινήσουμε με το NGINX, αφού όλα είναι λίγο πολύ προφανή με αυτό. Βουτώντας στη θεωρία, μαθαίνουμε ότι το NGINX έχει μια κύρια διαδικασία και πολλούς «εργάτες» - αυτές είναι θυγατρικές διεργασίες που επεξεργάζονται αιτήματα πελατών. Παρέχεται μια βολική επιλογή: χρησιμοποιώντας την εντολή nginx -s <SIGNAL> τερματίστε τις διαδικασίες είτε σε λειτουργία γρήγορου τερματισμού είτε σε χαριτωμένη λειτουργία τερματισμού λειτουργίας. Προφανώς, είναι η τελευταία επιλογή που μας ενδιαφέρει.

Τότε όλα είναι απλά: πρέπει να προσθέσετε preStop-hook μια εντολή που θα στείλει ένα χαριτωμένο σήμα τερματισμού λειτουργίας. Αυτό μπορεί να γίνει στο Deployment, στο μπλοκ κοντέινερ:

       lifecycle:
          preStop:
            exec:
              command:
              - /usr/sbin/nginx
              - -s
              - quit

Τώρα, όταν το pod κλείσει, θα δούμε τα ακόλουθα στα αρχεία καταγραφής κοντέινερ NGINX:

2018/01/25 13:58:31 [notice] 1#1: signal 3 (SIGQUIT) received, shutting down
2018/01/25 13:58:31 [notice] 11#11: gracefully shutting down

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

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

Τι συμβαίνει με το PHP-FPM; Πώς χειρίζεται το χαριτωμένο κλείσιμο; Ας το καταλάβουμε.

PHP-FPM

Στην περίπτωση του PHP-FPM, υπάρχουν λίγο λιγότερες πληροφορίες. Αν εστιάσετε σε επίσημο εγχειρίδιο σύμφωνα με το PHP-FPM, θα πει ότι τα ακόλουθα σήματα POSIX είναι αποδεκτά:

  1. SIGINT, SIGTERM — Γρήγορη διακοπή λειτουργίας·
  2. SIGQUIT — χαριτωμένο κλείσιμο (αυτό που χρειαζόμαστε).

Τα υπόλοιπα σήματα δεν απαιτούνται σε αυτήν την εργασία, επομένως θα παραλείψουμε την ανάλυσή τους. Για να τερματίσετε σωστά τη διαδικασία, θα χρειαστεί να γράψετε το ακόλουθο άγκιστρο preStop:

        lifecycle:
          preStop:
            exec:
              command:
              - /bin/kill
              - -SIGQUIT
              - "1"

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

Πρακτική. Πιθανά προβλήματα με χαριτωμένο τερματισμό λειτουργίας

nginx

Πρώτα απ 'όλα, είναι χρήσιμο να θυμάστε: εκτός από την εκτέλεση της εντολής nginx -s quit Υπάρχει ένα ακόμη στάδιο που αξίζει να προσέξετε. Αντιμετωπίσαμε ένα πρόβλημα όπου το NGINX θα εξακολουθούσε να στέλνει SIGTERM αντί για το σήμα SIGQUIT, με αποτέλεσμα τα αιτήματα να μην συμπληρώνονται σωστά. Παρόμοιες περιπτώσεις μπορούν να βρεθούν, για παράδειγμα, εδώ. Δυστυχώς, δεν μπορέσαμε να προσδιορίσουμε τον συγκεκριμένο λόγο αυτής της συμπεριφοράς: υπήρχε μια υποψία για την έκδοση NGINX, αλλά δεν επιβεβαιώθηκε. Το σύμπτωμα ήταν ότι παρατηρήθηκαν μηνύματα στα αρχεία καταγραφής κοντέινερ NGINX: "ανοιχτή υποδοχή #10 αριστερά στη σύνδεση 5", μετά από την οποία το λοβό σταμάτησε.

Μπορούμε να παρατηρήσουμε ένα τέτοιο πρόβλημα, για παράδειγμα, από τις απαντήσεις στο Ingress που χρειαζόμαστε:

Συμβουλές και κόλπα Kubernetes: χαρακτηριστικά χαριτωμένου τερματισμού λειτουργίας σε NGINX και PHP-FPM
Ενδείξεις κωδικών κατάστασης κατά τη στιγμή της ανάπτυξης

Σε αυτήν την περίπτωση, λαμβάνουμε μόνο έναν κωδικό σφάλματος 503 από την ίδια την Ingress: δεν μπορεί να έχει πρόσβαση στο κοντέινερ NGINX, καθώς δεν είναι πλέον προσβάσιμο. Αν κοιτάξετε τα αρχεία καταγραφής κοντέινερ με το NGINX, περιέχουν τα εξής:

[alert] 13939#0: *154 open socket #3 left in connection 16
[alert] 13939#0: *168 open socket #6 left in connection 13

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

Εάν αντιμετωπίσετε παρόμοιο πρόβλημα, είναι λογικό να καταλάβετε ποιο σήμα διακοπής χρησιμοποιείται στο δοχείο και πώς ακριβώς μοιάζει το άγκιστρο preStop. Είναι πολύ πιθανό ο λόγος να βρίσκεται ακριβώς σε αυτό.

PHP-FPM... και πολλά άλλα

Το πρόβλημα με το PHP-FPM περιγράφεται με ασήμαντο τρόπο: δεν περιμένει την ολοκλήρωση των θυγατρικών διεργασιών, τις τερματίζει, γι' αυτό παρουσιάζονται σφάλματα 502 κατά την ανάπτυξη και άλλες λειτουργίες. Υπάρχουν αρκετές αναφορές σφαλμάτων στο bugs.php.net από το 2005 (π.χ εδώ и εδώ), το οποίο περιγράφει αυτό το πρόβλημα. Αλλά πιθανότατα δεν θα δείτε τίποτα στα αρχεία καταγραφής: το PHP-FPM θα ανακοινώσει την ολοκλήρωση της διαδικασίας του χωρίς σφάλματα ή ειδοποιήσεις τρίτων.

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

Αποδεικνύεται ότι lifecycle για το δοχείο θα μοιάζει με αυτό:

    lifecycle:
      preStop:
        exec:
          command:
          - /bin/sleep
          - "30"

Ωστόσο, λόγω των 30 δευτερολέπτων sleep εμείς έντονα θα αυξήσουμε τον χρόνο ανάπτυξης, αφού κάθε pod θα τερματιστεί ελάχιστο 30 δευτερόλεπτα, που είναι κακό. Τι μπορεί να γίνει για αυτό;

Ας απευθυνθούμε στον υπεύθυνο για την άμεση εκτέλεση της αίτησης. Στην περίπτωσή μας είναι PHP-FPMΟ οποίος από προεπιλογή δεν παρακολουθεί την εκτέλεση των θυγατρικών διαδικασιών του: Η κύρια διαδικασία τερματίζεται αμέσως. Μπορείτε να αλλάξετε αυτήν τη συμπεριφορά χρησιμοποιώντας την οδηγία process_control_timeout, το οποίο καθορίζει τα χρονικά όρια για τις θυγατρικές διεργασίες για να περιμένουν σήματα από τον κύριο. Εάν ορίσετε την τιμή σε 20 δευτερόλεπτα, αυτό θα καλύψει τα περισσότερα από τα ερωτήματα που εκτελούνται στο κοντέινερ και θα σταματήσει την κύρια διαδικασία μόλις ολοκληρωθούν.

Με αυτή τη γνώση, ας επιστρέψουμε στο τελευταίο μας πρόβλημα. Όπως αναφέρθηκε, το Kubernetes δεν είναι μια μονολιθική πλατφόρμα: η επικοινωνία μεταξύ των διαφορετικών στοιχείων του απαιτεί κάποιο χρόνο. Αυτό ισχύει ιδιαίτερα όταν εξετάζουμε τη λειτουργία των Ingresses και άλλων συναφών στοιχείων, καθώς λόγω μιας τέτοιας καθυστέρησης τη στιγμή της ανάπτυξης είναι εύκολο να λάβετε ένα κύμα 500 σφαλμάτων. Για παράδειγμα, μπορεί να προκύψει ένα σφάλμα στο στάδιο της αποστολής ενός αιτήματος σε ένα upstream, αλλά η «χρονική καθυστέρηση» της αλληλεπίδρασης μεταξύ των στοιχείων είναι αρκετά μικρή - λιγότερο από ένα δευτερόλεπτο.

Ως εκ τούτου, Συνολικά με την ήδη αναφερθείσα οδηγία process_control_timeout μπορείτε να χρησιμοποιήσετε την παρακάτω κατασκευή για lifecycle:

lifecycle:
  preStop:
    exec:
      command: ["/bin/bash","-c","/bin/sleep 1; kill -QUIT 1"]

Σε αυτή την περίπτωση, θα αντισταθμίσουμε την καθυστέρηση με την εντολή sleep και μην αυξήσετε πολύ τον χρόνο ανάπτυξης: υπάρχει αξιοσημείωτη διαφορά μεταξύ 30 δευτερολέπτων και ενός;.. Στην πραγματικότητα, είναι το process_control_timeoutΚαι lifecycle χρησιμοποιείται μόνο ως «δίχτυ ασφαλείας» σε περίπτωση καθυστέρησης.

Σε γενικές γραμμές η περιγραφόμενη συμπεριφορά και η αντίστοιχη λύση δεν ισχύουν μόνο για το PHP-FPM. Μια παρόμοια κατάσταση μπορεί να προκύψει με τον ένα ή τον άλλο τρόπο όταν χρησιμοποιείτε άλλες γλώσσες/πλαίσια. Εάν δεν μπορείτε να διορθώσετε τον χαριτωμένο τερματισμό λειτουργίας με άλλους τρόπους - για παράδειγμα, ξαναγράφοντας τον κώδικα έτσι ώστε η εφαρμογή να επεξεργάζεται σωστά τα σήματα τερματισμού - μπορείτε να χρησιμοποιήσετε την περιγραφόμενη μέθοδο. Μπορεί να μην είναι το πιο όμορφο, αλλά λειτουργεί.

Πρακτική. Φόρτωση δοκιμής για έλεγχο της λειτουργίας του pod

Η δοκιμή φόρτωσης είναι ένας από τους τρόπους ελέγχου του τρόπου λειτουργίας του κοντέινερ, καθώς αυτή η διαδικασία το φέρνει πιο κοντά σε πραγματικές συνθήκες μάχης όταν οι χρήστες επισκέπτονται τον ιστότοπο. Για να δοκιμάσετε τις παραπάνω συστάσεις, μπορείτε να χρησιμοποιήσετε Yandex.Tankom: Καλύπτει τέλεια όλες τις ανάγκες μας. Ακολουθούν συμβουλές και συστάσεις για τη διεξαγωγή δοκιμών με ένα σαφές παράδειγμα από την εμπειρία μας, χάρη στα γραφήματα του ίδιου του Grafana και του Yandex.Tank.

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

Μια άλλη απόχρωση είναι να κοιτάξετε τα αρχεία καταγραφής του κοντέινερ κατά τον τερματισμό του. Καταγράφονται εκεί πληροφορίες για χαριτωμένο κλείσιμο; Υπάρχουν σφάλματα στα αρχεία καταγραφής κατά την πρόσβαση σε άλλους πόρους (για παράδειγμα, ένα γειτονικό κοντέινερ PHP-FPM); Σφάλματα στην ίδια την εφαρμογή (όπως στην περίπτωση του NGINX που περιγράφεται παραπάνω); Ελπίζω ότι οι εισαγωγικές πληροφορίες από αυτό το άρθρο θα σας βοηθήσουν να κατανοήσετε καλύτερα τι συμβαίνει στο κοντέινερ κατά τον τερματισμό του.

Έτσι, η πρώτη δοκιμαστική διαδρομή έγινε χωρίς lifecycle και χωρίς πρόσθετες οδηγίες για τον διακομιστή εφαρμογών (process_control_timeout σε PHP-FPM). Ο σκοπός αυτής της δοκιμής ήταν να εντοπίσει τον κατά προσέγγιση αριθμό των σφαλμάτων (και εάν υπάρχουν). Επίσης, από πρόσθετες πληροφορίες, θα πρέπει να γνωρίζετε ότι ο μέσος χρόνος ανάπτυξης για κάθε pod ήταν περίπου 5-10 δευτερόλεπτα μέχρι να είναι πλήρως έτοιμο. Τα αποτελέσματα είναι:

Συμβουλές και κόλπα Kubernetes: χαρακτηριστικά χαριτωμένου τερματισμού λειτουργίας σε NGINX και PHP-FPM

Ο πίνακας πληροφοριών Yandex.Tank εμφανίζει μια αιχμή 502 σφαλμάτων, τα οποία συνέβησαν τη στιγμή της ανάπτυξης και διήρκεσαν κατά μέσο όρο έως και 5 δευτερόλεπτα. Πιθανώς αυτό συνέβη επειδή τα υπάρχοντα αιτήματα στο παλιό pod τερματίζονταν όταν τερματιζόταν. Μετά από αυτό, εμφανίστηκαν 503 σφάλματα, τα οποία ήταν αποτέλεσμα ενός σταματημένου κοντέινερ NGINX, το οποίο επίσης διέκοψε τις συνδέσεις λόγω του backend (που εμπόδισε το Ingress να συνδεθεί σε αυτό).

Ας δούμε πώς process_control_timeout στο PHP-FPM θα μας βοηθήσει να περιμένουμε την ολοκλήρωση των θυγατρικών διεργασιών, π.χ. διορθώσει τέτοια λάθη. Εκ νέου ανάπτυξη χρησιμοποιώντας αυτήν την οδηγία:

Συμβουλές και κόλπα Kubernetes: χαρακτηριστικά χαριτωμένου τερματισμού λειτουργίας σε NGINX και PHP-FPM

Δεν υπάρχουν άλλα σφάλματα κατά την 500η ανάπτυξη! Η ανάπτυξη είναι επιτυχής, λειτουργεί χαριτωμένα τερματισμός λειτουργίας.

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

Συμπέρασμα

Για να τερματίσουμε τη διαδικασία με χάρη, αναμένουμε την ακόλουθη συμπεριφορά από την εφαρμογή:

  1. Περιμένετε μερικά δευτερόλεπτα και μετά σταματήστε να δέχεστε νέες συνδέσεις.
  2. Περιμένετε να ολοκληρωθούν όλα τα αιτήματα και κλείστε όλες τις συνδέσεις διατήρησης που δεν εκτελούν αιτήματα.
  3. Τερματίστε τη διαδικασία σας.

Ωστόσο, δεν μπορούν όλες οι εφαρμογές να λειτουργήσουν με αυτόν τον τρόπο. Μια λύση στο πρόβλημα στην πραγματικότητα του Kubernetes είναι:

  • προσθήκη ενός γάντζου προ-σταμάτησης που θα περιμένει μερικά δευτερόλεπτα.
  • μελετώντας το αρχείο ρυθμίσεων του backend μας για τις κατάλληλες παραμέτρους.

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

Ως εργαλείο δοκιμών, μπορείτε να χρησιμοποιήσετε το Yandex.Tank σε συνδυασμό με οποιοδήποτε σύστημα παρακολούθησης (στην περίπτωσή μας, τα δεδομένα ελήφθησαν από το Grafana με ένα σύστημα υποστήριξης Prometheus για τη δοκιμή). Τα προβλήματα με τη χαριτωμένη απενεργοποίηση είναι σαφώς ορατά κάτω από μεγάλα φορτία που μπορεί να δημιουργήσει το σημείο αναφοράς και η παρακολούθηση βοηθά στην ανάλυση της κατάστασης με περισσότερες λεπτομέρειες κατά τη διάρκεια ή μετά τη δοκιμή.

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

PS

Άλλο από τη σειρά K8s tips & tricks:

Πηγή: www.habr.com

Προσθέστε ένα σχόλιο