Quiero usar a Stream
para paralelizar el procesamiento de un conjunto heterogéneo de archivos JSON almacenados de forma remota de un número desconocido (el número de archivos no se conoce por adelantado). Los archivos pueden variar ampliamente en tamaño, desde 1 registro JSON por archivo hasta 100,000 registros en algunos otros archivos. Un registro JSON en este caso significa un objeto JSON autónomo representado como una línea en el archivo.
Realmente quiero usar Streams para esto, así que implementé esto 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;
}
}
}
}
El problema que tengo es que, si bien el Stream se paraleliza maravillosamente al principio, eventualmente el archivo más grande se deja procesar en un solo hilo. Creo que la causa proximal está bien documentada: el spliterator está "desequilibrado".
Más concretamente, parece que el trySplit
método no se llama después de cierto punto en el Stream.forEach
ciclo de vida del mismo, por lo que la lógica adicional para distribuir lotes pequeños al final trySplit
rara vez se ejecuta.
Observe cómo todos los spliteradores devueltos por trySplit comparten el mismo paths
iterador. Pensé que esta era una forma realmente inteligente de equilibrar el trabajo en todos los spliteradores, pero no ha sido suficiente para lograr un paralelismo completo.
Me gustaría que el procesamiento paralelo proceda primero a través de los archivos, y luego, cuando todavía quedan pocos archivos grandes divididos, quiero paralelizar a través de fragmentos de los archivos restantes. Esa fue la intención del else
bloque al final de trySplit
.
¿Hay alguna manera fácil / simple / canónica de solucionar este problema?
Long.MAX_VALUE
causa una división excesiva e innecesaria, mientras que cualquier estimación que no sea la Long.MAX_VALUE
causa de la división adicional se detiene, matando el paralelismo. Devolver una combinación de estimaciones precisas no parece llevar a ninguna optimización inteligente.
AbstractSpliterator
pero anulando, lo trySplit()
cual es un mal combo para otra cosa que no sea Long.MAX_VALUE
, ya que no está adaptando el tamaño estimado trySplit()
. Después trySplit()
, la estimación del tamaño debe reducirse por el número de elementos que se han dividido.