Γράφουμε σε PostgreSQL στο sublight: 1 κεντρικός υπολογιστής, 1 ημέρα, 1 TB

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

Γράφουμε σε PostgreSQL στο sublight: 1 κεντρικός υπολογιστής, 1 ημέρα, 1 TB

#1. Τομή

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

«Πράγματα περασμένων ημερών...»

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

Οι καιροί ήταν σχεδόν σαν επικές εποχές, διαφορετικές εκδόσεις του PostgreSQL 9.x ήταν σχετικές, οπότε όλη η κατάτμηση έπρεπε να γίνει "χειροκίνητα" - μέσω κληρονομικότητα πίνακα και ενεργοποιητές δρομολόγηση με δυναμική EXECUTE.

Γράφουμε σε PostgreSQL στο sublight: 1 κεντρικός υπολογιστής, 1 ημέρα, 1 TB
Η λύση που προέκυψε αποδείχθηκε αρκετά καθολική ώστε να μπορεί να μεταφραστεί σε όλους τους πίνακες:

  • Δηλώθηκε ένας κενός γονικός πίνακας "κεφαλίδα", ο οποίος περιέγραψε όλα απαραίτητους δείκτες και ενεργοποιητές.
  • Η εγγραφή από την πλευρά του πελάτη έγινε στον «ριζικό» πίνακα και εσωτερικά χρησιμοποιώντας σκανδάλης δρομολόγησης BEFORE INSERT η εγγραφή εισήχθη "φυσικά" στην απαιτούμενη ενότητα. Αν δεν υπήρχε ακόμα κάτι τέτοιο, πιάσαμε μια εξαίρεση και...
  • … με τη χρήση CREATE TABLE ... (LIKE ... INCLUDING ...) δημιουργήθηκε με βάση το πρότυπο του γονικού πίνακα ενότητα με περιορισμό στην επιθυμητή ημερομηνίαέτσι ώστε κατά την ανάκτηση δεδομένων, η ανάγνωση πραγματοποιείται μόνο σε αυτά.

PG10: πρώτη προσπάθεια

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

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

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

  • δεν υποστηρίζει περιγραφές ευρετηρίου
  • δεν υποστηρίζει σκανδάλες σε αυτό
  • δεν μπορεί να είναι «απόγονος» κανενός
  • δεν υποστηρίζουν INSERT ... ON CONFLICT
  • δεν μπορεί να δημιουργήσει μια ενότητα αυτόματα

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

PG10: δεύτερη ευκαιρία

Έτσι, αρχίσαμε να λύνουμε τα προβλήματα που προέκυψαν ένα προς ένα:

  1. Επειδή πυροδοτεί και ON CONFLICT Διαπιστώσαμε ότι τα χρειαζόμασταν ακόμα εδώ κι εκεί, οπότε κάναμε ένα ενδιάμεσο στάδιο για να τα επεξεργαστούμε πίνακας μεσολάβησης.
  2. Απαλλαγήκαμε από τη "δρομολόγηση" σε σκανδάλες - δηλαδή από EXECUTE.
  3. Το έβγαλαν χωριστά πρότυπο πίνακα με όλα τα ευρετήριαώστε να μην υπάρχουν καν στον πίνακα μεσολάβησης.

Γράφουμε σε PostgreSQL στο sublight: 1 κεντρικός υπολογιστής, 1 ημέρα, 1 TB
Τελικά, μετά από όλα αυτά, χωρίσαμε τον κεντρικό πίνακα εγγενώς. Η δημιουργία μιας νέας ενότητας παραμένει στη συνείδηση ​​της εφαρμογής.

Λεξικά «πριονίσματος».

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

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

Για να μην πω ότι ήταν πολλοί, αλλά περίπου 100 TB "γεγονότων" οδήγησαν σε ένα λεξικό 2.5 TB. Δεν μπορείτε εύκολα να διαγράψετε τίποτα από έναν τέτοιο πίνακα, δεν μπορείτε να το συμπιέσετε σε επαρκή χρόνο και η εγγραφή σε αυτόν έγινε σταδιακά πιο αργή.

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

  • γράφουν/διαβάζουν πιο γρήγορα λόγω μικρότερου μεγέθους τμήματος
  • καταναλώνουν λιγότερη μνήμη δουλεύοντας με πιο συμπαγείς δείκτες
  • αποθηκεύουν λιγότερα δεδομένα λόγω της δυνατότητας γρήγορης αφαίρεσης των ξεπερασμένων

