Πέντε φοιτητές και τρία κατανεμημένα καταστήματα βασικής αξίας

Ή πώς γράψαμε μια βιβλιοθήκη πελάτη C++ για το ZooKeeper, etcd και το Consul KV

Στον κόσμο των κατανεμημένων συστημάτων, υπάρχουν ορισμένες τυπικές εργασίες: αποθήκευση πληροφοριών σχετικά με τη σύνθεση του συμπλέγματος, διαχείριση της διαμόρφωσης των κόμβων, ανίχνευση ελαττωματικών κόμβων, επιλογή αρχηγού και άλλοι. Για την επίλυση αυτών των προβλημάτων, έχουν δημιουργηθεί ειδικά κατανεμημένα συστήματα - υπηρεσίες συντονισμού. Τώρα θα μας ενδιαφέρουν τρία από αυτά: ZooKeeper, etcd και Consul. Από όλη την πλούσια λειτουργικότητα του Consul, θα επικεντρωθούμε στο Consul KV.

Πέντε φοιτητές και τρία κατανεμημένα καταστήματα βασικής αξίας

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

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

Καταφέραμε να δημιουργήσουμε μια βιβλιοθήκη που παρέχει μια κοινή διεπαφή για εργασία με το ZooKeeper, etcd και Consul KV. Η βιβλιοθήκη είναι γραμμένη σε C++, αλλά υπάρχουν σχέδια για μεταφορά της σε άλλες γλώσσες.

Μοντέλα Δεδομένων

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

ZooKeeper

Πέντε φοιτητές και τρία κατανεμημένα καταστήματα βασικής αξίας

Τα κλειδιά είναι οργανωμένα σε δέντρο και ονομάζονται κόμβοι. Αντίστοιχα, για έναν κόμβο μπορείτε να λάβετε μια λίστα με τα παιδιά του. Οι λειτουργίες δημιουργίας znode (δημιουργία) και αλλαγής τιμής (setData) διαχωρίζονται: μόνο τα υπάρχοντα κλειδιά μπορούν να διαβαστούν και να αλλάξουν. Τα ρολόγια μπορούν να συνδεθούν με τις λειτουργίες ελέγχου της ύπαρξης ενός κόμβου, ανάγνωσης μιας τιμής και λήψης παιδιών. Το Watch είναι μια εφάπαξ ενεργοποίηση που ενεργοποιείται όταν αλλάζει η έκδοση των αντίστοιχων δεδομένων στον διακομιστή. Οι εφήμεροι κόμβοι χρησιμοποιούνται για την ανίχνευση αστοχιών. Είναι συνδεδεμένα με τη συνεδρία του πελάτη που τα δημιούργησε. Όταν ένας πελάτης κλείνει μια περίοδο λειτουργίας ή σταματά να ειδοποιεί το ZooKeeper για την ύπαρξή του, αυτοί οι κόμβοι διαγράφονται αυτόματα. Υποστηρίζονται απλές συναλλαγές - ένα σύνολο πράξεων που είτε πετυχαίνουν είτε αποτυγχάνουν εάν αυτό δεν είναι δυνατό για τουλάχιστον μία από αυτές.

κλπ

Πέντε φοιτητές και τρία κατανεμημένα καταστήματα βασικής αξίας

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

Το etcd δεν έχει μια τυπική λειτουργία σύγκρισης και ορισμού, αλλά έχει κάτι καλύτερο: συναλλαγές. Φυσικά, υπάρχουν και στα τρία συστήματα, αλλά οι συναλλαγές etcd είναι ιδιαίτερα καλές. Αποτελούνται από τρία μπλοκ: έλεγχος, επιτυχία, αποτυχία. Το πρώτο μπλοκ περιέχει ένα σύνολο συνθηκών, το δεύτερο και το τρίτο - λειτουργίες. Η συναλλαγή εκτελείται ατομικά. Εάν ισχύουν όλες οι συνθήκες, τότε εκτελείται το μπλοκ επιτυχίας, διαφορετικά το μπλοκ αποτυχίας. Στο API 3.3, τα μπλοκ επιτυχίας και αποτυχίας μπορούν να περιέχουν ένθετες συναλλαγές. Δηλαδή, είναι δυνατό να εκτελεστούν ατομικά κατασκευές υπό όρους σχεδόν αυθαίρετου επιπέδου ένθεσης. Μπορείτε να μάθετε περισσότερα σχετικά με τους ελέγχους και τις λειτουργίες από τις οποίες προέρχονται τεκμηρίωση.

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

