Dans quelles conditions (le cas échéant) est-ce une bonne pratique d'interroger deux serveurs et de ne consommer que la réponse la plus rapide?

12

J'ai demandé ce qui est maintenant une question supprimée par la communauté sur SO sur pourquoi quelqu'un utiliserait-il le javascript Promise.race, et un utilisateur de haut niveau a commenté ceci:

Si vous avez deux services qui calculent une valeur, vous pouvez les interroger en parallèle et utiliser la valeur qui est renvoyée en premier, plutôt que d'en interroger un, d'attendre un échec, puis d'interroger le second.

J'ai cherché sur la redondance et ce cas d'utilisation en général, mais je n'ai rien trouvé et, à partir de mon POV, ce n'est jamais une bonne idée d'ajouter simplement une charge de travail à un serveur / service si vous n'utilisez pas la réponse.

Adelin
la source
Exemple de jouet: plutôt que de toujours utiliser quicksort, vous copiez les données, les envoyez à un quicksort, à un mergesort et à un heapsort, ... etc. Vous n'avez pas à inspecter l'entrée pour voir s'il s'agit d'un cas pathalogique pour tout de ceux -ci , car il ne sera pas un cas pathalogical pour tous d'entre eux
Caleth
Le document de Dean et Barroso, The Tail at Scale, appelle une variante de cette approche des «demandes couvertes». Il examine également les avantages et les inconvénients de plusieurs approches connexes pour contrôler la variabilité à longue traîne des taux d'erreur et de la latence.
Daniel Pryden
La deuxième "demande de serveur" pourrait être un faux. Il pourrait simplement vouloir pendant 5 secondes, puis retourner une réponse d'espace réservé. Cela vous donne un délai d'attente sur la vraie demande.
user253751
Commandez un Lyft puis commandez un Uber. Prenez celui qui vient en premier.
user2023861
@ user2023861 dans cette analogie, alors qu'un conducteur conduisait inutilement vers votre position, il aurait pu prendre une autre demande à la place
Adelin

Réponses:

11

Je dirais que c'est davantage une question économique. Cependant, c'est un jugement que les ingénieurs devraient pouvoir faire. Par conséquent, je réponds.

Je divise ma réponse en quatre parties:

  • Gestion des risques
  • Stratégies
  • Frais
  • Intuition

Gestion des risques

Ainsi, parfois, votre client ne parvient pas à obtenir une réponse du serveur. Je suppose que ce n'est pas à cause d'une erreur de programmation (sinon la solution est de le réparer, alors allez-y). Au contraire, cela doit être dû à une situation fortuite échappant à votre contrôle ...

Mais pas au-delà de vos connaissances. Tu dois savoir:

  • Combien de fois cela se produit-il?
  • Quel impact cela a-t-il.

Par exemple, si l'échec et la nouvelle tentative ne se produisent que dans environ 2% des cas, cela ne vaut probablement pas la peine d'y remédier. Si cela se produit environ 80% du temps, eh bien ... ça dépend ...

Combien de temps le client doit-il attendre? Et comment cela se traduit-il en coûts ... vous voyez, vous avez un petit retard dans une application régulière, ce n'est probablement pas un gros problème. S'il est important et que vous disposez d'une application en temps réel ou d'un jeu vidéo en ligne, cela détournera les utilisateurs et vous feriez probablement mieux d'investir dans des serveurs plus nombreux ou meilleurs. Sinon, vous pouvez probablement mettre un message "chargement" ou "attente de serveur". À moins que le délai ne soit vraiment important (de l'ordre de dizaines de secondes), il peut être trop important même pour l'application régulière.


Stratégies

Comme je l'ai dit plus haut, il y a plus d'une façon de résoudre ce problème. Je suppose que vous disposez déjà de l'implémentation de la boucle try-fail-retry. Voyons donc ...

  • Mettez un message de chargement. C'est bon marché, aide à la rétention des utilisateurs.
  • Requête en parallèle. Peut être plus rapide, peut encore échouer. Nécessite un serveur redondant (peut être coûteux), gaspillera le temps du serveur et le trafic réseau.
  • Recherchez en parallèle pour établir le serveur le plus rapide et utilisez-le à partir de là. Peut être plus rapide, peut encore échouer. Nécessite un serveur redondant (peut être coûteux), ne gaspillera pas autant de temps serveur et de trafic réseau.

Maintenant, remarquez que je dis que ceux-ci peuvent toujours échouer. Si nous supposons qu'une requête à un serveur a 80% de chances d'échec, alors une requête parallèle à deux serveurs a 64% de chances d'échec. Par conséquent, vous devrez peut-être encore réessayer.

Un avantage supplémentaire de choisir le serveur le plus rapide et de continuer à l'utiliser, est que le serveur le plus rapide est également le moins susceptible d'échouer en raison de problèmes de réseau.

