2010-08-04 7 views
18

Ayant récemment travaillé sur un projet qui nécessitait plus d'interactions IO que je ne le faisais auparavant, j'avais l'impression de vouloir regarder au-delà des bibliothèques régulières (Commons IO, en particulier) et d'en aborder d'autres problèmes de profondeur d'E/S. En tant que test académique, j'ai décidé d'implémenter un téléchargeur HTTP multi-thread. L'idée est simple: fournir une URL à télécharger, et le code va télécharger le fichier. Pour augmenter les vitesses de téléchargement, le fichier est fragmenté et chaque bloc est téléchargé simultanément (en utilisant l'en-tête HTTP Range: bytes=x-x) pour utiliser autant de bande passante que possible.Performance de téléchargement de fichiers multithread Java

J'ai un prototype fonctionnel, mais comme vous l'avez peut-être deviné, ce n'est pas exactement l'idéal. Au moment où je démarre manuellement 3 threads "téléchargeur" ​​qui téléchargent chacun 1/3 du fichier. Ces threads utilisent une instance "d'écriture de fichier" commune et synchronisée pour écrire réellement les fichiers sur le disque. Lorsque tous les threads sont terminés, le "writer de fichier" est terminé et tous les flux ouverts sont fermés. Quelques extraits de code pour vous donner une idée:

Le fil de démarrage:

ExecutorService downloadExecutor = Executors.newFixedThreadPool(3); 
... 
downloadExecutor.execute(new Downloader(fileWriter, download, start1, end1)); 
downloadExecutor.execute(new Downloader(fileWriter, download, start2, end2)); 
downloadExecutor.execute(new Downloader(fileWriter, download, start3, end3)); 

Chaque thread « téléchargeur » télécharge un morceau (tampon) et utilise le « écrivain de fichier » pour écrire sur le disque:

int bytesRead = 0; 
byte[] buffer = new byte[1024*1024]; 
InputStream inStream = entity.getContent(); 
long seekOffset = chunkStart; 
while ((bytesRead = inStream.read(buffer)) != -1) 
{ 
    fileWriter.write(buffer, bytesRead, seekOffset); 
    seekOffset += bytesRead; 
} 

Le "écrivain de fichier", écrit sur le disque à l'aide d'un RandomAccessFile à seek() et write() les morceaux sur le disque:

public synchronized void write(byte[] bytes, int len, long start) throws IOException 
{ 
     output.seek(start); 
     output.write(bytes, 0, len); 
} 

Tout bien considéré, cette approche semble fonctionner. Cependant, cela ne fonctionne pas très bien. J'apprécierais quelques conseils/aide/opinions sur les points suivants. Très appréciée.

  1. L'utilisation du processeur de ce code est à travers le toit. Pour ce faire, il utilise la moitié de mon processeur (50% de chacun des 2 cœurs), ce qui est exponentiellement plus que les outils de téléchargement comparables qui ne font que très peu de stress sur le processeur. Je suis un peu mystifié quant à l'origine de cette utilisation du processeur, car je ne m'attendais pas à cela.
  2. Habituellement, il semble y avoir 1 des 3 threads qui est en retard de de manière significative. Les 2 autres threads se terminent, après quoi il prend le troisième thread (qui semble être le premier thread avec le premier morceau) 30 secondes ou plus pour terminer. Je peux voir à partir du gestionnaire de tâches que le processus javaw fait encore de petites écritures d'E/S, mais je ne sais pas vraiment pourquoi cela se produit (je devine les conditions de course?).
  3. Malgré le fait que j'ai choisi un tampon assez volumineux (1 Mo), j'ai l'impression que le InputStream ne remplit pratiquement jamais le tampon, ce qui provoque plus d'écritures IO que je ne le souhaite. J'ai l'impression que dans ce scénario, il serait préférable de garder un minimum d'accès aux IO, mais je ne sais pas avec certitude si c'est la meilleure approche.
  4. Je réalise que Java n'est peut-être pas le langage idéal pour faire quelque chose comme ça, mais je suis convaincu qu'il y a beaucoup plus de performance à avoir que ce que j'obtiens dans mon implémentation actuelle. La NIO mérite-t-elle d'être explorée dans ce cas?

Note: J'utilise Apache HTTPClient faire l'interaction HTTP, qui est où le entity.getContent() vient de (au cas où quelqu'un se demande).

+0

trouvé un bon sujet connexe ici: http://stackoverflow.com/questions/921262/how-to-download-and-save-a-file-from-internet-using-java POUVAIT que un essai ce soir quand je rentre à la maison :) – tmbrggmn

+0

Mise à jour: l'utilisation élevée du processeur était due à une boucle while() lors de l'appel à la méthode ExecutorService isTerminated(). Doh! – tmbrggmn

+0

Je pense que beaucoup dépend également des configurations réseau et des cartes d'interface réseau (physiques). Même si vous avez plusieurs threads qui travaillent sur le téléchargement du même fichier, NIC, qui est responsable de sérialiser les octets, peut devenir le goulot d'étranglement !! – TriCore

Répondre

6

Pour répondre à mes questions:

  1. L'utilisation du processeur accrue est due à une boucle while() {} qui attendait les fils pour terminer. Comme il se trouve, awaitTermination est une bien meilleure alternative à attendre un Executor pour terminer :)
  2. (Et 3 et 4) Cela semble être la nature de la bête; à la fin, j'ai réalisé ce que je voulais faire en utilisant une synchronisation minutieuse des différents threads qui téléchargent chacun un morceau de données (enfin, en particulier les écritures de ces morceaux sur le disque).
2

Ma pensée immédiate pour de meilleures performances sur Windows serait d'utiliser IO completions ports.Ce que je ne sais pas est (a) s'il existe des concepts similaires dans d'autres systèmes d'exploitation, et (b) s'il existe des wrappers Java appropriés? Si la portabilité n'est pas importante pour vous, il est peut-être possible de rouler votre propre wrapper avec JNI.

3

On peut supposer que le client HTTP Apache fera une mise en mémoire tampon, avec un tampon plus petit. Il aura besoin d'un tampon pour lire raisonnablement l'en-tête HTTP et probablement gérer le codage en segments.

0

Définissez un très grand tampon de réception de socket. Mais vraiment vos performances seront limitées par la bande passante du réseau, pas par la bande passante CPU. Tout ce que vous faites est d'allouer 1/3 de la bande passante réseau à chaque téléchargeur. Je serais surpris si vous obtenez beaucoup d'avantages.

+1

Trois connexions peuvent être plus rapides qu'une, brièvement au début d'un transfert. Il faut un peu de temps à TCP pour trouver la taille de fenêtre optimale, donc si vous utilisez des connexions parallèles, ce processus va 3 fois plus vite! – Karmastan

+0

C'est la raison pour laquelle je suis en train de découper en premier lieu, en divisant le fichier en 3 morceaux me permet de télécharger le même fichier 3 fois plus vite, supposant qu'un seul morceau est trop lent pour saturer la connexion. La vitesse de téléchargement est maximale, donc ce n'est pas un problème. C'est l'utilisation du processeur qui m'inquiète. Cela peut-il être lié au fait que le code s'exécute dans Eclipse? – tmbrggmn

+3

Eh bien, vous faites N-1 redondants cherche, et la dernière fois que je l'ai regardé, ce qui est il y a des décennies, chercher était une opération étonnamment coûteuse. Chaque écrivain a seulement besoin de chercher une fois; après cela, il s'agit simplement d'E/S séquentielles. – EJP

Questions connexes