Πρόξενος K.V.

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

Πέντε φοιτητές και τρία κατανεμημένα καταστήματα βασικής αξίας
Αντί για ρολόγια, ο Consul έχει μπλοκάρει αιτήματα HTTP. Στην ουσία πρόκειται για συνηθισμένες κλήσεις στη μέθοδο ανάγνωσης δεδομένων, για τις οποίες, μαζί με άλλες παραμέτρους, υποδεικνύεται η τελευταία γνωστή έκδοση των δεδομένων. Εάν η τρέχουσα έκδοση των αντίστοιχων δεδομένων στον διακομιστή είναι μεγαλύτερη από την καθορισμένη, η απόκριση επιστρέφεται αμέσως, διαφορετικά - όταν αλλάξει η τιμή. Υπάρχουν επίσης συνεδρίες που μπορούν να προσαρτηθούν σε κλειδιά ανά πάσα στιγμή. Αξίζει να σημειωθεί ότι σε αντίθεση με το etcd και το ZooKeeper, όπου η διαγραφή περιόδων σύνδεσης οδηγεί στη διαγραφή των συσχετισμένων κλειδιών, υπάρχει μια λειτουργία στην οποία η συνεδρία απλώς αποσυνδέεται από αυτά. Διαθέσιμος συναλλαγές, χωρίς υποκαταστήματα, αλλά με κάθε είδους επιταγές.

Βάζοντάς τα όλα μαζί

Το ZooKeeper έχει το πιο αυστηρό μοντέλο δεδομένων. Τα ερωτήματα εκφραστικού εύρους που είναι διαθέσιμα στο etcd δεν μπορούν να εξομοιωθούν αποτελεσματικά ούτε στο ZooKeeper ούτε στο Consul. Προσπαθώντας να ενσωματώσουμε το καλύτερο από όλες τις υπηρεσίες, καταλήξαμε σε μια διεπαφή σχεδόν ισοδύναμη με τη διεπαφή ZooKeeper με τις ακόλουθες σημαντικές εξαιρέσεις:

  • ακολουθία, κοντέινερ και κόμβοι TTL Δεν υποστηρίζεται
  • Τα ACL δεν υποστηρίζονται
  • η μέθοδος set δημιουργεί ένα κλειδί αν δεν υπάρχει (στο ZK setData επιστρέφει ένα σφάλμα σε αυτήν την περίπτωση)
  • Οι μέθοδοι set και cas διαχωρίζονται (στο ZK είναι ουσιαστικά το ίδιο πράγμα)
  • η μέθοδος διαγραφής διαγράφει έναν κόμβο μαζί με το υποδέντρο του (στο ZK delete επιστρέφει ένα σφάλμα εάν ο κόμβος έχει παιδιά)
  • Για κάθε κλειδί υπάρχει μόνο μία έκδοση - η έκδοση τιμής (σε ZK υπάρχουν τρεις από αυτές)

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

Η εφαρμογή συμπεριφοράς παρόμοιας με το ZooKeeper κατά τη διαγραφή μιας κορυφής θα απαιτούσε τη διατήρηση ενός ξεχωριστού θυγατρικού μετρητή για κάθε κλειδί στο etcd και στο Consul. Επειδή προσπαθήσαμε να αποφύγουμε την αποθήκευση μετα-πληροφοριών, αποφασίστηκε να διαγραφεί ολόκληρο το υποδέντρο.

