[JAVA] Ich habe versucht, die wichtigsten Punkte des gRPC-Designs und der Entwicklung zusammenzufassen

Einführung

Es ist der 22. Tag von Future Advent Calender2 2019. Der Artikel von Adventskalender 1 stammt übrigens von hier.

Dies ist mein fünftes Jahr im Unternehmen. Welche Art von Geschäft und Technologie habe ich jedes Jahr zu dieser Jahreszeit im vergangenen Jahr erlebt? Es ist eine gute Gelegenheit, es sich noch einmal zu überlegen, aber da alle Projekte für den Namen Shizuru geeignet sind, muss ich wertvolle Feiertage nutzen. Ich kann keinen Artikel schreiben, deshalb ist es ziemlich schwierig. .. .. Stellen Sie zunächst die Qualität des Artikels sicher.

Überblick

2019 leitete er das Design und die Entwicklung von Anwendungen mit NoSQL-Datenbanken wie KVS. Rückblickend habe ich nur Cassandra-Artikel geschrieben, daher möchte ich mich dieses Mal auf gRPC konzentrieren und Artikel schreiben. Bis jetzt hatte ich Probleme, weil ich mich nie ernsthaft mit API-Design und -Entwicklung befasst habe und mich auf Infrastruktur- und Middleware-Design und -Konstruktion konzentriert habe, aber ich hoffe, dass ich jedem das Wissen zurückgeben kann, das ich durch das Design und die Entwicklung von gRPC gewonnen habe.

Was ist gRPC?

