Voglio utilizzare a Streamper 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 trySplitmetodo non venga chiamato dopo un certo punto del Stream.forEachciclo di vita, quindi trySplitraramente viene eseguita la logica aggiuntiva per distribuire piccoli lotti alla fine .
Notare come tutti i divisori restituiti da trySplit condividano lo stesso pathsiteratore. 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 elseblocco alla fine di trySplit.
Esiste un modo semplice / semplice / canonico per aggirare questo problema?
Long.MAX_VALUEprovochi una divisione eccessiva e non necessaria, mentre qualsiasi stima oltre a Long.MAX_VALUEcausare un'ulteriore divisione per arrestare, uccidendo il parallelismo. Restituire un mix di stime accurate non sembra portare a ottimizzazioni intelligenti.
AbstractSpliteratorma 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.