Λειτουργική ανάλυση στην αρχιτεκτονική microservice: βοήθεια και προτροπή Postgres FDW

Η αρχιτεκτονική μικροϋπηρεσιών, όπως όλα σε αυτόν τον κόσμο, έχει τα θετικά και τα αρνητικά της. Μερικές διαδικασίες γίνονται ευκολότερες με αυτό, άλλες πιο δύσκολες. Και για χάρη της ταχύτητας της αλλαγής και της καλύτερης επεκτασιμότητας, πρέπει να κάνετε θυσίες. Ένα από αυτά είναι η αυξανόμενη πολυπλοκότητα των αναλυτικών στοιχείων. Εάν σε ένα μονόλιθο όλα τα λειτουργικά αναλυτικά στοιχεία μπορούν να περιοριστούν σε ερωτήματα SQL σε αναλυτικό αντίγραφο, τότε σε μια αρχιτεκτονική πολλαπλών υπηρεσιών κάθε υπηρεσία έχει τη δική της βάση δεδομένων και φαίνεται ότι δεν μπορεί να γίνει ένα ερώτημα (ή μήπως μπορεί;). Για όσους ενδιαφέρονται για το πώς λύσαμε το πρόβλημα των λειτουργικών αναλυτικών στοιχείων στην εταιρεία μας και πώς μάθαμε να ζούμε με αυτήν τη λύση - καλώς ήρθατε.

Λειτουργική ανάλυση στην αρχιτεκτονική microservice: βοήθεια και προτροπή Postgres FDW
Ονομάζομαι Pavel Sivash, στο DomClick εργάζομαι σε μια ομάδα που είναι υπεύθυνη για τη συντήρηση της αποθήκης αναλυτικών δεδομένων. Συμβατικά, οι δραστηριότητές μας μπορούν να ταξινομηθούν ως μηχανική δεδομένων, αλλά, στην πραγματικότητα, το εύρος των εργασιών είναι πολύ ευρύτερο. Υπάρχουν πρότυπα ETL/ELT για μηχανική δεδομένων, υποστήριξη και προσαρμογή εργαλείων για ανάλυση δεδομένων και ανάπτυξη των δικών σας εργαλείων. Ειδικότερα, για την επιχειρησιακή αναφορά, αποφασίσαμε να «προσποιηθούμε» ότι έχουμε μονόλιθο και να δώσουμε στους αναλυτές μια βάση δεδομένων που θα περιέχει όλα τα δεδομένα που χρειάζονται.

Σε γενικές γραμμές, εξετάσαμε διαφορετικές επιλογές. Ήταν δυνατό να δημιουργηθεί ένα πλήρες αποθετήριο - προσπαθήσαμε ακόμη, αλλά, για να είμαι ειλικρινής, δεν μπορέσαμε να συνδυάσουμε αρκετά συχνές αλλαγές στη λογική με τη μάλλον αργή διαδικασία δημιουργίας ενός αποθετηρίου και πραγματοποίησης αλλαγών σε αυτό (αν κάποιος πέτυχε , γράψτε στα σχόλια πώς). Ήταν δυνατό να πούμε στους αναλυτές: «Παιδιά, μάθετε python και πηγαίνετε σε αναλυτικά αντίγραφα», αλλά αυτή είναι μια πρόσθετη απαίτηση για τη στρατολόγηση και φαινόταν ότι αυτό θα έπρεπε να αποφευχθεί αν είναι δυνατόν. Αποφασίσαμε να προσπαθήσουμε να χρησιμοποιήσουμε την τεχνολογία FDW (Foreign Data Wrapper): ουσιαστικά, αυτό είναι ένα τυπικό dblink, το οποίο είναι στο πρότυπο SQL, αλλά με τη δική του πολύ πιο βολική διεπαφή. Με βάση αυτό, φτιάξαμε μια λύση, η οποία τελικά έπιασε και κατασταλάξαμε. Οι λεπτομέρειές του είναι το θέμα ενός ξεχωριστού άρθρου, και ίσως και περισσότερων του ενός, αφού θέλω να μιλήσω για πολλά: από τον συγχρονισμό σχημάτων βάσεων δεδομένων έως τον έλεγχο πρόσβασης και την αποπροσωποποίηση των προσωπικών δεδομένων. Είναι επίσης απαραίτητο να κάνουμε μια επιφύλαξη ότι αυτή η λύση δεν αντικαθιστά πραγματικές βάσεις δεδομένων και αποθετήρια ανάλυσης· επιλύει μόνο ένα συγκεκριμένο πρόβλημα.

Στο κορυφαίο επίπεδο μοιάζει με αυτό:

Λειτουργική ανάλυση στην αρχιτεκτονική microservice: βοήθεια και προτροπή Postgres FDW
Υπάρχει μια βάση δεδομένων PostgreSQL όπου οι χρήστες μπορούν να αποθηκεύουν τα δεδομένα εργασίας τους και το πιο σημαντικό, αναλυτικά αντίγραφα όλων των υπηρεσιών συνδέονται σε αυτήν τη βάση δεδομένων μέσω FDW. Αυτό καθιστά δυνατή τη σύνταξη ενός ερωτήματος σε πολλές βάσεις δεδομένων και δεν έχει σημασία τι είναι: PostgreSQL, MySQL, MongoDB ή κάτι άλλο (αρχείο, API, αν ξαφνικά δεν υπάρχει κατάλληλο περιτύλιγμα, μπορείτε να γράψετε το δικό σας). Λοιπόν, όλα φαίνονται υπέροχα! Χωρίζουμε;

Αν όλα τελείωναν τόσο γρήγορα και απλά, τότε, μάλλον, δεν θα υπήρχε άρθρο.

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

Μια απλή ερώτηση και ένα σχέδιο με αυτό

Για να δείξουμε πώς η Postgres ρωτά έναν πίνακα 6 εκατομμυρίων σειρών σε έναν απομακρυσμένο διακομιστή, ας δούμε ένα απλό σχέδιο.

explain analyze verbose  
SELECT count(1)
FROM fdw_schema.table;

Aggregate  (cost=418383.23..418383.24 rows=1 width=8) (actual time=3857.198..3857.198 rows=1 loops=1)
  Output: count(1)
  ->  Foreign Scan on fdw_schema."table"  (cost=100.00..402376.14 rows=6402838 width=0) (actual time=4.874..3256.511 rows=6406868 loops=1)
        Output: "table".id, "table".is_active, "table".meta, "table".created_dt
        Remote SQL: SELECT NULL FROM fdw_schema.table
Planning time: 0.986 ms
Execution time: 3857.436 ms

Η χρήση της δήλωσης VERBOSE μας επιτρέπει να δούμε το ερώτημα που θα σταλεί στον απομακρυσμένο διακομιστή και τα αποτελέσματα του οποίου θα λάβουμε για περαιτέρω επεξεργασία (γραμμή RemoteSQL).

Ας πάμε λίγο παρακάτω και ας προσθέσουμε πολλά φίλτρα στο αίτημά μας: ένα για boolean πεδίο, ένα κατά περίπτωση timestamp στο μεσοδιάστημα και ένα από jsonb.

explain analyze verbose
SELECT count(1)
FROM fdw_schema.table 
WHERE is_active is True
AND created_dt BETWEEN CURRENT_DATE - INTERVAL '7 month' 
AND CURRENT_DATE - INTERVAL '6 month'
AND meta->>'source' = 'test';

Aggregate  (cost=577487.69..577487.70 rows=1 width=8) (actual time=27473.818..25473.819 rows=1 loops=1)
  Output: count(1)
  ->  Foreign Scan on fdw_schema."table"  (cost=100.00..577469.21 rows=7390 width=0) (actual time=31.369..25372.466 rows=1360025 loops=1)
        Output: "table".id, "table".is_active, "table".meta, "table".created_dt
        Filter: (("table".is_active IS TRUE) AND (("table".meta ->> 'source'::text) = 'test'::text) AND ("table".created_dt >= (('now'::cstring)::date - '7 mons'::interval)) AND ("table".created_dt <= ((('now'::cstring)::date)::timestamp with time zone - '6 mons'::interval)))
        Rows Removed by Filter: 5046843
        Remote SQL: SELECT created_dt, is_active, meta FROM fdw_schema.table
Planning time: 0.665 ms
Execution time: 27474.118 ms

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

Αυτό είναι μια βλακεία

Με τα boolean πεδία όλα είναι απλά. Στο αρχικό αίτημα, το πρόβλημα οφειλόταν στον χειριστή is. Εάν το αντικαταστήσετε με =, τότε έχουμε το ακόλουθο αποτέλεσμα:

explain analyze verbose
SELECT count(1)
FROM fdw_schema.table
WHERE is_active = True
AND created_dt BETWEEN CURRENT_DATE - INTERVAL '7 month' 
AND CURRENT_DATE - INTERVAL '6 month'
AND meta->>'source' = 'test';

Aggregate  (cost=508010.14..508010.15 rows=1 width=8) (actual time=19064.314..19064.314 rows=1 loops=1)
  Output: count(1)
  ->  Foreign Scan on fdw_schema."table"  (cost=100.00..507988.44 rows=8679 width=0) (actual time=33.035..18951.278 rows=1360025 loops=1)
        Output: "table".id, "table".is_active, "table".meta, "table".created_dt
        Filter: ((("table".meta ->> 'source'::text) = 'test'::text) AND ("table".created_dt >= (('now'::cstring)::date - '7 mons'::interval)) AND ("table".created_dt <= ((('now'::cstring)::date)::timestamp with time zone - '6 mons'::interval)))
        Rows Removed by Filter: 3567989
        Remote SQL: SELECT created_dt, meta FROM fdw_schema.table WHERE (is_active)