Ce qui me rappelle, si vous pouvez comprendre pourquoi la demande échoue, faites-le. Il peut vous aider à mieux gérer la situation, même si vous ne pouvez pas empêcher les échecs. Par exemple, avez-vous besoin de plus de vitesse de transfert côté serveur?

Un peu plus:

  • Déployez plusieurs serveurs à travers le monde et choisissez un serveur par géolocalisation.
  • Effectuez l'équilibrage de charge côté serveur (une machine dédiée prendra toutes les requêtes et les reliera à vos serveurs, vous pouvez y avoir votre parallélisme ou une meilleure stratégie d'équilibrage).

Et qui a dit que vous ne deviez en faire qu'une? Vous pouvez mettre un message de chargement, interroger plusieurs serveurs répartis sur le wrold pour choisir le plus rapide et l'utiliser uniquement à partir de là, en cas d'échec, réessayer sur une boucle, et faire en sorte que chacun de ces serveurs soit un cluster de machines avec équilibrage de charge . Pourquoi pas? Eh bien, les coûts ...


Frais

Il y a quatre coûts:

  • Le coût du développement (généralement très bon marché)
  • Le coût du déploiement (généralement élevé)
  • Le temps d'exécution des coûts (dépend du type d'application et du modèle commercial)
  • Le coût de l'échec (probablement faible, mais pas nécessairement)

Vous devez les équilibrer.

Par exemple, disons que vous gagnez environ un dollar par utilisateur satisfait. Que vous avez 3000 utilisateurs par jour. Que les demandes échouent environ 50% du temps. Et que 2% des utilisateurs partent sans payer lorsque la demande échoue. Cela signifie que vous perdez (3000 * 50% * 2%) 30 dollars par jour. Maintenant, disons que le développement de la nouvelle fonctionnalité vous coûtera 100 dollars et le déploiement des serveurs vous coûtera 800 dollars - et en ignorant les coûts d'exécution - cela signifie que vous auriez un retour sur investissement en ((100 + 800) / 30 ) 30 jours. Maintenant, vous pouvez vérifier votre budget et décider.

Ne considérez pas ces valeurs comme représentatives de la réalité, je les ai choisies pour des raisons mathématiques.

Addendums:

  • N'oubliez pas que j'ignore également les détails. Par exemple, vous pouvez avoir peu de coûts de déploiement, mais vous payez pour le temps CPU et vous devez en tenir compte.
  • Certains clients peuvent apprécier si vous ne gaspillez pas leur paquet de données dans des demandes redondantes.
  • Améliorer votre produit peut aider à apporter une publicité naturelle.
  • N'oubliez pas les coûts d'opportunité. Devriez-vous développer autre chose?

Le fait est que si vous considérez le problème en termes d'équilibrage des coûts, vous pouvez faire une estimation du coût des stratégies que vous envisagez et utiliser cette analyse pour décider.


Intuition

L'intuition est favorisée par l'expérience. Je ne suggère pas de faire ce genre d'analyse à chaque fois. Certaines personnes le font, et ça va. Je vous suggère d'avoir une certaine compréhension de cela et de développer une intuition pour cela.

De plus, en ingénierie, en plus des connaissances que nous obtenons de la science réelle, nous apprenons également dans la pratique et compilons des directives sur ce qui fonctionne et ce qui ne fonctionne pas. Par conséquent, il est souvent sage de voir l'état de l'art ... bien que, parfois, vous ayez besoin de voir en dehors de votre région.

Dans ce cas, je regarderais les jeux vidéo en ligne. Ils ont des écrans de chargement, ils ont plusieurs serveurs, ils choisiront un serveur en fonction de la latence, et ils peuvent même permettre à l'utilisateur de changer de serveur. Nous savons que cela fonctionne.

Je suggérerais de le faire au lieu de gaspiller le trafic réseau et le temps du serveur à chaque demande, sachez également que même avec un serveur redondant, une panne peut se produire.

Theraot
la source
2
Je ne pense pas avoir besoin de le dire mais c'est une excellente réponse :) Je savais que je l'accepterais dans les 10 premières lignes, mais je vous ai donné l'occasion d'échouer encore et de le lire jusqu'à la fin. Vous n'avez pas
Adelin
9

Ceci est acceptable si le temps du client est plus précieux que le temps sur le serveur.

Si le client doit être rapide et précis. Vous pouvez justifier l'interrogation de plusieurs serveurs. Et il est agréable d'annuler la demande si une réponse valide est reçue.

Et bien sûr, il est toujours sage de consulter les propriétaires / gestionnaires des serveurs.

