Simple et en C ++. Bases d'Userver - Un cadre pour écrire des microservices asynchrones

Yandex.Taxi adhère à l'architecture de microservices. Avec l'augmentation du nombre de microservices, nous avons remarqué que les développeurs passent beaucoup de temps sur des problèmes standard et standard, alors que les solutions ne fonctionnent pas toujours de manière optimale.

Nous avons décidé de créer notre propre framework, avec C ++ 17 et coroutines. Voici à quoi ressemble un code de microservice typique:

Response View::Handle(Request&& request, const Dependencies& dependencies) { auto cluster = dependencies.pg->GetCluster(); auto trx = cluster->Begin(storages::postgres::ClusterHostType::kMaster); const char* statement = "SELECT ok, baz FROM some WHERE id = $1 LIMIT 1"; auto row = psql::Execute(trx, statement, request.id)[0]; if (!row["ok"].As<bool>()) { LOG_DEBUG() << request.id << " is not OK of " << GetSomeInfoFromDb(); return Response400(); } psql::Execute(trx, queries::kUpdateRules, request.foo, request.bar); trx.Commit(); return Response200{row["baz"].As<std::string>()}; } 

Et voici pourquoi il est extrêmement efficace et rapide - nous le dirons sous la coupe.

Userver - Asynchrone


Notre équipe n'est pas seulement composée de développeurs C ++ chevronnés: il y a des stagiaires, des développeurs juniors et même des gens qui ne sont pas particulièrement habitués à écrire en C ++. Par conséquent, la conception du serveur est basée sur la facilité d'utilisation. Cependant, avec nos volumes de données et notre charge, nous ne pouvons pas non plus nous permettre de gaspiller les ressources en fer de manière inefficace.

Les microservices sont caractérisés par l'attente des entrées / sorties: souvent la réponse d'un microservice est formée de plusieurs réponses d'autres microservices et bases de données. Le problème de l'attente efficace des E / S est résolu par des méthodes et des rappels asynchrones: avec des opérations asynchrones, il n'est pas nécessaire de produire des threads d'exécution, et en conséquence, il n'y a pas de gros frais généraux pour la commutation des flux ... seul le code est assez difficile à écrire et à maintenir:

 void View::Handle(Request&& request, const Dependencies& dependencies, Response response) { auto cluster = dependencies.pg->GetCluster(); cluster->Begin(storages::postgres::ClusterHostType::kMaster, [request = std::move(request), response](auto& trx) { const char* statement = "SELECT ok, baz FROM some WHERE id = $1 LIMIT 1"; psql::Execute(trx, statement, request.id, [request = std::move(request), response, trx = std::move(trx)](auto& res) { auto row = res[0]; if (!row["ok"].As<bool>()) { if (LogDebug()) { GetSomeInfoFromDb([id = request.id](auto info) { LOG_DEBUG() << id << " is not OK of " << info; }); } *response = Response400{}; } psql::Execute(trx, queries::kUpdateRules, request.foo, request.bar, [row = std::move(row), trx = std::move(trx), response]() { trx.Commit([row = std::move(row), response]() { *response = Response200{row["baz"].As<std::string>()}; }); }); }); }); } 

Et ici, des coroutines empilables viennent à la rescousse. L'utilisateur du framework pense qu'il écrit le code synchrone habituel:

  auto row = psql::Execute(trx, queries::kGetRules, request.id)[0]; 

Cependant, environ ce qui suit se produit sous le capot:

  1. Les paquets TCP sont générés et envoyés avec une requête à la base de données;
  2. l'exécution de coroutine, dans laquelle la fonction View :: Handle est en cours d'exécution, est suspendue;
  3. nous disons au noyau du système d'exploitation: «« Mettez la coroutine suspendue dans la file d'attente des tâches prêtes à être exécutées dès que suffisamment de paquets TCP proviennent de la base de données »;
  4. sans attendre l'étape précédente, nous prenons et lançons une autre coroutine prête à être exécutée à partir de la file d'attente.

En d'autres termes, la fonction du premier exemple fonctionne de manière asynchrone et est proche de ce code à l'aide de Coroutines C ++ 20:

 Response View::Handle(Request&& request, const Dependencies& dependencies) { auto cluster = dependencies.pg->GetCluster(); auto trx = co_await cluster->Begin(storages::postgres::ClusterHostType::kMaster); const char* statement = "SELECT ok, baz FROM some WHERE id = $1 LIMIT 1"; auto row = co_await psql::Execute(trx, statement, request.id)[0]; if (!row["ok"].As<bool>()) { LOG_DEBUG() << request.id << " is not OK of " << co_await GetSomeInfoFromDb(); co_return Response400{"NOT_OK", "Please provide different ID"}; } co_await psql::Execute(trx, queries::kUpdateRules, request.foo, request.bar); co_await trx.Commit(); co_return Response200{row["baz"].As<std::string>()}; } 

C'est juste que l'utilisateur n'a pas besoin de penser à co_await et co_return, tout fonctionne "tout seul".

Dans notre framework, basculer entre coroutines est plus rapide que d'appeler std :: this_thread :: yield (). L'ensemble du microservice coûte un très petit nombre de threads.

