Optimierung für die PostgreSQL-Serving-Rails-Anwendung

Als Senior Software Engineer beim Aufbau einer Messaging-Plattform für das Gesundheitswesen bin ich einschließlich anderer Aufgaben für die Leistung unserer Anwendung verantwortlich. Wir entwickeln einen ziemlich standardmäßigen Webdienst unter Verwendung der Ruby on Rails-Anwendung für Geschäftslogik und API, React + Redux für Benutzeranwendungen mit nur einer Seite, da wir als Datenbank PostgreSQL verwenden. Häufige Gründe für Leistungsprobleme in ähnlichen Stacks sind umfangreiche Datenbankabfragen, und ich möchte die Geschichte erzählen, wie wir nicht standardmäßige, aber recht einfache Optimierungen angewendet haben, um die Leistung zu verbessern.


Unser Geschäft ist in den USA tätig, daher müssen wir HIPAA-konform sein und bestimmte Sicherheitsrichtlinien befolgen. Sicherheitsüberprüfungen sind etwas, auf das wir immer vorbereitet sind. Um Risiken und Kosten zu reduzieren, verlassen wir uns auf einen speziellen Cloud-Anbieter, um unsere Anwendungen und Datenbanken auszuführen, ähnlich wie bei Heroku. Einerseits können wir uns auf den Aufbau unserer Plattform konzentrieren, andererseits wird unsere Infrastruktur zusätzlich eingeschränkt. Kurz gesagt - wir können nicht unendlich skalieren. Als erfolgreicher Start verdoppeln wir alle paar Monate die Anzahl der Benutzer, und eines Tages ergab unsere Überwachung, dass wir das Festplatten-E / A-Kontingent auf dem Datenbankserver überschritten haben. Das zugrunde liegende AWS begann mit der Drosselung, was zu einer erheblichen Leistungsverschlechterung führte. Die Ruby-Anwendung war nicht in der Lage, den gesamten eingehenden Datenverkehr zu bedienen, da die Mitarbeiter von Unicorn zu lange auf die Antwort der Datenbank warteten und die Kunden unglücklich waren.


Standardlösungen


Am Anfang des Artikels erwähnte ich den Ausdruck "nicht standardmäßige Optimierungen", da alle niedrig hängenden Früchte bereits gepflückt waren:


  • Wir haben alle N + 1-Abfragen entfernt. Ruby Gem Bullet war das Hauptwerkzeug
  • Alle benötigten Indizes in der Datenbank wurden hinzugefügt, alle nicht benötigten wurden dank pg_stat_statements entfernt
  • Einige Abfragen mit mehreren Verknüpfungen wurden zur Verbesserung der Effizienz neu geschrieben
  • Wir haben Abfragen getrennt, um paginierte Sammlungen von Dekorationsabfragen abzurufen. Zum Beispiel haben wir anfangs einen Zähler für Nachrichten pro Dialog hinzugefügt, indem wir Tabellen verknüpft haben, aber er wurde durch eine zusätzliche Abfrage ersetzt, um die Ergebnisse zu verbessern. Die nächste Abfrage führt Index Only Scan durch und ist wirklich günstig:

SELECT COUNT(1), dialog_id FROM messages WHERE dialog_id IN (1, 2, 3, 4) GROUP BY dialog_id; 

  • fügte ein paar Caches hinzu. Eigentlich hat das nicht gut funktioniert, weil wir als Messaging-Anwendung viele Updates haben

Alle diese Tricks haben ein paar Monate lang großartige Arbeit geleistet, bis wir erneut mit dem gleichen Leistungsproblem konfrontiert waren - mehr Benutzer, höhere Last. Wir haben nach etwas anderem gesucht.


Fortschrittliche Lösungen


Wir wollten keine schwere Artillerie einsetzen und Denormalisierung und Partitionierung implementieren, da diese Lösungen fundierte Kenntnisse in Datenbanken erfordern, den Fokus des Teams von der Implementierung von Funktionen auf die Wartung verlagern und am Ende die Komplexität unserer Anwendung vermeiden wollten. Zuletzt haben wir PostgreSQL 9.3 verwendet, bei dem Partitionen auf Triggern mit all ihren Kosten basieren. KISS-Prinzip in Aktion.


Kundenspezifische Lösungen


Daten komprimieren