Planning time: 0.834 ms
Execution time: 19064.534 ms

Όπως μπορείτε να δείτε, το φίλτρο πέταξε σε έναν απομακρυσμένο διακομιστή και ο χρόνος εκτέλεσης μειώθηκε από 27 σε 19 δευτερόλεπτα.

Αξίζει να σημειωθεί ότι ο χειριστής is διαφορετικό από τον χειριστή = γιατί μπορεί να λειτουργήσει με την τιμή Null. Αυτό σημαίνει ότι δεν είναι αλήθεια θα αφήσει τις τιμές False και Null στο φίλτρο, ενώ != Αλήθεια θα αφήσει μόνο ψευδείς τιμές. Επομένως, κατά την αντικατάσταση του χειριστή δεν είναι δύο συνθήκες με τον τελεστή OR πρέπει να περάσουν στο φίλτρο, για παράδειγμα, WHERE (col != True) OR (col είναι null).

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

timestamptz; hz

Γενικά, συχνά πρέπει να πειραματιστείτε με το πώς να γράψετε σωστά ένα αίτημα που περιλαμβάνει απομακρυσμένους διακομιστές και μόνο τότε να αναζητήσετε μια εξήγηση για το γιατί συμβαίνει αυτό. Πολύ λίγες πληροφορίες σχετικά με αυτό μπορούν να βρεθούν στο Διαδίκτυο. Έτσι, σε πειράματα διαπιστώσαμε ότι ένα φίλτρο σταθερής ημερομηνίας πετάει στον απομακρυσμένο διακομιστή με έκρηξη, αλλά όταν θέλουμε να ορίσουμε την ημερομηνία δυναμικά, για παράδειγμα, now() ή CURRENT_DATE, αυτό δεν συμβαίνει. Στο παράδειγμά μας, προσθέσαμε ένα φίλτρο έτσι ώστε η στήλη create_at να περιέχει δεδομένα για ακριβώς 1 μήνα στο παρελθόν (BETWEEN CURRENT_DATE - INTERVAL '7 month' AND CURRENT_DATE - INTERVAL '6 month'). Τι κάναμε σε αυτή την περίπτωση;

explain analyze verbose
SELECT count(1)
FROM fdw_schema.table 
WHERE is_active is True
AND created_dt >= (SELECT CURRENT_DATE::timestamptz - INTERVAL '7 month') 
AND created_dt <(SELECT CURRENT_DATE::timestamptz - INTERVAL '6 month')
AND meta->>'source' = 'test';

Aggregate  (cost=306875.17..306875.18 rows=1 width=8) (actual time=4789.114..4789.115 rows=1 loops=1)
  Output: count(1)
  InitPlan 1 (returns $0)
    ->  Result  (cost=0.00..0.02 rows=1 width=8) (actual time=0.007..0.008 rows=1 loops=1)
          Output: ((('now'::cstring)::date)::timestamp with time zone - '7 mons'::interval)
  InitPlan 2 (returns $1)
    ->  Result  (cost=0.00..0.02 rows=1 width=8) (actual time=0.002..0.002 rows=1 loops=1)
          Output: ((('now'::cstring)::date)::timestamp with time zone - '6 mons'::interval)
  ->  Foreign Scan on fdw_schema."table"  (cost=100.02..306874.86 rows=105 width=0) (actual time=23.475..4681.419 rows=1360025 loops=1)
        Output: "table".id, "table".is_active, "table".meta, "table".created_dt
        Filter: (("table".is_active IS TRUE) AND (("table".meta ->> 'source'::text) = 'test'::text))
        Rows Removed by Filter: 76934
        Remote SQL: SELECT is_active, meta FROM fdw_schema.table WHERE ((created_dt >= $1::timestamp with time zone)) AND ((created_dt < $2::timestamp with time zone))
Planning time: 0.703 ms
Execution time: 4789.379 ms

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

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

Ας επαναφέρουμε το φίλτρο ημερομηνίας στην αρχική του τιμή.

Freddy vs. Jsonb

Γενικά, τα πεδία και οι ημερομηνίες Boolean έχουν ήδη επιταχύνει επαρκώς το ερώτημά μας, αλλά είχε απομείνει ένας ακόμη τύπος δεδομένων. Η μάχη με το φιλτράρισμα από αυτό, για να είμαι ειλικρινής, δεν έχει ακόμα τελειώσει, αν και υπάρχει επιτυχία και εδώ. Έτσι, έτσι καταφέραμε να περάσουμε το φίλτρο jsonb πεδίο στον απομακρυσμένο διακομιστή.

