Wie wir den Prozess der Entwicklung, Fehlerbehebung und Bereitstellung von Datenbankänderungen im Jahr 2020 erfunden haben

Es ist 2020 im Hof ​​und Sie sind es bereits gewohnt, mit Hintergrundgeräuschen zu hören: "Kubernetes ist die Antwort!", "Microservices!", "Service Mesh!", "Sesuriti-Richtlinien!" Alle um uns herum rennen in eine glänzende Zukunft.



Unser Unternehmen geht bei Datenbanken konservativer vor als bei Anwendungen. Die Datenbank dreht sich nicht in Kubernetes, sondern auf Hardware oder in einer virtuellen Maschine. Wir haben einen gut etablierten Prozess für Änderungen an der Datenbank für die Zahlungsabwicklung, der viele automatisierte Schecks, eine umfassende Überprüfung und eine Freigabe unter Beteiligung des DBA umfasst. Die Anzahl der in diesem Fall beteiligten Schecks und Personen wirkt sich negativ auf die Markteinführungszeit aus. Auf der anderen Seite ist es debuggt und ermöglicht es Ihnen, zuverlässig Änderungen an der Produktion vorzunehmen, wodurch die Wahrscheinlichkeit minimiert wird, dass etwas kaputt geht. Und wenn etwas kaputt geht, sind bereits die richtigen Personen in den Reparaturprozess einbezogen. Dieser Ansatz macht die Arbeit des Hauptdienstes des Unternehmens stabiler.



Wir starten die meisten neuen relationalen Datenbanken für Microservices unter PostgreSQL. Ein fein abgestimmter Prozess für Oracle ist zwar robust, bringt jedoch unnötige Komplexität für kleine Datenbanken mit sich. Niemand möchte schwierige Prozesse aus der Vergangenheit in eine glänzende Zukunft ziehen. Niemand hat im Voraus begonnen, an dem Prozess für eine glänzende Zukunft zu arbeiten. Infolgedessen fehlte uns ein Standard und ein Raznozhopitsu. Wenn Sie wissen möchten, zu welchen Problemen dies geführt hat und wie wir sie gelöst haben, sind Sie bei cat willkommen.











Probleme, die wir gelöst haben



Es gibt keine einheitlichen Versionsstandards



Im besten Fall handelt es sich um DDL-SQL-Dateien, die sich irgendwo im Datenbankverzeichnis im Repository mit dem Microservice befinden. Es ist sehr schlecht, wenn dies nur der aktuelle Status der Datenbank ist, der sich im Test und in der Produktion unterscheidet, und es keine Referenzskripte für das Datenbankschema gibt.



Während des Debuggens zerstören wir die Testbasis



"Ich werde die Testdatenbank jetzt ein wenig erschüttern, seien Sie dort nicht beunruhigt" - und ging, um den neu geschriebenen Schemaänderungscode in der Testdatenbank zu debuggen. Manchmal dauert es lange und die ganze Zeit funktioniert die Testschaltung nicht.



Gleichzeitig kann die Testschaltung in dem Teil unterbrochen werden, in dem andere Mikrodienste mit dem Mikrodienst interagieren, dessen Basis der Entwickler zerstört hat.



DAO-Methoden werden nicht durch Tests abgedeckt und in CI nicht validiert



Beim Entwickeln und Debuggen werden DAO-Methoden aufgerufen, indem die äußeren Griffe einige Ebenen darüber gezogen werden. Dadurch werden ganze Szenarien der Geschäftslogik anstelle spezifischer Interaktionen zwischen dem Microservice und der Datenbank verfügbar gemacht.



Es gibt keine Garantie dafür, dass in Zukunft nichts mehr auseinander fällt. Die Qualität und Wartbarkeit des Microservices leidet.



Nichtisomorphismus von Medien



Wenn die Änderungsschleifen für Test und Produktion unterschiedlich geliefert werden, können Sie nicht sicher sein, dass sie auf die gleiche Weise funktionieren. Insbesondere, wenn die Entwicklung und das Debuggen tatsächlich im Test durchgeführt werden.



Objekte im Test können unter dem Konto des Entwicklers oder der Anwendung erstellt werden. Zuschüsse werden nach dem Zufallsprinzip vergeben und gewähren in der Regel alle Berechtigungen. Für den Antrag werden Zuschüsse nach dem Grundsatz „Ich sehe einen Fehler im Protokoll - ich gebe einen Zuschuss“ vergeben. Zuschüsse werden bei der Veröffentlichung oft vergessen. Manchmal decken Rauchtests nach der Veröffentlichung nicht alle neuen Funktionen ab, und das Fehlen eines Zuschusses wird nicht sofort ausgelöst.