Wir haben uns entschlossen, uns auf das Hauptsymptom zu konzentrieren - Disk IO. Da weniger Daten gespeichert werden und weniger E / A-Kapazität benötigt wird, war dies die Hauptidee. Wir suchten nach Möglichkeiten, Daten zu komprimieren, und die ersten Kandidaten waren Spalten wie user_type die von user_type mit polymorphen Assoziationen versehen wurden. In der Anwendung verwenden wir häufig Module, die dazu führen, dass lange Zeichenfolgen wie Module::SubModule::ModelName für polymorphe Assoziationen verwendet werden. Was wir getan haben - konvertieren Sie alle diese Spaltentypen von varchar nach ENUM. Die Rails-Migration sieht folgendermaßen aus:


 class AddUserEnumType < ActiveRecord::Migration[5.2] disable_ddl_transaction! def up ActiveRecord::Base.connection.execute <<~SQL CREATE TYPE user_type_enum AS ENUM ( 'Module::Submodule::UserModel1', 'Module::Submodule::UserModel2', 'Module::Submodule::UserModel3' ); SQL add_column :messages, :sender_type_temp, :user_type_enum Message .in_batches(of: 10000) .update_all('sender_type_temp = sender_type::user_type_enum') safety_assured do rename_column :messages, :sender_type, :sender_type_old rename_column :messages, :sender_type_temp, :sender_type end end end 

Einige Hinweise zu dieser Migration für Personen, die mit Rails nicht vertraut sind:


  • disable_ddl_transaction! Deaktiviert die Transaktionsmigration. Dies ist sehr riskant, aber wir wollten lange Transaktionen vermeiden. Stellen Sie sicher, dass Sie Transaktionen bei der Migration nicht deaktivieren, ohne dass dies erforderlich ist.
  • Im ersten Schritt erstellen wir einen neuen ENUM-Datentyp unter PostgreSQL. Das beste Merkmal von ENUM ist eine geringe Größe, die im Vergleich zu Varchar wirklich klein ist. ENUM hat einige Schwierigkeiten beim Hinzufügen neuer Werte, aber normalerweise fügen wir nicht oft neue Benutzertypen hinzu.
  • Fügen Sie eine neue Spalte sender_type_temp mit dem user_type_enum hinzu
  • Füllen Sie die neue Spalte in_batches mit Werten , um eine lange Sperre der Tabellennachrichten zu vermeiden
  • Der letzte Schritt tauscht die alte Spalte gegen eine neue aus. Dies ist der gefährlichste Schritt, denn wenn die Spalte sender_type in sender_type_old geändert würde, aber sender_type_temp nicht zu sender_type werden könnte , würden wir viele Probleme bekommen.
  • security_assured stammt aus dem Juwel strong_migration, das hilft, Fehler beim Schreiben von Migrationen zu vermeiden. Das Umbenennen von Spalten ist kein sicherer Vorgang, daher mussten wir bestätigen, dass wir verstehen, was wir taten. Tatsächlich gibt es einen sichereren, aber längeren Weg, der mehrere Bereitstellungen umfasst.

Es ist unnötig zu erwähnen, dass wir alle ähnlichen Migrationen während der niedrigsten Aktivitätsperioden mit geeigneten Tests durchführen.


Wir haben alle polymorphen Spalten in ENUM konvertiert, alte Spalten nach einigen Tagen der Überwachung gelöscht und schließlich VACUUM ausgeführt, um die Fragmentierung zu verringern. Dies sparte uns aber rund 10% des gesamten Speicherplatzes
Einige Tabellen mit einigen Spalten wurden zweimal komprimiert! Was noch wichtiger war: Einige Tabellen wurden von PostgreSQL im Speicher zwischengespeichert (denken Sie daran, wir können nicht einfach mehr RAM hinzufügen), was die erforderliche Festplatten-E / A drastisch verringerte.


Vertrauen Sie Ihrem Dienstanbieter nicht