gRPC ist eine von Google entwickelte Open Source, die Protokollpuffer zum Serialisieren von Daten und Daten verwendet Das Merkmal ist, dass es eine schnellere Kommunikation als REST realisieren kann. In gRPC werden API-Spezifikationen mithilfe von IDL (Interface Definition Language) zur Definitionsdatei namens Protodatei hinzugefügt. Durch Definieren der für den Client / Server erforderlichen Quellvorlage, auch zwischen verschiedenen Sprachen wie Java, C ++, Python, Go usw. Eine der Funktionen ist, dass die jeweils passende IF automatisch aus der Protodatei generiert werden kann. Die Protodatei wird gemäß der Sprachspezifikation von [proto3] definiert (https://developers.google.com/protocol-buffers/docs/proto3).

grpc_concept_diagram_00.png

Punkte zur Einführung von gRPC

REST wird häufig verglichen, wenn gRPC als API verwendet wird. Der API-Server, für den ich dieses Mal verantwortlich war, ist eine API für CRUD für die Back-End-Datenspeicherschicht. Der Punkt, an dem von KVS usw. erfasste Daten durch ** proto strukturell definiert werden können und schneller als REST ** kommunizieren können Der größte Vorteil der Einführung als Datenspeicher-API war.

Es ist auch sehr kostspielig, die API-Spezifikationen anzupassen, um die Mikrodienstarchitektur zu realisieren. Das ist die Aufgabe, und mit gRPC ** kann die Schnittstelle nach strengen Regeln ** gehalten werden. Es kann gesagt werden, dass der Nutzen der Übernahme höher ist als der von REST, was freies Design ermöglicht.

Es gibt jedoch keinen absoluten Vorteil gegenüber REST, daher unter dem Gesichtspunkt, die oben genannten Vorteile zu nutzen. Ich denke, es ist besser, von Fall zu Fall auszuwählen.

gRPC-Design- und Entwicklungstipps

1. So verwalten Sie Protodateien

1-1 Definieren Sie tief verschachtelte Strukturdaten, indem Sie die Protodatei teilen

Da Cassandra für den Datenspeicher verwendet wurde, handelt es sich bei den verarbeiteten Daten nicht um eine flache Datenhierarchie, sondern Es waren tief verschachtelte Strukturdaten. Daher ist es möglich, tief verschachtelte Strukturdaten in einer Protodatei zu definieren. Dies ist möglich, aber nicht einfach zu lesen und zu warten. Daher wurde die Protodatei wie unten gezeigt aufgeteilt und definiert.

Beispiel für die Definition in einer Datei

Es ist möglich, die hierarchische Struktur in einer Datei zu definieren, aber Sie können ein mehrschichtiges Modell in einem Proto ausdrücken. Wenn die Verschachtelung in der 2. und 3. Schicht tiefer wird, verschlechtern sich sowohl die Lesbarkeit als auch die Wartbarkeit.


syntax = "proto3";
option java_package = "jp.co.sample.datastore.common.model";

package common;

message ParentModel {
  string parentId    = 1;
  string parentNm    = 2;
  ChildModel child   = 3; //Geben Sie das in der Datei definierte ChildModel als Typ an
}

message ChildModel {
  string childId    = 1;
  string childNm    = 2;
}

Beispiel für die Definition mit mehreren Dateien

Da ChildModel und ParentModel können separat in separaten Dateien definiert werden Dieses Mal wurden die Dateien für jede Struktur separat verwaltet. Wie ich später erwähnen werde, ist Cassandra ein benutzerdefinierter Typ. (Da jede als UDT bezeichnete Struktur durch DDL definiert werden kann, wird das Protomodell auch für jede UDT unterteilt.


syntax = "proto3";
option java_package = "jp.co.sample.datastore.common.model";

package common;

message ChildModel {
  string childId    = 1;
  string childNm    = 2;
}

syntax = "proto3";
option java_multiple_files = true;
option java_package = "jp.co.sample.datastore.common.model";

package common;

import "common/child_model.proto"; //Geben Sie das Proto an, das das ChildModel definiert

message ParentModel {
  string parentId    = 1;
  string parentNm    = 2;
  ChildModel child   = 3;
}

1-2. Verwalten Sie Protodateien zusammen mit DDL

In gRPC werden API-Spezifikationen in der Protodatei definiert und verwaltet, sodass diese Protodatei im Grunde genommen von Git usw. verwaltet wird. Man kann sagen, dass die API-Spezifikationen durch die Verwaltung der Version immer auf dem neuesten Stand gehalten werden können.

Wenn Sie es jedoch als Datenspeicher-API verwenden, wird die Anforderung / Antwort von der Anwendung angezeigt Ich hätte das Proto für die Parameterdefinition verwalten sollen, aber ich habe das Proto verwendet, um die Daten von Cassandra zu erhalten. Um strukturell damit umgehen zu können, war es notwendig, die Konsistenz mit der Tabellendefinition auf der Cassandra-Seite aufrechtzuerhalten.

Es ist üblich, dass DDL-Änderungen und -Updates während der Anwendungsentwicklung auftreten. Daher wurde Cassandras DDL von der internen Standardformat-Tabellendefinitionsdatei verwaltet Auch wenn die Tabellendefinition geändert wird, indem automatisch eine Protodatei mit der Definition als Eingabe generiert wird Die Konsistenz zwischen beiden kann nun garantiert werden.

Mit zunehmendem Entwicklungsumfang wird es schwierig, den Unterschied zwischen Proto und DDL zu absorbieren. Es ist besser, den Mechanismus von Anfang an anzuordnen.

1-3 IF-Modulverwaltungsmethode

Von der Protodatei über die Protodatei bis zur Client / Server-Schnittstelle Quellen können automatisch für jede Sprache generiert werden.

Es ist jedoch sehr ärgerlich, automatisch aus der Protodatei zu generieren und die Quelle jedes Mal festzuschreiben. Wenn die Anzahl der Entwickler zunimmt, wird dies zu einer komplizierten Aufgabe. Generieren Sie daher ein Schnittstellenmodul aus der neuesten Protodatei Die Paketverwaltung wurde durch Verknüpfung mit dem Repository mit nexus durchgeführt.

Dieses Mal wurden beide Clients / Server in Java entwickelt, also über Gradle Definiert, um das Paket von nexus zu erhalten.

2. Implementierung der allgemeinen Verarbeitung mit benutzerdefinierten Optionen

Beim API-Design muss ein Validierungsdesign für Anforderungsparameter durchgeführt werden. In gRPC muss beim Definieren von Daten in einer Protodatei ein Typ wie string oder int angegeben werden. Sammlungstypen wie Map und Set können ebenfalls definiert und verarbeitet werden.

Daher ist es nicht erforderlich, eine Typprüfung für die Anforderungsparameter vom Client durchzuführen. Bei anderen Validierungen wie obligatorischen Prüfungen und Ziffernprüfungen ist eine Berücksichtigung erforderlich.

In einer Protodatei können Sie eine Datei oder ein Feld mit [Benutzerdefinierte Optionen] definieren (https://developers.google.com/protocol-buffers/docs/proto3#options). Sie können benutzerdefinierte Optionen aus dem gRPC-Modell übernehmen und jede Behandlung implementieren.

2-1. Beispiel für die Definition einer benutzerdefinierten Option


syntax = "proto3";

option java_multiple_files = true;
option java_package = "jp.co.sample.datastore.option.model";

package option;

import "google/protobuf/descriptor.proto";

extend google.protobuf.FieldOptions {
  bool required = 50000; //Obligatorische Überprüfungsoption
}

extend google.protobuf.FieldOptions {
  int32 strlen = 50001; //Option zur Überprüfung der Ziffern
}

Definieren Sie die oben vorbereiteten benutzerdefinierten Optionen in einem beliebigen Feld.

syntax = "proto3";

option java_multiple_files = true;
option java_package = "jp.co.sample.datastore.common.model";

package common;

import "option/custom_option.proto"; //Importieren Sie Proto mit definierten benutzerdefinierten Optionen

message User {
  string user_id            = 1[(required)=true,(strlen)=8]; //Es können mehrere Optionen definiert werden
  string user_name          = 2[(required)=true];
}

2-2. So erhalten Sie benutzerdefinierte Optionen (Java)

Dies ist ein Beispiel, um die benutzerdefinierte Option zu erhalten, die im Feld vom Benutzer des oben definierten Nachrichtenmodells festgelegt wurde. In "User.getDescriptorForType (). GetFields ()" ist der FieldDescriptor, der die Metainformationen des Benutzermodells darstellt Sie können es erhalten, und Sie können die Optionsinformationen erhalten, indem Sie den FieldDescriptor behandeln.

for(Descriptors.FieldDescriptor fds: User.getDescriptorForType().getFields()){
    System.out.println(fds.getName())
    for(Map.Entry<Descriptors.FieldDescriptor,Object> entry : fds.getOptions.getAllFields().entrySet()){
        System.out.println("option:" + entry.getKey().getName() + "=" entry.getValue());
    }
}

/*Ausgabeergebnis*/
// user_id
// option:required=true
// option:strlen=8
// user_nm
// option:required=true

2-3 Beispiel für die Implementierung der Validierung

Sie können die Existenz des Nachrichtenfelddeskriptors auch mit "hasExtension ()" überprüfen. Implementierung einer beliebigen optionalen Validierungsverarbeitung für jedes Feld aus dem gRPC-Modell Es wird möglich sein. Darüber hinaus erbt das gRPC-Modell eine gemeinsame Schnittstellenklasse namens Nachrichtentyp. Durch Umwandeln in den Nachrichtentyp und die Behandlung von FieldDescriptor kann die allgemeine Verarbeitung ohne Abhängigkeit vom Modell implementiert werden.


if(fds.getOptions().hasExtension(CustomOption.required)){
  //Aus Feldmetainformationen mit hasExtension"required"Überprüfen Sie, ob die Option vorhanden ist

  Object value = fds.getOptions().getExtension(CustomOption.required); //Holen Sie sich den Inhalt der Option mit getExtension
  //Implementierung der Validierungsverarbeitung
}

3. Lassen Sie das gRPC-Modell explizit leere Zeichen 0 behandeln

In der Modellschnittstelle, die durch Definieren als Zeichenfolge oder int in der Protodatei extrahiert wird, wird kein Wert festgelegt. Wenn der Wert des Feldes abgerufen wird, ** ist der Standardwert für stirng leer und 0 ** für int32 / int64.

Beispielsweise empfängt es ein gRPC-Modell vom Client und sendet es basierend auf dem im Feld festgelegten Wert an den Datenspeicher. Ob der Client beim Aktualisieren absichtlich mit Leerzeichen oder 0 initialisieren möchte, mit dem Standardwert des gRPC-Modells Es gibt ein Problem, bei dem es auf der Serverseite nicht möglich ist, zu beurteilen, ob es nicht eingestellt ist (Aktualisierung ist nicht erforderlich), und es zu verarbeiten.

Um dieses Problem zu lösen, bietet gRPC Wrapper-Klassen an, die durch Definieren dieser Klassen ermittelt werden können.

3-1. Beispiel für die Definition einer Protodatei mithilfe der Wrapper-Klasse


message Test{
  string       value1  = 1; //Es kann nicht festgestellt werden, ob ein Leerzeichen oder der Standardwert festgelegt ist
  int32        value2  = 2; //Es kann nicht festgestellt werden, ob 0 oder der Standardwert festgelegt ist
  StringValue  value3  = 3; //Kann bestimmen, ob ein Leerzeichen oder der Standardwert festgelegt ist
  Int32Value   value4  = 4; //Kann bestimmt werden, ob 0 oder der Standardwert gesetzt ist
}

3-2. Implementierungsbeispiel für die Überprüfung der Wertexistenz


    Test.Builder testBuilder = Test.newBuilder();

    //Explizit leere Zeichen,Auf 0 setzen
    testBuilder
        .setValue1("")
        .setValue2(0)
        .setValue3(StringValue.newBuilder().setValue(""))
        .setValue4(Int32Value.newBuilder().setValue(0))
        ;

    for(Descriptors.FieldDescriptor fds : testBuilder.build().getDescriptorForType().getFields()) {
        if (testBuilder.hasField(fds)) {
            System.out.println(fds.getName() + " has field");
        } else {
            System.out.println(fds.getName() + " has not field");
        }
    }

    /*Ausgabebeispiel*/
    // value1 has not field
    // value2 has not field
    // value3 has field
    // value4 has field

4. Generieren Sie dynamisch Abfragen aus dem gRPC-Modell

Da Cassandra als Datenspeicher für Cassandra-Tabellen verwendet wurde Um CRUD-Operationen ausführen zu können, musste ich meine eigene Abfrage namens CQL implementieren.

Cassandras CQL basiert im Wesentlichen auf SQL, daher ist die Implementierung relativ intuitiv. Für CAS-fähige Abfragen und Elemente mit tiefer struktureller Hierarchie (eingefrorenes UDT) zur gleichzeitigen Aktualisierungssteuerung Entwicklern sind Abfragen bekannt, die in SQL nicht ausgedrückt werden können, z. B. das Hinzufügen / Löschen von Update-Anweisungen, Maps und Set-Elementen. Da es implementiert werden musste, kann es durch Übergeben der gRPC-Modellklasse als Argument an den Datenspeicher übergeben werden. Der Prozess wurde versteckt. (KVS-Version ODER / Mapper-ähnlich)

Der Punkt des dynamischen Generierens einer Abfrage aus der Modellklasse wurde auch im Beispiel für benutzerdefinierte Optionen beschrieben. Der Punkt ist, dass die Verarbeitung für allgemeine Zwecke implementiert werden kann, indem der Felddeskriptor unter Verwendung des Nachrichtentyps behandelt wird. Beachten Sie beim Entwerfen einer allgemeinen Verarbeitung für das gRPC-Modell die Verwendung des Nachrichtentyps.

4.1 Beispiel für die Implementierung einer Cql SELECT-Anweisung


	public BuiltStatement select(Message message) {
		BuiltStatement select;
		try {
			//Tabellenname festgelegt
			String table = message.getDescriptorForType().getOptions().getExtension(CustomOption.entityOptions)
					.getTableName();

			//CQL-Generierung
			Select.Selection selection = QueryBuilder.select();
			Map<String, Object> partitionKeyMap = new HashMap<>();

			for (Descriptors.FieldDescriptor fds : message.getDescriptorForType().getFields()) {

				//Erstellen Sie die SELECT-Klausel
				if (fds.getName().equals("select_enum")) {
					if (message.getRepeatedFieldCount(fds) > 0) {
						IntStream.range(0, message.getRepeatedFieldCount(fds)).forEach(
								i -> selection.column(message.getRepeatedField(fds, i).toString()));
					} else {
						selection.all();
					}
				}

				//Extraktion des Partitionsschlüssels
				if (fds.getOptions().getExtension(CustomOption.attributeOptions).getPartitionKey() > 0
						|| fds.getOptions().getExtension(CustomOption.attributeOptions).getClusteringKey() > 0) {
					partitionKeyMap.put(fds.getName(), message.getField(fds));
				}
			}

			//FROM-Klauselgenerierung
			select = selection.json().from(getTableMetadata(table));

			//Erstellung der WHERE-Klausel
			for (Map.Entry<String, Object> entry : partitionKeyMap.entrySet()) {

				Object value = entry.getValue();

				if (value instanceof String) {
					((Select) select).where(eq(entry.getKey(), value));
				} else if 
                    ...Typunterscheidungsverarbeitung weggelassen
				} else {
					logger.debug("Der Partitionstyp ist falsch");
					throw new RuntimeException("unsupported type");
				}
			}
			return select;
		} catch (Exception e) {
			e.printStackTrace();
			throw new RuntimeException(e);
		}
	}

Neben Cassandra verwenden wir auch Elastic Search als Volltextsuchmaschine. Abfragen, die an ElasticSearch gesendet werden sollen, generieren auch dynamisch Abfragen aus dem gRPC-Modell unter Verwendung der obigen Message-Klasse. Es wurde so konzipiert, dass App-Entwickler auf den Datenspeicher zugreifen können, ohne die Abfrage direkt implementieren zu müssen.

5. Bequeme Verarbeitungstipps mit dem gRPC-Modell

Wie oben bereits erwähnt, finden Sie hier einige Tipps, die Sie bei der Arbeit mit gRPC-Modellen beachten sollten. Dieses Mal ist es in gRPC-Java implementiert. Bitte bewahren Sie es als Referenz auf, wenn Sie es in einer anderen Sprache als Java implementieren. (Ich hatte nicht genug Zeit, also werde ich später Tipps hinzufügen ...)

5-1. Ausgabe im Json-Format vom gRPC-Modell

Ausgabe des Json-Formats aus dem gRPC-Modell. Wenn Erhaltung von ProtoFieldNames hinzugefügt wird, werden die in proto definierten Feldnamen ausgegeben. Wenn Sie ConservationProtoFieldNames nicht hinzufügen, wird es im Camel-Fall ausgegeben. Verwenden Sie es daher je nach Verwendungszweck ordnungsgemäß.


JsonFormat.printer().preservingProtoFieldNames().print(gRPC-Modell) //Ausgabe mit Feldname gemäß Proto-Definition
JsonFormat.printer().print(gRPC-Modell) //Ausgabe im Kamelkoffer

5-2. Typbestimmung des gRPC-Modells


for (Descriptors.FieldDescriptor fds :gRPC-Modell.getDescriptorForType().getFields()) {
	if (fds.isMapField()) {
        //Stellen Sie fest, ob das Feld vom Kartentyp ist
	} else if (fds).isRepeated()) {
        //Bestimmen Sie, ob das Feld ein Set-Typ ist
	} else {
        //Andere Typen als Sammlung
    }
}

5-3. Rufen Sie den Wert ab, indem Sie den Feldnamen aus der Message-Klasse angeben


String val = (String) messageModel.getField(messageModel.getDescriptorForType().findFieldByName("Feldname"));

5-4 Zusammenführen zwischen gRPC-Modellen

Ein Beispiel für das Zusammenführen von Werten von einem Modell zu einem anderen. Wenn Sie .ignoringUnknownFields () verwenden, wird es ignoriert, auch wenn das Zusammenführungsziel kein Zielfeld enthält.


JsonFormat.parser().ignoringUnknownFields().merge(
		JsonFormat.printer().preservingProtoFieldNames().print(Originalmodell zusammenführen),Zielmodell zusammenführen);

Recommended Posts

Ich habe versucht, die wichtigsten Punkte des gRPC-Designs und der Entwicklung zusammenzufassen
Ich habe versucht, die Grundlagen von Kotlin und Java zusammenzufassen
Ich habe versucht, die Methoden von Java String und StringBuilder zusammenzufassen
Ich habe die grundlegende Grammatik von Ruby kurz zusammengefasst
Ich habe versucht, persönlich nützliche Apps und Entwicklungstools (Entwicklungstools) zusammenzufassen.
Ich habe versucht, persönlich nützliche Apps und Entwicklungstools (Apps) zusammenzufassen.
Ich habe versucht, die verwendeten Methoden zusammenzufassen
Ich habe versucht, die Stream-API zusammenzufassen
Ich habe versucht, den Betrieb des gRPC-Servers mit grpcurl zu überprüfen
[Für Swift-Anfänger] Ich habe versucht, den chaotischen Layoutzyklus von ViewController und View zusammenzufassen
[Ruby] Ich möchte nur den Wert des Hash und nur den Schlüssel extrahieren
Bevor Sie die Funktionen und Punkte der Furima-App vergessen
Ich habe versucht, die Geschwindigkeit von Graal VM mit JMH zu messen und zu vergleichen
05. Ich habe versucht, die Quelle von Spring Boot zu löschen
Ich habe versucht, dies und das von Spring @ Transactional zu überprüfen
Ich habe JAX-RS ausprobiert und mir das Verfahren notiert
Ich habe versucht, eine Umgebung mit WSL2 + Docker + VSCode zu erstellen
Ich habe versucht, die Unterstützung für iOS 14 zusammenzufassen
Ich habe versucht, die Methode zu erklären
[Rails 6.0, Docker] Ich habe versucht, die Konstruktion der Docker-Umgebung und die zum Erstellen eines Portfolios erforderlichen Befehle zusammenzufassen
Ich habe versucht, das Java-Lernen zusammenzufassen (1)
Ich habe jetzt versucht, Java 8 zusammenzufassen
Ich habe versucht, das Problem der "mehrstufigen Auswahl" mit Ruby zu lösen
Ich habe versucht zusammenzufassen, was bei der Site-Java-Ausgabe gefragt wurde.
Ich habe versucht, mit Docker eine Plant UML Server-Umgebung zu erstellen
[Rubiy] Heute Abend habe ich versucht, die Schleifenverarbeitung zusammenzufassen [Zeiten, Pause ...]
Sondervortrag über Multiskalensimulation: Ich habe versucht, den 5. zusammenzufassen
Sondervortrag über Multi-Scale-Simulation: Ich habe versucht, den 8. zusammenzufassen
Sondervortrag über Multi-Scale-Simulation: Ich habe versucht, den 7. zusammenzufassen
Ich habe versucht, das Problem des Google Tech Dev Guide zu lösen
Ich habe versucht, die Ergebnisse vor und nach der Date-Klasse mit einer geraden Zahl auszudrücken
Ich habe versucht, das Iterator-Muster zu implementieren
Was ist Docker? Ich habe versucht zusammenzufassen
Ich habe die Punkte zusammengefasst, die bei der kombinierten Verwendung von Ressourcen und Ressourcen zu beachten sind
[Einführung in Java] Ich habe versucht, das Wissen zusammenzufassen, das ich für wesentlich halte
Ich möchte das Argument der Annotation und das Argument der aufrufenden Methode an den Aspekt übergeben
Ich habe versucht, den CPU-Kern mit Ruby voll auszunutzen
Ich habe versucht, den Zugriff von Lambda → Athena mit AWS X-Ray zu visualisieren
[Ruby] Ich habe versucht, die häufigen Methoden in Paiza zusammenzufassen
[Ruby] Ich habe versucht, die häufigen Methoden mit paiza ② zusammenzufassen
Ich habe versucht, die Grammatik von R und Java zu übersetzen [Von Zeit zu Zeit aktualisiert]
Ich habe versucht, das auf Vagrant erstellte Portfolio in die Entwicklungsumgebung von Docker zu migrieren
Ich möchte, dass Sie Enum # name () für den Schlüssel von SharedPreference verwenden
Ich habe versucht, ein Beispielprogramm mit dem Problem des Datenbankspezialisten für domänengesteuertes Design zu erstellen
Ich habe versucht, über JVM / Garbage Collection zusammenzufassen
[Rails] Ich habe versucht, die Version von Rails von 5.0 auf 5.2 zu erhöhen
Ich habe versucht, die Sitzung in Rails zu organisieren
Was ich versucht habe, als ich alle Felder einer Bohne bekommen wollte
[Individuelle Entwicklung des Einkaufsagenturservices - Nr. 009] Ein wenig mehr Arbeit beim Wechseln und Entwerfen von Unternehmen mit Figma