Toon Krijthe
la source
Pourquoi avez - vous besoin d'annuler la demande? C'est certainement subjectif.
JᴀʏMᴇᴇ
@ JᴀʏMᴇᴇ, c'est construire dans la paranoïa. Une fois, j'ai travaillé avec un système qui n'a pas effacé sa file d'attente et il s'est bloqué lorsque la file d'attente était pleine (Oui, c'était un logiciel professionnel).
Toon Krijthe
4

Cette technique peut réduire la latence. Le temps de réponse du serveur n'est pas déterministe. À grande échelle, il est probable qu'au moins un serveur affiche des temps de réponse médiocres. Tout ce qui utilise ce serveur aura donc également des temps de réponse médiocres. En soumettant à plusieurs serveurs, on atténue le risque de parler à un serveur peu performant.

Les coûts incluent des trafics réseau supplémentaires, un traitement de serveur gaspillé et la complexité des applications (cela peut être caché dans une bibliothèque). Ces coûts peuvent être réduits en annulant les demandes inutilisées ou en attendant brièvement avant d'envoyer une deuxième demande.

Voici un papier , et un autre . Je me souviens avoir lu un article de Google sur leur mise en œuvre également.

Michael Green
la source
2

Je suis principalement d'accord avec les autres réponses, mais je pense que cela devrait être extrêmement rare dans la pratique. Je voulais partager un exemple beaucoup plus courant et raisonnable pour le moment où vous l'utiliseriez Promise.race(), quelque chose que j'ai utilisé il y a quelques semaines (enfin, l'équivalent de python).

Supposons que vous ayez une longue liste de tâches, certaines pouvant être exécutées en parallèle et d'autres devant être exécutées avant d'autres. Vous pouvez démarrer toutes les tâches sans dépendances, puis attendre sur cette liste avec Promise.race(). Dès que la première tâche est terminée, vous pouvez démarrer toutes les tâches qui dépendaient de cette première tâche, et à Promise.race()nouveau sur la nouvelle liste combinée avec les tâches inachevées de la liste d'origine. Continuez à répéter jusqu'à ce que toutes les tâches soient terminées.

Remarque L'API de Javascript n'est pas idéalement conçue pour cela. C'est à peu près le strict minimum qui fonctionne, et vous devez ajouter un peu de code de colle. Cependant, mon point est que les fonctions comme race()sont rarement utilisées pour la redondance. Ils sont principalement là pour quand vous voulez réellement les résultats de toutes les promesses, mais ne voulez pas attendre qu'ils se terminent avant de prendre des mesures ultérieures.

Karl Bielefeldt
la source
Le problème est que, au moins avec Promise.race de Javascript, vous démarrez réellement la tâche chaque fois que vous exécutez la méthode de course. Ce ne sera pas sur la tâche inachevée, ce serait un nouvel ensemble de tâches, sans égard à ce qui a été exécuté auparavant (sauf si vous implémentez cette logique au niveau de la tâche). Sinon, la liste d'origine est oubliée et il ne reste que la valeur de retour de la première tâche
Adelin
1
Les promesses en Javascript sont lancées avec impatience, quand new Promiseest appelé, et ne sont pas redémarrées quand Promise.race()est appelé. Certaines implémentations prometteuses sont paresseuses, mais avides sont beaucoup plus courantes. Vous pouvez tester en créant une promesse dans la console qui se connecte à la console. Vous le verrez immédiatement. Ensuite, passez cette promesse à Promise.race(). Vous verrez qu'il ne se connecte plus.
Karl Bielefeldt
Ah c'est vrai. Mais afaik la valeur de retour du reste des promesses sauf la première est oubliée, avec promise.race
Adelin
C'est pourquoi j'ai dit que l'API n'était pas conçue de manière idéale. Vous devez stocker l'ensemble de tâches d'origine dans une variable quelque part.
Karl Bielefeldt
1

En plus des considérations techniques, vous souhaiterez peut-être utiliser cette approche lorsqu'elle fait partie de votre modèle commercial réel.

Les variantes de cette approche sont relativement courantes dans les enchères en temps réel sur les annonces. Dans ce modèle, un éditeur (fournisseur d'espace publicitaire) demandera aux annonceurs (fournisseurs d'annonces) de miser sur une impression d'un utilisateur particulier. Ainsi , pour chaque telle impression, vous interroger chacun des annonceurs interrogés, l' envoi d' une requête avec les détails d'impression à un point final fourni par chaque annonceur (ou bien, un script fourni par l'annonceur en cours d' exécution en tant que point final sur vos propres serveurs), course toutes ces demandes jusqu'à un délai (par exemple 100 ms), puis en prenant l'offre la plus élevée, en ignorant les autres.

Une variante particulière de cela qui permet de réduire le temps d'attente du client consiste à demander à l'éditeur de prévoir une valeur d'objectif minimale pour l'enchère, de sorte que la première enchère d'annonceur qui dépasse cette valeur soit immédiatement acceptée (ou, si aucune des offres ne dépasse la valeur, la valeur maximale sera prise). Ainsi, dans cette variante, la première requête arrivant pourrait gagner et l'autre rejetée, même si elles sont aussi bonnes ou même meilleures.

yoniLavi
la source