C'est le 22e jour du Future Advent Calender2 2019. À propos, l'article du Calendrier de l'Avent 1 est de ici.
C'est ma cinquième année dans l'entreprise, et chaque année à cette période de l'année, à quel type d'entreprise et de technologie ai-je été confronté au cours de l'année écoulée? C'est une bonne occasion de reconsidérer, mais comme tous les projets sont adaptés au nom de Shizuru, je dois profiter de vacances précieuses. Je ne peux pas écrire d'article, donc c'est assez difficile. .. .. Assurez-vous d'abord de la qualité de l'article.
En 2019, il a dirigé la conception et le développement d'applications utilisant des bases de données NoSQL telles que KVS. Avec le recul, je n'ai écrit que des articles sur Cassandra, donc cette fois je voudrais me concentrer sur gRPC et écrire des articles. Jusqu'à présent, j'ai eu du mal parce que je n'ai jamais été sérieux dans la conception et le développement d'API, en me concentrant sur la conception et la construction d'infrastructures et d'intergiciels, mais j'espère pouvoir redonner à tout le monde les connaissances que j'ai acquises grâce à la conception et au développement de gRPC.
gRPC est une source ouverte développée par Google qui utilise des tampons de protocole pour sérialiser les données. La caractéristique est qu'il peut réaliser une communication plus rapide que REST. Dans gRPC, les spécifications d'API sont ajoutées au fichier de définition appelé fichier proto à l'aide de l'IDL (langage de définition d'interface). En définissant le modèle source requis pour le client / serveur, même entre différents langages tels que Java, C ++, Python, Go, etc. L'une des caractéristiques est que le IF qui correspond à chacun peut être généré automatiquement à partir du fichier proto. Le fichier proto est défini selon la spécification de langage de proto3.
REST est souvent comparé lors de l'adoption de gRPC en tant qu'API. Le serveur API dont j'étais en charge de la conception et du développement cette fois-ci est une API pour CRUD vers la couche de stockage de données back-end. Le point que les données acquises de KVS etc. peuvent être structurellement définies par ** proto et peuvent communiquer plus rapidement que REST ** Le mérite de l'adopter en tant qu'API de magasin de données était le plus grand.
De plus, il est très coûteux de correspondre aux spécifications de l'API pour réaliser l'architecture de micro-service. C'est le travail, et avec gRPC **, l'interface peut être conservée avec des règles strictes **. On peut dire que le mérite de l'adopter est supérieur à celui de REST, qui permet une conception libre.
Cependant, il n'y a pas d'avantage absolu par rapport à REST, donc du point de vue de profiter des avantages ci-dessus. Je pense qu'il vaut mieux sélectionner au cas par cas.
Puisque Cassandra a été utilisé pour le magasin de données, les données traitées ne sont pas une hiérarchie de données plate, mais C'était une donnée structurelle profondément imbriquée. Par conséquent, il est possible de définir des données structurelles profondément imbriquées dans un fichier proto. Cela peut être fait, mais ce n'était pas facile à lire et à maintenir, donc le fichier proto a été divisé et défini comme indiqué ci-dessous.
Il est possible de définir la structure hiérarchique dans un fichier, mais vous pouvez exprimer un modèle multicouche dans un seul proto. À mesure que l'imbrication devient plus profonde dans les 2e et 3e couches, la lisibilité et la maintenabilité se détériorent.
syntax = "proto3";
option java_package = "jp.co.sample.datastore.common.model";
package common;
message ParentModel {
string parentId = 1;
string parentNm = 2;
ChildModel child = 3; //Spécifiez le ChildModel défini dans le fichier comme type
}
message ChildModel {
string childId = 1;
string childNm = 2;
}
Étant donné que ChildModel et ParentModel peuvent être définis séparément dans des fichiers séparés Cette fois, les fichiers ont été gérés séparément pour chaque structure. Comme je parlerai plus tard, Cassandra est un type défini par l'utilisateur. (Comme toute structure appelée UDT peut être définie par DDL, le modèle proto est également divisé pour chaque UDT.
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"; //Spécifiez le proto qui définit le ChildModel
message ParentModel {
string parentId = 1;
string parentNm = 2;
ChildModel child = 3;
}
Dans gRPC, les spécifications d'API sont définies et gérées dans le fichier proto, donc fondamentalement ce fichier proto est géré par Git, etc. On peut dire que les spécifications de l'API peuvent toujours être tenues à jour en gérant la version.
Cependant, lors de son utilisation en tant qu'API de magasin de données, la demande / réponse de l'application J'aurais dû gérer le proto pour la définition des paramètres, mais j'ai utilisé le proto pour obtenir les données de Cassandra. Afin de le gérer structurellement, il était nécessaire de maintenir la cohérence avec la définition de la table côté Cassandra.
Il est courant que les modifications et les mises à jour DDL se produisent pendant le développement de l'application. Par conséquent, le DDL de Cassandra était géré par le fichier de définition de table de format standard interne. Même si la définition de la table est modifiée en générant automatiquement un fichier proto en utilisant la définition comme entrée La cohérence entre les deux peut désormais être garantie.
Au fur et à mesure que l'échelle de développement augmente, il devient difficile d'absorber la différence entre proto et DDL. Il est préférable d'organiser le mécanisme dès le début.
Du fichier proto au fichier proto à l'interface requise client / serveur Les sources peuvent être générées automatiquement pour chaque langue.
Cependant, il est très ennuyeux de générer automatiquement à partir du fichier proto et de valider la source à chaque fois. À mesure que le nombre de développeurs augmente, cela devient une tâche compliquée, alors générez un module d'interface à partir du dernier fichier proto La gestion des paquets a été effectuée par liaison avec le référentiel avec nexus.
Cette fois, le client / serveur a été développé en Java, donc via gradle Défini pour obtenir le package de nexus.
Dans la conception d'API, il est nécessaire d'effectuer une conception de validation pour les paramètres de demande. Dans gRPC, il est nécessaire de spécifier un type tel que string ou int lors de la définition de données dans un fichier proto. Les types de collection tels que la carte et l'ensemble peuvent également être définis et gérés.
Par conséquent, il n'est pas nécessaire d'effectuer une vérification de type sur les paramètres de demande du client, Il faut tenir compte d'autres validations telles que les vérifications obligatoires et la vérification des chiffres.
Dans un fichier proto, vous pouvez définir un fichier ou un champ à l'aide des Options personnalisées. Vous pouvez prendre des options personnalisées du modèle gRPC et implémenter n'importe quelle gestion.
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; //Option de vérification obligatoire
}
extend google.protobuf.FieldOptions {
int32 strlen = 50001; //Option de vérification des chiffres
}
Définissez les options personnalisées préparées ci-dessus dans n'importe quel champ.
syntax = "proto3";
option java_multiple_files = true;
option java_package = "jp.co.sample.datastore.common.model";
package common;
import "option/custom_option.proto"; //Importer un proto avec des options personnalisées définies
message User {
string user_id = 1[(required)=true,(strlen)=8]; //Plusieurs options peuvent être définies
string user_name = 2[(required)=true];
}
Ceci est un exemple pour obtenir l'option personnalisée définie dans le champ de l'utilisateur du modèle de message défini ci-dessus. Dans "User.getDescriptorForType (). GetFields ()", le FieldDescriptor qui est la méta-information du modèle User est Vous pouvez l'obtenir et vous pouvez obtenir les informations sur les options en manipulant le FieldDescriptor.
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());
}
}
/*Résultat de sortie*/
// user_id
// option:required=true
// option:strlen=8
// user_nm
// option:required=true
Vous pouvez également vérifier l'existence du descripteur de champ de message avec "hasExtension ()". Implémentation d'un traitement de validation optionnel arbitraire pour chaque champ du modèle gRPC Ce sera possible. De plus, le modèle gRPC hérite d'une classe d'interface commune appelée Type de message. En convertissant le type de message et en gérant FieldDescriptor, le traitement à usage général peut être mis en œuvre sans dépendre du modèle.
if(fds.getOptions().hasExtension(CustomOption.required)){
//À partir des méta-informations de champ avec hasExtension"required"Vérifiez si l'option existe
Object value = fds.getOptions().getExtension(CustomOption.required); //Obtenez le contenu de l'option avec getExtension
//Mise en œuvre du traitement de validation
}
Aucune valeur n'est définie dans l'interface du modèle extraite en la définissant comme chaîne ou int dans le fichier proto. Lorsque la valeur du champ est récupérée, ** la valeur par défaut est vide pour l'agitation et 0 ** pour int32 / int64.
Par exemple, il reçoit un modèle gRPC du client et l'envoie au magasin de données en fonction de la valeur définie dans le champ. Si le client souhaite intentionnellement s'initialiser avec des caractères vides ou 0 lors de la mise à jour, avec la valeur par défaut du modèle gRPC Il y a un problème en ce qu'il n'est pas possible de juger côté serveur s'il n'est tout simplement pas défini (la mise à jour n'est pas nécessaire) et de le traiter.
Pour résoudre ce problème, gRPC fournit des classes wrapper, qui peuvent être déterminées en les définissant.
message Test{
string value1 = 1; //Impossible de déterminer si un caractère vide est défini ou la valeur par défaut
int32 value2 = 2; //Impossible de déterminer si 0 est défini ou la valeur par défaut
StringValue value3 = 3; //Vous pouvez déterminer si vous avez défini un caractère vide ou la valeur par défaut
Int32Value value4 = 4; //Peut être déterminé si 0 est défini ou la valeur par défaut
}
Test.Builder testBuilder = Test.newBuilder();
//Caractères explicitement vides,Mettre à 0
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");
}
}
/*Exemple de sortie*/
// value1 has not field
// value2 has not field
// value3 has field
// value4 has field
Puisque Cassandra a été utilisé comme magasin de données, pour les tables Cassandra Afin d'effectuer des opérations CRUD, j'ai dû implémenter ma propre requête appelée CQL.
Le CQL de Cassandra est essentiellement basé sur SQL, il est donc relativement intuitif à implémenter Pour les requêtes compatibles CAS et les éléments de hiérarchie structurelle profonde (UDT gelé) pour le contrôle de mise à jour simultanée Les développeurs sont conscients des requêtes qui ne peuvent pas être exprimées en SQL, telles que l'ajout et la suppression d'instructions de mise à jour, de cartes et d'éléments Set. Puisqu'il a dû être implémenté, il peut être CRUDed au magasin de données en passant la classe de modèle gRPC comme argument. Le processus était caché. (Version KVS OU / mappeur)
Le point de générer dynamiquement une requête à partir de la classe Model a également été décrit dans l'exemple d'option personnalisée. Le fait est que le traitement peut être mis en œuvre à des fins générales en manipulant le descripteur de champ à l'aide du type de message. Lors de la conception d'un traitement commun pour le modèle gRPC, n'oubliez pas d'utiliser le type Message.
public BuiltStatement select(Message message) {
BuiltStatement select;
try {
//Jeu de noms de table
String table = message.getDescriptorForType().getOptions().getExtension(CustomOption.entityOptions)
.getTableName();
//Génération CQL
Select.Selection selection = QueryBuilder.select();
Map<String, Object> partitionKeyMap = new HashMap<>();
for (Descriptors.FieldDescriptor fds : message.getDescriptorForType().getFields()) {
//Créer une clause SELECT
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();
}
}
//Extraction de la clé de partition
if (fds.getOptions().getExtension(CustomOption.attributeOptions).getPartitionKey() > 0
|| fds.getOptions().getExtension(CustomOption.attributeOptions).getClusteringKey() > 0) {
partitionKeyMap.put(fds.getName(), message.getField(fds));
}
}
//Génération de clause FROM
select = selection.json().from(getTableMetadata(table));
//Création de clause WHERE
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
...Traitement de discrimination de type omis
} else {
logger.debug("Le type de partition est incorrect");
throw new RuntimeException("unsupported type");
}
}
return select;
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException(e);
}
}
En plus de Cassandra, nous utilisons également Elastic Search comme moteur de recherche en texte intégral. Les requêtes à envoyer à ElasticSearch génèrent également dynamiquement des requêtes à partir du modèle gRPC à l'aide de la classe Message ci-dessus. Il a été conçu pour que les développeurs d'applications puissent CRUD vers le magasin de données sans avoir à implémenter directement la requête.
Comme je l'ai mentionné un peu ci-dessus, voici quelques conseils à garder à l'esprit lorsque vous travaillez avec des modèles gRPC. Cette fois, il est implémenté dans gRPC-Java, veuillez donc le conserver comme référence lors de son implémentation dans un langage autre que Java. (Je n'ai pas eu assez de temps, donc j'ajouterai des astuces plus tard ...)
Sortie au format Json à partir du modèle gRPC. Si la préservation deProtoFieldNames est ajoutée, les noms de champ définis dans proto seront affichés. Si vous n'ajoutez pas de préservingProtoFieldNames, il sera affiché dans le cas Camel, alors utilisez-le correctement en fonction de l'objectif.
JsonFormat.printer().preservingProtoFieldNames().print(modèle gRPC) //Sortie avec nom de champ selon la définition de proto
JsonFormat.printer().print(modèle gRPC) //Sortie en étui chameau
for (Descriptors.FieldDescriptor fds :modèle gRPC.getDescriptorForType().getFields()) {
if (fds.isMapField()) {
//Déterminer si le champ est de type Carte
} else if (fds).isRepeated()) {
//Déterminer si le champ est de type Set
} else {
//Types autres que la collection
}
}
String val = (String) messageModel.getField(messageModel.getDescriptorForType().findFieldByName("Nom de domaine"));
Un exemple de fusion de valeurs d'un modèle à un autre. En utilisant .ignoringUnknownFields (), même si le champ cible n'existe pas dans la destination de fusion, il sera ignoré.
JsonFormat.parser().ignoringUnknownFields().merge(
JsonFormat.printer().preservingProtoFieldNames().print(fusionner le modèle d'origine),Fusionner le modèle de destination);