Πέντε αστοχίες κατά την ανάπτυξη της πρώτης εφαρμογής στο Kubernetes

Πέντε αστοχίες κατά την ανάπτυξη της πρώτης εφαρμογής στο KubernetesΑποτυχία από τον Άρη Ονειροπόλο

Πολλοί πιστεύουν ότι αρκεί να μεταφέρετε την εφαρμογή στο Kubernetes (είτε χρησιμοποιώντας Helm είτε χειροκίνητα) - και θα υπάρχει ευτυχία. Δεν είναι όμως όλα τόσο απλά.

Ομάδα Mail.ru Cloud Solutions μετέφρασε ένα άρθρο του μηχανικού DevOps Julian Gindy. Λέει ποιες παγίδες αντιμετώπισε η εταιρεία του κατά τη διαδικασία μετανάστευσης, ώστε να μην πατήσετε στην ίδια τσουγκράνα.

Βήμα πρώτο: Ρύθμιση αιτημάτων και ορίων pod

Ας ξεκινήσουμε δημιουργώντας ένα καθαρό περιβάλλον όπου θα τρέχουν τα pods μας. Το Kubernetes είναι εξαιρετικό στον προγραμματισμό pod και στο failover. Αλλά αποδείχθηκε ότι ο προγραμματιστής μερικές φορές δεν μπορεί να τοποθετήσει μια ομάδα αν είναι δύσκολο να εκτιμήσει πόσους πόρους χρειάζεται για να λειτουργήσει με επιτυχία. Εδώ εμφανίζονται αιτήματα για πόρους και όρια. Υπάρχει πολλή συζήτηση σχετικά με την καλύτερη προσέγγιση για τον καθορισμό αιτημάτων και ορίων. Μερικές φορές φαίνεται ότι αυτό είναι πραγματικά περισσότερο τέχνη παρά επιστήμη. Εδώ είναι η προσέγγισή μας.

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

από Τεκμηρίωση Kubernetes: Το βήμα φίλτρου ορίζει ένα σύνολο κόμβων όπου μπορεί να προγραμματιστεί ένα Pod. Για παράδειγμα, το φίλτρο PodFitsResources ελέγχει εάν ένας κόμβος έχει αρκετούς πόρους για να ικανοποιήσει συγκεκριμένα αιτήματα πόρων από ένα pod.

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

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

Όρια λοβών είναι ένα σαφέστερο όριο για ένα pod. Αντιπροσωπεύει το μέγιστο ποσό πόρων που θα διαθέσει το σύμπλεγμα στο κοντέινερ.

Και πάλι, από επίσημη τεκμηρίωση: Εάν ένα κοντέινερ έχει όριο μνήμης 4 GiB, τότε το kubelet (και ο χρόνος εκτέλεσης του κοντέινερ) θα το επιβάλει. Ο χρόνος εκτέλεσης εμποδίζει το κοντέινερ να χρησιμοποιεί περισσότερα από το καθορισμένο όριο πόρων. Για παράδειγμα, όταν μια διεργασία σε ένα κοντέινερ προσπαθεί να χρησιμοποιήσει περισσότερη από την επιτρεπόμενη ποσότητα μνήμης, ο πυρήνας του συστήματος τερματίζει τη διαδικασία με ένα σφάλμα "εκτός μνήμης" (OOM).

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

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

Δυστυχώς, δεν μπορώ να δώσω συγκεκριμένες οδηγίες για το ποιες τιμές πρέπει να ορίσω, αλλά εμείς οι ίδιοι τηρούμε τους ακόλουθους κανόνες:

  1. Χρησιμοποιώντας ένα εργαλείο δοκιμής φόρτωσης, προσομοιώνουμε ένα βασικό επίπεδο κίνησης και παρατηρούμε τη χρήση πόρων pod (μνήμη και επεξεργαστή).
  2. Ρυθμίστε τα αιτήματα pod σε μια αυθαίρετα χαμηλή τιμή (με όριο πόρων περίπου 5 φορές την τιμή των αιτημάτων) και παρατηρήστε. Όταν τα αιτήματα είναι σε πολύ χαμηλό επίπεδο, η διαδικασία δεν μπορεί να ξεκινήσει, προκαλώντας συχνά κρυπτικά σφάλματα χρόνου εκτέλεσης Go.

Σημειώνω ότι τα υψηλότερα όρια πόρων καθιστούν πιο δύσκολο τον προγραμματισμό επειδή το pod χρειάζεται έναν κόμβο-στόχο με αρκετούς διαθέσιμους πόρους.

