RPC

RPC

Les RPC existent depuis longtemps, ils ont leurs heures de gloire (CORBA, SOAP), et de déchéance (CORBA, SOAP). Ils reviennent sur le devant de la scène avec l’invasion de Javascript au dépens des templates cotés serveurs, et surtout des micro services.

Techniquement, un RPC, c’est : une sérialisation, un protocole, une couche transport. Certains RPC proposant différentes sérialisations (une binaire, une texte), et même différents transports.

Sérialisation

La réponse magique est JSON, ce qui est un progrès par rapport à la réponse magique XML, d’il y a 20 ans. Mais le JSON est ambigu, chaque langage et même chaque bibliothèque va gérer de manière différente chaque point mal spécifié. Il ne gère ni différencie pas les entiers des flottants, ni les nombres en 64 bits, le binaire doit passer par du base64 et donc prendre plein de place.

BSON corrige le JSON en ajoutant de nouveaux types et un format binaire, mais il reste très lié à Mongodb.

MessagePack fait peu ou prou la même chose, mais de manière neutre, sans être lié à un produit.

On reste dans la famille des sérialisations auto descriptive, qui permet techniquement de faire un peu n’importe quoi de chaque coté d’un RPC.

Cette approche prend beaucoup de place, avec Mongodb, il n’est pas rare que les clefs prennent plus de place que les contenus.

Avro propose une approche intermédiaire. On envoie la grammaire, en JSON, puis le contenu en binaire, bien compacte.

La mode est maintenant à la sérialisation avec grammaire. Cette grammaire étant neutre, elle peut être utilisée par différents langages pour effectuer les sérialisations/deserialisation. Ces grammaires sont incrémentales, il est possible d’ajouter des informations, sans casser la compatibilité avec l’existant.

Petite collision de date, Google avait déjà son protobuff (qu’il utilisait bien au-delà des RPC) quand Facebook a publié spécification et implémentation de référence pour son Thrift, fort similaire. Protobuff a été libéré après Thrift.

Pour aller un cran plus loin dans la recherche de performance, il existe des sérialisations qui ne sérialisent pas, et permettent le zéro-copie, ce qui est fort pratique pour les RPC locaux, directement en mémoire, sans réseau. Apache Arraow et Cap’n Proto font ce genre de chose.

Protocole

La première étape est la notion de question du client au serveur, et la réponse correspondante, dans l’autre sens.

REST

REST n’est tout simplement pas un protocole RPC. C’est un standard largement diffusé, le meilleur ami de curl, mais ce n’est pas un RPC.

REST permet d’exposer rapidement des données hiérarchisées, pas plus, et GraphQL (quand il sera sec) est bien parti pour lui ravir cette niche.

Des formalisations comme Swagger (maintenant OpenAPI) sont clairement les bienvenus, et même indispensable, mais ne suffisent pas à définir de bout en bout les échanges. Du code spécifique côté client et serveur reste indispensable, et fragilise l’ensemble.

ElasticSearch reste le parfait exemple des limites de REST : le protocole/sérialisation ne brime pas les performances, par contre, les fonctions spécifiques comme les batchs, la pagination, la gestion du cluster, rendent indispensable l’utilisation des drivers officiels.

Docker a le même souci, avec son truandage pour gérer les flots TTY, et ils sont en train de tout re-ranger en grpc avec Containerd, pour faciliter les échanges de serveur à serveur.

Multiplexage

Les connexions ne sont pas illimitées, que ce soit des clients web vers un serveur, ou même en interne, avec le nombre de connexions simultanées que peuvent ouvrir les technologies asynchrones. Le multiplexage s’impose assez vite. Les requêtes ont des identifiants, et les réponses arrivent dans l’ordre de leur résolution.

XMPP a formalisé cette approche avec ses stanzas, tout comme JSON-RPC.

Flot

Les RPC sont confrontés aux mêmes problèmes que les Big Data, où il faut choisir entre le gros débit des traitements par lots (Hadoop), et les faibles latences des flots (Spark, Storm).

La gestion de flot facilite le multiplexage, limite la taille des buffers, et permet de commencer tôt le traitement du message.

HTTP/1 propose une gestion de flot descendant, avec EventSource, mais rien en flot montant, il faut alors dégainer Websocket. Hum, est-ce bien raisonnable d’utiliser WebSocket pour de la communication de serveur à serveur?

HTTP/2 gère le multiplexage, et permet la gestion de flot montant et descendant.

Finagle, de Twitter, propose un RPC complet basé sur Thrift, mais surtout explique bien la notion de multiplexage, de streaming et de méta information. Finagle a publié beaucoup de lecture pédagogique indispensable, mais il est fortement lié à Scala, et même à Twitter, en fait.

Méta information

Les RPCs ont besoin d’informations complètement différentes de ce qui sera utilisé par les fonctions exposées. La notion de format, de trace, d’authentification, de chiffrage et de compression par exemple. Toutes les belles choses que l’on est habitué à utiliser avec HTTP, en fait.

Parralélisation

On est habitué aux contraintes d’HTTP qui a l’habitude d’imposer des temps de réponse courts, et de se faire couper en cas de “timeout”. Les RPC n’ont pas les mêmes contraintes sur les temps de réponse. Les langages synchrones sont alors brimés par la difficulté de paralléliser des taches de durée très variables. L’implémentation de GRPC en python, par exemple, travaille avec un pool de 10 connexions, ce qui n’est pas farfelu si on garde la règle magique de 2 workers par coeur de processeurs. En augmentant le pool, on prend le risque d’avoir des traitements avec peu d’attentes IO et de faire exploser le load.

