Voglio utilizzare a Stream
per parallelizzare l'elaborazione di un insieme eterogeneo di file JSON memorizzati in remoto di numero sconosciuto (il numero di file non è noto in anticipo). Le dimensioni dei file possono variare notevolmente, da 1 record JSON per file fino a 100.000 record in alcuni altri file. Un record JSON in questo caso significa un oggetto JSON autonomo rappresentato come una riga nel file.
Voglio davvero usare Streams per questo e quindi ho implementato questo 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;
}
}
}
}
Il problema che sto riscontrando è che mentre lo Stream si parallelizza magnificamente all'inizio, alla fine il file più grande viene lasciato elaborato in un singolo thread. Credo che la causa prossimale sia ben documentata: il divisore è "sbilanciato".
Più concretamente, sembra che il trySplit
metodo non venga chiamato dopo un certo punto del Stream.forEach
ciclo di vita, quindi trySplit
raramente viene eseguita la logica aggiuntiva per distribuire piccoli lotti alla fine .
Notare come tutti i divisori restituiti da trySplit condividano lo stesso paths
iteratore. Ho pensato che fosse un modo davvero intelligente per bilanciare il lavoro tra tutti i divisori, ma non è stato abbastanza per raggiungere il pieno parallelismo.
Vorrei che l'elaborazione parallela procedesse dapprima tra i file e poi, quando pochi file di grandi dimensioni rimangono ancora divisi, voglio parallelizzare tra i pezzi dei file rimanenti. Questo era l'intento del else
blocco alla fine di trySplit
.
Esiste un modo semplice / semplice / canonico per aggirare questo problema?
Long.MAX_VALUE
provochi una divisione eccessiva e non necessaria, mentre qualsiasi stima oltre a Long.MAX_VALUE
causare un'ulteriore divisione per arrestare, uccidendo il parallelismo. Restituire un mix di stime accurate non sembra portare a ottimizzazioni intelligenti.
AbstractSpliterator
ma ignorando il trySplit()
che è una cattiva combinazione per qualsiasi cosa diversa da Long.MAX_VALUE
, poiché non stai adattando la stima delle dimensioni in trySplit()
. Successivamente trySplit()
, la stima delle dimensioni dovrebbe essere ridotta del numero di elementi che sono stati suddivisi.