ProHoster > Blog > διαχείριση > 6 διασκεδαστικά σφάλματα συστήματος στη λειτουργία του Kubernetes [και η λύση τους]
6 διασκεδαστικά σφάλματα συστήματος στη λειτουργία του Kubernetes [και η λύση τους]
Κατά τη διάρκεια των ετών χρήσης του Kubernetes στην παραγωγή, έχουμε συγκεντρώσει πολλές ενδιαφέρουσες ιστορίες για το πώς τα σφάλματα σε διάφορα στοιχεία του συστήματος οδήγησαν σε δυσάρεστες ή/και ακατανόητες συνέπειες που επηρεάζουν τη λειτουργία των δοχείων και των λοβών. Σε αυτό το άρθρο έχουμε κάνει μια επιλογή από μερικά από τα πιο κοινά ή ενδιαφέροντα. Ακόμα κι αν δεν είστε ποτέ αρκετά τυχεροί να αντιμετωπίσετε τέτοιες καταστάσεις, το να διαβάζετε για τέτοιες μικρές αστυνομικές ιστορίες - ειδικά «από πρώτο χέρι» - είναι πάντα ενδιαφέρον, έτσι δεν είναι;
Ιστορία 1. Supercronic και Docker hanging
Σε ένα από τα συμπλέγματα, λαμβάναμε περιοδικά ένα παγωμένο Docker, το οποίο παρενέβαινε στην κανονική λειτουργία του συμπλέγματος. Ταυτόχρονα, παρατηρήθηκαν τα ακόλουθα στα αρχεία καταγραφής του Docker:
level=error msg="containerd: start init process" error="exit status 2: "runtime/cgo: pthread_create failed: No space left on device
SIGABRT: abort
PC=0x7f31b811a428 m=0
goroutine 0 [idle]:
goroutine 1 [running]:
runtime.systemstack_switch() /usr/local/go/src/runtime/asm_amd64.s:252 fp=0xc420026768 sp=0xc420026760
runtime.main() /usr/local/go/src/runtime/proc.go:127 +0x6c fp=0xc4200267c0 sp=0xc420026768
runtime.goexit() /usr/local/go/src/runtime/asm_amd64.s:2086 +0x1 fp=0xc4200267c8 sp=0xc4200267c0
goroutine 17 [syscall, locked to thread]:
runtime.goexit() /usr/local/go/src/runtime/asm_amd64.s:2086 +0x1
…
Αυτό που μας ενδιαφέρει περισσότερο για αυτό το σφάλμα είναι το μήνυμα: pthread_create failed: No space left on device. Γρήγορη Μελέτη τεκμηρίωση εξήγησε ότι ο Docker δεν μπορούσε να διαχωρίσει μια διαδικασία, γι' αυτό και κατά διαστήματα πάγωσε.
Στην παρακολούθηση, η παρακάτω εικόνα αντιστοιχεί σε αυτό που συμβαίνει:
Παρόμοια κατάσταση παρατηρείται και σε άλλους κόμβους:
Αποδείχθηκε ότι αυτή η συμπεριφορά είναι συνέπεια της εργασίας του pod υπερκρονικός (ένα βοηθητικό πρόγραμμα Go που χρησιμοποιούμε για την εκτέλεση εργασιών cron σε pods):
Το πρόβλημα είναι το εξής: όταν μια εργασία εκτελείται σε supercronic, η διαδικασία γεννιέται από αυτήν δεν μπορεί να τερματιστεί σωστά, μετατρέπεται σε βρυκόλακας.
Σημείωση: Για να είμαστε πιο ακριβείς, οι διεργασίες δημιουργούνται από εργασίες cron, αλλά το supercronic δεν είναι ένα αρχικό σύστημα και δεν μπορεί να «υιοθετήσει» διαδικασίες που δημιούργησαν τα παιδιά του. Όταν εγείρονται σήματα SIGHUP ή SIGTERM, δεν μεταβιβάζονται στις θυγατρικές διεργασίες, με αποτέλεσμα οι θυγατρικές διεργασίες να μην τερματίζονται και να παραμένουν σε κατάσταση ζόμπι. Μπορείτε να διαβάσετε περισσότερα για όλα αυτά, για παράδειγμα, στο ένα τέτοιο άρθρο.
Υπάρχουν δύο τρόποι επίλυσης προβλημάτων:
Ως προσωρινή λύση - αυξήστε τον αριθμό των PID στο σύστημα σε μία μόνο χρονική στιγμή:
/proc/sys/kernel/pid_max (since Linux 2.5.34)
This file specifies the value at which PIDs wrap around (i.e., the value in this file is one greater than the maximum PID). PIDs greater than this value are not allo‐
cated; thus, the value in this file also acts as a system-wide limit on the total number of processes and threads. The default value for this file, 32768, results in the
same range of PIDs as on earlier kernels
Ή εκκινήστε εργασίες στο supercronic όχι απευθείας, αλλά χρησιμοποιώντας το ίδιο τίνι, το οποίο είναι σε θέση να τερματίζει σωστά τις διαδικασίες και να μην δημιουργεί ζόμπι.
Ιστορία 2. "Ζόμπι" κατά τη διαγραφή μιας cgroup
Η Kubelet άρχισε να καταναλώνει πολύ CPU:
Σε κανέναν δεν θα αρέσει αυτό, οπότε οπλιστήκαμε Perf και άρχισε να αντιμετωπίζει το πρόβλημα. Τα αποτελέσματα της έρευνας ήταν τα εξής:
Η Kubelet ξοδεύει περισσότερο από το ένα τρίτο του χρόνου της CPU για να τραβήξει δεδομένα μνήμης από όλες τις cgroups:
Στη λίστα αλληλογραφίας προγραμματιστών πυρήνα μπορείτε να βρείτε συζήτηση του προβλήματος. Εν ολίγοις, η ουσία καταλήγει σε αυτό: διάφορα αρχεία tmpfs και άλλα παρόμοια πράγματα δεν αφαιρούνται εντελώς από το σύστημα κατά τη διαγραφή μιας cgroup, το λεγόμενο memcg βρυκόλακας. Αργά ή γρήγορα θα διαγραφούν από την προσωρινή μνήμη της σελίδας, αλλά υπάρχει πολλή μνήμη στον διακομιστή και ο πυρήνας δεν βλέπει το νόημα να χάνει χρόνο για τη διαγραφή τους. Γι' αυτό συσσωρεύονται συνέχεια. Γιατί συμβαίνει αυτό; Αυτός είναι ένας διακομιστής με cron jobs που δημιουργεί συνεχώς νέες θέσεις εργασίας, και μαζί τους νέα pods. Έτσι, δημιουργούνται νέες cgroups για κοντέινερ σε αυτά, τα οποία σύντομα διαγράφονται.
Γιατί το cAdvisor στο kubelet σπαταλά τόσο πολύ χρόνο; Αυτό είναι εύκολο να το δει κανείς με την απλούστερη εκτέλεση time cat /sys/fs/cgroup/memory/memory.stat. Εάν σε ένα υγιές μηχάνημα η λειτουργία διαρκεί 0,01 δευτερόλεπτα, τότε στο προβληματικό cron02 διαρκεί 1,2 δευτερόλεπτα. Το θέμα είναι ότι το cAdvisor, το οποίο διαβάζει πολύ αργά δεδομένα από το sysfs, προσπαθεί να λάβει υπόψη τη μνήμη που χρησιμοποιείται σε ζόμπι cgroups.
Για να αφαιρέσουμε αναγκαστικά τα ζόμπι, δοκιμάσαμε να καθαρίσουμε τις κρυφές μνήμες όπως συνιστάται στο LKML: sync; echo 3 > /proc/sys/vm/drop_caches, - αλλά ο πυρήνας αποδείχθηκε πιο περίπλοκος και τράκαρε το αυτοκίνητο.
Τι να κάνω? Το πρόβλημα διορθώνεται (διαπράττω, και για περιγραφή βλ μήνυμα απελευθέρωσης) ενημέρωση του πυρήνα Linux στην έκδοση 4.16.
Ιστορία 3. Το Systemd και η βάση του
Και πάλι, το kubelet καταναλώνει πάρα πολλούς πόρους σε ορισμένους κόμβους, αλλά αυτή τη φορά καταναλώνει πάρα πολλή μνήμη:
Αποδείχθηκε ότι υπάρχει πρόβλημα στο systemd που χρησιμοποιείται στο Ubuntu 16.04 και παρουσιάζεται κατά τη διαχείριση προσαρτήσεων που δημιουργούνται για σύνδεση subPath από ConfigMaps ή μυστικά. Αφού το λοβό ολοκληρώσει την εργασία του η υπηρεσία systemd και η βάση σέρβις παραμένουν στο σύστημα. Με την πάροδο του χρόνου, ένας τεράστιος αριθμός από αυτούς συσσωρεύεται. Υπάρχουν ακόμη και θέματα σχετικά με αυτό το θέμα:
...το τελευταίο από τα οποία αναφέρεται στο PR στο systemd: #7811 (θέμα στο systemd - #7798).
Το πρόβλημα δεν υπάρχει πλέον στο Ubuntu 18.04, αλλά αν θέλετε να συνεχίσετε να χρησιμοποιείτε το Ubuntu 16.04, μπορεί να βρείτε χρήσιμη τη λύση μας σε αυτό το θέμα.
#!/bin/bash
# we will work only on xenial
hostrelease="/etc/lsb-release-host"
test -f ${hostrelease} && grep xenial ${hostrelease} > /dev/null || exit 0
# sleeping max 30 minutes to dispense load on kube-nodes
sleep $((RANDOM % 1800))
stoppedCount=0
# counting actual subpath units in systemd
countBefore=$(systemctl list-units | grep subpath | grep "run-" | wc -l)
# let's go check each unit
for unit in $(systemctl list-units | grep subpath | grep "run-" | awk '{print $1}'); do
# finding description file for unit (to find out docker container, who born this unit)
DropFile=$(systemctl status ${unit} | grep Drop | awk -F': ' '{print $2}')
# reading uuid for docker container from description file
DockerContainerId=$(cat ${DropFile}/50-Description.conf | awk '{print $5}' | cut -d/ -f6)
# checking container status (running or not)
checkFlag=$(docker ps | grep -c ${DockerContainerId})
# if container not running, we will stop unit
if [[ ${checkFlag} -eq 0 ]]; then
echo "Stopping unit ${unit}"
# stoping unit in action
systemctl stop $unit
# just counter for logs
((stoppedCount++))
# logging current progress
echo "Stopped ${stoppedCount} systemd units out of ${countBefore}"
fi
done
... και εκτελείται κάθε 5 λεπτά χρησιμοποιώντας το προαναφερθέν supercronic. Το Dockerfile του μοιάζει με αυτό:
Ιστορία 4. Ανταγωνιστικότητα κατά τον προγραμματισμό ομάδων
Παρατηρήθηκε ότι: εάν έχουμε ένα λοβό τοποθετημένο σε έναν κόμβο και η εικόνα του αντλείται για πολύ μεγάλο χρονικό διάστημα, τότε ένα άλλο λοβό που «χτυπά» τον ίδιο κόμβο απλά θα δεν αρχίζει να τραβάει την εικόνα του νέου pod. Αντίθετα, περιμένει μέχρι να τραβήξει την εικόνα του προηγούμενου pod. Ως αποτέλεσμα, ένα pod που ήταν ήδη προγραμματισμένο και του οποίου η εικόνα θα μπορούσε να είχε ληφθεί σε μόλις ένα λεπτό θα καταλήξει στην κατάσταση containerCreating.
Τα γεγονότα θα μοιάζουν κάπως έτσι:
Normal Pulling 8m kubelet, ip-10-241-44-128.ap-northeast-1.compute.internal pulling image "registry.example.com/infra/openvpn/openvpn:master"
Αποδεικνύεται ότι μια μεμονωμένη εικόνα από ένα αργό μητρώο μπορεί να εμποδίσει την ανάπτυξη ανά κόμβο.
Δυστυχώς, δεν υπάρχουν πολλοί τρόποι εξόδου από την κατάσταση:
Προσπαθήστε να χρησιμοποιήσετε το Μητρώο Docker απευθείας στο σύμπλεγμα ή απευθείας με το σύμπλεγμα (για παράδειγμα, Μητρώο GitLab, Nexus κ.λπ.).
Κατά τη λειτουργία διαφόρων εφαρμογών, συναντήσαμε επίσης μια κατάσταση όπου ένας κόμβος παύει εντελώς να είναι προσβάσιμος: το SSH δεν αποκρίνεται, όλοι οι δαίμονες παρακολούθησης πέφτουν και μετά δεν υπάρχει τίποτα (ή σχεδόν τίποτα) ανώμαλο στα αρχεία καταγραφής.
Θα σας το πω σε εικόνες χρησιμοποιώντας το παράδειγμα ενός κόμβου όπου λειτουργούσε το MongoDB.
Έτσι φαίνεται το top να ατυχήματα:
Και κάπως έτσι - μετά ατυχήματα:
Στην παρακολούθηση, υπάρχει επίσης ένα απότομο άλμα, στο οποίο ο κόμβος παύει να είναι διαθέσιμος:
Έτσι, από τα στιγμιότυπα οθόνης είναι σαφές ότι:
Η μνήμη RAM στο μηχάνημα είναι κοντά στο τέλος.
Υπάρχει ένα απότομο άλμα στην κατανάλωση RAM, μετά από το οποίο η πρόσβαση σε ολόκληρο το μηχάνημα απενεργοποιείται απότομα.
Μια μεγάλη εργασία φτάνει στο Mongo, η οποία αναγκάζει τη διαδικασία DBMS να χρησιμοποιεί περισσότερη μνήμη και να διαβάζει ενεργά από το δίσκο.
Αποδεικνύεται ότι εάν το Linux εξαντληθεί η ελεύθερη μνήμη (πέσει η πίεση μνήμης) και δεν υπάρχει ανταλλαγή, τότε να Όταν φτάσει ο δολοφόνος OOM, μπορεί να προκύψει μια πράξη εξισορρόπησης μεταξύ της εισαγωγής σελίδων στην προσωρινή μνήμη σελίδων και της εγγραφής τους πίσω στο δίσκο. Αυτό γίνεται από το kswapd, το οποίο με γενναιότητα ελευθερώνει όσο το δυνατόν περισσότερες σελίδες μνήμης για μετέπειτα διανομή.
Δυστυχώς, με μεγάλο φορτίο I/O σε συνδυασμό με μικρή ποσότητα ελεύθερης μνήμης, Το kswapd γίνεται το σημείο συμφόρησης ολόκληρου του συστήματος, γιατί είναι δεμένοι με αυτό όλα εκχωρήσεις (σφάλματα σελίδας) σελίδων μνήμης στο σύστημα. Αυτό μπορεί να συνεχιστεί για πολύ μεγάλο χρονικό διάστημα εάν οι διεργασίες δεν θέλουν πλέον να χρησιμοποιούν μνήμη, αλλά είναι σταθερές στην άκρη της αβύσσου του OOM-killer.
Το φυσικό ερώτημα είναι: γιατί ο δολοφόνος του OOM έρχεται τόσο αργά; Στην τρέχουσα επανάληψη του, το OOM killer είναι εξαιρετικά ανόητο: θα σκοτώσει τη διαδικασία μόνο όταν αποτύχει η προσπάθεια εκχώρησης μιας σελίδας μνήμης, π.χ. εάν αποτύχει το σφάλμα σελίδας. Αυτό δεν συμβαίνει για μεγάλο χρονικό διάστημα, επειδή το kswapd ελευθερώνει με γενναιότητα σελίδες μνήμης, απορρίπτοντας την προσωρινή μνήμη σελίδων (ολόκληρο το I/O του δίσκου στο σύστημα, στην πραγματικότητα) πίσω στο δίσκο. Πιο αναλυτικά, με μια περιγραφή των βημάτων που απαιτούνται για την εξάλειψη τέτοιων προβλημάτων στον πυρήνα, μπορείτε να διαβάσετε εδώ.
Ιστορία 6. Τα pod κολλάνε σε κατάσταση εκκρεμότητας
Σε ορισμένα συμπλέγματα, στα οποία λειτουργούν πραγματικά πολλά λοβοί, αρχίσαμε να παρατηρούμε ότι τα περισσότερα από αυτά «κολλάνε» για πολύ μεγάλο χρονικό διάστημα στην πολιτεία Pending, αν και τα ίδια τα κοντέινερ Docker εκτελούνται ήδη στους κόμβους και μπορούν να εργαστούν με το χέρι.
Ταυτόχρονα, στο describe δεν υπάρχει τίποτα λάθος:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal Scheduled 1m default-scheduler Successfully assigned sphinx-0 to ss-dev-kub07
Normal SuccessfulAttachVolume 1m attachdetach-controller AttachVolume.Attach succeeded for volume "pvc-6aaad34f-ad10-11e8-a44c-52540035a73b"
Normal SuccessfulMountVolume 1m kubelet, ss-dev-kub07 MountVolume.SetUp succeeded for volume "sphinx-config"
Normal SuccessfulMountVolume 1m kubelet, ss-dev-kub07 MountVolume.SetUp succeeded for volume "default-token-fzcsf"
Normal SuccessfulMountVolume 49s (x2 over 51s) kubelet, ss-dev-kub07 MountVolume.SetUp succeeded for volume "pvc-6aaad34f-ad10-11e8-a44c-52540035a73b"
Normal Pulled 43s kubelet, ss-dev-kub07 Container image "registry.example.com/infra/sphinx-exporter/sphinx-indexer:v1" already present on machine
Normal Created 43s kubelet, ss-dev-kub07 Created container
Normal Started 43s kubelet, ss-dev-kub07 Started container
Normal Pulled 43s kubelet, ss-dev-kub07 Container image "registry.example.com/infra/sphinx/sphinx:v1" already present on machine
Normal Created 42s kubelet, ss-dev-kub07 Created container
Normal Started 42s kubelet, ss-dev-kub07 Started container
Μετά από λίγο σκάψιμο, υποθέσαμε ότι το kubelet απλά δεν έχει χρόνο να στείλει όλες τις πληροφορίες σχετικά με την κατάσταση των pods και τις δοκιμές ζωντανότητας/ ετοιμότητας στον διακομιστή API.
Και αφού μελετήσαμε τη βοήθεια, βρήκαμε τις ακόλουθες παραμέτρους:
--kube-api-qps - QPS to use while talking with kubernetes apiserver (default 5)
--kube-api-burst - Burst to use while talking with kubernetes apiserver (default 10)
--event-qps - If > 0, limit event creations per second to this value. If 0, unlimited. (default 5)
--event-burst - Maximum size of a bursty event records, temporarily allows event records to burst to this number, while still not exceeding event-qps. Only used if --event-qps > 0 (default 10)
--registry-qps - If > 0, limit registry pull QPS to this value.
--registry-burst - Maximum size of bursty pulls, temporarily allows pulls to burst to this number, while still not exceeding registry-qps. Only used if --registry-qps > 0 (default 10)
Όπως φαίνεται, οι προεπιλεγμένες τιμές είναι αρκετά μικρές, και στο 90% καλύπτουν όλες τις ανάγκες... Ωστόσο, στην περίπτωσή μας αυτό δεν ήταν αρκετό. Επομένως, ορίζουμε τις ακόλουθες τιμές:
... και επανεκκινήσαμε τα kubelets, μετά από την οποία είδαμε την ακόλουθη εικόνα στα γραφήματα των κλήσεων προς τον διακομιστή API:
... και ναι, όλα άρχισαν να πετάνε!
PS
Για τη βοήθειά τους στη συλλογή σφαλμάτων και στην προετοιμασία αυτού του άρθρου, εκφράζω τη βαθιά μου ευγνωμοσύνη στους πολυάριθμους μηχανικούς της εταιρείας μας, και ιδιαίτερα στον συνάδελφό μου από την ομάδα Έρευνας και Ανάπτυξης Andrey Klimentyev (zuzzas).