Λεπτές λεπτομέρειες εφαρμογής

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

Ιεραρχία σε κ.λπ

Η διατήρηση μιας ιεραρχικής άποψης στο etcd αποδείχθηκε μια από τις πιο ενδιαφέρουσες εργασίες. Τα ερωτήματα εύρους διευκολύνουν την ανάκτηση μιας λίστας κλειδιών με ένα καθορισμένο πρόθεμα. Για παράδειγμα, αν χρειάζεστε όλα όσα ξεκινούν "/foo", ζητάτε εύρος ["/foo", "/fop"). Αλλά αυτό θα επέστρεφε ολόκληρο το υποδέντρο του κλειδιού, το οποίο μπορεί να μην είναι αποδεκτό εάν το υποδέντρο είναι μεγάλο. Στην αρχή σχεδιάζαμε να χρησιμοποιήσουμε έναν βασικό μηχανισμό μετάφρασης, υλοποιείται στο zetcd. Περιλαμβάνει την προσθήκη ενός byte στην αρχή του κλειδιού, ίσο με το βάθος του κόμβου στο δέντρο. Επιτρέψτε μου να σας δώσω ένα παράδειγμα.

"/foo" -> "u01/foo"
"/foo/bar" -> "u02/foo/bar"

Τότε πάρτε όλα τα άμεσα παιδιά του κλειδιού "/foo" είναι δυνατό ζητώντας ένα εύρος ["u02/foo/", "u02/foo0"). Ναι, σε ASCII "0" στέκεται αμέσως μετά "/".

Αλλά πώς να εφαρμόσετε την αφαίρεση μιας κορυφής σε αυτήν την περίπτωση; Αποδεικνύεται ότι πρέπει να διαγράψετε όλες τις περιοχές του τύπου ["uXX/foo/", "uXX/foo0") για XX από 01 έως FF. Και τότε πέσαμε όριο αριθμού λειτουργίας μέσα σε μία συναλλαγή.

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

"/very" -> "/u00very"
"/very/long" -> "/very/u00long"
"/very/long/path" -> "/very/long/u00path"

Στη συνέχεια διαγράφοντας το κλειδί "/very" μετατρέπεται σε διαγραφή "/u00very" και εμβέλεια ["/very/", "/very0"), και να πάρει όλα τα παιδιά - σε ένα αίτημα για κλειδιά από τη σειρά ["/very/u00", "/very/u01").

Αφαίρεση κλειδιού στο ZooKeeper

Όπως ανέφερα ήδη, στο ZooKeeper δεν μπορείτε να διαγράψετε έναν κόμβο εάν έχει παιδιά. Θέλουμε να διαγράψουμε το κλειδί μαζί με το υποδέντρο. Τι πρέπει να κάνω? Αυτό το κάνουμε με αισιοδοξία. Αρχικά, διασχίζουμε αναδρομικά το υποδέντρο, λαμβάνοντας τα παιδιά κάθε κορυφής με ένα ξεχωριστό ερώτημα. Στη συνέχεια, χτίζουμε μια συναλλαγή που επιχειρεί να διαγράψει όλους τους κόμβους του υποδέντρου με τη σωστή σειρά. Φυσικά, μπορεί να συμβούν αλλαγές μεταξύ της ανάγνωσης ενός υποδέντρου και της διαγραφής του. Σε αυτή την περίπτωση, η συναλλαγή θα αποτύχει. Επιπλέον, το υποδέντρο μπορεί να αλλάξει κατά τη διαδικασία ανάγνωσης. Ένα αίτημα για τα παιδιά του επόμενου κόμβου μπορεί να εμφανίσει σφάλμα εάν, για παράδειγμα, αυτός ο κόμβος έχει ήδη διαγραφεί. Και στις δύο περιπτώσεις, επαναλαμβάνουμε ξανά ολόκληρη τη διαδικασία.

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