De toute façon, la notion de file d’attente devra être géré, soit par le langage qui va accepter plein de connections, soit via un service spécifique, comme Redis, RabbitMQ, nsq, kafka…. Les services de file d’attente permettent de faire de jolies choses, souvent pénibles à reproduire dans son code, comme la répartition de charge ou la reprise de traitement en cas de vautrage. Google a une sainte horreur des serveurs de file d’attentes, déjà qu’il n’aiment pas les proxy, car il se focalise sur la course à la latence. Twitter semble les suivre dans cette approche.

Débogabilité

Déboguer un RPC bancal peut rapidement devenir un enfer.

Traces

Il est indispensable d’utiliser une gestion d’erreur centralisée (à la Sentry), des métriques (statsd ou des compteurs Prometheus), et rapidement des logs unifiés, des traces en fait, comme le proposent OpenTracing (Zipkin, Jaeger, Appdash…).

Ligne de commande

Il est difficile de se passer du confort de curl + JSON, tous les RPC de la galaxie proposent un équivalent, avec plus ou moins de complétion et de retour d’erreurs.

Sniffer

Encore plus culte que curl, il existe tcpdump, pour filtrer et voir passer un flot sur le réseau. PacketBeat propose de jolies bases pour construire un sniffeur.

La généralisation de TLS, même sur les réseaux privés rends ça rapidement pénible.

L’approche sniffing est du débogage en boite noir, il est tellement plus simple d’être dans la place, et de profiter de l’approche boite blanche, comme Google le préconise : ticket grpc et tecpdump.

Finagle confirme cette approche avec Twitter-Server, mais comme Finagle n’impose pas TLS, il est possible de tricher : thrift-tools.

Rapidement, les RPC vont être distribués (sur plusieurs serveurs), il est assez illusoire de penser pouvoir attraper l’ensemble des flots pour ensuite isoler celui qui nous intéresse. Bricoler son Mysql/Postgres/Mongodb ou autres services tiers pour avoir du log spécifique n’est pas la meilleur des idées, dans ce cas, le pcap est légitime, mais pour du code qui nous appartiens, avec la possibilité d’utiliser des middlewares ou pires des proxys, ce serait dommage de ne pas en profiter.

Transport

Les rpc ne sont pas forcément fascinés par la couche transport, mais la plupart s’épanouissent dans HTTP, la garantie de pouvoir circuler sur internet sans drame, et sans faire crier les firewalls corporates. Il existe des transports plus spécialisés, comme zeromq, nats ou même plus exotique comme STOMP.

Hum, peut-on considérer un broker comme une couche transport? Dans ce cas, Kafka et AMQP en font partis.

HTTP/2 permet de profiter de l’héritage HTTP/1 (Internet, proxy…) mais le protocole est bien récent, et les bibliothèques pour le gérer ne sont pas bien répandues. C’est mature pour des serveurs webs (nginx, traefik…), pour des clients (Chrome, Firefox…), mais pour du code, golang écrase un peu tout.

Ceinture et bretelle

Rien n’interdit d’exposer une fonction avec différentes approches.

Les usages sont trop divers pour qu’une seule réponse universelle puisse être envisagée. Les interfaces utilisateurs sont maintenant en HTML5 avec beaucoup de Javascript, et proposent des interfaces utilisateurs réactives. Il est donc indispensable d’utiliser HTTP pour les UI web. L’outillage HTTP2 en javascript est pour l’instant introuvable, il faut donc basculer sur les plus classiques HTTP/1.1 et ses extensions, comme SSE et surtout Websocket.

Pour les appels internes, HTTP/1 est un peu court des pattes et va poser des problèmes avec les parallélisations massives que permettent les technologies asynchrones.

L’approche la plus saine me parait de surtout se concentrer sur l’utilisation de grammaire neutre, qui permettent de générer (ou d’introspecter pour les langages qui n’aiment pas le code écrit par des robots), les clients et serveurs, et même de générer plusieurs protocoles pour une seule fonction exposée.

Twirp propose cette approche après s’être cassé les dents sur du grpc, qui reste pourtant la référence de ce genre d’approche.

Grpc fait des choix plutôt autoritaires pour présenter ce que Google estime être l’état de l’art. Personnellement, aucun de ces choix ne me frustre, c’est plutôt le manque de maturité des bibliothèques qui m’inquiètent. Les fonctions avancées de la bibliothèque golang n’a pas de numéro de release, avec une documentation légère. La bibliothèque Python embarque un gros machin en C pour gérer toute la partie HTTP/2, avec un pool de workers. Je suppose que les autres langages sont dans le même état. Par contre, je n’ai aucune inquiétude sur la pérennité de cette technologie et sur le fait que les bibliothèques vont évolués, et surtout que tous les langages vont avoir l’outillage requis pour gérer comme il faut HTTP/2/

Il existe une passerelle Grpc/REST, Grpc-gateway qui permet d’exposer une fonction, en utilisant la grammaire grpc, sous deux formes, du REST avec la documentation Swagger, et du Grpc.

La couche HTTP/2 n’est pas forcément la réponse universelle, pour certains projets discrets, utilisant un grand nombre de connexions, le surcout en RAM peut être pénalisant. Le projet a fait le choix d’utiliser un protocole différent (du TCP sans stream), et de générer le code avec les grammaires GRPC.

Bref

L’informatique est maintenant distribué, que ce soit sur des coeurs de processeurs, ou des serveurs. Les RPC sont maintenant indispensable (ne serait-ce que par la traction des UI complexes en Javascript), mais le domaine n’est pas encore stabilisé.

En l’état, la seul recommendation sans risque est d’utiliser des grammaires pour déclarer ses APIs, et d’instrumenter son code (log, erreurs, métriques).

blogroll

social