Φανταστείτε μια κατάσταση όπου έχετε έναν ελαφρύ διακομιστή ιστού με πολύ υψηλό όριο πόρων, όπως 4 GB μνήμης. Αυτή η διαδικασία πιθανότατα θα πρέπει να κλιμακωθεί οριζόντια και κάθε νέο pod θα πρέπει να προγραμματίζεται σε έναν κόμβο με τουλάχιστον 4 GB διαθέσιμης μνήμης. Εάν δεν υπάρχει τέτοιος κόμβος, το σύμπλεγμα πρέπει να εισαγάγει έναν νέο κόμβο για την επεξεργασία αυτού του pod, κάτι που μπορεί να πάρει κάποιο χρόνο. Είναι σημαντικό να επιτύχετε μια ελάχιστη διαφορά μεταξύ των αιτημάτων πόρων και των ορίων για να εξασφαλίσετε γρήγορη και ομαλή κλιμάκωση.

Βήμα δεύτερο: Ρύθμιση δοκιμών ζωντάνιας και ετοιμότητας

Αυτό είναι ένα άλλο λεπτό θέμα που συζητείται συχνά στην κοινότητα Kubernetes. Είναι σημαντικό να έχετε καλή κατανόηση των δοκιμών Liveness και Readiness καθώς παρέχουν έναν μηχανισμό για σταθερή λειτουργία του λογισμικού και ελαχιστοποιούν το χρόνο διακοπής λειτουργίας. Ωστόσο, μπορούν να επηρεάσουν σοβαρά την απόδοση της εφαρμογής σας εάν δεν ρυθμιστούν σωστά. Παρακάτω είναι μια περίληψη του τι είναι και τα δύο δείγματα.

Ζωτικότητα δείχνει εάν το κοντέινερ λειτουργεί. Εάν αποτύχει, το kubelet σκοτώνει το κοντέινερ και ενεργοποιείται η πολιτική επανεκκίνησης για αυτό. Εάν το δοχείο δεν είναι εξοπλισμένο με ανιχνευτή Liveness, τότε η προεπιλεγμένη κατάσταση θα είναι επιτυχής - όπως αναφέρεται στο Τεκμηρίωση Kubernetes.

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

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

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

Έχουμε δημιουργήσει ένα τελικό σημείο "υγείας" στις εφαρμογές που απλώς επιστρέφει έναν κωδικό απόκρισης 200. Αυτό είναι μια ένδειξη ότι η διαδικασία εκτελείται και είναι ικανή να χειριστεί αιτήματα (αλλά όχι ακόμη την κυκλοφορία).

Δείγμα Ετοιμότητα υποδεικνύει εάν το κοντέινερ είναι έτοιμο να εξυπηρετήσει αιτήματα. Εάν ο ανιχνευτής ετοιμότητας αποτύχει, ο ελεγκτής τελικού σημείου αφαιρεί τη διεύθυνση IP του pod από τα τελικά σημεία όλων των υπηρεσιών που ταιριάζουν με το pod. Αυτό αναφέρεται επίσης στην τεκμηρίωση της Kubernetes.

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

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

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

SELECT small_item FROM table LIMIT 1

Ακολουθεί ένα παράδειγμα του τρόπου με τον οποίο διαμορφώνουμε αυτές τις δύο τιμές στο Kubernetes:

livenessProbe: 
 httpGet:   
   path: /api/liveness    
   port: http 
readinessProbe:  
 httpGet:    
   path: /api/readiness    
   port: http  periodSeconds: 2

Μπορείτε να προσθέσετε μερικές επιπλέον επιλογές διαμόρφωσης:

  • initialDelaySeconds - πόσα δευτερόλεπτα θα περάσουν από την εκτόξευση του κοντέινερ και την έναρξη της εκτόξευσης των ανιχνευτών.
  • periodSeconds — διάστημα αναμονής μεταξύ των δειγματοληψιών.
  • timeoutSeconds — τον αριθμό των δευτερολέπτων μετά από τα οποία το λοβό θεωρείται έκτακτο. Κανονικό τάιμ άουτ.
  • failureThreshold είναι ο αριθμός των αποτυχιών της δοκιμής πριν σταλεί σήμα επανεκκίνησης στο pod.
  • successThreshold είναι ο αριθμός των επιτυχημένων δοκιμών πριν από τη μετάβαση του pod στην κατάσταση ετοιμότητας (μετά από μια αποτυχία κατά την εκκίνηση ή την ανάκτηση του pod).

Βήμα τρίτο: Ορισμός των προεπιλεγμένων πολιτικών δικτύου του Pod

Το Kubernetes έχει μια "επίπεδη" τοπογραφία δικτύου, από προεπιλογή όλα τα pods επικοινωνούν απευθείας μεταξύ τους. Σε ορισμένες περιπτώσεις αυτό δεν είναι επιθυμητό.

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

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

---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:  
 name: default-deny-ingress
spec:  
 podSelector: {}  
 policyTypes:  
   - Ingress