τοποθετείται στο ZooKeeper

Στο ZooKeeper υπάρχουν ξεχωριστές μέθοδοι που λειτουργούν με τη δομή δέντρου (δημιουργία, διαγραφή, getChildren) και που λειτουργούν με δεδομένα σε κόμβους (setData, getData).Επιπλέον, όλες οι μέθοδοι έχουν αυστηρές προϋποθέσεις: η δημιουργία θα επιστρέψει ένα σφάλμα εάν ο κόμβος έχει ήδη έχει δημιουργηθεί, διαγραφεί ή setData – εάν δεν υπάρχει ήδη. Χρειαζόμασταν μια μέθοδο καθορισμού που μπορεί να καλείται χωρίς να σκεφτόμαστε την παρουσία κλειδιού.

Μια επιλογή είναι να ακολουθήσετε μια αισιόδοξη προσέγγιση, όπως συμβαίνει με τη διαγραφή. Ελέγξτε εάν υπάρχει κόμβος. Εάν υπάρχει, καλέστε το setData, διαφορετικά δημιουργήστε. Εάν η τελευταία μέθοδος επέστρεψε ένα σφάλμα, επαναλάβετε το ξανά. Το πρώτο πράγμα που πρέπει να σημειωθεί είναι ότι το τεστ ύπαρξης είναι άσκοπο. Μπορείτε να καλέσετε αμέσως τη δημιουργία. Η επιτυχής ολοκλήρωση θα σημαίνει ότι ο κόμβος δεν υπήρχε και δημιουργήθηκε. Διαφορετικά, η δημιουργία θα επιστρέψει το κατάλληλο σφάλμα, μετά το οποίο θα πρέπει να καλέσετε το setData. Φυσικά, μεταξύ των κλήσεων, μια κορυφή θα μπορούσε να διαγραφεί από μια ανταγωνιστική κλήση και το setData θα επέστρεφε επίσης ένα σφάλμα. Σε αυτή την περίπτωση, μπορείτε να το κάνετε ξανά από την αρχή, αλλά αξίζει τον κόπο;

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

Περισσότερες τεχνικές λεπτομέρειες

Σε αυτή την ενότητα θα κάνουμε ένα διάλειμμα από τα κατανεμημένα συστήματα και θα μιλήσουμε για την κωδικοποίηση.
Μία από τις κύριες απαιτήσεις του πελάτη ήταν η πολλαπλή πλατφόρμα: τουλάχιστον μία από τις υπηρεσίες πρέπει να υποστηρίζεται σε Linux, MacOS και Windows. Αρχικά, αναπτύξαμε μόνο για Linux και αρχίσαμε να δοκιμάζουμε σε άλλα συστήματα αργότερα. Αυτό προκάλεσε πολλά προβλήματα, τα οποία για κάποιο διάστημα ήταν εντελώς ασαφές πώς να προσεγγιστούν. Ως αποτέλεσμα, και οι τρεις υπηρεσίες συντονισμού υποστηρίζονται πλέον σε Linux και MacOS, ενώ μόνο το Consul KV υποστηρίζεται σε Windows.

Από την αρχή προσπαθήσαμε να χρησιμοποιήσουμε έτοιμες βιβλιοθήκες για πρόσβαση σε υπηρεσίες. Στην περίπτωση του ZooKeeper, η επιλογή έπεσε ZooKeeper C++, το οποίο τελικά απέτυχε να μεταγλωττιστεί στα Windows. Αυτό, ωστόσο, δεν προκαλεί έκπληξη: η βιβλιοθήκη είναι τοποθετημένη ως μόνο linux. Για τον πρόξενο η μόνη επιλογή ήταν ppconsul. Έπρεπε να προστεθεί υποστήριξη συνεδρίες и συναλλαγές. Για το etcd, δεν βρέθηκε μια πλήρης βιβλιοθήκη που υποστηρίζει την πιο πρόσφατη έκδοση του πρωτοκόλλου, επομένως απλά δημιουργημένος πελάτης grpc.