Pour le moment, userver contient des pilotes asynchrones:
* pour les sockets OS;
* http et https (client et serveur);
* PostgreSQL;
* MongoDB;
* Redis;
* travailler avec des fichiers;
* minuteries;
* primitives pour synchroniser et lancer de nouvelles coroutines.

L'approche asynchrone ci-dessus pour résoudre les tâches liées aux E / S doit être familière aux développeurs Go. Mais, contrairement à Go, nous n'obtenons pas de surcharge pour la mémoire et le CPU du garbage collector. Les développeurs peuvent utiliser un langage plus riche, avec divers conteneurs et bibliothèques hautes performances, sans souffrir d'un manque de cohérence, de RAII ou de modèles.

Userver - composants


Bien sûr, un cadre à part entière n'est pas seulement des coroutines. Les tâches des développeurs de Taxi sont extrêmement diverses, et chacune d'entre elles nécessite son propre ensemble d'outils à résoudre. Par conséquent, userver a tout ce dont vous avez besoin:
* pour la journalisation;
* mise en cache;
* travailler avec différents formats de données;
* travailler avec des configurations et mettre à jour des configurations sans redémarrer le service;
* serrures distribuées;
* tests;
* autorisation et authentification;
* créer et envoyer des métriques;
* écriture de gestionnaires REST;
+ support de génération de code et de dépendances (réalisé dans une partie séparée du framework).

Userver - génération de code


Revenons à la première ligne de notre exemple et voyons ce qui se cache derrière Response and Request:

 Response Handle(Request&& request, const Dependencies& dependencies); 

Avec userver, vous pouvez écrire n'importe quel microservice, mais il est nécessaire pour nos microservices que leurs API doivent être documentées (décrites par des schémas de swagger).

Par exemple, pour la poignée de l'exemple, le diagramme de swagger peut ressembler à ceci:

 paths: /some/sample/{bar}: post: description: |     Habr. summary: | ,  -   . parameters: - in: query name: id type: string required: true - in: header name: foo type: string enum: - foo1 - foo2 required: true - in: path name: bar type: string required: true responses: '200': description: OK schema: type: object additionalProperties: false required: - baz properties: baz: type: string '400': $ref: '#/responses/ResponseCommonError' 

Eh bien, puisque le développeur a déjà un schéma avec une description des demandes et des réponses, alors pourquoi ne pas générer ces demandes et réponses en fonction de cela? Dans le même temps, des liens vers des fichiers protobuf / flatbuffer / ... peuvent également être indiqués dans le schéma - la génération de code à partir de la demande elle-même obtiendra tout, validera les données d'entrée selon le schéma et les décomposera dans les champs de la structure de réponse. L'utilisateur a seulement besoin d'écrire des fonctionnalités dans la méthode Handle, sans être distrait par le passe-partout avec l'analyse des requêtes et la sérialisation de la réponse.

Dans le même temps, la génération de code fonctionne pour les clients du service. Vous pouvez indiquer que votre service a besoin d'un client travaillant selon un tel schéma et obtenir une classe prête à l'emploi pour créer des demandes asynchrones:

 Request req; req.id = id; req.foo = foo; req.bar = bar; dependencies.sample_client.SomeSampleBarPost(req); 

Cette approche a un autre avantage: une documentation toujours à jour. Si un développeur essaie soudainement d'utiliser des paramètres qui ne figurent pas dans la documentation, il obtiendra une erreur de compilation.

Userver - journalisation


Nous aimons écrire des journaux. Si vous enregistrez uniquement les informations les plus importantes, plusieurs téraoctets de journaux par heure s'exécuteront. Par conséquent, il n'est pas surprenant que notre journalisation ait ses propres astuces:
* il est asynchrone (bien sûr :-));
* nous pouvons nous connecter en contournant les std :: locale et std :: ostream lents;
* nous pouvons changer le niveau d'enregistrement à la volée (sans redémarrer le service);
* nous n'exécutons pas de code utilisateur s'il n'est nécessaire que pour la journalisation.

Par exemple, pendant le fonctionnement normal du microservice, le niveau de journalisation sera défini sur INFO, et l'expression entière

  LOG_DEBUG() << request.id << " is not OK of " << GetSomeInfoFromDb(); 

ne sera pas calculé. L'inclusion de l'appel à la fonction gourmande en ressources GetSomeInfoFromDb () ne se produira pas.

Si soudain le service commence à "tromper", le développeur peut toujours dire au service de travail: "Connectez-vous en mode DEBUG". Et dans ce cas, les entrées «n'est pas OK» commenceront à apparaître dans les journaux, la fonction GetSomeInfoFromDb () sera exécutée.

Au lieu de totaux


Dans un article, il est impossible de parler immédiatement de toutes les fonctionnalités et astuces. Par conséquent, nous avons commencé par une courte introduction. Écrivez dans les commentaires sur les éléments de userver que vous seriez intéressé d'apprendre et de lire.

Nous envisageons maintenant de publier le framework en open source. Si nous décidons que oui, la préparation du cadre d'ouverture de la source demandera beaucoup d'efforts.

Source: https://habr.com/ru/post/fr474438/


All Articles