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.
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.
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).
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.
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.
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;
}
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;
}
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.
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.
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.
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];
}
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
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
}
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.
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
}
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
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.
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.
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 ...)
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
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
}
}
String val = (String) messageModel.getField(messageModel.getDescriptorForType().findFieldByName("Feldname"));
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