Εμπνευσμένοι από την ασύγχρονη διεπαφή της βιβλιοθήκης ZooKeeper C++, αποφασίσαμε να εφαρμόσουμε επίσης μια ασύγχρονη διεπαφή. Το ZooKeeper C++ χρησιμοποιεί πρωτόγονα μελλοντικά/υπόσχεσης για αυτό. Στο STL, δυστυχώς, εφαρμόζονται πολύ μέτρια. Για παράδειγμα, όχι στη συνέχεια μέθοδος, το οποίο εφαρμόζει τη συνάρτηση που πέρασε στο αποτέλεσμα του μέλλοντος όταν γίνει διαθέσιμη. Στην περίπτωσή μας, μια τέτοια μέθοδος είναι απαραίτητη για τη μετατροπή του αποτελέσματος στη μορφή της βιβλιοθήκης μας. Για να ξεπεράσουμε αυτό το πρόβλημα, έπρεπε να εφαρμόσουμε το δικό μας απλό νήμα, καθώς κατόπιν αιτήματος του πελάτη δεν μπορούσαμε να χρησιμοποιήσουμε βαριές βιβλιοθήκες τρίτων, όπως το Boost.

Η τότε εφαρμογή μας λειτουργεί έτσι. Όταν καλείται, δημιουργείται ένα επιπλέον ζεύγος υπόσχεσης/μελλοντικού. Το νέο μέλλον επιστρέφεται και το περασμένο τοποθετείται μαζί με την αντίστοιχη συνάρτηση και μια επιπλέον υπόσχεση στην ουρά. Ένα νήμα από το pool επιλέγει πολλά συμβόλαια μελλοντικής εκπλήρωσης από την ουρά και τα δημοσκοπεί χρησιμοποιώντας το wait_for. Όταν ένα αποτέλεσμα είναι διαθέσιμο, καλείται η αντίστοιχη συνάρτηση και η τιμή επιστροφής της μεταβιβάζεται στην υπόσχεση.

Χρησιμοποιήσαμε το ίδιο νήμα για να εκτελέσουμε ερωτήματα στο etcd και στο Consul. Αυτό σημαίνει ότι οι υποκείμενες βιβλιοθήκες είναι προσβάσιμες από πολλά διαφορετικά νήματα. Το ppconsul δεν είναι ασφαλές για νήματα, επομένως οι κλήσεις σε αυτό προστατεύονται από κλειδαριές.
Μπορείτε να εργαστείτε με grpc από πολλά νήματα, αλλά υπάρχουν λεπτές λεπτομέρειες. Στο etcd τα ρολόγια υλοποιούνται μέσω grpc streams. Αυτά είναι αμφίδρομα κανάλια για μηνύματα συγκεκριμένου τύπου. Η βιβλιοθήκη δημιουργεί ένα νήμα για όλα τα ρολόγια και ένα νήμα που επεξεργάζεται τα εισερχόμενα μηνύματα. Άρα το grpc απαγορεύει την παράλληλη εγγραφή στη ροή. Αυτό σημαίνει ότι κατά την προετοιμασία ή τη διαγραφή ενός ρολογιού, πρέπει να περιμένετε έως ότου ολοκληρωθεί η αποστολή του προηγούμενου αιτήματος πριν στείλετε το επόμενο. Χρησιμοποιούμε για συγχρονισμό υπό όρους μεταβλητές.

Σύνολο

Δείτε και μόνοι σας: liboffkv.

Η ομάδα μας: Ράεντ Ρομάνοφ, Ιβάν Γκλουσένκοφ, Ντμίτρι Καμαλντίνοφ, Victor Krapivensky, Βιτάλι Ιβάνιν.

Πηγή: www.habr.com

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