Schwerer und zerbrechlicher Prozess des Rollens in die Produktion



Das Roll-In in die Produktion erfolgte manuell, jedoch in Analogie zum Oracle-Prozess, durch Genehmigung des DBA, der Release-Manager und durch Roll-Forward durch Release-Ingenieure.



Dies verlangsamt die Freigabe. Bei Problemen erhöht sich die Ausfallzeit, was den Zugriff des Entwicklers auf die Datenbank erschwert. Die Skripts exec.sql und rollback.sql wurden beim Test häufig nicht getestet, da es keinen Patchsetting-Standard für Nicht-Oracle gibt und der Test vollständig durchgeführt wurde.



Daher kommt es vor, dass Entwickler Änderungen an nicht kritischen Diensten ohne diesen Prozess vornehmen.



Wie kannst du tun, um gut zu sein?



Debuggen in einer lokalen Datenbank in einem Docker-Container



Für einige scheinen alle im Artikel beschriebenen technischen Lösungen offensichtlich zu sein. Aber aus irgendeinem Grund sehe ich von Jahr zu Jahr Menschen, die begeistert auf denselben Rechen treten.



Sie gehen nicht über ssh zum Testserver, um Anwendungscode zu schreiben und zu debuggen, oder? Ich finde es genauso absurd, Datenbankcode auf einer Test-DB-Instanz zu entwickeln und zu debuggen. Es gibt Ausnahmen, es kommt vor, dass es sehr schwierig ist, die Datenbank lokal zu erhöhen. Wenn es sich jedoch um etwas Leichtes und Nicht-Legacy handelt, ist es normalerweise nicht schwierig, die Basis lokal zu erhöhen und alle Migrationen konsistent darauf durchzuführen. Im Gegenzug erhalten Sie eine stabile Instanz an Ihrer Seite, die nicht von einem anderen Entwickler blockiert wird, auf die Sie keinen Zugriff verlieren und auf die Sie die für die Entwicklung erforderlichen Rechte haben.



Hier ist ein Beispiel dafür, wie einfach es ist, eine lokale Datenbank



aufzurufen: Schreiben wir eine zweizeilige Docker-Datei:



FROM postgres:12.3
ADD init.sql /docker-entrypoint-initdb.d/


In init.sql erstellen wir eine "saubere" Datenbank, die wir voraussichtlich sowohl im Test als auch in der Produktion erhalten. Es sollte enthalten:



  • Der Eigentümer des Schemas und das Schema selbst.
  • Anwendungsbenutzer mit einer Berechtigung zur Verwendung des Schemas.
  • Erforderliche ERWEITERUNGEN


Init.sql Beispiel
create role my_awesome_service
with login password *** NOSUPERUSER inherit CREATEDB CREATEROLE NOREPLICATION;
create tablespace my_awesome_service owner my_awesome_service location '/u01/postgres/my_awesome_service_data';
create schema my_awesome_service authorization my_awesome_service;
grant all on schema my_awesome_service to my_awesome_service;
grant usage on schema my_awesome_service to my_awesome_service;
alter role my_awesome_service set search_path to my_awesome_service,pg_catalog, public;

create user my_awesome_service_app with LOGIN password *** NOSUPERUSER inherit NOREPLICATION;
grant usage on schema my_awesome_service to my_awesome_service_app;

create extension if not exists "uuid-ossp";




Der Einfachheit halber können Sie die Datenbank-Task zum Makefile hinzufügen, wodurch der Container mit der Basis (neu) gestartet und der Port für die Verbindung herausragt:



db:
    docker container rm -f my_awesome_service_db || true
    docker build -t my_awesome_service_db docker/db/.
    docker run -d --name my_awesome_service_db -p 5433:5432 my_awesome_service_db


Versionierung von Änderungssätzen mit einem Industriestandard



Es sieht auch offensichtlich aus: Sie müssen Migrationen schreiben und im Versionskontrollsystem behalten. Aber sehr oft sehe ich "nackte" SQL-Skripte ohne Bindung. Und das bedeutet, dass es keine Kontrolle über Rollback und Rollback gibt, von wem, was und wann gepumpt wurde. Es gibt nicht einmal eine Garantie dafür, dass Ihre SQL-Skripte in der Test- und Produktionsdatenbank ausgeführt werden können, da sich deren Struktur möglicherweise geändert hat.



Im Allgemeinen benötigen Sie Kontrolle. Bei Migrationssystemen geht es nur um Kontrolle.

