Ich möchte a verwenden, Stream
um die Verarbeitung eines heterogenen Satzes von remote gespeicherten JSON-Dateien mit unbekannter Anzahl zu parallelisieren (die Anzahl der Dateien ist im Voraus nicht bekannt). Die Dateien können sehr unterschiedlich groß sein, von 1 JSON-Datensatz pro Datei bis zu 100.000 Datensätzen in einigen anderen Dateien. Ein JSON-Datensatz in diesem Fall ein in sich geschlossenes JSON-Objekt, das als eine Zeile in der Datei dargestellt wird.
Ich möchte wirklich Streams dafür verwenden und habe dies implementiert Spliterator
:
public abstract class JsonStreamSpliterator<METADATA, RECORD> extends AbstractSpliterator<RECORD> {
abstract protected JsonStreamSupport<METADATA> openInputStream(String path);
abstract protected RECORD parse(METADATA metadata, Map<String, Object> json);
private static final int ADDITIONAL_CHARACTERISTICS = Spliterator.IMMUTABLE | Spliterator.DISTINCT | Spliterator.NONNULL;
private static final int MAX_BUFFER = 100;
private final Iterator<String> paths;
private JsonStreamSupport<METADATA> reader = null;
public JsonStreamSpliterator(Iterator<String> paths) {
this(Long.MAX_VALUE, ADDITIONAL_CHARACTERISTICS, paths);
}
private JsonStreamSpliterator(long est, int additionalCharacteristics, Iterator<String> paths) {
super(est, additionalCharacteristics);
this.paths = paths;
}
private JsonStreamSpliterator(long est, int additionalCharacteristics, Iterator<String> paths, String nextPath) {
this(est, additionalCharacteristics, paths);
open(nextPath);
}
@Override
public boolean tryAdvance(Consumer<? super RECORD> action) {
if(reader == null) {
String path = takeNextPath();
if(path != null) {
open(path);
}
else {
return false;
}
}
Map<String, Object> json = reader.readJsonLine();
if(json != null) {
RECORD item = parse(reader.getMetadata(), json);
action.accept(item);
return true;
}
else {
reader.close();
reader = null;
return tryAdvance(action);
}
}
private void open(String path) {
reader = openInputStream(path);
}
private String takeNextPath() {
synchronized(paths) {
if(paths.hasNext()) {
return paths.next();
}
}
return null;
}
@Override
public Spliterator<RECORD> trySplit() {
String nextPath = takeNextPath();
if(nextPath != null) {
return new JsonStreamSpliterator<METADATA,RECORD>(Long.MAX_VALUE, ADDITIONAL_CHARACTERISTICS, paths, nextPath) {
@Override
protected JsonStreamSupport<METADATA> openInputStream(String path) {
return JsonStreamSpliterator.this.openInputStream(path);
}
@Override
protected RECORD parse(METADATA metaData, Map<String,Object> json) {
return JsonStreamSpliterator.this.parse(metaData, json);
}
};
}
else {
List<RECORD> records = new ArrayList<RECORD>();
while(tryAdvance(records::add) && records.size() < MAX_BUFFER) {
// loop
}
if(records.size() != 0) {
return records.spliterator();
}
else {
return null;
}
}
}
}
Das Problem, das ich habe, ist, dass, während der Stream zunächst wunderbar parallelisiert, die größte Datei schließlich in einem einzigen Thread verarbeitet wird. Ich glaube, die proximale Ursache ist gut dokumentiert: Der Spliterator ist "unausgeglichen".
Genauer gesagt scheint die trySplit
Methode nach einem bestimmten Punkt im Stream.forEach
Lebenszyklus des Systems nicht mehr aufgerufen zu werden , daher die zusätzliche Logik, kleine Stapel am Ende von zu verteilentrySplit
selten ausgeführt wird.
Beachten Sie, dass alle von trySplit zurückgegebenen Spliteratoren denselben paths
Iterator verwenden. Ich dachte, dies sei eine wirklich clevere Methode, um die Arbeit über alle Spliteratoren hinweg auszugleichen, aber es hat nicht ausgereicht, um eine vollständige Parallelität zu erreichen.
Ich möchte, dass die parallele Verarbeitung zuerst über Dateien hinweg erfolgt. Wenn dann nur noch wenige große Dateien splittert, möchte ich über Teile der verbleibenden Dateien parallelisieren. Das war die Absicht des else
Blocks am Ende vontrySplit
.
Gibt es einen einfachen / einfachen / kanonischen Weg, um dieses Problem zu umgehen?
Long.MAX_VALUE
zu einer übermäßigen und unnötigen Aufteilung führen, während jede andere Schätzung als Long.MAX_VALUE
die weitere Aufteilung zum Stillstand kommt und die Parallelität zunichte macht. Die Rückgabe einer Mischung aus genauen Schätzungen scheint nicht zu intelligenten Optimierungen zu führen.
AbstractSpliterator
aber überschreiben, trySplit()
was eine schlechte Kombination für etwas anderes ist Long.MAX_VALUE
, da Sie die Größenschätzung in nicht anpassen trySplit()
. Danach trySplit()
sollte die Größenschätzung um die Anzahl der abgespaltenen Elemente reduziert werden.