Boyutu bilinmeyen dengesiz bir Spliterator'ı yeniden dengeleyebilir misiniz?


12

Ben bir Streambilinmeyen numara (dosya sayısı açık olarak bilinmemektedir) uzaktan depolanan JSON dosyaları heterojen bir dizi işleme paralelleştirmek için kullanmak istiyorum . Dosyaların boyutu, dosya başına 1 JSON kaydından diğer bazı dosyalarda 100.000 kayda kadar büyük ölçüde değişebilir. Bu durumda bir JSON kaydı , dosyada bir satır olarak temsil edilen bağımsız bir JSON nesnesi anlamına gelir.

Bunun için Akışları gerçekten kullanmak istiyorum ve bu yüzden bunu uyguladım 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;
            }
        }
    }
}

Sahip olduğum sorun, Akış ilk başta güzel bir şekilde paralel olsa da, en sonunda en büyük dosyanın tek bir iş parçacığında işlenmeye bırakılmasıdır. Proksimal nedenin iyi belgelendiğine inanıyorum: ayırıcı "dengesiz".

Daha somut olarak, trySplityöntemin Stream.forEachyaşam döngüsünde belirli bir noktadan sonra çağrılmadığı anlaşılır , bu nedenle sonunda küçük partileri dağıtmak için ekstra mantık trySplitnadiren yürütülür.

TrySplit'ten dönen tüm ayırıcıların aynı pathsyineleyiciyi nasıl paylaştığına dikkat edin . Bunun tüm bölücülerdeki işi dengelemek için gerçekten akıllıca bir yol olduğunu düşündüm, ancak tam paralelliğe ulaşmak için yeterli değildi.

Paralel işlemenin önce dosyalar arasında ilerlemesini istiyorum ve daha sonra birkaç büyük dosya bölünmeye bırakıldığında, kalan dosyaların parçaları arasında paralellik yapmak istiyorum. elseSonunda bloğun amacı buydu trySplit.

Bu sorunun kolay / basit / kanonik bir yolu var mı?


2
Boyut tahminine ihtiyacınız var. Dengesiz bölünme oranınızı kabaca yansıttığı sürece tamamen sahte olabilir. Aksi takdirde, akış bölmelerin dengesiz olduğunu bilmez ve belirli sayıda parça oluşturulduktan sonra duracaktır.
Holger

@Holger "belirli sayıda parça oluşturulduktan sonra duracak" veya bunun için beni JDK kaynağına yönlendirebilir misiniz? Durduğu parça sayısı nedir?
Alex R

Kod, herhangi bir zamanda değişebilecek çok fazla alakasız uygulama ayrıntısı göstereceği için önemsizdir. İlgili nokta, uygulamanın yeterince sık bölünmeyi çağırmaya çalışmasıdır, böylece her çalışan iş parçacığı (CPU çekirdeği sayısına göre ayarlanır) yapacak bir şeyleri olur. Hesaplama süresinde öngörülemeyen farklılıkları telafi etmek için, iş çalmaya izin vermek ve tahmini boyutları sezgisel olarak kullanmak için (örneğin hangi alt ayırıcıyı daha fazla böleceğine karar vermek için) çalışan iş parçacıklarından daha fazla yığın üretecektir. Ayrıca bkz. Stackoverflow.com/a/48174508/2711488
Holger

Yorumunuzu anlamaya çalışmak için bazı deneyler yaptım. Buluşsal yöntemler oldukça ilkel görünüyor. Görünüşe göre, geri dönmek Long.MAX_VALUEaşırı ve gereksiz bölünmeye neden olurken, daha Long.MAX_VALUEfazla bölünmenin durmasına neden olmaktan başka bir tahmin , paralelliği öldürür. Doğru tahminlerin bir karışımını döndürmek akıllı optimizasyonlara yol açıyor gibi görünmüyor.
Alex R

Uygulamanın stratejisinin çok akıllı olduğunu iddia etmiyorum, ancak en azından, tahmini boyutlara sahip bazı senaryolar için çalışıyor (aksi halde, bu konuda çok daha fazla hata raporu vardı). Öyle görünüyor ki, deneyler sırasında yanınızda bazı hatalar vardı. Örneğin, sorunuzun kodunda, boyut tahminini uyarlamadığınız için başka bir şey için kötü bir kombinasyon olan genişletiyorsunuz AbstractSpliteratorama geçersiz kılıyorsunuz . Bundan sonra , boyut tahmini, bölünmüş elemanların sayısıyla azaltılmalıdır. trySplit()Long.MAX_VALUEtrySplit()trySplit()
Holger

Yanıtlar:


0

