Je veux utiliser un Stream
pour paralléliser le traitement d'un ensemble hétérogène de fichiers JSON stockés à distance de nombre inconnu (le nombre de fichiers n'est pas connu à l'avance). La taille des fichiers peut varier considérablement, de 1 enregistrement JSON par fichier jusqu'à 100 000 enregistrements dans certains autres fichiers. Un enregistrement JSON dans ce cas signifie un objet JSON autonome représenté comme une ligne dans le fichier.
Je veux vraiment utiliser Streams pour cela et j'ai donc implémenté ceci 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;
}
}
}
}
Le problème que j'ai, c'est que même si le Stream se parallélise magnifiquement au début, le fichier le plus volumineux est finalement traité en un seul thread. Je crois que la cause proximale est bien documentée: le séparateur est "déséquilibré".
Plus concrètement, il semble que la trySplit
méthode ne soit pas appelée après un certain point du Stream.forEach
cycle de vie de l ', de sorte que la logique supplémentaire de distribution de petits lots à la fin de trySplit
est rarement exécutée.
Remarquez comment tous les séparateurs renvoyés par trySplit partagent le même paths
itérateur. Je pensais que c'était un moyen très intelligent d'équilibrer le travail entre tous les séparateurs, mais cela n'a pas été suffisant pour atteindre un parallélisme complet.
Je voudrais que le traitement parallèle se poursuive d'abord sur les fichiers, puis lorsque quelques fichiers volumineux sont encore divisés, je veux paralléliser sur des morceaux des fichiers restants. C'était l'intention du else
bloc à la fin de trySplit
.
Existe-t-il un moyen facile / simple / canonique de contourner ce problème?
Long.MAX_VALUE
provoque un fractionnement excessif et inutile, tandis que toute estimation autre que celle-ci Long.MAX_VALUE
provoque l'arrêt du fractionnement, tuant le parallélisme. Le retour d'un mélange d'estimations précises ne semble pas conduire à des optimisations intelligentes.
AbstractSpliterator
mais remplacez trySplit()
ce qui est un mauvais combo pour autre chose que Long.MAX_VALUE
, car vous n'adaptez pas l'estimation de taille dans trySplit()
. Ensuite trySplit()
, l'estimation de la taille doit être réduite du nombre d'éléments qui ont été séparés.