[JAVA] J'ai essayé de résumer les points clés de la conception et du développement de gRPC

introduction

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.

Aperçu

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.

Qu'est-ce que 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.

grpc_concept_diagram_00.png

Points pour adopter le gRPC

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.

Conseils de conception et de développement gRPC

1. Comment gérer les fichiers proto

1-1. Définissez des données structurelles profondément imbriquées en divisant le fichier proto

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.

Exemple de définition dans un fichier

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;
}

Exemple de définition avec plusieurs fichiers

É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;
}

1-2. Gérer les fichiers proto avec DDL

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.

1-3. Méthode de gestion du module IF

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.

2. Mise en œuvre d'un traitement commun à l'aide d'options personnalisées

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.

2-1. Exemple de définition d'option personnalisée


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];
}

2-2. Comment obtenir des options personnalisées (Java)

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

2-3. Exemple de mise en œuvre de la validation

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
}

3. Autoriser le modèle gRPC à gérer explicitement les caractères vides, 0

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.

3-1. Exemple de définition de fichier proto utilisant la classe wrapper


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
}

3-2. Exemple d'implémentation du contrôle d'existence de valeur


    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

4. Générez dynamiquement des requêtes à partir du modèle gRPC

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.

4.1 Exemple d'implémentation de l'instruction Cql SELECT


	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.

5. Conseils de traitement pratiques utilisant le modèle gRPC

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 ...)

5-1. Sortie au format Json à partir du modèle gRPC

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

5-2. Détermination du type du modèle gRPC


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
    }
}

5-3. Obtenez la valeur en spécifiant le nom du champ dans la classe Message


String val = (String) messageModel.getField(messageModel.getDescriptorForType().findFieldByName("Nom de domaine"));

5-4. Fusion entre les modèles gRPC

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);

Recommended Posts

J'ai essayé de résumer les points clés de la conception et du développement de gRPC
J'ai essayé de résumer les bases de kotlin et java
J'ai essayé de résumer les méthodes de Java String et StringBuilder
J'ai brièvement résumé la grammaire de base de Ruby
J'ai essayé de résumer les applications et les outils de développement personnellement utiles (outils de développement)
J'ai essayé de résumer les applications et les outils de développement personnellement utiles (Apps)
J'ai essayé de résumer les méthodes utilisées
J'ai essayé de résumer l'API Stream
J'ai essayé de vérifier le fonctionnement du serveur gRPC avec grpcurl
[Pour les débutants Swift] J'ai essayé de résumer le cycle de mise en page désordonné de ViewController et View
[Ruby] Je souhaite extraire uniquement la valeur du hachage et uniquement la clé
Avant d'oublier, les fonctions et les points de l'application Furima
J'ai essayé de mesurer et de comparer la vitesse de Graal VM avec JMH
05. J'ai essayé de supprimer la source de Spring Boot
J'ai essayé de vérifier ceci et celui de Spring @ Transactional
J'ai essayé JAX-RS et pris note de la procédure
J'ai essayé de créer un environnement de WSL2 + Docker + VSCode
J'ai essayé de résumer le support d'iOS 14
J'ai essayé d'expliquer la méthode
[Rails 6.0, Docker] J'ai essayé de résumer la construction de l'environnement Docker et les commandes nécessaires pour créer un portfolio
J'ai essayé de résumer l'apprentissage Java (1)
J'ai essayé de résumer Java 8 maintenant
J'ai essayé de résoudre le problème de la "sélection multi-étapes" avec Ruby
J'ai essayé de résumer ce qui était demandé lors de l'édition site-java-
J'ai essayé de créer un environnement de serveur UML Plant avec Docker
[Rubiy] J'ai essayé de résumer le traitement de la boucle ce soir [fois, pause ...]
Conférence spéciale sur la simulation multi-échelles: j'ai essayé de résumer le 5e
Conférence spéciale sur la simulation multi-échelles: j'ai essayé de résumer le 8
Conférence spéciale sur la simulation multi-échelles: j'ai essayé de résumer le 7
J'ai essayé de résoudre le problème de Google Tech Dev Guide
J'ai essayé d'exprimer les résultats avant et après de la classe Date avec une ligne droite numérique
J'ai essayé d'implémenter le modèle Iterator
Qu'est-ce que Docker? J'ai essayé de résumer
J'ai résumé les points à noter lors de l'utilisation combinée des ressources et des ressources
[Introduction à Java] J'ai essayé de résumer les connaissances que j'estime essentielles
Je veux passer l'argument d'Annotation et l'argument de la méthode d'appel à aspect
J'ai essayé d'utiliser pleinement le cœur du processeur avec Ruby
J'ai essayé de visualiser l'accès de Lambda → Athena avec AWS X-Ray
[Ruby] J'ai essayé de résumer les méthodes fréquentes dans paiza
[Ruby] J'ai essayé de résumer les méthodes fréquentes avec paiza ②
J'ai essayé de traduire la grammaire de R et Java [Mis à jour de temps en temps]
J'ai essayé de migrer le portfolio créé sur Vagrant vers l'environnement de développement de Docker
Je veux que vous utilisiez Enum # name () pour la clé de SharedPreference
J'ai essayé de créer un exemple de programme en utilisant le problème du spécialiste des bases de données dans la conception pilotée par domaine
J'ai essayé de résumer sur JVM / garbage collection
[Rails] J'ai essayé de faire passer la version de Rails de 5.0 à 5.2
J'ai essayé d'organiser la session en Rails
Ce que j'ai essayé quand je voulais obtenir tous les champs d'un haricot
[Développement individuel de service d’agence d’achat - N ° 009] Un peu plus de travail sur le changement et la conception d’entreprises avec Figma