Sizin trySplitbakılmaksızın temel dosyaların boyutuna eşit büyüklükte çıkış böler, gerektiği. Tüm dosyalara tek bir birim gibi davranmalı ve yedeklenmiş ArrayListayırıcıyı her seferinde aynı sayıda JSON nesnesi ile doldurmalısınız . Nesnelerin sayısı, bir bölmenin işlenmesi 1 ile 10 milisaniye arasında sürecek şekilde olmalıdır: 1 ms'den daha düşük ve toplu işi bir iş parçacığına teslim etme maliyetlerine yaklaşmaya başlıyorsunuz ve bundan dolayı düzensiz CPU yükü riskini almaya başlıyorsunuz çok iri taneli görevler.

Ayırıcı bir boyut tahmini bildirmek zorunda değildir ve bunu zaten doğru bir şekilde yapıyorsunuz: tahmininiz, Long.MAX_VALUE"sınırsız" anlamına gelen özel bir değerdir. Ancak, tek bir JSON nesnesine sahip, 1 büyüklüğünde partilerle sonuçlanan çok sayıda dosyanız varsa, bu durum performansınıza iki şekilde zarar verir: dosyayı açma-okuma-kapama yükü bir darboğaz haline gelebilir ve kaçmayı başarırsanız iş parçacığının ele alınmasının maliyeti bir öğenin işlenme maliyetine kıyasla önemli olabilir ve bu yine bir darboğaz oluşturur.

Ben benzer bir sorun çözme edildi Beş yıl önce, sen bir göz olabilir benim çözüm .


Evet, "bir boyut tahmini bildirmek zorunda değilsiniz" ve Long.MAX_VALUEbilinmeyen bir boyutu doğru bir şekilde açıklıyorsunuz, ancak gerçek Stream uygulaması o zaman kötü performans gösterdiğinde yardımcı olmuyor. Sonucun ThreadLocalRandom.current().nextInt(100, 100_000)tahmini boyut olarak kullanılması bile daha iyi sonuç verir.
Holger

Her bir öğenin hesaplama maliyetinin önemli olduğu kullanım durumlarım için iyi performans gösterdi. Paralellik ile neredeyse doğrusal olarak ölçeklendirilmiş toplam CPU kullanımı ve verimini% 98 kolayca elde edebildim. Temel olarak, parti boyutunu doğru bir şekilde almak önemlidir, böylece işlemenin 1 ila 10 milisaniye sürmesi gerekir. Bu, iş parçacığı teslim maliyetlerinin çok üzerindedir ve görev ayrıntı düzeyi sorunlarına neden olmak için çok uzun değildir. Ben sonuna doğru kıyaslama sonuçları yayınlanmamış bu yazı .
Marko Topolnik

Çözümün kapalı böler bir ArraySpliteratorhangi sahiptir tahmini boyutu (hatta tam boyut). Böylece Stream uygulaması vs dizisi boyutunu görür, Long.MAX_VALUEbu dengesiz olarak düşünür ve "daha büyük" ayırıcıyı ( Long.MAX_VALUE"bilinmeyen" anlamına gelmez gibi gözükerek ) daha fazla bölünemeyene kadar böler. Daha sonra, yeterli parça yoksa, bilinen boyutlarını kullanarak dizi tabanlı ayırıcıları böler. Evet, bu çok iyi çalışıyor, ancak ne kadar fakir olduğuna bakılmaksızın bir boyut tahminine ihtiyacınız olduğunu ifade etmemle çelişmiyor.
Holger

Tamam, bu bir yanlış anlama gibi görünüyor --- çünkü girişte bir boyut tahminine ihtiyacınız yok. Sadece bireysel bölünmelerde ve her zaman sahip olabilirsiniz.
Marko Topolnik

Eh, benim ilk açıklama "idi . Bu tamamen düzmece, yeter ki kabaca senin dengesiz bölünmenin oranını yansıttığı olabilir. Sen bir boyut tahmini ihtiyaç püf noktası burada" OP'ın kod tek eleman içeren başka spliterator yarattığını ama hala bilinmeyen bir boyut bildiriyor. Akış uygulamasını çaresiz yapan da budur. Yeni ayırıcı için herhangi bir tahmin numarası Long.MAX_VALUEyapacağı önemli ölçüde daha küçük olacaktır.
Holger

0

Fazla denemeden sonra, hala boyut tahminleriyle oynayarak ilave bir paralellik elde edemedim. Temel olarak, herhangi bir değer Long.MAX_VALUEayırıcının çok erken sonlanmasına (ve ayrılmadan) Long.MAX_VALUEneden olurken, diğer taraftan bir tahmin trySplitdönene kadar acımasızca çağrılmaya neden olur null.

Bulduğum çözüm, kaynakları bölücüler arasında paylaşmak ve kendi aralarında yeniden dengelemelerine izin vermektir.

Çalışma kodu:

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;
            }
        }
    }
}
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.