
Der Artikel
wird nicht über verantwortungslose Mitarbeiter sprechen, wie man aus dem Titel des Artikels schließen könnte. Wir werden eine echte technische Gefahr diskutieren, die Sie erwarten kann, wenn Sie verteilte Systeme erstellen.
In einem Enterprise-System gab es eine Komponente. Diese Komponente sammelte Daten von Benutzern zu einem bestimmten Produkt und zeichnete sie in einer Datenbank auf. Und es bestand aus drei Standardteilen: der Benutzeroberfläche, der Geschäftslogik auf dem Server und den Tabellen in der Datenbank.
Die Komponente funktionierte gut und mehrere Jahre lang berührte niemand ihren Code.
Aber einmal, ohne Grund, passierten seltsame Dinge mit der Komponente.
Bei der Arbeit mit einigen Benutzern hat eine Komponente mitten in einer Sitzung plötzlich Fehler ausgelöst. Es passierte selten, aber wie immer im ungünstigsten Moment. Und was am unverständlichsten ist, die ersten Fehler traten in einer stabilen Version des Systems in der Produktion auf. In der Version, in der mehrere Monate lang überhaupt keine Komponenten geändert wurden.
Wir begannen die Situation zu analysieren und überprüften die Komponente unter starker Last. Es funktioniert gut. Wiederholte ziemlich umfangreiche Integrationstests. In den Integrationstests hat unsere Komponente einwandfrei funktioniert.
Mit einem Wort, der Fehler wurde unklar, wann und wo.
Sie begannen tiefer zu graben. Eine detaillierte Analyse und ein Vergleich der Protokolldateien zeigten, dass die Ursache für die dem Benutzer angezeigten Fehlermeldungen eine Verletzung der Einschränkungen im Primärschlüssel in der bereits erwähnten Tabelle in der Datenbank ist.
Die Komponente hat Daten im Ruhezustand in die Tabelle geschrieben, und manchmal hat Hibernate beim Versuch, die nächste Zeile zu schreiben, eine Einschränkungsverletzung gemeldet.
Ich werde die Leser nicht mit weiteren technischen Details langweilen und Sie sofort über das Wesentliche des Fehlers informieren. Es stellte sich heraus, dass nicht nur unsere Komponente in die obige Tabelle schreibt, sondern manchmal (äußerst selten) auch eine andere Komponente. Und sie macht es sehr einfach mit einer einfachen SQL INSERT-Anweisung. Ein Ruhezustand funktioniert standardmäßig beim Schreiben wie folgt. Um den Schreibprozess zu optimieren, wird der Index des nächsten Primärschlüssels einmal im Index abgefragt und dann mehrmals geschrieben, indem einfach der Schlüsselwert erhöht wird (standardmäßig 10-mal). Wenn nach der Anforderung die zweite Komponente im Prozess stecken blieb und Daten mit dem folgenden Primärschlüsselwert in die Tabelle schrieb, führte der nachfolgende Versuch, aus dem Ruhezustand zu schreiben, zu einer Verletzung der Einschränkungen.
Wenn Sie an technischen Details interessiert sind, sehen Sie diese unten.
Technische Details.
Der Klassencode begann folgendermaßen:
@Entity @Table(name="PRODUCT_XXX") public class ProductXXX { @Id @Basic(optional=false) @Column( name="PROD_ID", columnDefinition="integer not null", insertable=true, updatable=false) @SequenceGenerator( name="GEN_PROD_ID", sequenceName="SEQ_PROD_ID", allocationSize=10) @GeneratedValue( strategy=GenerationType.SEQUENCE, generator="GEN_PROD_ID") private long prodId;
Eine Diskussion über ein ähnliches Problem bei Stackoverflow:
https://stackoverflow.com/questions/12745751/hibernate-sequencegenerator-and-allocationsize Und so kam es, dass sich die Prozesse zum Schreiben der ersten und zweiten Komponente viele Monate lang nach dem Ändern der zweiten Komponente und dem Implementieren der Einträge in der Tabelle nie zeitlich überschneiden. Und sie begannen sich zu überschneiden, als sich in einer der Einheiten, die das System verwendeten, der Arbeitsplan geringfügig änderte.
Nun, die Integrationstests verliefen reibungslos, da sich auch die Zeitintervalle für das Testen beider Komponenten innerhalb der Integrationstests nicht überschnitten.
In gewisser Weise können wir sagen, dass niemand wirklich für den Fehler verantwortlich war.
Oder ist es nicht so?
Beobachtungen und Gedanken
Nachdem die wahre Fehlerursache ermittelt wurde, wurde sie behoben.
Aber nicht mit diesem Happy End möchte ich diesen Artikel beenden, sondern über diesen Fehler als Vertreter der großen Kategorie von Fehlern nachdenken, die nach dem Übergang von monolithischen zu verteilten Systemen an Popularität gewonnen haben.
Aus Sicht der einzelnen Komponenten oder Services im beschriebenen Enterprise-System wurde alles getan, alles scheint richtig zu sein. Alle Komponenten oder Dienstleistungen hatten unabhängige Lebenszyklen. Und als sich aufgrund der Bedeutungslosigkeit der Operation die Notwendigkeit ergab, in die Tabelle in der zweiten Komponente zu schreiben, wurde eine pragmatische Entscheidung getroffen, dies auf einfachste Weise direkt in dieser Komponente zu implementieren und die stabil funktionierende erste Komponente nicht zu berühren.
Leider geschah das, was in verteilten Systemen häufig vorkam (und in monolithischen Systemen relativ seltener): Die Verantwortung für die Ausführung von Operationen an einem bestimmten Objekt wurde
auf die Subsysteme verteilt. Wenn beide Schreibvorgänge im selben Mikrodienst implementiert würden, würde sicherlich eine einzige Technologie für ihre Implementierung ausgewählt. Und dann wäre der beschriebene Fehler nicht aufgetreten.
Verteilte Systeme, insbesondere das Konzept der Mikrodienste, haben effektiv dazu beigetragen, eine Reihe von Problemen zu lösen, die monolithischen Systemen inhärent sind. Paradoxerweise führt die Aufgabentrennung für einzelne Dienste jedoch zu dem gegenteiligen Effekt. Komponenten "leben" jetzt so unabhängig wie möglich. Und zwangsläufig besteht die Versuchung, große Änderungen an einer Komponente vorzunehmen, um hier eine kleine Funktionalität zu „schrauben“, die besser in einer anderen Komponente implementiert werden könnte. Dies erreicht schnell den endgültigen Effekt, reduziert das Volumen an Zulassungen und Tests. Von Änderung zu Änderung werden die Komponenten mit für sie ungewöhnlichen Merkmalen überwachsen, dieselben internen Algorithmen und Funktionen werden dupliziert, es entsteht eine Multivarianz der Problemlösung (und manchmal auch deren Nichtdeterminismus). Mit anderen Worten, ein verteiltes System verschlechtert sich mit der Zeit, jedoch anders als ein monolithisches.
Das „Verschmieren“ der Verantwortung für Komponenten in großen Systemen, die aus vielen Diensten bestehen, ist eines der typischen und schmerzhaften Probleme moderner verteilter Systeme. Die Situation wird durch die gemeinsam genutzten Optimierungssubsysteme wie Caching, Vorhersage der folgenden Operationen (Vorhersage) sowie Orchestrierung von Diensten usw. weiter kompliziert und verwirrt.
Durch die Zentralisierung des Zugriffs auf die Datenbank, zumindest auf der Ebene einer einzelnen Bibliothek, liegt die Anforderung auf der Hand. Viele moderne verteilte Systeme sind jedoch in der Vergangenheit um Datenbanken gewachsen und verwenden die darin gespeicherten Daten direkt (über SQL) und nicht über Zugriffsdienste.
"Unterstützung" bei der Verbreitung von Verantwortung und ORM-Frameworks und -Bibliotheken wie Hibernate. Mit ihnen möchten viele Entwickler von Datenbankzugriffsdiensten aufgrund der Anforderung unabsichtlich so viele Objekte wie möglich angeben. Ein typisches Beispiel ist die Anforderung von Benutzerdaten, um diese in einer Begrüßung oder im Feld mit dem Authentifizierungsergebnis anzuzeigen. Anstatt den Benutzernamen in Form von drei Textvariablen (Vorname, Mittlerer Name, Nachname) zurückzugeben, gibt eine solche Anforderung häufig ein vollwertiges Benutzerobjekt mit Dutzenden von Attributen und verbundenen Objekten zurück, z. B. die Liste der Rollen des angeforderten Benutzers. Dies verkompliziert wiederum die Logik der Verarbeitung des Ergebnisses der Anforderung, erzeugt unnötige Abhängigkeiten des Handlers vom Typ des zurückgegebenen Objekts und ... provoziert die Verteilung der Verantwortung aufgrund der Möglichkeit, die mit dem Objekt verknüpfte Logik von außen zu implementieren, die für dieses Serviceobjekt verantwortlich ist.
Was tun? (Empfehlungen)
Leider ist das Verschmieren von Verantwortung in bestimmten Fällen manchmal erzwungen und manchmal sogar unvermeidlich und gerechtfertigt.
Wenn möglich, sollten Sie dennoch versuchen, den Grundsatz der Verteilung der Verantwortung zwischen den Komponenten einzuhalten. Eine Komponente ist eine Verantwortung.
Nun, wenn es unmöglich ist, Operationen auf bestimmte Objekte ausschließlich in einem System zu konzentrieren, muss ein solches Verschmieren in der systemweiten Dokumentation („Superkomponente“) sehr sorgfältig aufgezeichnet werden, wie die spezifische Abhängigkeit der Komponenten vom Datenelement, vom Domänenobjekt oder voneinander.
Es wäre interessant, Ihre Meinung zu diesem Thema sowie Fälle aus der Praxis zu kennen, die die Thesen dieses Artikels bestätigen oder widerlegen.
Vielen Dank, dass Sie den Artikel bis zum Ende gelesen haben.
Illustration "Multimedia Mikher" vom Autor des Artikels.