サイズが不明なアンバランスなスプリッターを再バランスできますか?


12

を使用し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

この問題を回避する簡単な/単純な/正規の方法はありますか?


2
サイズの見積もりが必要です。アンバランスな分割の比率を大まかに反映している限り、それは完全に偽である可能性があります。そうでない場合、ストリームは分割が不均衡であることを認識せず、特定の数のチャンクが作成されると停止します。
Holger、

@Holgerは、「特定の数のチャンクが作成されたら停止する」について詳しく説明できますか、それともJDKソースを参照してください。停止するチャンクの数はいくつですか?
Alex R

無関係な実装の詳細が多すぎるため、いつでも変更される可能性があるため、コードは無関係です。関連する点は、実装がスプリットを頻繁に呼び出そうとするため、(CPUコアの数に合わせて調整された)すべてのワーカースレッドが何らかの処理を行うようにすることです。コンピューティング時間の予測不可能な違いを補うために、ワーキングスレッドを許可し、推定サイズをヒューリスティックとして使用する(たとえば、さらに分割するサブスプリッターを決定する)ために、ワーカースレッドよりも多くのチャンクを生成する可能性があります。stackoverflow.com/a/48174508/2711488
Holger

私はあなたのコメントを理解しようとするためにいくつかの実験をしました。ヒューリスティックは非常に原始的であるようです。返されるLong.MAX_VALUEと過剰で不必要な分割が発生するように見えますが、それ以外の推定を行うと、それ以上Long.MAX_VALUE分割すると停止し、並列処理が停止します。正確な見積もりの​​組み合わせを返すことは、インテリジェントな最適化につながらないようです。
Alex R

実装の戦略が非常に賢明であったとは主張していませんが、少なくとも、それは推定サイズのいくつかのシナリオで機能します(そうでない場合、それについてはるかに多くのバグレポートがありました)。だから、実験中にあなたの側でいくつかのエラーがあったようです。たとえば、質問のコードでは、でサイズの見積もりを調整していないため、を拡張してAbstractSpliteratorいますがオーバーライドしtrySplit()ています。これは以外の場合の悪い組み合わせです。の後で、サイズの見積もりは、分割された要素の数だけ減らす必要があります。Long.MAX_VALUEtrySplit()trySplit()
ホルガー

回答:


0

あなたはtrySplit関係なく、基礎となるファイルのサイズの同じ大きさの出力分割、必要があります。すべてのファイルを1つの単位として扱い、ArrayList-backedスプリッターに毎回同じ数のJSONオブジェクトを入力する必要があります。オブジェクトの数は、1スプリットの処理に1〜10ミリ秒かかるようにする必要があります。1ミリ秒未満で、バッチをワーカースレッドに渡すコストに近づき始め、それよりも高くなり、CPU負荷が不均一になるリスクを冒し始めます。粒度が粗すぎるタスク。

スプリテレーターはサイズの見積もりを報告する義務がなく、すでにこれを正しく行っています。見積もりはLong.MAX_VALUEであり、これは「無制限」を意味する特別な値です。ただし、単一のJSONオブジェクトを持つ多くのファイルがあり、サイズが1のバッチになる場合、これは2つの点でパフォーマンスを低下させます。ファイルを開く、読み取る、閉じるのオーバーヘッドがボトルネックになる可能性があるつまり、スレッドハンドオフのコストは、1つのアイテムを処理するコストと比較してかなり高くなる可能性があり、やはりボトルネックになります。

5年前、私は同様の問題を解決していました。私のソリューションをご覧いただけます。


はい、「サイズの見積もりを報告する義務はありません」、そしてLong.MAX_VALUE未知のサイズを正しく記述していますが、実際のStream実装のパフォーマンスが低い場合、それは役に立ちません。ThreadLocalRandom.current().nextInt(100, 100_000)推定サイズとしての結果を使用しても、より良い結果が得られます。
Holger、

これは、各アイテムの計算コストが多額である私のユースケースでうまく機能しました。合計CPU使用率98%を簡単に達成し、スループットは並列処理でほぼ直線的にスケーリングしました。基本的に、バッチサイズを正しく設定して、処理に1〜10ミリ秒かかることが重要です。これは、スレッドのハンドオフコストをはるかに上回っており、タスクの粒度の問題を引き起こすほど長くはありません。この投稿の終わりに向けて、ベンチマーク結果を公開しました。
Marko Topolnik

ソリューション、推定サイズ(正確なサイズでさえも)ArraySpliterator持つを分割します。したがって、Stream実装は配列サイズvsを参照し、Long.MAX_VALUEこれを不均衡と見なして、「より大きな」スプリテレーターを分割します(Long.MAX_VALUEそれが「不明」を意味することを無視して)、それ以上分割できなくなります。次に、十分なチャンクがない場合は、既知のサイズを使用して、配列ベースのスプリッターを分割します。はい、これは非常にうまく機能しますが、サイズがどれほど劣っていても、サイズの見積もりが必要であるという私の意見と矛盾しません。
Holger、