Wir werden nicht auf einen Vergleich verschiedener Versionsverwaltungssysteme für Datenbankschemata eingehen. FlyWay vs Liquibase ist nicht das Thema dieses Artikels. Wir haben uns für Liquibase entschieden.



Wir Version:



  • DDL-Struktur von Datenbankobjekten (Tabelle erstellen).
  • DML-Inhalt von Nachschlagetabellen (Einfügen, Aktualisieren).
  • DCL-Zuschüsse für UZ-Anwendungen (Zuschuss auswählen, einfügen auf ...).


Beim Starten und Debuggen eines Microservices in einer lokalen Datenbank muss sich ein Entwickler um Zuschüsse kümmern. Die einzige legale Möglichkeit besteht darin, dem Änderungssatz ein DCL-Skript hinzuzufügen. Dies stellt sicher, dass die Zuschüsse zum Verkauf angeboten werden.



Beispiel für ein SQL-Patchset
0_ddl.sql:

create table my_awesome_service.ref_customer_type
(
    customer_type_code    	varchar not null,
    customer_type_description varchar not null,
    constraint ref_customer_type_pk primary key (customer_type_code)
);
 
alter table my_awesome_service.ref_customer_type
    add constraint customer_type_code_ck check ( (customer_type_code)::text = upper((customer_type_code)::text) );


1_dcl.sql:



grant select on all tables in schema my_awesome_service to ru_svc_qw_my_awesome_service_app;
grant insert, update on my_awesome_service.some_entity to ru_svc_qw_my_awesome_service_app;


2_dml_refs.sql:



insert into my_awesome_service.ref_customer_type (customer_type_code, customer_type_description)
values ('INDIVIDUAL', '. ');
insert into my_awesome_service.ref_customer_type (customer_type_code, customer_type_description)
values ('LEGAL_ENTITY', '. ');
insert into my_awesome_service.ref_customer_type (customer_type_code, customer_type_description)
values ('FOREIGN_AGENCY', ' . ');


Fixtures. dev

3_dml_dev.sql:



insert into my_awesome_service.some_entity_state (state_type_code, state_data, some_entity_id)
values ('BINDING_IN_PROGRESS', '{}', 1);


rollback.sql:



drop table my_awesome_service.ref_customer_type;




Beispiel "Changeset.yaml"
databaseChangeLog:
 - changeSet:
     id: 1
     author: "mr.awesome"
     changes:
       - sqlFile:
           path: db/changesets/001_init/0_ddl.sql
       - sqlFile:
           path: db/changesets/001_init/1_dcl.sql
       - sqlFile:
           path: db/changesets/001_init/2_dml_refs.sql
     rollback:
       sqlFile:
         path: db/changesets/001_init/rollback.sql
 - changeSet:
     id: 2
     author: "mr.awesome"
     context: dev
     changes:
       - sqlFile:
           path: db/changesets/001_init/3_dml_dev.sql




Liquibase erstellt eine Datenbank-Änderungsprotokolltabelle in der Datenbank, in der die aufgepumpten Änderungssätze notiert werden.

Berechnet automatisch, wie viele Änderungssätze Sie für die Datenbank benötigen.



Es gibt ein Maven- und ein Gradle-Plugin mit der Möglichkeit, ein Skript aus mehreren Änderungssätzen zu generieren, die in die Datenbank übernommen werden müssen.



Integration des Datenbankmigrationssystems in die Anwendungsstartphase



Dies kann ein beliebiger Adapter des Migrationssteuerungssystems und des Frameworks sein, auf dem Ihre Anwendung basiert. Bei vielen Frameworks wird es mit dem ORM gebündelt. Zum Beispiel Ruby-On-Rails, Yii2, Nest.JS.



Dieser Mechanismus wird benötigt, um Migrationen zu rollen, wenn der Anwendungskontext gestartet wird.

Zum Beispiel:



  1. In der Testdatenbank die Patchsets 001, 002, 003.
  2. Der Pogromist entwickelte die Patchsets 004, 005 und stellte die Anwendung nicht für den Test bereit.
  3. Zum Test bereitstellen. Die Patchsets 004, 005 werden eingeführt.


Wenn sie nicht rollen, wird die Anwendung nicht gestartet. Das rollende Update tötet keine alten Pods.



Unser Stack ist JVM + Spring und wir verwenden kein ORM. Daher brauchten wir die Spring-Liquibase-Integration .



