Performances de téléchargement de fichiers multithread Java


Ayant récemment travaillé sur un projet qui nécessitait plus d'interactions IO que je n'en ai l'habitude, j'ai eu l'impression de vouloir dépasser les bibliothèques ordinaires (Commons IO, en particulier) et aborder des problèmes IO plus approfondis.

En tant que test académique, j'ai décidé d'implémenter un téléchargeur HTTP multi-thread de base. L'idée est simple: fournir une URL à télécharger, et le code téléchargera le fichier. Pour augmenter les vitesses de téléchargement, le fichier est découpé en morceaux et chaque morceau 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.

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

Le démarrage du thread:

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 "downloader" télécharge un morceau (mis en mémoire tampon) et utilise le "file writer" 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 "graveur de fichiers" écrit sur le disque en utilisant un RandomAccessFile vers 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. Toutefois, il ne fonctionne pas très bien. J'apprécierais quelques conseils/aide/avis sur les points suivants. Très apprécié.

  1. L'utilisation du processeur ce code est à travers le toit. Il utilise la moitié de mon processeur (50% de chacun des 2 cœurs) pour ce faire, ce qui est exponentiellement plus que des outils de téléchargement comparables qui stressent à peine le PROCESSEUR. Je suis un peu mystifié quant à l'origine de cette utilisation du PROCESSEUR, car je ne m'y attendais pas.
  2. Habituellement, il semble y avoir 1 des 3 threads qui est en retard par rapport à de manière significative. Les 2 autres threads se termineront, après quoi il prendra le troisième thread (qui semble être principalement le premier thread avec le premier morceau) 30 secondes ou plus pour terminer. Je peux voir dans le gestionnaire de tâches que le processus javaw fait toujours de petites écritures IO, 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 gros (1 Mo), j'ai l'impression que le InputStream ne remplit presque jamais le tampon, ce qui provoque plus d'écritures IO que je ne le souhaiterais. J'ai l'impression que dans ce scénario, il serait préférable de garder l'IO l'accès à un minimum, mais je ne sais pas 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 performances à avoir que dans mon implémentation actuelle. NIO vaut-il la peine d'être exploré dans ce cas?

Remarque: J'utilise Apache HttpClient pour faire l'interaction HTTP, d'où vient le entity.getContent() (au cas où quelqu'un se demanderait).

Author: tmbrggmn, 2010-08-05

4 answers

Pour répondre à mes propres questions:

  1. L'augmentation de l'utilisation du PROCESSEUR était due à une boucle while() {} qui attendait la fin des threads. Il s'avère que {[1] } est une bien meilleure alternative pour attendre la fin d'un Executor:)
  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 vers disque).
 6
Author: tmbrggmn, 2010-12-05 18:16:09

Vraisemblablement, 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 l'encodage en morceaux.

 3
Author: Tom Hawtin - tackline, 2010-08-04 22:29:38

Ma pensée immédiate pour les meilleures performances sur Windows serait d'utiliser Les ports de complétions IO. Ce que je ne sais pas, c'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 peut être possible de rouler votre propre wrapper avec JNI.

 2
Author: pdbartlett, 2010-08-04 21:31:29

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 la bande passante du PROCESSEUR. Tout ce que vous faites est vraiment d'allouer 1/3 de la bande passante réseau à chaque téléchargeur. Je serais surpris si vous obtenez beaucoup d'avantages.

 0
Author: user207421, 2010-08-05 02:33:17