Πώς και γιατί γράψαμε μια κλιμακωτή υπηρεσία υψηλής φόρτωσης για 1C: Enterprise: Java, PostgreSQL, Hazelcast

Σε αυτό το άρθρο θα μιλήσουμε για το πώς και γιατί αναπτύξαμε Σύστημα αλληλεπίδρασης - ένας μηχανισμός που μεταφέρει πληροφορίες μεταξύ των εφαρμογών-πελατών και των διακομιστών 1C: Enterprise - από τον καθορισμό μιας εργασίας έως τη σκέψη μέσω της αρχιτεκτονικής και των λεπτομερειών υλοποίησης.

Το Σύστημα Αλληλεπίδρασης (εφεξής - CB) είναι ένα κατανεμημένο σύστημα ανταλλαγής μηνυμάτων με ανοχή σφαλμάτων με εγγυημένη παράδοση. Το CB έχει σχεδιαστεί ως υπηρεσία υψηλού φορτίου με υψηλή επεκτασιμότητα, διαθέσιμη τόσο ως διαδικτυακή υπηρεσία (παρέχεται από την 1C) όσο και ως προϊόν μαζικής παραγωγής που μπορεί να αναπτυχθεί στις δικές του εγκαταστάσεις διακομιστή.

Η SW χρησιμοποιεί κατανεμημένη αποθήκευση φουντουκιά και μηχανή αναζήτησης Ελαστική αναζήτηση. Θα μιλήσουμε επίσης για την Java και τον τρόπο οριζόντιας κλίμακας της PostgreSQL.
Πώς και γιατί γράψαμε μια κλιμακωτή υπηρεσία υψηλής φόρτωσης για 1C: Enterprise: Java, PostgreSQL, Hazelcast

Δήλωση προβλήματος

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

Αρχικά, λίγα λόγια για εμάς για όσους δεν ξέρουν τι κάνουμε ακόμα :) Αναπτύσσουμε την τεχνολογική πλατφόρμα 1C:Enterprise. Η πλατφόρμα περιλαμβάνει ένα εργαλείο ανάπτυξης επιχειρηματικών εφαρμογών, καθώς και χρόνο εκτέλεσης που επιτρέπει στις επιχειρηματικές εφαρμογές να λειτουργούν σε περιβάλλον πολλαπλών πλατφορμών.

Παράδειγμα ανάπτυξης πελάτη-διακομιστή

Οι επιχειρηματικές εφαρμογές που δημιουργούνται στο 1C:Enterprise λειτουργούν σε τρία επίπεδα διακομιστή-πελάτη αρχιτεκτονική "DBMS - διακομιστής εφαρμογών - πελάτης". Ο κωδικός εφαρμογής είναι γραμμένος ενσωματωμένη γλώσσα 1C, μπορεί να εκτελεστεί στον διακομιστή εφαρμογών ή στον πελάτη. Όλες οι εργασίες με αντικείμενα εφαρμογής (καταλόγους, έγγραφα κ.λπ.), καθώς και η ανάγνωση και η εγγραφή της βάσης δεδομένων, εκτελούνται μόνο στον διακομιστή. Η λειτουργία διασύνδεσης φορμών και εντολών υλοποιείται επίσης στον διακομιστή. Στον πελάτη, λαμβάνονται, ανοίγουν και εμφανίζονται φόρμες, "επικοινωνία" με τον χρήστη (προειδοποιήσεις, ερωτήσεις ...), μικροί υπολογισμοί σε φόρμες που απαιτούν γρήγορη απάντηση (για παράδειγμα, πολλαπλασιασμός της τιμής με την ποσότητα), εργασία με τοπικά αρχεία, εργασία με εξοπλισμό.

Στον κώδικα εφαρμογής, οι κεφαλίδες των διαδικασιών και των συναρτήσεων πρέπει να αναφέρουν ρητά πού θα εκτελεστεί ο κώδικας - χρησιμοποιώντας τις οδηγίες &AtClient / &AtServer (&AtClient / &AtServer στην αγγλική έκδοση της γλώσσας). Οι προγραμματιστές του 1C θα με διορθώσουν τώρα λέγοντας ότι οι οδηγίες είναι στην πραγματικότητα περισσότερο από, αλλά για εμάς δεν είναι σημαντικό τώρα.

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

Πώς και γιατί γράψαμε μια κλιμακωτή υπηρεσία υψηλής φόρτωσης για 1C: Enterprise: Java, PostgreSQL, Hazelcast
Κώδικας που χειρίζεται ένα κλικ κουμπιού: η κλήση μιας διαδικασίας διακομιστή από τον πελάτη θα λειτουργήσει, η κλήση μιας διαδικασίας πελάτη από τον διακομιστή δεν θα

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