Οπτικοποίηση αυτής της διαμόρφωσης:

Πέντε αστοχίες κατά την ανάπτυξη της πρώτης εφαρμογής στο Kubernetes
(https://miro.medium.com/max/875/1*-eiVw43azgzYzyN1th7cZg.gif)
Λεπτομερώς εδώ.

Βήμα τέταρτο: Προσαρμοσμένη συμπεριφορά με άγκιστρα και δοχεία εκκίνησης

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

Ιδιαίτερες δυσκολίες προέκυψαν με nginx. Παρατηρήσαμε ότι κατά τη διαδοχική ανάπτυξη αυτών των Pods, οι ενεργές συνδέσεις διακόπηκαν πριν ολοκληρωθούν επιτυχώς.

Μετά από εκτεταμένη έρευνα στο Διαδίκτυο, αποδείχθηκε ότι η Kubernetes δεν περιμένει τις συνδέσεις Nginx να εξαντληθούν πριν κλείσει το pod. Με τη βοήθεια του γάντζου προ-σταμάτησης, εφαρμόσαμε την ακόλουθη λειτουργικότητα και απαλλαγήκαμε εντελώς από το χρόνο διακοπής λειτουργίας:

lifecycle: 
 preStop:
   exec:
     command: ["/usr/local/bin/nginx-killer.sh"]

Αλλά nginx-killer.sh:

#!/bin/bash
sleep 3
PID=$(cat /run/nginx.pid)
nginx -s quit
while [ -d /proc/$PID ]; do
   echo "Waiting while shutting down nginx..."
   sleep 10
done

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

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

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

Βήμα πέμπτο: Διαμόρφωση πυρήνα

Τέλος, ας μιλήσουμε για μια πιο προηγμένη τεχνική.

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

Ωστόσο, το Kubernetes σάς επιτρέπει να εκτελέσετε ένα προνομιακό κοντέινερ που αλλάζει μόνο τις παραμέτρους του πυρήνα για ένα συγκεκριμένο pod. Δείτε τι χρησιμοποιήσαμε για να αλλάξουμε τον μέγιστο αριθμό ανοιχτών συνδέσεων:

initContainers:
  - name: sysctl
     image: alpine:3.10
     securityContext:
         privileged: true
      command: ['sh', '-c', "sysctl -w net.core.somaxconn=32768"]

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

Εν κατακλείδι

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

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

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

Να κάνετε πάντα αυτές τις ερωτήσεις στον εαυτό σας:

  1. Πόσους πόρους καταναλώνουν οι εφαρμογές και πώς θα αλλάξει αυτό το ποσό;
  2. Ποιες είναι οι πραγματικές απαιτήσεις κλιμάκωσης; Πόση επισκεψιμότητα θα χειριστεί η εφαρμογή κατά μέσο όρο; Τι γίνεται με την κυκλοφορία αιχμής;
  3. Πόσο συχνά θα χρειαστεί να κλιμακώνεται η υπηρεσία; Πόσο γρήγορα πρέπει να τεθούν σε λειτουργία τα νέα pods για να λάβουν επισκεψιμότητα;
  4. Πόσο χαριτωμένα κλείνουν τα pods; Είναι καθόλου απαραίτητο; Είναι δυνατόν να επιτευχθεί ανάπτυξη χωρίς διακοπή λειτουργίας;
  5. Πώς να ελαχιστοποιήσετε τους κινδύνους για την ασφάλεια και να περιορίσετε τη ζημιά από τυχόν παραβιασμένους δίσκους; Υπάρχουν υπηρεσίες που έχουν δικαιώματα ή προσβάσεις που δεν χρειάζονται;

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

Ευτυχώς, η Kubernetes παρέχει τις απαραίτητες ρυθμίσεις για την επίτευξη όλων των τεχνικών στόχων. Χρησιμοποιώντας έναν συνδυασμό αιτημάτων και ορίων πόρων, ανιχνευτών Liveness και Readiness, κοντέινερ init, πολιτικών δικτύου και προσαρμοσμένου συντονισμού πυρήνα, μπορείτε να επιτύχετε υψηλή απόδοση μαζί με ανοχή σφαλμάτων και γρήγορη επεκτασιμότητα.

Τι άλλο να διαβάσετε:

  1. Βέλτιστες πρακτικές και βέλτιστες πρακτικές για τη λειτουργία κοντέινερ και Kubernetes σε περιβάλλοντα παραγωγής.
  2. 90+ Χρήσιμα εργαλεία για Kubernetes: Ανάπτυξη, Διαχείριση, Παρακολούθηση, Ασφάλεια και άλλα.
  3. Το κανάλι μας Around Kubernetes στο Telegram.

Πηγή: www.habr.com

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