を使用しStream
て、リモートで保存された不明な数のJSONファイルの異種のセットの処理を並列化したいと思います(ファイルの数は事前に不明です)。ファイルのサイズは、ファイルごとに1つのJSONレコードから、他のいくつかのファイルでは最大100,000レコードまでさまざまです。JSONレコードこの場合には、自己完結型のJSONオブジェクトは、ファイル内の1つの行として表現します。
私は本当にこれにストリームを使いたいので、これを実装しました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;
}
}
}
}
私が抱えている問題は、最初はストリームが美しく並列化されている一方で、最終的に最大のファイルが単一のスレッドで処理されたままになることです。近位の原因は十分に文書化されていると思います。スプリッターは「アンバランス」です。
より具体的には、はライフサイクルのtrySplit
特定のポイントの後でメソッドが呼び出されないStream.forEach
ため、の最後に小さなバッチを分散する追加のロジックがtrySplit
実行されることはほとんどありません。
trySplitから返されたすべてのスプリッターが同じpaths
イテレーターを共有していることに注目してください。これはすべてのスプリッターで作業のバランスをとるのに本当に賢い方法だと思いましたが、完全な並列処理を実現するには十分ではありませんでした。
最初にファイル間で並列処理を続行し、次にいくつかの大きなファイルがまだ分割されていないときに、残りのファイルのチャンク全体で並列化したいと思います。それがのelse
終わりのブロックの意図でしたtrySplit
。
この問題を回避する簡単な/単純な/正規の方法はありますか?
Long.MAX_VALUE
と過剰で不必要な分割が発生するように見えますが、それ以外の推定を行うと、それ以上Long.MAX_VALUE
分割すると停止し、並列処理が停止します。正確な見積もりの組み合わせを返すことは、インテリジェントな最適化につながらないようです。
AbstractSpliterator
いますがオーバーライドしtrySplit()
ています。これは以外の場合の悪い組み合わせです。の後で、サイズの見積もりは、分割された要素の数だけ減らす必要があります。Long.MAX_VALUE
trySplit()
trySplit()