Bonnes pratiques en matière de performance des microservices

ID de la région

Le REGION_ID est un code abrégé que Google attribue en fonction de la région que vous sélectionnez lors de la création de votre application. Le code ne correspond pas à un pays ou une province, même si certains ID de région peuvent ressembler aux codes de pays et de province couramment utilisés. Pour les applications créées après février 2020, REGION_ID.r est inclus dans les URL App Engine. Pour les applications existantes créées avant cette date, l'ID de région est facultatif dans l'URL.

En savoir plus sur les ID de région

Le développement de logiciels est une affaire de compromis et les microservices ne font pas exception. Ce que vous gagnez en déploiement de code et en indépendance opérationnelle, vous le payez en impact sur les performances. Cette section présente certaines pratiques recommandées qui vous permettront de minimiser cet impact.

Transformer les opérations CRUD en microservices

Les microservices sont particulièrement bien adaptés aux entités accessibles via le modèle CRUD (créer, récupérer, mettre à jour, supprimer). Lorsque vous travaillez avec de telles entités, vous n'en utilisez généralement qu'une seule à la fois, par exemple un utilisateur, et vous n'exécutez qu'une seule des actions CRUD à la fois. Par conséquent, vous n'avez besoin que d'un seul appel de microservice pour l'opération. Recherchez les entités associées à des opérations CRUD ainsi qu'à un ensemble de procédés pouvant être employés dans de nombreux domaines de votre application. Ces entités sont parfaitement adaptées aux microservices.

Fournir des API par lot

En plus des API de type CRUD, les API par lot permettent aussi de fournir de bonnes performances de microservice à des groupes d'entités. Par exemple, plutôt que de n'exposer qu'une méthode d'API GET pour récupérer un utilisateur unique, fournissez une API qui regroupe un ensemble d'ID utilisateur et renvoie un dictionnaire des utilisateurs correspondants :

Requête :

/user-service/v1/?userId=ABC123&userId=DEF456&userId=GHI789

Réponse :

{
  "ABC123": {
    "userId": "ABC123",
    "firstName": "Jake",
    … },
  "DEF456": {
    "userId": "DEF456",
    "firstName": "Sue",
    … },
  "GHI789": {
    "userId": "GHI789",
    "firstName": "Ted",
    … }
}

Le SDK App Engine est compatible avec de nombreuses API de traitement par lot qui permettent, par exemple, d'extraire des entités de Cloud Datastore en nombre via un seul RPC. La maintenance de ces types d'API de traitement par lot peut donc s'avérer particulièrement intéressante.

Utiliser des requêtes asynchrones

Vous serez souvent amené à interagir avec de nombreux microservices pour composer une réponse. Par exemple, vous devrez peut-être récupérer les préférences de l'utilisateur connecté, ainsi que ses informations d'entreprise. Ces données ne sont pas dépendantes les unes des autres et vous pouvez les récupérer en parallèle. La bibliothèque Urlfetch du SDK App Engine accepte les requêtes asynchrones, ce qui vous permet d'appeler des microservices en parallèle.

L'exemple de code Python suivant utilise directement les RPC pour exécuter des requêtes asynchrones :

from google.appengine.api import urlfetch

preferences_rpc = urlfetch.create_rpc()
urlfetch.make_fetch_call(preferences_rpc,
                         'https://preferences-service-dot-my-app.uc.r.appspot.com/preferences-service/v1/?userId=ABC123')

company_rpc = urlfetch.create_rpc()
urlfetch.make_fetch_call(company_rpc,
                         'https://company-service-dot-my-app.uc.r.appspot.com/company-service/v3/?companyId=ACME')

 ### microservice requests are now occurring in parallel

try:
  preferences_response = preferences_rpc.get_result()  # blocks until response
  if preferences_response.status_code == 200:
    # deserialize JSON, or whatever is appropriate
  else:
    # handle error
except urlfetch.DownloadError:
  # timeout, or other transient error

try:
  company_response = company_rpc.get_result()  # blocks until response
  if company_response.status_code == 200:
    # deserialize JSON, or whatever is appropriate
  else:
    # handle error
except urlfetch.DownloadError:
  # timeout, or other transient error

Le travail en parallèle est souvent contraire aux bonnes pratiques de structure de code. En effet, dans une application concrète, une classe est généralement utilisée pour regrouper les méthodes de préférence, et une autre classe pour encapsuler les méthodes de l'entreprise. Il est difficile d'utiliser des appels asynchrones Urlfetch sans rompre cette encapsulation. Le package NDB du SDK App Engine pour Python contient une solution adaptée : les tasklets. Les tasklets permettent de conserver une bonne encapsulation dans votre code tout en offrant un mécanisme permettant de réaliser des appels de microservice en parallèle. Notez que les tasklets utilisent des futures au lieu des RPC, mais l'idée est semblable.

Utiliser le chemin le plus court