OK、それで誤解のようです---入力にサイズの見積もりが必要ないからです。ちょうど個々の分割で、あなたはいつでもそれを持つことができます。
Marko Topolnik、

さて、私の最初のコメントは「サイズの見積もりが必要です。不均衡な分割の比率を大まかに反映している限り、それは完全に偽である可能性があります。」ここでの重要なポイントは、OPのコードが単一の要素を含む別のスプリッターを作成することですが、まだ不明なサイズを報告しています。これが、Streamの実装を無力にしている理由です。新しいスプリッターの推定値は、大幅に小さくLong.MAX_VALUEなります。
ホルガー

0

多くの実験を行った後も、サイズの見積もりを試しても並列処理を追加することはできませんでした。基本的に、以外の値Long.MAX_VALUEを指定すると、スプリテレーターが早く終了し(分割せずに)終了する傾向がありますが、一方、Long.MAX_VALUE見積もりはtrySplit、が返されるまで容赦なく呼び出されますnull

私が見つけた解決策は、スプリッターの間でリソースを内部的に共有し、それらをそれらの間でリバランスさせることです。

作業コード:

public class AwsS3LineSpliterator<LINE> extends AbstractSpliterator<AwsS3LineInput<LINE>> {

    public final static class AwsS3LineInput<LINE> {
        final public S3ObjectSummary s3ObjectSummary;
        final public LINE lineItem;
        public AwsS3LineInput(S3ObjectSummary s3ObjectSummary, LINE lineItem) {
            this.s3ObjectSummary = s3ObjectSummary;
            this.lineItem = lineItem;
        }
    }

    private final class InputStreamHandler {
        final S3ObjectSummary file;
        final InputStream inputStream;
        InputStreamHandler(S3ObjectSummary file, InputStream is) {
            this.file = file;
            this.inputStream = is;
        }
    }

    private final Iterator<S3ObjectSummary> incomingFiles;

    private final Function<S3ObjectSummary, InputStream> fileOpener;

    private final Function<InputStream, LINE> lineReader;

    private final Deque<S3ObjectSummary> unopenedFiles;

    private final Deque<InputStreamHandler> openedFiles;

    private final Deque<AwsS3LineInput<LINE>> sharedBuffer;

    private final int maxBuffer;

    private AwsS3LineSpliterator(Iterator<S3ObjectSummary> incomingFiles, Function<S3ObjectSummary, InputStream> fileOpener,
            Function<InputStream, LINE> lineReader,
            Deque<S3ObjectSummary> unopenedFiles, Deque<InputStreamHandler> openedFiles, Deque<AwsS3LineInput<LINE>> sharedBuffer,
            int maxBuffer) {
        super(Long.MAX_VALUE, 0);
        this.incomingFiles = incomingFiles;
        this.fileOpener = fileOpener;
        this.lineReader = lineReader;
        this.unopenedFiles = unopenedFiles;
        this.openedFiles = openedFiles;
        this.sharedBuffer = sharedBuffer;
        this.maxBuffer = maxBuffer;
    }

    public AwsS3LineSpliterator(Iterator<S3ObjectSummary> incomingFiles, Function<S3ObjectSummary, InputStream> fileOpener, Function<InputStream, LINE> lineReader, int maxBuffer) {
        this(incomingFiles, fileOpener, lineReader, new ConcurrentLinkedDeque<>(), new ConcurrentLinkedDeque<>(), new ArrayDeque<>(maxBuffer), maxBuffer);
    }

    @Override
    public boolean tryAdvance(Consumer<? super AwsS3LineInput<LINE>> action) {
        AwsS3LineInput<LINE> lineInput;
        synchronized(sharedBuffer) {
            lineInput=sharedBuffer.poll();
        }
        if(lineInput != null) {
            action.accept(lineInput);
            return true;
        }
        InputStreamHandler handle = openedFiles.poll();
        if(handle == null) {
            S3ObjectSummary unopenedFile = unopenedFiles.poll();
            if(unopenedFile == null) {
                return false;
            }
            handle = new InputStreamHandler(unopenedFile, fileOpener.apply(unopenedFile));
        }
        for(int i=0; i < maxBuffer; ++i) {
            LINE line = lineReader.apply(handle.inputStream);
            if(line != null) {
                synchronized(sharedBuffer) {
                    sharedBuffer.add(new AwsS3LineInput<LINE>(handle.file, line));
                }
            }
            else {
                return tryAdvance(action);
            }
        }
        openedFiles.addFirst(handle);
        return tryAdvance(action);
    }

    @Override
    public Spliterator<AwsS3LineInput<LINE>> trySplit() {
        synchronized(incomingFiles) {
            if (incomingFiles.hasNext()) {
                unopenedFiles.add(incomingFiles.next());
                return new AwsS3LineSpliterator<LINE>(incomingFiles, fileOpener, lineReader, unopenedFiles, openedFiles, sharedBuffer, maxBuffer);
            } else {
                return null;
            }
        }
    }
}
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.