Latence et débit du réseau
Trois problèmes majeurs sont liés à l’utilisation optimale du réseau :
- Latence du réseau
- Saturation du réseau
- Implications du traitement des paquets
Cette section présente une tâche de programmation nécessitant l’utilisation de RPC, puis conçoit deux solutions : l’une mal écrite et l’autre bien écrite. Les deux solutions sont ensuite examinées de près et leur impact sur les performances du réseau est abordé.
Avant d’aborder les deux solutions, les sections suivantes traitent et clarifient les problèmes de performances liés au réseau.
Latence du réseau
La bande passante réseau et la latence du réseau sont des termes distincts. Les réseaux avec une bande passante élevée ne garantissent pas une faible latence. Par exemple, un chemin réseau traversant une liaison satellite a souvent une latence élevée, même si le débit est très élevé. Il n’est pas rare qu’un aller-retour réseau traversant une liaison satellite ait cinq secondes ou plus de latence. L’implication d’un tel délai est la suivante : une application conçue pour envoyer une demande, attendre une réponse, envoyer une autre demande, attendre une autre réponse, et ainsi de suite, attend au moins cinq secondes pour chaque échange de paquets, quelle que soit la vitesse du serveur. Malgré la vitesse croissante des ordinateurs, les transmissions par satellite et les médias réseau sont basés sur la vitesse de la lumière, qui reste généralement constante. Par conséquent, il est peu probable que des améliorations de la latence pour les réseaux satellites existants se produisent.
Saturation du réseau
Une certaine saturation se produit dans de nombreux réseaux. Les réseaux les plus faciles à saturer sont les liaisons de modem lentes, telles que les modems analogiques standard de 56 Ko. Toutefois, les liaisons Ethernet avec de nombreux ordinateurs sur un seul segment peuvent également devenir saturées. Il en va de même pour les réseaux étendus avec une faible bande passante ou une liaison surchargée, comme un routeur ou un commutateur qui peut gérer une quantité limitée de trafic. Dans ce cas, si le réseau envoie plus de paquets que son lien le plus faible peut en gérer, il supprime les paquets. Pour éviter la congestion, la pile TCP Windows effectue un scale-back lorsque des paquets supprimés sont détectés, ce qui peut entraîner des retards importants.
Implications du traitement des paquets
Lorsque des programmes sont développés pour des environnements de niveau supérieur comme RPC, COM et même des sockets Windows, les développeurs ont tendance à oublier la quantité de travail qui se déroule en arrière-plan pour chaque paquet envoyé ou reçu. Lorsqu’un paquet arrive du réseau, une interruption du carte réseau est prise en compte par l’ordinateur. Ensuite, un appel de procédure différée (DPC) est mis en file d’attente et doit passer par les pilotes. Si une forme de sécurité est utilisée, le paquet peut être déchiffré ou le hachage de chiffrement vérifié. Un certain nombre de vérifications de validité doivent également être effectuées à chaque état. Ce n’est qu’alors que le paquet arrive à la destination finale : le code du serveur. L’envoi de nombreux petits blocs de données entraîne une surcharge de traitement des paquets pour chaque petit bloc de données. L’envoi d’un gros bloc de données a tendance à consommer beaucoup moins de temps processeur dans l’ensemble du système, même si le coût d’exécution de nombreux petits blocs par rapport à un bloc volumineux peut être le même pour l’application serveur.
Exemple 1 : Un serveur RPC mal conçu
Imaginez une application qui doit accéder aux fichiers distants, et la tâche à accomplir consiste à concevoir une interface RPC pour manipuler le fichier distant. La solution la plus simple consiste à miroir les routines de fichiers Studio pour les fichiers locaux. Cela peut entraîner une interface faussement propre et familière. Voici un fichier .idl abrégé :
typedef [context_handle] void *remote_file;
... .
interface remote_file
{
remote_file remote_fopen(file_name);
void remote_fclose(remote_file ...);
size_t remote_fread(void *, size_t, size_t, remote_file ...);
size_t remote_fwrite(const void *, size_t, size_t, remote_file ...);
size_t remote_fseek(remote_file ..., long, int);
}
Cela semble assez élégant, mais en fait, il s’agit d’une recette qui a fait ses honneurs pour les performances. Contrairement à l’opinion populaire, l’appel de procédure à distance n’est pas simplement un appel de procédure locale avec un fil entre l’appelant et l’appelé.
Pour voir comment cette recette brûle les performances, envisagez un fichier de 2 Ko, où 20 octets sont lus depuis le début, puis 20 octets à partir de la fin, et voyez comment cela fonctionne. Côté client, les appels suivants sont effectués (de nombreux chemins de code sont omis par souci de concision) :
rfp = remote_fopen("c:\\sample.txt");
remote_read(...);
remote_fseek(...);
remote_read(...);
remote_fclose(rfp);
Imaginez maintenant que le serveur est séparé du client par une liaison satellite avec un aller-retour de cinq secondes. Chacun de ces appels doit attendre une réponse avant de pouvoir continuer, ce qui signifie un minimum absolu pour l’exécution de cette séquence de 25 secondes. Étant donné que nous ne récupérons que 40 octets, les performances sont outrageusement lentes. Les clients de cette application seraient furieux.
Imaginez maintenant que le réseau est saturé, car la capacité d’un routeur quelque part dans le chemin réseau est surchargé. Cette conception force le routeur à gérer au moins 10 paquets si nous n’avons pas de sécurité (un pour chaque demande et un pour chaque réponse). Ce n’est pas bon non plus.
Cette conception force également le serveur à recevoir cinq paquets et à envoyer cinq paquets. Là encore, ce n’est pas une très bonne implémentation.
Exemple 2 : Un serveur RPC mieux conçu
Nous allons reconcevoir l’interface décrite dans l’exemple 1 et voir si nous pouvons l’améliorer. Il est important de noter que la qualité de ce serveur nécessite une connaissance du modèle d’utilisation des fichiers donnés : une telle connaissance n’est pas supposée pour cet exemple. Par conséquent, il s’agit d’un serveur RPC mieux conçu, mais pas d’un serveur RPC de manière optimale.
L’idée dans cet exemple est de réduire autant d’opérations distantes que possible en une seule opération. La première tentative est la suivante :
typedef [context_handle] void *remote_file;
typedef struct
{
long position;
int origin;
} remote_seek_instruction;
... .
interface remote_file
{
remote_fread(file_name, void *, size_t, size_t, [in, out] remote_file ..., BOOL CloseWhenDone, remote_seek_instruction *...);
size_t remote_fwrite(file_name, const void *, size_t, size_t, [in, out] remote_file ..., BOOL CloseWhenDone, remote_seek_instruction *...);
}
Cet exemple réduit toutes les opérations en lecture et en écriture, ce qui permet une ouverture facultative sur la même opération, ainsi qu’une fermeture et une recherche facultatives.
Cette même séquence d’opération, lorsqu’elle est écrite sous forme abrégée, ressemble à ceci :
remote_read("c:\\sample.txt", ..., &rfp, FALSE, NULL);
remote_read(NULL, ..., &rfp, TRUE, seek_to_20_bytes_before_end);
Lorsque vous envisagez le serveur RPC mieux conçu, lors du deuxième appel, le serveur vérifie que le file_name est NULL et utilise le fichier ouvert stocké dans rfp. Ensuite, il voit qu’il existe des instructions de recherche et positionne le pointeur de fichier 20 octets avant la fin avant qu’il ne soit lu. Lorsque vous avez terminé, il reconnaît l’indicateur CloseWhenDone est défini sur TRUE, ferme le fichier, puis ferme rfp.
Sur le réseau à latence élevée, cette meilleure version prend 10 secondes (2,5 fois plus rapide) et ne nécessite que quatre paquets ; deux reçoivent du serveur et deux envoient du serveur. Les ifs supplémentaires et les opérations de démarshalation effectuées par le serveur sont négligeables par rapport à tout le reste.
Si l’ordre causal est spécifié correctement, l’interface peut même être rendue asynchrone et les deux appels peuvent être envoyés en parallèle. Lorsque l’ordre causal est utilisé, les appels sont toujours distribués dans l’ordre, ce qui signifie que sur le réseau à latence élevée, seul un délai de cinq secondes est subi, même si le nombre de paquets envoyés et reçus est le même.
Nous pouvons réduire cela encore plus en créant une méthode qui prend un tableau de structures, chaque membre du tableau décrivant une opération de fichier particulière ; variation distante d’E/S de nuages de points/regroupements. L’approche est payante tant que le résultat de chaque opération ne nécessite pas de traitement supplémentaire sur le client ; en d’autres termes, l’application va lire les 20 octets à la fin, quels que soient les 20 premiers octets lus.
Toutefois, si un traitement doit être effectué sur les 20 premiers octets après les avoir lus pour déterminer l’opération suivante, le fait de tout réduire en une seule opération ne fonctionne pas (du moins pas dans tous les cas). L’élégance de RPC est qu’une application peut avoir les deux méthodes dans l’interface et appeler l’une ou l’autre méthode en fonction des besoins.
En général, lorsque le réseau est impliqué, il est préférable de combiner autant d’appels que possible sur un seul appel. Si une application a deux activités indépendantes, utilisez des opérations asynchrones et laissez-les s’exécuter en parallèle. Essentiellement, gardez le pipeline plein.