Eine andere Sache wurde in dem Artikel gefunden. Wie eine einzelne Änderung der PostgreSQL-Konfiguration die Leistung langsamer Abfragen um das 50-fache verbesserte - unser PostgreSQL-Anbieter erstellt eine automatische Konfiguration für den Server basierend auf dem angeforderten RAM-, Festplatten- und CPU-Volumen, aber aus welchem ​​Grund auch immer, er ließ den Parameter random_page_cost mit dem Standardwert 4, optimiert für Festplatte. Sie verlangen von uns, dass wir Datenbanken auf SSD ausführen, haben PostgreSQL jedoch nicht ordnungsgemäß konfiguriert. Nachdem wir sie kontaktiert hatten, bekamen wir viel bessere Ausführungspläne:


 EXPLAIN ANALYSE SELECT COUNT(*) AS count_dialog_id, dialog_id as dialog_id FROM messages WHERE sender_type = 'Module::Submodule::UserModel1' AND sender_id = 1234 GROUP BY dialog_id; db=# SET random_page_cost = 4; QUERY PLAN ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- HashAggregate (cost=76405.45..76411.92 rows=647 width=12) (actual time=2428.412..2428.420 rows=12 loops=1) Group Key: dialog_id -> Bitmap Heap Scan on messages (cost=605.90..76287.72 rows=23545 width=4) (actual time=82.442..2376.033 rows=79466 loops=1) Recheck Cond: ((sender_id = 1234) AND (sender_type = 'Module::Submodule::UserModel1'::user_type_enum)) Heap Blocks: exact=23672 -> Bitmap Index Scan on index_messages_on_sender_id_and_sender_type_and_message_type (cost=0.00..600.01 rows=23545 width=0) (actual time=76.067..76.068 rows=79466 loops=1) Index Cond: ((sender_id = 1234) AND (sender_type = 'Module::Submodule::UserModel1'::user_type_enum)) Planning time: 3.849 ms Execution time: 2428.691 ms (9 rows) db=# SET random_page_cost = 1; QUERY PLAN ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ HashAggregate (cost=21359.54..21366.01 rows=647 width=12) (actual time=97.905..97.913 rows=12 loops=1) Group Key: dialog_id -> Index Scan using index_messages_on_sender_id_and_sender_type_and_message_type on messages (cost=0.56..21241.81 rows=23545 width=4) (actual time=0.058..60.444 rows=79466 loops=1) Index Cond: ((sender_id = 1234) AND (sender_type = 'Module::Submodule::UserModel1'::user_type_enum)) Planning time: 0.277 ms Execution time: 98.070 ms (6 rows) 

Verschieben Sie Daten weg


Wir haben eine riesige Tabelle in eine andere Datenbank verschoben. Wir müssen jede Änderung des Systems gesetzlich prüfen und diese Anforderung wird mit gem PaperTrail umgesetzt . Diese Bibliothek erstellt eine Tabelle in der Produktionsdatenbank, in der alle Änderungen der überwachten Objekte gespeichert werden. Wir verwenden Library Multiverse, um eine weitere Datenbankinstanz in unsere Rails-App zu integrieren. Übrigens - es wird eine Standardfunktion von Rails 6 sein. Es gibt einige Konfigurationen:


Beschreiben der Verbindung in der Datei config/database.yml


 external_default: &external_default url: "<%= ENV['AUDIT_DATABASE_URL'] %>" external_development: <<: *external_default 

Basisklasse für ActiveRecord-Modelle aus einer anderen Datenbank:


 class ExternalRecord < ActiveRecord::Base self.abstract_class = true establish_connection :"external_#{Rails.env}" end 

Modell, das PaperTrail-Versionen implementiert:


 class ExternalVersion < ExternalRecord include PaperTrail::VersionConcern end 

Anwendungsfall im zu prüfenden Modell:


 class Message < ActiveRecord::Base has_paper_trail class_name: "ExternalVersion" end 

Zusammenfassung


Wir haben unserer PostgreSQL-Instanz endlich mehr RAM hinzugefügt und verbrauchen derzeit nur 10% der verfügbaren Festplatten-E / A. Wir haben bis zu diesem Zeitpunkt überlebt, weil wir einige Tricks angewendet haben - komprimierte Daten in unserer Produktionsdatenbank, korrigierte Konfiguration und nicht relevante Daten entfernt. Wahrscheinlich helfen diese in Ihrem speziellen Fall nicht weiter, aber ich hoffe, dieser Artikel kann einige Ideen zur benutzerdefinierten und einfachen Optimierung geben. Vergessen Sie natürlich nicht, die am Anfang aufgeführte Checkliste der Standardprobleme durchzugehen.


PS: Ich kann ein großartiges DBA-Add-On für psql nur empfehlen.

Source: https://habr.com/ru/post/de445426/


All Articles