Wir haben eine wichtige Sicherheitsanforderung in unserem Unternehmen: Der Benutzer der Anwendung sollte über eine begrenzte Anzahl von Berechtigungen verfügen und auf keinen Fall über Zugriff auf Schemaeigentümerebene verfügen. Mit Spring-Liquibase ist es möglich, Migrationen im Namen des Benutzers des Schemabesitzers durchzuführen. Gleichzeitig hat der Verbindungspool der Anwendungsanwendungsebene keinen Zugriff auf die Liquibase DataSource. Daher erhält die Anwendung keinen Zugriff vom Benutzer des Schemabesitzers.



Application-testing.yaml Beispiel
spring:
  liquibase:
    enabled: true
    database-change-log-lock-table: "databasechangeloglock"
    database-change-log-table: "databasechangelog"
    user: ${secret.liquibase.user:}
    password: ${secret.liquibase.password:}
    url: "jdbc:postgresql://my.test.db:5432/my_awesome_service?currentSchema=my_awesome_service"




DAO-Tests in der CI-Phase überprüfen



Unser Unternehmen hat eine solche CI-Phase - überprüfen Sie. In dieser Phase werden Änderungen auf Übereinstimmung mit internen Qualitätsstandards überprüft. Bei Microservices ist dies normalerweise ein Linter-Lauf zur Überprüfung des Codestils und auf Fehler, ein Unit-Test-Lauf und ein Anwendungsstart mit Kontext-Hoisting. Jetzt können Sie in der Überprüfungsphase die Datenbankmigrationen und die Interaktion der Anwendungs-DAO-Schicht mit der Datenbank überprüfen.



Durch Erhöhen des Containers mit der Datenbank und Rollen der Patch-Sets wird die Startzeit des Spring-Kontexts um 1,5 bis 10 Sekunden erhöht, abhängig von der Leistung der Arbeitsmaschine und der Anzahl der Patch-Sets.



Dies sind keine wirklichen Komponententests, sondern Tests zur Integration der DAO-Schicht der Anwendung in die Datenbank.

Wenn wir eine Datenbank als Teil eines Mikrodienstes bezeichnen, testen wir die Integration von zwei Teilen eines Mikrodienstes. Keine externen Abhängigkeiten. Daher sind diese Tests stabil und können während der Überprüfungsphase ausgeführt werden. Sie reparieren den Microservice- und Datenbankvertrag und geben Sicherheit für zukünftige Verbesserungen.



Es ist auch eine praktische Möglichkeit, DAOs zu debuggen. Anstatt RestController aufzurufen und das Benutzerverhalten in einem Geschäftsszenario zu simulieren, rufen wir sofort das DAO mit den erforderlichen Argumenten auf.



DAO-Testbeispiel
@Test
@Transactional
@Rollback
fun `create cheque positive flow`() {
      jdbcTemplate.update(
       "insert into my_awesome_service.some_entity(inn, registration_source_code)" +
               "values (:inn, 'QIWICOM') returning some_entity_id",
       MapSqlParameterSource().addValue("inn", "526317984689")
   )
   val insertedCheque = chequeDao.addCheque(cheque)
   val resultCheque = jdbcTemplate.queryForObject(
       "select cheque_id from my_awesome_service.cheque " +
               "order by cheque_id desc limit 1", MapSqlParameterSource(), Long::class.java
   )
   Assert.assertTrue(insertedCheque.isRight())
   Assert.assertEquals(insertedCheque, Right(resultCheque))
}




Es gibt zwei verwandte Aufgaben zum Ausführen dieser Tests in der Überprüfungspipeline:



  1. Der Build-Agent kann möglicherweise mit dem Standard-PostgreSQL-Port 5432 oder einem statischen Port beschäftigt sein. Man weiß nie, jemand hat den Behälter mit dem Boden nach Abschluss der Tests nicht gelöscht.
  2. Daraus ergibt sich die zweite Aufgabe: Sie müssen den Container nach Abschluss der Tests löschen.


Die TestContainers- Bibliothek löst diese beiden Aufgaben . Es verwendet ein vorhandenes Docker-Image, um den Datenbankcontainer im Status init.sql aufzurufen.



Beispiel für die Verwendung von TestContainern
@TestConfiguration
public class DatabaseConfiguration {

   @Bean
   GenericContainer postgreSQLContainer() {
       GenericContainer container = new GenericContainer("my_awesome_service_db")
               .withExposedPorts(5432);

       container.start();
       return container;
   }

   @Bean
   @Primary
   public DataSource onlineDbPoolDataSource(GenericContainer postgreSQLContainer) {
       return DataSourceBuilder.create()
               .driverClassName("org.postgresql.Driver")
               .url("jdbc:postgresql://localhost:"
                       + postgreSQLContainer.getMappedPort(5432)
                       + "/postgres")
               .username("my_awesome_service_app")
               .password("my_awesome_service_app_pwd")
               .build();
   }
    