Και υπάρχει επίσης ανάγκη, για παράδειγμα, όταν ένα τηλέφωνο SIP-καλέστε, ειδοποιήστε την εφαρμογή πελάτη σχετικά με αυτό, ώστε να τη βρει στη βάση δεδομένων του αντισυμβαλλομένου με τον αριθμό του καλούντος και να εμφανίσει στον χρήστη πληροφορίες σχετικά με τον καλούντα αντισυμβαλλόμενο. Ή, για παράδειγμα, όταν φτάσει μια παραγγελία στην αποθήκη, ειδοποιήστε την εφαρμογή πελάτη του πελάτη σχετικά. Γενικά, υπάρχουν πολλές περιπτώσεις που ένας τέτοιος μηχανισμός θα ήταν χρήσιμος.

Στην πραγματικότητα ρύθμιση

Δημιουργήστε έναν μηχανισμό ανταλλαγής μηνυμάτων. Γρήγορο, αξιόπιστο, με εγγυημένη παράδοση, με δυνατότητα ευέλικτης αναζήτησης μηνυμάτων. Με βάση τον μηχανισμό, εφαρμόστε ένα messenger (μηνύματα, βιντεοκλήσεις) που λειτουργεί μέσα σε εφαρμογές 1C.

Σχεδιάστε το σύστημα με οριζόντια κλιμάκωση. Ένα αυξανόμενο φορτίο θα πρέπει να καλύπτεται από την αύξηση του αριθμού των κόμβων.

Реализация

Αποφασίσαμε να μην ενσωματώσουμε το τμήμα διακομιστή του CB απευθείας στην πλατφόρμα 1C:Enterprise, αλλά να το εφαρμόσουμε ως ξεχωριστό προϊόν, το API του οποίου μπορεί να καλείται από τον κώδικα των λύσεων εφαρμογής 1C. Αυτό έγινε για διάφορους λόγους, ο κύριος από τους οποίους ήταν να καταστεί δυνατή η ανταλλαγή μηνυμάτων μεταξύ διαφορετικών εφαρμογών 1C (για παράδειγμα, μεταξύ του Υπουργείου Εμπορίου και Λογιστικής). Διαφορετικές εφαρμογές 1C μπορούν να εκτελούνται σε διαφορετικές εκδόσεις της πλατφόρμας 1C:Enterprise, να βρίσκονται σε διαφορετικούς διακομιστές κ.λπ. Υπό αυτές τις συνθήκες, η εφαρμογή του CB ως ξεχωριστού προϊόντος, που βρίσκεται «στο πλάι» των εγκαταστάσεων 1C, είναι η βέλτιστη λύση.

