Frage Java-Multithread-Download-Leistung


Nachdem ich kürzlich an einem Projekt gearbeitet habe, das mehr IO-Interaktionen erforderte, als ich es gewohnt bin, hatte ich das Gefühl, dass ich die regulären Bibliotheken (insbesondere Commons IO) hinter mir lassen und eingehendere IO-Probleme angehen wollte.

Als akademischer Test habe ich beschlossen, einen einfachen, mehrkanaligen HTTP-Downloader zu implementieren. Die Idee ist einfach: Stellen Sie eine URL zum Herunterladen bereit, und der Code lädt die Datei herunter. Um die Download-Geschwindigkeit zu erhöhen, wird die Datei chunked und jeder Chunk wird gleichzeitig heruntergeladen (über HTTP) Range: bytes=x-xHeader), um so viel Bandbreite wie möglich zu verwenden.

Ich habe einen funktionierenden Prototyp, aber wie Sie vielleicht vermutet haben, ist es nicht gerade ideal. Im Moment starte ich manuell 3 "Downloader" Threads, die jeweils 1/3 der Datei herunterladen. Diese Threads verwenden eine gemeinsame, synchronisierte "file writer" -Instanz, um die Dateien tatsächlich auf die Festplatte zu schreiben. Wenn alle Threads fertig sind, ist der "Dateischreiber" abgeschlossen und alle offenen Streams sind geschlossen. Einige Codeschnipsel, um Ihnen eine Idee zu geben:

Der Thread-Start:

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));

Jeder "Downloader" -Thread lädt einen Chunk herunter (gepuffert) und verwendet den "File Writer", um auf den Datenträger zu schreiben:

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;
}

Der "Dateischreiber" schreibt mit a auf die Festplatte RandomAccessFile zu seek()und write() die Stücke auf die Festplatte:

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

Alles in allem scheint dieser Ansatz zu funktionieren. Es funktioniert jedoch nicht sehr gut. Ich würde mich über Ratschläge / Hilfe / Meinungen zu den folgenden Punkten freuen. Sehr geschätzt.

  1. Das CPU auslastung dieser Code ist durch das Dach. Es verwendet die Hälfte meiner CPU (50% von jedem der 2 Kerne), um dies zu tun, was exponentiell mehr ist als vergleichbare Download-Tools, die die CPU kaum belasten. Ich bin ein wenig verwirrt darüber, woher diese CPU-Auslastung kommt, da ich das nicht erwartet habe.
  2. Normalerweise scheint es 1 der 3 Threads zu geben Entwicklungsrückstand bedeutend. Die anderen 2 Threads werden beendet, danach dauert der dritte Thread (der meistens der erste Thread mit dem ersten Chunk zu sein scheint) mindestens 30 Sekunden. Ich kann vom Taskmanager sehen, dass der JavaW-Prozess immer noch kleine IO-Schreibvorgänge ausführt, aber ich weiß nicht wirklich, warum dies passiert (ich vermute Race Conditions?).
  3. Trotz der Tatsache, dass ich einen ziemlich großen Puffer (1MB) gewählt habe, habe ich das Gefühl, dass der InputStream fast füllt nie den Puffer, der mehr IO-Schreibvorgänge verursacht, als ich möchte. Ich habe den Eindruck, dass es in diesem Szenario am besten wäre, den IO-Zugriff auf ein Minimum zu beschränken, aber ich weiß nicht genau, ob dies der beste Ansatz ist.
  4. Mir ist klar, dass Java vielleicht nicht die ideale Sprache ist, um so etwas zu tun, aber ich bin überzeugt, dass es viel mehr Leistung zu geben gibt als in meiner aktuellen Implementierung. Ist NIO in diesem Fall eine Erkundung wert?

Hinweis: Ich benutze Apache HTTPClient, um die HTTP-Interaktion zu machen, wo die entity.getContent() kommt von (falls jemand sich wundert).


18
2017-08-04 20:35


Ursprung


Antworten:


Um meine eigenen Fragen zu beantworten:

  1. Die erhöhte CPU-Auslastung war bedingt durch a while() {}Schleife, die darauf wartete, dass die Threads beendet wurden. Wie sich herausstellt, awaitTermination ist eine viel bessere Alternative, um auf einen zu warten Executor beenden :)
  2. (Und und 3 und 4) Dies scheint die Natur des Tieres zu sein; am Ende habe ich erreicht, was ich tun wollte, indem ich die verschiedenen Threads, die jeweils einen Datenblock herunterladen (insbesondere die Schreibvorgänge dieser Chunks zurück auf die Festplatte), sorgfältig synchronisiere.

6
2017-12-05 18:16



Vermutlich wird der Apache-HTTP-Client eine Pufferung mit einem kleineren Puffer durchführen. Es wird einen Puffer benötigen, um den HTTP-Header vernünftig zu lesen und wahrscheinlich die Chunked-Codierung zu handhaben.


3
2017-08-04 22:29



Mein direkter Gedanke für die beste Leistung unter Windows wäre zu verwenden IO-Abschluss-Ports. Was ich nicht weiß, ist (a) ob es ähnliche Konzepte in anderen Betriebssystemen gibt, und (b) ob es geeignete Java-Wrapper gibt? Wenn die Portabilität für Sie nicht wichtig ist, können Sie möglicherweise Ihren eigenen Wrapper mit JNI rollen.


2
2017-08-04 21:31



Legen Sie einen sehr großen Socket-Empfangspuffer fest. Ihre Leistung wird jedoch durch die Netzwerkbandbreite und nicht durch die CPU-Bandbreite begrenzt. Alles, was Sie tun, ist, jedem Downloader 1/3 der Netzwerkbandbreite zuzuweisen. Ich wäre überrascht, wenn Sie viel profitieren würden.


0
2017-08-05 02:33