Selon la manière dont vous appelez Urlfetch, les infrastructures et les routes utilisées seront différentes. Pour utiliser la route la plus performante, tenez compte des recommandations suivantes :

Plutôt qu'un domaine personnalisé, utilisez REGION_ID.r.appspot.com.
Un domaine personnalisé entraîne l'utilisation d'une autre route lors du routage via l'infrastructure Google. Étant donné que vos appels de microservices sont internes, il est facile d'obtenir de meilleures performances si vous utilisez https://PROJECT_ID.REGION_ID.r.appspot.com.
Définissez follow_redirects sur False.
Définissez follow_redirects=False de façon explicite lorsque vous appelez Urlfetch. Vous éviterez ainsi de solliciter un service plus lourd conçu pour suivre les redirections. Les points de terminaison de votre API n'ont pas besoin de rediriger les clients car il s'agit de vos propres microservices. Ils ne devraient renvoyer que des réponses HTTP 200, 400 et 500.
Privilégiez les services d'un seul projet plutôt que sur plusieurs projets
Lorsque vous créez une application basée sur des microservices, l'utilisation de plusieurs projets est souvent justifiée. Cependant, si les performances sont votre objectif principal, il est préférable d'utiliser les services d'un seul projet. Les services d'un projet sont hébergés dans le même centre de données et, même si le débit du réseau inter-datacenter de Google est excellent, les appels locaux sont plus rapides.

Éviter les communications excessives pendant l'application des mesures de sécurité

Les mécanismes de sécurité qui impliquent de nombreuses communications dans les deux sens pour authentifier l'API appelante nuisent aux performances. Par exemple, si votre microservice doit valider un ticket provenant de votre application en rappelant celle-ci, vous avez engagé plusieurs allers-retours pour obtenir les données.

La mise en œuvre d'un protocole OAuth2 permet d'amortir ce coût au fil du temps grâce à l'utilisation de jetons d'actualisation et à la mise en cache d'un jeton d'accès entre les appels Urlfetch. Cependant, si le jeton d'accès en cache est stocké dans memcache, vous devrez solliciter une surcharge du cache pour le récupérer. Pour éviter cette surcharge, vous pouvez mettre en cache le jeton d'accès dans la mémoire de l'instance, mais l'activité OAuth2 se poursuivra à la même fréquence, puisque chaque nouvelle instance négocie un jeton d'accès. N'oubliez pas que les instances App Engine sont fréquemment démarrées et arrêtées. Certaines combinaisons de memcache et cache d'instance peuvent atténuer le problème, mais votre solution n'en sera que plus complexe.

Une autre approche intéressante consiste à partager un jeton secret entre microservices transmis, par exemple, sous forme d'en-tête HTTP personnalisé. Dans cette approche, chaque microservice peut être doté d'un jeton unique pour chaque appelant. En règle générale, les clés secrètes partagées sont à éviter dans les stratégies de sécurité. Toutefois, étant donné que tous les microservices se trouvent dans la même application, le risque est faible comparé aux avantages en matière de performances. Dans le cas d'une clé secrète partagée, il suffit que le microservice compare une chaîne de caractères de la clé secrète entrante avec un dictionnaire vraisemblablement en mémoire, et le protocole de sécurité appliqué est très léger.

Si tous les microservices sont sur App Engine, vous pouvez également inspecter l'en-tête X-Appengine-Inbound-Appid entrant. Cet en-tête est ajouté par l'infrastructure Urlfetch lors de l'envoi d'une requête à un autre projet App Engine et ne peut pas être défini par une partie externe. Selon vos exigences de sécurité, votre protocole pourrait être appliqué au niveau de l'en-tête entrant lorsqu'il est inspecté par vos microservices.

Tracer les requêtes de microservice

À mesure que vous développez votre application basée sur des microservices, vous commencez à accumuler du temps système en raison des appels Urlfetch successifs. Lorsque cela se produit, vous pouvez utiliser Cloud Trace pour comprendre quels appels sont passés et où se situe la surcharge. Il est important de noter que Cloud Trace permet aussi d'identifier les endroits où des microservices indépendants sont appelés en série. Vous pouvez ainsi refactoriser votre code pour exécuter ces extractions en parallèle.

L'utilisation de plusieurs services au sein d'un même projet permet d'accéder à une fonctionnalité de Cloud Trace particulièrement utile. À mesure que les appels entre microservices sont passés dans votre projet, Cloud Trace les regroupe en un seul graphique permettant de visualiser la requête de bout en bout sous la forme d'une trace unique.

Capture d'écran de Google Cloud Trace

Notez que dans l'exemple ci-dessus, les appels à pref-service et à user-service sont effectués en parallèle à l'aide d'une requête Urlfetch asynchrone, de sorte que les RPC apparaissent brouillés dans la visualisation. Cependant, cela reste un outil précieux pour diagnostiquer la latence.

Étapes suivantes