Έτσι, αποφασίσαμε να κάνουμε το CB ως ξεχωριστό προϊόν. Για μικρότερες εταιρείες, συνιστούμε τη χρήση του διακομιστή CB που εγκαταστήσαμε στο σύννεφο (wss://1cdialog.com) για να αποφύγετε την επιβάρυνση που σχετίζεται με την εγκατάσταση και τη διαμόρφωση τοπικού διακομιστή. Ωστόσο, οι μεγάλοι πελάτες μπορεί να θεωρήσουν σκόπιμο να εγκαταστήσουν τον δικό τους διακομιστή CB στις εγκαταστάσεις τους. Χρησιμοποιήσαμε παρόμοια προσέγγιση στο προϊόν μας cloud SaaS. 1cΦρέσκο – κυκλοφορεί ως προϊόν παραγωγής για εγκατάσταση από πελάτες, και χρησιμοποιείται επίσης στο cloud μας https://1cfresh.com/.

App

Για κατανομή φορτίου και ανοχή σφαλμάτων, δεν θα αναπτύξουμε μία εφαρμογή Java, αλλά πολλές, θα βάλουμε μπροστά τους έναν εξισορροπητή φορτίου. Εάν πρέπει να στείλετε ένα μήνυμα από κόμβο σε κόμβο, χρησιμοποιήστε τη δημοσίευση/εγγραφή στο Hazelcast.

Επικοινωνία μεταξύ του πελάτη και του διακομιστή - μέσω websocket. Είναι κατάλληλο για συστήματα σε πραγματικό χρόνο.

Κατανεμημένη κρυφή μνήμη

Επιλέξτε ανάμεσα σε Redis, Hazelcast και Ehcache. Έξω το 2015. Ο Redis μόλις κυκλοφόρησε ένα νέο cluster (πολύ νέο, τρομακτικό), υπάρχει ένα Sentinel με πολλούς περιορισμούς. Το Ehcache δεν γνωρίζει πώς να συγκεντρώνει (αυτή η λειτουργία εμφανίστηκε αργότερα). Αποφασίσαμε να δοκιμάσουμε με το Hazelcast 3.4.
Το Hazelcast είναι ομαδοποιημένο από το κουτί. Στη λειτουργία ενός κόμβου, δεν είναι πολύ χρήσιμο και μπορεί να χωρέσει μόνο ως προσωρινή μνήμη - δεν ξέρει πώς να απορρίπτει δεδομένα στο δίσκο, εάν χαθεί ο μοναδικός κόμβος, τα δεδομένα χάνονται. Αναπτύσσουμε πολλά Hazelcast, μεταξύ των οποίων δημιουργούμε αντίγραφα ασφαλείας κρίσιμων δεδομένων. Δεν δημιουργούμε αντίγραφα ασφαλείας της κρυφής μνήμης - δεν τον λυπόμαστε.

Για εμάς, το Hazelcast είναι:

  • Αποθήκευση συνεδριών χρήστη. Χρειάζεται πολύς χρόνος για να μεταβείτε στη βάση δεδομένων για μια συνεδρία, επομένως βάζουμε όλες τις συνεδρίες στο Hazelcast.
  • Κρύπτη. Αναζήτηση προφίλ χρήστη - ελέγξτε την προσωρινή μνήμη. Έγραψε ένα νέο μήνυμα - τοποθετήστε το στην κρυφή μνήμη.
  • Θέματα επικοινωνίας περιπτώσεων εφαρμογής. Ο κόμβος δημιουργεί ένα συμβάν και το τοποθετεί σε ένα θέμα Hazelcast. Άλλοι κόμβοι εφαρμογής που είναι εγγεγραμμένοι σε αυτό το θέμα λαμβάνουν και επεξεργάζονται το συμβάν.
  • κλειδαριές συμπλέγματος. Για παράδειγμα, δημιουργούμε μια συζήτηση με ένα μοναδικό κλειδί (συζήτηση-singleton στο πλαίσιο της βάσης 1C):

conversationKeyChecker.check("БЕНЗОКОЛОНКА");

      doInClusterLock("БЕНЗОКОЛОНКА", () -> {

          conversationKeyChecker.check("БЕНЗОКОЛОНКА");

          createChannel("БЕНЗОКОЛОНКА");
      });

Ελέγξαμε ότι δεν υπάρχει κανάλι. Πήραν την κλειδαριά, την έλεγξαν ξανά, τη δημιούργησαν. Εάν δεν κάνετε έλεγχο μετά τη λήψη της κλειδαριάς, τότε υπάρχει περίπτωση να έχει τσεκάρει και άλλο νήμα εκείνη τη στιγμή και να προσπαθήσει τώρα να δημιουργήσει την ίδια συζήτηση - και υπάρχει ήδη. Είναι αδύνατο να δημιουργήσετε ένα κλείδωμα μέσω συγχρονισμένου ή κανονικού java Lock. Μέσα από τη βάση - σιγά σιγά, και η βάση είναι κρίμα, μέσω Hazelcast - αυτό που χρειάζεστε.

Επιλέγοντας ένα DBMS

Έχουμε μεγάλη και επιτυχημένη εμπειρία με την PostgreSQL και συνεργασία με τους προγραμματιστές αυτού του DBMS.

Με ένα σύμπλεγμα, η PostgreSQL δεν είναι εύκολη - υπάρχει XL, XC, Citus, αλλά, γενικά, δεν είναι η noSQL που κλιμακώνεται από το κουτί. Η NoSQL δεν θεωρήθηκε ως η κύρια αποθήκευση, αρκούσε να πάρουμε το Hazelcast, με το οποίο δεν είχαμε δουλέψει πριν.

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

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

Για παράδειγμα, μπορείτε να διαβάσετε σχετικά με τον πολυενοικιαστή στον ιστότοπο Δεδομένα Citus.

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

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

Πώς και γιατί γράψαμε μια κλιμακωτή υπηρεσία υψηλής φόρτωσης για 1C: Enterprise: Java, PostgreSQL, Hazelcast

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

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

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

Εάν χαθεί το σύγχρονο αντίγραφο, το ασύγχρονο αντίγραφο γίνεται σύγχρονο.
Εάν χαθεί η κύρια βάση δεδομένων, το σύγχρονο αντίγραφο γίνεται η κύρια βάση δεδομένων, το ασύγχρονο αντίγραφο γίνεται σύγχρονο αντίγραφο.

Elastics αναζήτηση για αναζήτηση

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

Στο github βρήκαμε Ρωσική προσθήκη μορφολογίας για το Elasticsearch και χρησιμοποιήστε το. Στο ευρετήριο Elasticsearch, αποθηκεύουμε ρίζες λέξεων (τις οποίες ορίζει το πρόσθετο) και N-γραμμάρια. Καθώς ο χρήστης εισάγει κείμενο για αναζήτηση, αναζητούμε το πληκτρολογημένο κείμενο μεταξύ N-γραμμαρίων. Όταν αποθηκευτεί στο ευρετήριο, η λέξη "κείμενα" θα χωριστεί στα ακόλουθα N-γραμμάρια:

[te, tech, tex, text, texts, ek, eks, ext, exts, ks, kst, ksty, st, sty, you],

Και επίσης θα αποθηκευτεί η ρίζα της λέξης "κείμενο". Αυτή η προσέγγιση σάς επιτρέπει να κάνετε αναζήτηση στην αρχή, στη μέση και στο τέλος της λέξης.

Μεγάλη εικόνα

Πώς και γιατί γράψαμε μια κλιμακωτή υπηρεσία υψηλής φόρτωσης για 1C: Enterprise: Java, PostgreSQL, Hazelcast
Επαναλαμβάνοντας την εικόνα από την αρχή του άρθρου, αλλά με επεξηγήσεις:

  • Ισορροπητής που εκτίθεται στο Διαδίκτυο. έχουμε nginx, μπορεί να είναι οποιοδήποτε.
  • Τα στιγμιότυπα εφαρμογών Java επικοινωνούν μεταξύ τους μέσω Hazelcast.
  • Για να δουλέψουμε με μια πρίζα web, χρησιμοποιούμε Netty.
  • Η εφαρμογή Java γραμμένη σε Java 8, αποτελείται από πακέτα OSGi. Τα σχέδια είναι η μετάβαση στην Java 10 και η μετάβαση σε λειτουργικές μονάδες.

Ανάπτυξη και δοκιμή

Κατά την ανάπτυξη και τη δοκιμή του CB, συναντήσαμε μια σειρά από ενδιαφέροντα χαρακτηριστικά των προϊόντων που χρησιμοποιούμε.

Δοκιμή φόρτωσης και διαρροές μνήμης

Η απελευθέρωση κάθε έκδοσης CB είναι μια δοκιμή φορτίου. Πέρασε με επιτυχία όταν:

  • Το τεστ λειτούργησε για αρκετές ημέρες και δεν υπήρξαν αρνήσεις εξυπηρέτησης
  • Ο χρόνος απόκρισης για βασικές λειτουργίες δεν ξεπέρασε ένα άνετο όριο
  • Η υποβάθμιση της απόδοσης σε σύγκριση με την προηγούμενη έκδοση δεν είναι μεγαλύτερη από 10%

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

Εκτελούμε δοκιμές φορτίου του συστήματος αλληλεπίδρασης σε τρεις διαμορφώσεις:

  1. τεστ άγχους
  2. Μόνο συνδέσεις
  3. Εγγραφή συνδρομητή

Κατά τη διάρκεια ενός stress test, εκκινούμε αρκετές εκατοντάδες νήματα και φορτώνουν το σύστημα χωρίς διακοπή: γράφουν μηνύματα, δημιουργούν συζητήσεις, λαμβάνουν μια λίστα μηνυμάτων. Προσομοιώνουμε τις ενέργειες των απλών χρηστών (λαμβάνω μια λίστα με τα μη αναγνωσμένα μηνύματά μου, γράφω σε κάποιον) και προγραμματίζουμε αποφάσεις (μεταφορά πακέτου σε άλλη διαμόρφωση, επεξεργασία ειδοποίησης).

Για παράδειγμα, αυτό είναι το μέρος του stress test:

  • Ο χρήστης συνδέεται
    • Ζητάει τα μη αναγνωσμένα νήματα σας
    • 50% πιθανότητα να διαβάσετε μηνύματα
    • 50% πιθανότητα να γράψετε μηνύματα
    • Επόμενος χρήστης:
      • 20% πιθανότητα να δημιουργήσετε ένα νέο νήμα
      • Επιλέγει τυχαία οποιαδήποτε από τις συζητήσεις του
      • Μπαίνει μέσα
      • Ζητάει μηνύματα, προφίλ χρηστών
      • Δημιουργεί πέντε μηνύματα που απευθύνονται σε τυχαίους χρήστες από αυτό το νήμα
      • Εκτός συζήτησης
      • Επαναλαμβάνεται 20 φορές
      • Αποσυνδέεται, επιστρέφει στην αρχή του σεναρίου

    • Ένα chatbot εισέρχεται στο σύστημα (προσομοιώνει τα μηνύματα από τον κώδικα των εφαρμοζόμενων λύσεων)
      • 50% πιθανότητα δημιουργίας νέου καναλιού δεδομένων (ειδική συζήτηση)
      • 50% πιθανότητα να γράψετε ένα μήνυμα σε οποιοδήποτε από τα υπάρχοντα κανάλια

Το σενάριο "Μόνο Συνδέσεις" εμφανίστηκε για κάποιο λόγο. Υπάρχει μια κατάσταση: οι χρήστες έχουν συνδέσει το σύστημα, αλλά δεν έχουν ακόμη εμπλακεί. Κάθε χρήστης το πρωί στις 09:00 ανοίγει τον υπολογιστή, δημιουργεί σύνδεση με τον διακομιστή και μένει σιωπηλός. Αυτοί οι τύποι είναι επικίνδυνοι, υπάρχουν πολλοί - έχουν μόνο PING / PONG έξω από τα πακέτα, αλλά διατηρούν τη σύνδεση με τον διακομιστή (δεν μπορούν να τη διατηρήσουν - και ξαφνικά ένα νέο μήνυμα). Η δοκιμή αναπαράγει την κατάσταση όταν ένας μεγάλος αριθμός τέτοιων χρηστών προσπαθούν να συνδεθούν στο σύστημα σε μισή ώρα. Μοιάζει με τεστ αντοχής, αλλά η εστίασή του είναι ακριβώς σε αυτήν την πρώτη εισαγωγή - έτσι ώστε να μην υπάρχουν αποτυχίες (ένα άτομο δεν χρησιμοποιεί το σύστημα, αλλά ήδη πέφτει - είναι δύσκολο να βρεις κάτι χειρότερο).

Το σενάριο εγγραφής συνδρομητή ξεκινά από την πρώτη κυκλοφορία. Πραγματοποιήσαμε ένα stress test και ήμασταν σίγουροι ότι το σύστημα δεν επιβραδύνθηκε στην αλληλογραφία. Αλλά οι χρήστες πήγαν και η εγγραφή άρχισε να πέφτει λόγω του χρονικού ορίου. Κατά την εγγραφή, χρησιμοποιήσαμε /dev/τυχαία, το οποίο συνδέεται με την εντροπία του συστήματος. Ο διακομιστής δεν είχε χρόνο να συγκεντρώσει αρκετή εντροπία και, όταν ζητήθηκε ένα νέο SecureRandom, πάγωσε για δεκάδες δευτερόλεπτα. Υπάρχουν πολλοί τρόποι εξόδου από αυτήν την κατάσταση, για παράδειγμα: μεταβείτε σε ένα λιγότερο ασφαλές /dev/urandom, εγκαταστήστε έναν ειδικό πίνακα που δημιουργεί εντροπία, δημιουργήστε τυχαίους αριθμούς εκ των προτέρων και αποθηκεύστε τους στην πισίνα. Κλείσαμε προσωρινά το πρόβλημα με την πισίνα, αλλά από τότε τρέχουμε ξεχωριστό τεστ για την εγγραφή νέων συνδρομητών.

Ως γεννήτρια φορτίου χρησιμοποιούμε jμέτρο. Δεν ξέρει πώς να δουλεύει με websocket, χρειάζεται πρόσθετο. Τα πρώτα αποτελέσματα αναζήτησης για το ερώτημα "jmeter websocket" είναι άρθρα με το BlazeMeterστο οποίο συνιστούν πρόσθετο από τον Maciej Zaleski.

Από εκεί αποφασίσαμε να ξεκινήσουμε.

Σχεδόν αμέσως μετά την έναρξη των σοβαρών δοκιμών, ανακαλύψαμε ότι άρχισαν διαρροές μνήμης στο JMeter.

Το πρόσθετο είναι μια ξεχωριστή μεγάλη ιστορία, με 176 αστέρια έχει 132 πιρούνια στο github. Ο ίδιος ο συγγραφέας δεν έχει δεσμευτεί σε αυτό από το 2015 (το πήραμε το 2015, τότε δεν δημιούργησε υποψίες), αρκετά θέματα github σχετικά με διαρροές μνήμης, 7 unclosed pull requests.
Εάν επιλέξετε να φορτώσετε τη δοκιμή με αυτήν την προσθήκη, σημειώστε τις ακόλουθες συζητήσεις:

  1. Σε ένα περιβάλλον πολλαπλών νημάτων, χρησιμοποιήθηκε η συνηθισμένη LinkedList, ως αποτέλεσμα, λάβαμε NPE κατά το χρόνο εκτέλεσης. Επιλύεται είτε με εναλλαγή στο ConcurrentLinkedDeque, είτε με συγχρονισμένα μπλοκ. Διαλέξαμε την πρώτη επιλογή για εμάςhttps://github.com/maciejzaleski/JMeter-WebSocketSampler/issues/43).
  2. Διαρροή μνήμης, οι πληροφορίες σύνδεσης δεν διαγράφονται κατά την αποσύνδεση (https://github.com/maciejzaleski/JMeter-WebSocketSampler/issues/44).
  3. Στη λειτουργία ροής (όταν η υποδοχή web δεν είναι κλειστή στο τέλος του δείγματος, αλλά χρησιμοποιείται περαιτέρω στο σχέδιο), τα μοτίβα απόκρισης δεν λειτουργούν (https://github.com/maciejzaleski/JMeter-WebSocketSampler/issues/19).

Αυτό είναι ένα από αυτά στο github. Τι κάναμε:

  1. έχουν πάρει πιρούνι του Elyran Kogan (@elyrank) - διορθώνει τα ζητήματα 1 και 3
  2. Λύθηκε το πρόβλημα 2
  3. Ενημερώθηκε η προβλήτα από 9.2.14 έως 9.3.12
  4. Περιτυλιγμένο SimpleDateFormat στο ThreadLocal. Το SimpleDateFormat δεν είναι ασφαλές νήμα που οδηγεί σε NPE κατά το χρόνο εκτέλεσης
  5. Διορθώθηκε μια άλλη διαρροή μνήμης (η σύνδεση έκλεισε λανθασμένα κατά την αποσύνδεση)

Κι όμως ρέει!

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

Έχουν περάσει δύο μέρες...

Τώρα το Hazelcast εξαντλείται από τη μνήμη. Τα αρχεία καταγραφής έδειξαν ότι μετά από μερικές ημέρες δοκιμών, ο Hazelcast αρχίζει να παραπονιέται για την έλλειψη μνήμης και μετά από λίγο το σύμπλεγμα καταρρέει και οι κόμβοι συνεχίζουν να πεθαίνουν ένας προς έναν. Συνδέσαμε το JVisualVM στο hazelcast και είδαμε το "ανοδικό πριόνι" - καλούσε τακτικά το GC, αλλά δεν μπορούσε να καθαρίσει τη μνήμη με κανέναν τρόπο.

Πώς και γιατί γράψαμε μια κλιμακωτή υπηρεσία υψηλής φόρτωσης για 1C: Enterprise: Java, PostgreSQL, Hazelcast

Αποδείχθηκε ότι στο hazelcast 3.4, κατά τη διαγραφή ενός χάρτη / multiMap (map.destroy()), η μνήμη δεν ελευθερώνεται πλήρως:

github.com/hazelcast/hazelcast/issues/6317
github.com/hazelcast/hazelcast/issues/4888

Το σφάλμα διορθώθηκε τώρα στο 3.5, αλλά τότε ήταν πρόβλημα. Δημιουργήσαμε νέο multiMap με δυναμικά ονόματα και διαγράψαμε σύμφωνα με τη λογική μας. Ο κώδικας έμοιαζε κάπως έτσι:

public void join(Authentication auth, String sub) {
    MultiMap<UUID, Authentication> sessions = instance.getMultiMap(sub);
    sessions.put(auth.getUserId(), auth);
}

public void leave(Authentication auth, String sub) {
    MultiMap<UUID, Authentication> sessions = instance.getMultiMap(sub);
    sessions.remove(auth.getUserId(), auth);

    if (sessions.size() == 0) {
        sessions.destroy();
    }
}

Βάζω:

service.join(auth1, "НОВЫЕ_СООБЩЕНИЯ_В_ОБСУЖДЕНИИ_UUID1");
service.join(auth2, "НОВЫЕ_СООБЩЕНИЯ_В_ОБСУЖДЕНИИ_UUID1");

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

public void join(Authentication auth, String sub) {
    addValueToMap(sub, auth.getSessionId());
}

public void leave(Authentication auth, String sub) { 
    removeValueFromMap(sub, auth.getSessionId());
}

Τα διαγράμματα έχουν βελτιωθεί.

Πώς και γιατί γράψαμε μια κλιμακωτή υπηρεσία υψηλής φόρτωσης για 1C: Enterprise: Java, PostgreSQL, Hazelcast

Τι άλλο μάθαμε για τη δοκιμή φορτίου

  1. Το JSR223 πρέπει να γραφτεί σε groovy και να περιλαμβάνει cache μεταγλώττισης - είναι πολύ πιο γρήγορο. Σύνδεσμος.
  2. Τα γραφήματα Jmeter-Plugins είναι πιο κατανοητά από τα τυπικά. Σύνδεσμος.

Σχετικά με την εμπειρία μας με το Hazelcast

Το Hazelcast ήταν ένα νέο προϊόν για εμάς, ξεκινήσαμε να εργαζόμαστε με αυτό από την έκδοση 3.4.1, τώρα έχουμε την έκδοση 3.9.2 στον διακομιστή παραγωγής μας (τη στιγμή που γράφονται αυτές οι γραμμές, η τελευταία έκδοση του Hazelcast είναι η 3.10).

δημιουργία ταυτότητας

Ξεκινήσαμε με ακέραια αναγνωριστικά. Ας φανταστούμε ότι χρειαζόμαστε ένα άλλο Long για μια νέα οντότητα. Η αλληλουχία στη βάση δεδομένων δεν είναι κατάλληλη, οι πίνακες εμπλέκονται σε κοινή χρήση - αποδεικνύεται ότι υπάρχει ένα μήνυμα ID=1 στο DB1 και ένα μήνυμα ID=1 στο DB2, δεν μπορείτε να βάλετε αυτό το αναγνωριστικό στο Elasticsearch, στο Hazelcast επίσης, αλλά το χειρότερο είναι εάν θέλετε να μειώσετε τα δεδομένα από δύο βάσεις δεδομένων σε μία (για παράδειγμα, να αποφασίσετε ότι μια βάση δεδομένων είναι αρκετή για αυτούς τους συνδρομητές). Μπορείτε να έχετε πολλά AtomicLongs στο Hazelcast και να κρατήσετε τον μετρητή εκεί, τότε η απόδοση της λήψης ενός νέου αναγνωριστικού είναι αυξητικήΚαι Λάβετε συν το χρόνο για να κάνετε ερώτημα στο Hazelcast. Αλλά το Hazelcast έχει κάτι πιο βέλτιστο - το FlakeIdGenerator. Κατά την επικοινωνία, σε κάθε πελάτη δίνεται μια σειρά από αναγνωριστικά, για παράδειγμα, το πρώτο - από 1 έως 10, το δεύτερο - από 000 έως 10 κ.ο.κ. Τώρα ο πελάτης μπορεί να εκδώσει από μόνος του νέα αναγνωριστικά μέχρι να τελειώσει το εύρος που του έχει εκδοθεί. Λειτουργεί γρήγορα, αλλά η επανεκκίνηση της εφαρμογής (και του προγράμματος-πελάτη Hazelcast) ξεκινά μια νέα ακολουθία - ως εκ τούτου οι παραλείψεις κ.λπ. Επιπλέον, δεν είναι πολύ ξεκάθαρο στους προγραμματιστές γιατί τα αναγνωριστικά είναι ακέραιοι αριθμοί, αλλά είναι τόσο πολύ σε αντίθεση. Ζυγίσαμε τα πάντα και περάσαμε σε UUID.

Παρεμπιπτόντως, για όσους θέλουν να είναι σαν το Twitter, υπάρχει μια τέτοια βιβλιοθήκη Snowcast - αυτή είναι μια εφαρμογή του Snowflake πάνω από το Hazelcast. Μπορείτε να δείτε εδώ:

github.com/noctarius/snowcast
github.com/twitter/snowflake

Αλλά δεν το έχουμε καταφέρει ακόμα.

TransactionalMap.replace

Μια άλλη έκπληξη: Το TransactionalMap.replace δεν λειτουργεί. Εδώ είναι ένα τεστ:

@Test
public void replaceInMap_putsAndGetsInsideTransaction() {

    hazelcastInstance.executeTransaction(context -> {
        HazelcastTransactionContextHolder.setContext(context);
        try {
            context.getMap("map").put("key", "oldValue");
            context.getMap("map").replace("key", "oldValue", "newValue");
            
            String value = (String) context.getMap("map").get("key");
            assertEquals("newValue", value);

            return null;
        } finally {
            HazelcastTransactionContextHolder.clearContext();
        }        
    });
}

Expected : newValue
Actual : oldValue

Έπρεπε να γράψω τη δική μου αντικατάσταση χρησιμοποιώντας το getForUpdate:

protected <K,V> boolean replaceInMap(String mapName, K key, V oldValue, V newValue) {
    TransactionalTaskContext context = HazelcastTransactionContextHolder.getContext();
    if (context != null) {
        log.trace("[CACHE] Replacing value in a transactional map");
        TransactionalMap<K, V> map = context.getMap(mapName);
        V value = map.getForUpdate(key);
        if (oldValue.equals(value)) {
            map.put(key, newValue);
            return true;
        }

        return false;
    }
    log.trace("[CACHE] Replacing value in a not transactional map");
    IMap<K, V> map = hazelcastInstance.getMap(mapName);
    return map.replace(key, oldValue, newValue);
}

Δοκιμάστε όχι μόνο τις κανονικές δομές δεδομένων, αλλά και τις συναλλακτικές τους εκδόσεις. Συμβαίνει ότι το IMap λειτουργεί, αλλά το TransactionalMap δεν υπάρχει πλέον.

Συνδέστε νέο JAR χωρίς χρόνο διακοπής λειτουργίας

Αρχικά, αποφασίσαμε να γράψουμε αντικείμενα της τάξης μας στο Hazelcast. Για παράδειγμα, έχουμε μια κλάση Εφαρμογές, θέλουμε να την αποθηκεύσουμε και να την διαβάσουμε. Αποθηκεύσετε:

IMap<UUID, Application> map = hazelcastInstance.getMap("application");
map.set(id, application);

Διαβάζουμε:

IMap<UUID, Application> map = hazelcastInstance.getMap("application");
return map.get(id);

Όλα λειτουργούν. Στη συνέχεια, αποφασίσαμε να δημιουργήσουμε ένα ευρετήριο στο Hazelcast για να το αναζητήσουμε:

map.addIndex("subscriberId", false);

Και όταν γράφουν μια νέα οντότητα, άρχισαν να λαμβάνουν ένα ClassNotFoundException. Ο Hazelcast προσπάθησε να προσθέσει στο ευρετήριο, αλλά δεν ήξερε τίποτα για την τάξη μας και ήθελε να βάλει ένα JAR με αυτήν την κατηγορία. Κάναμε ακριβώς αυτό, όλα λειτούργησαν, αλλά εμφανίστηκε ένα νέο πρόβλημα: πώς να ενημερώσετε το JAR χωρίς να σταματήσετε εντελώς το σύμπλεγμα; Το Hazelcast δεν λαμβάνει νέο JAR σε μια ενημέρωση ανά κόμβο. Σε αυτό το σημείο, αποφασίσαμε ότι θα μπορούσαμε να ζήσουμε χωρίς αναζητήσεις ευρετηρίου. Σε τελική ανάλυση, εάν χρησιμοποιείτε το Hazelcast ως κατάστημα βασικής αξίας, τότε όλα θα λειτουργήσουν; Όχι πραγματικά. Εδώ πάλι διαφορετική συμπεριφορά του IMap και του TransactionalMap. Όπου το IMap δεν ενδιαφέρεται, το TransactionalMap παρουσιάζει ένα σφάλμα.

IMap. Γράφουμε 5000 αντικείμενα, διαβάζουμε. Όλα είναι αναμενόμενα.

@Test
void get5000() {
    IMap<UUID, Application> map = hazelcastInstance.getMap("application");
    UUID subscriberId = UUID.randomUUID();

    for (int i = 0; i < 5000; i++) {
        UUID id = UUID.randomUUID();
        String title = RandomStringUtils.random(5);
        Application application = new Application(id, title, subscriberId);
        
        map.set(id, application);
        Application retrieved = map.get(id);
        assertEquals(id, retrieved.getId());
    }
}

Αλλά δεν λειτουργεί σε μια συναλλαγή, παίρνουμε ένα ClassNotFoundException:

@Test
void get_transaction() {
    IMap<UUID, Application> map = hazelcastInstance.getMap("application_t");
    UUID subscriberId = UUID.randomUUID();
    UUID id = UUID.randomUUID();

    Application application = new Application(id, "qwer", subscriberId);
    map.set(id, application);
    
    Application retrievedOutside = map.get(id);
    assertEquals(id, retrievedOutside.getId());

    hazelcastInstance.executeTransaction(context -> {
        HazelcastTransactionContextHolder.setContext(context);
        try {
            TransactionalMap<UUID, Application> transactionalMap = context.getMap("application_t");
            Application retrievedInside = transactionalMap.get(id);

            assertEquals(id, retrievedInside.getId());
            return null;
        } finally {
            HazelcastTransactionContextHolder.clearContext();
        }
    });
}

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

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

Πώς προσφέρουμε υψηλή απόδοση

Τέσσερα ταξίδια στο Hazelcast είναι καλά, δύο ταξίδια στη βάση δεδομένων είναι κακά

Η αναζήτηση δεδομένων στην κρυφή μνήμη είναι πάντα καλύτερη από ό,τι στη βάση δεδομένων, αλλά δεν θέλετε να αποθηκεύσετε και μη διεκδικημένες εγγραφές. Η απόφαση για το τι θα αποθηκευτεί προσωρινά αφήνεται στο τελευταίο στάδιο ανάπτυξης. Όταν κωδικοποιηθεί η νέα λειτουργικότητα, ενεργοποιούμε την καταγραφή όλων των ερωτημάτων στο PostgreSQL (log_min_duration_statement στο 0) και εκτελούμε τη δοκιμή φόρτωσης για 20 λεπτά. Βοηθητικά προγράμματα όπως το pgFouine και το pgBadger μπορούν να δημιουργήσουν αναλυτικές αναφορές με βάση τα συλλεγμένα αρχεία καταγραφής. Στις αναφορές, αναζητούμε κυρίως αργά και συχνά ερωτήματα. Για αργά ερωτήματα, χτίζουμε ένα σχέδιο εκτέλεσης (EXPLAIN) και αξιολογούμε εάν ένα τέτοιο ερώτημα μπορεί να επιταχυνθεί. Οι συχνές αιτήσεις για την ίδια είσοδο ταιριάζουν καλά στη μνήμη cache. Προσπαθούμε να κρατάμε τα ερωτήματα «επίπεδα», έναν πίνακα ανά ερώτημα.

Εκμετάλλευση

Το CB ως διαδικτυακή υπηρεσία κυκλοφόρησε την άνοιξη του 2017, καθώς ένα ξεχωριστό προϊόν CB κυκλοφόρησε τον Νοέμβριο του 2017 (εκείνη την εποχή σε κατάσταση beta).

Για περισσότερο από ένα χρόνο λειτουργίας, δεν υπάρχουν σοβαρά προβλήματα στη λειτουργία της διαδικτυακής υπηρεσίας CB. Παρακολουθούμε την ηλεκτρονική υπηρεσία μέσω Zabbix, συλλογή και ανάπτυξη από Μπαμπού.

Η διανομή διακομιστή CB έρχεται με τη μορφή εγγενών πακέτων: RPM, DEB, MSI. Επιπλέον, για τα Windows, παρέχουμε ένα μόνο πρόγραμμα εγκατάστασης με τη μορφή ενός ενιαίου EXE που εγκαθιστά τον διακομιστή, το Hazelcast και το Elasticsearch σε ένα μηχάνημα. Στην αρχή ονομάσαμε αυτήν την έκδοση της εγκατάστασης "demo", αλλά τώρα έγινε σαφές ότι αυτή είναι η πιο δημοφιλής επιλογή ανάπτυξης.

Πηγή: www.habr.com

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