   @Bean
   @LiquibaseDataSource
   public DataSource liquibaseDataSource(GenericContainer postgreSQLContainer) {
       return DataSourceBuilder.create()
               .driverClassName("org.postgresql.Driver")
               .url("jdbc:postgresql://localhost:"
                       + postgreSQLContainer.getMappedPort(5432)
                       + "/postgres")
               .username("my_awesome_service")
               .password("my_awesome_service_app_pwd")
               .build();
   }




Mit der Entwicklung und dem Debugging herausgefunden. Jetzt müssen wir die Änderungen des Datenbankschemas an die Produktion liefern.



Kubernetes ist die Antwort! Was war deine Frage?



Sie müssen also einige CI / CD-Prozesse automatisieren. Wir haben einen bewährten Team-City-Ansatz. Es scheint, wo ist der Grund für einen anderen Artikel?



Und es gibt einen Grund. Neben dem bewährten Ansatz gibt es auch langweilige Probleme eines großen Unternehmens.



  • Es gibt nicht genug Team-Städtebauer für alle.
  • Eine Lizenz kostet Geld.
  • Die Einstellungen von Virtualok-Buildagenten erfolgen auf altmodische Weise über Repositories mit Konfigurationen und Puppen.
  • Der Zugriff von Buildern auf Zielnetzwerke muss auf altmodische Weise erfolgen.
  • Anmeldungen und Kennwörter für die Weiterleitung von Änderungen an der Datenbank werden ebenfalls auf altmodische Weise gespeichert.


Und bei all dieser "altmodischen Art" ist das Problem - jeder rennt in eine glänzende Zukunft und die Unterstützung von Legacy ... wissen Sie. Es funktioniert und okay. Funktioniert nicht - wir werden uns später darum kümmern. Irgendwann mal. Nicht heute.



Angenommen, Sie haben in der glänzenden Zukunft bereits ein knietiefes Bein und verfügen bereits über eine Kubernetes-Infrastruktur. Es besteht sogar die Möglichkeit, einen weiteren Microservice zu generieren, der sofort in dieser Infrastruktur gestartet wird, die erforderliche Konfiguration und Geheimnisse abruft, über den erforderlichen Zugriff verfügt und sich in der Service-Mesh-Infrastruktur registriert. Und all dieses Glück kann ein gewöhnlicher Entwickler erlangen, ohne eine Person mit der * OPS-Rolle einzubeziehen. Wir erinnern uns, dass es in Kubernetes eine Art Job-Workload gibt, die nur für irgendeine Art von Servicearbeit gedacht ist. Nun, wir sind gefahren, um eine Anwendung auf Kotlin + Spring-Liquibase zu erstellen, und haben versucht, die im Unternehmen vorhandene Infrastruktur für Microservices auf JVM in Kubera so weit wie möglich wiederzuverwenden.



Lassen Sie uns die folgenden Aspekte wiederverwenden:



  • Generierung des Projekts.
  • Bereitstellen.
  • Lieferung von Konfigurationen und Geheimnissen.
  • Zugriff.
  • Protokollierung und Übermittlung von Protokollen an ELK.


Wir bekommen eine solche Pipeline : Klickbar









Wir haben nun



  • Changeset-Versionierung.
  • Wir prüfen sie auf Machbarkeitsaktualisierung → Rollback.
  • Schreibtests für DAO. Manchmal folgen wir sogar TDD: Wir führen das DAO-Debugging mithilfe von Tests durch. Tests werden in einer frisch erstellten Datenbank in TestContainers durchgeführt.
  • Führen Sie die Docker-Datenbank lokal auf einem Standardport aus. Wir debuggen und schauen uns an, was in der Datenbank noch übrig ist. Bei Bedarf können wir die lokale Datenbank manuell verwalten.
  • Wir rollen in die Test- und Auto-Release-Patchsets mit einer Standard-Pipeline in Teamcity ein, analog zu Microservices. Die Pipeline ist ein untergeordnetes Element des Mikrodienstes, dem die Datenbank gehört.
  • Wir speichern keine Credits aus der Datenbank in Team City. Und Zugriffe von virtuellen Buildern sind uns egal.


Ich weiß, dass dies für viele keine Offenbarung ist. Aber da Sie mit dem Lesen fertig sind, teilen wir Ihre Erfahrungen gerne in den Kommentaren mit.



All Articles