explain analyze verbose
SELECT count(1)
FROM fdw_schema.table 
WHERE is_active is True
AND created_dt BETWEEN CURRENT_DATE - INTERVAL '7 month' 
AND CURRENT_DATE - INTERVAL '6 month'
AND meta @> '{"source":"test"}'::jsonb;

Aggregate  (cost=245463.60..245463.61 rows=1 width=8) (actual time=6727.589..6727.590 rows=1 loops=1)
  Output: count(1)
  ->  Foreign Scan on fdw_schema."table"  (cost=1100.00..245459.90 rows=1478 width=0) (actual time=16.213..6634.794 rows=1360025 loops=1)
        Output: "table".id, "table".is_active, "table".meta, "table".created_dt
        Filter: (("table".is_active IS TRUE) AND ("table".created_dt >= (('now'::cstring)::date - '7 mons'::interval)) AND ("table".created_dt <= ((('now'::cstring)::date)::timestamp with time zone - '6 mons'::interval)))
        Rows Removed by Filter: 619961
        Remote SQL: SELECT created_dt, is_active FROM fdw_schema.table WHERE ((meta @> '{"source": "test"}'::jsonb))
Planning time: 0.747 ms
Execution time: 6727.815 ms

Αντί να φιλτράρετε τελεστές, πρέπει να χρησιμοποιήσετε την παρουσία ενός τελεστή jsonb σε μια διαφορετική. 7 δευτερόλεπτα αντί για το αρχικό 29. Μέχρι στιγμής αυτή είναι η μόνη επιτυχημένη επιλογή για τη μετάδοση φίλτρων μέσω jsonb σε έναν απομακρυσμένο διακομιστή, αλλά εδώ είναι σημαντικό να λάβετε υπόψη έναν περιορισμό: χρησιμοποιούμε την έκδοση 9.6 της βάσης δεδομένων, αλλά μέχρι το τέλος Απριλίου σχεδιάζουμε να ολοκληρώσουμε τις τελευταίες δοκιμές και να προχωρήσουμε στην έκδοση 12. Μόλις ενημερώσουμε, θα γράψουμε πώς επηρέασε, επειδή υπάρχουν πολλές αλλαγές για τις οποίες υπάρχει μεγάλη ελπίδα: json_path, νέα συμπεριφορά CTE, push down (υπάρχει από την έκδοση 10). Θέλω πολύ να το δοκιμάσω σύντομα.

Αποτελείωσέ τον

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

explain analyze verbose
SELECT count(1)
FROM fdw_schema.table 
WHERE is_active = True
AND created_dt >= (SELECT CURRENT_DATE::timestamptz - INTERVAL '7 month') 
AND created_dt <(SELECT CURRENT_DATE::timestamptz - INTERVAL '6 month')
AND meta @> '{"source":"test"}'::jsonb;

Aggregate  (cost=322041.51..322041.52 rows=1 width=8) (actual time=2278.867..2278.867 rows=1 loops=1)
  Output: count(1)
  InitPlan 1 (returns $0)
    ->  Result  (cost=0.00..0.02 rows=1 width=8) (actual time=0.010..0.010 rows=1 loops=1)
          Output: ((('now'::cstring)::date)::timestamp with time zone - '7 mons'::interval)
  InitPlan 2 (returns $1)
    ->  Result  (cost=0.00..0.02 rows=1 width=8) (actual time=0.003..0.003 rows=1 loops=1)
          Output: ((('now'::cstring)::date)::timestamp with time zone - '6 mons'::interval)
  ->  Foreign Scan on fdw_schema."table"  (cost=100.02..322041.41 rows=25 width=0) (actual time=8.597..2153.809 rows=1360025 loops=1)
        Output: "table".id, "table".is_active, "table".meta, "table".created_dt
        Remote SQL: SELECT NULL FROM fdw_schema.table WHERE (is_active) AND ((created_dt >= $1::timestamp with time zone)) AND ((created_dt < $2::timestamp with time zone)) AND ((meta @> '{"source": "test"}'::jsonb))
Planning time: 0.820 ms
Execution time: 2279.087 ms

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

Συνοψίζοντας: εάν χρησιμοποιείτε το PostgreSQL με FDW, ελέγχετε πάντα ότι όλα τα φίλτρα αποστέλλονται στον απομακρυσμένο διακομιστή και θα είστε ευχαριστημένοι... Τουλάχιστον μέχρι να φτάσετε να συνδέσετε πίνακες από διαφορετικούς διακομιστές. Αλλά αυτό είναι μια ιστορία για άλλο άρθρο.

Σας ευχαριστώ για την προσοχή σας! Θα ήθελα να ακούσω ερωτήσεις, σχόλια και ιστορίες σχετικά με τις εμπειρίες σας στα σχόλια.

Πηγή: www.habr.com

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