Ως αποτέλεσμα του συνόλου των μέτρων Το φορτίο της CPU μειώθηκε κατά ~30%, το φορτίο του δίσκου κατά ~50%:

Γράφουμε σε PostgreSQL στο sublight: 1 κεντρικός υπολογιστής, 1 ημέρα, 1 TB
Ταυτόχρονα, συνεχίσαμε να γράφουμε ακριβώς το ίδιο πράγμα στη βάση δεδομένων, απλώς με λιγότερο φορτίο.

#2. Εξέλιξη και ανακατασκευή βάσεων δεδομένων

Οπότε καταλήξαμε σε αυτά που έχουμε κάθε μέρα έχει το δικό της τμήμα με στοιχεία. Πράγματι, CHECK (dt = '2018-10-12'::date) — και υπάρχει ένα κλειδί κατάτμησης και η προϋπόθεση για να εμπίπτει μια εγγραφή σε μια συγκεκριμένη ενότητα.

Δεδομένου ότι όλες οι αναφορές στην υπηρεσία μας δημιουργούνται στο πλαίσιο μιας συγκεκριμένης ημερομηνίας, τα ευρετήρια για αυτές από τους "χωρίς κατατμήσεις χρόνους" είναι όλων των τύπων (Υπηρέτης, Ημερομηνία, Πρότυπο σχεδίου), (Υπηρέτης, Ημερομηνία, κόμβος σχεδίου), (Ημερομηνία, Κατηγορία σφαλμάτων, Διακομιστής), ...

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

Γράφουμε σε PostgreSQL στο sublight: 1 κεντρικός υπολογιστής, 1 ημέρα, 1 TB
Η κατεύθυνση της βελτιστοποίησης είναι προφανής - απλή αφαιρέστε το πεδίο ημερομηνίας από όλα τα ευρετήρια σε χωρισμένα τραπέζια. Δεδομένων των όγκων μας, το κέρδος είναι περίπου 1 TB/εβδομάδα!

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

Γράφουμε σε PostgreSQL στο sublight: 1 κεντρικός υπολογιστής, 1 ημέρα, 1 TB

#3. «Διασπορά» του φορτίου αιχμής

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

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

Γράφουμε σε PostgreSQL στο sublight: 1 κεντρικός υπολογιστής, 1 ημέρα, 1 TB

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

setInterval(sendToDB, interval)

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

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

setInterval(sendToDB, interval * (1 + 0.1 * (Math.random() - 0.5)))

#4. Αποθηκεύουμε κρυφά ό,τι χρειαζόμαστε

Το τρίτο παραδοσιακό πρόβλημα υψηλής φόρτωσης είναι χωρίς κρυφή μνήμη που είναι αυτός θα μπορούσε να είναι.

Για παράδειγμα, καταστήσαμε δυνατή την ανάλυση με όρους κόμβων σχεδίου (όλα αυτά Seq Scan on users), αλλά αμέσως σκεφτείτε ότι είναι, ως επί το πλείστον, τα ίδια - ξέχασαν.

Όχι, φυσικά, τίποτα δεν γράφεται ξανά στη βάση δεδομένων, αυτό διακόπτει τη σκανδάλη με INSERT ... ON CONFLICT DO NOTHING. Αλλά αυτά τα δεδομένα εξακολουθούν να φτάνουν στη βάση δεδομένων και δεν είναι απαραίτητα ανάγνωση για έλεγχο σύγκρουσης έχω να κάνω. Ωχ #3...

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

Γράφουμε σε PostgreSQL στο sublight: 1 κεντρικός υπολογιστής, 1 ημέρα, 1 TB

Και αυτή είναι η συνοδευτική πτώση στο φορτίο αποθήκευσης:

Γράφουμε σε PostgreSQL στο sublight: 1 κεντρικός υπολογιστής, 1 ημέρα, 1 TB

Σε συνολικά

Το "Terabyte-per-day" ακούγεται τρομακτικό. Εάν τα κάνετε όλα σωστά, τότε αυτό είναι ακριβώς 2^40 byte / 86400 δευτερόλεπτα = ~12.5 MB/sπου κρατούσαν ακόμη και οι βίδες IDE της επιφάνειας εργασίας. 🙂

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

Γράφουμε σε PostgreSQL στο sublight: 1 κεντρικός υπολογιστής, 1 ημέρα, 1 TB

Πηγή: www.habr.com

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