คุณสามารถปรับสมดุล Spliterator ที่ไม่ทราบขนาดได้หรือไม่?


12

ฉันต้องการใช้การStreamประมวลผลแบบขนานของชุดไฟล์ JSON ที่จัดเก็บจากระยะไกลซึ่งเป็นหมายเลขที่ไม่รู้จัก (จำนวนไฟล์ไม่ทราบล่วงหน้า) ไฟล์สามารถมีขนาดแตกต่างกันอย่างมากตั้งแต่ 1 ระเบียน JSON ต่อไฟล์สูงสุด 100,000 ระเบียนในไฟล์อื่น ๆ บันทึก JSONในกรณีนี้หมายถึงการที่ตนเองมีวัตถุ JSON แสดงเป็นหนึ่งบรรทัดในไฟล์

ฉันต้องการใช้ Streams สำหรับสิ่งนี้และดังนั้นฉันจึงใช้สิ่งนี้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วนซ้ำเดียวกัน ฉันคิดว่านี่เป็นวิธีที่ชาญฉลาดจริงๆในการสร้างสมดุลระหว่างงานของผู้เรียงข้อความทั้งหมด แต่มันก็ยังไม่เพียงพอที่จะบรรลุความเท่าเทียมแบบเต็ม

ฉันต้องการให้การประมวลผลแบบขนานดำเนินการข้ามไฟล์เป็นครั้งแรกจากนั้นเมื่อไฟล์ขนาดใหญ่บางไฟล์ยังคงมีการแยกกันฉันต้องการที่จะขนานขนานกับไฟล์ที่เหลือ นั่นคือความตั้งใจของบล็อกในตอนท้ายของelsetrySplit

มีวิธีที่ง่าย / ง่าย / บัญญัติวิธีแก้ปัญหานี้หรือไม่?


2
คุณต้องประมาณขนาด มันอาจเป็นการหลอกลวงโดยสิ้นเชิงตราบใดที่มันสะท้อนอัตราส่วนของการแบ่งที่ไม่สมดุลของคุณ มิฉะนั้นสตรีมจะไม่ทราบว่าการแบ่งนั้นไม่สมดุลและจะหยุดเมื่อมีการสร้างชิ้นส่วนจำนวนหนึ่งขึ้นมา
ฮอลเกอร์

@ Holger คุณสามารถอธิบายรายละเอียดเกี่ยวกับ "จะหยุดเมื่อจำนวนชิ้นที่ถูกสร้าง" หรือชี้ให้ฉันที่แหล่ง JDK สำหรับสิ่งนี้? จำนวนชิ้นที่มันหยุดคืออะไร?
Alex R

รหัสไม่เกี่ยวข้องเนื่องจากจะแสดงรายละเอียดการใช้งานที่ไม่เกี่ยวข้องมากเกินไปซึ่งอาจเปลี่ยนแปลงได้ตลอดเวลา จุดที่เกี่ยวข้องคือว่าการใช้งานพยายามที่จะเรียกแยกบ่อยพอเพื่อให้ทุกคนงานด้าย (ปรับไปยังจำนวนแกนประมวลผลของ CPU) มีสิ่งที่ต้องทำ เพื่อชดเชยความแตกต่างที่คาดเดาไม่ได้ในเวลาประมวลผลมันน่าจะสร้างชิ้นงานได้มากกว่าเธรดของผู้ปฏิบัติงานเพื่อให้สามารถขโมยงานและใช้ขนาดที่ประมาณไว้เป็นฮิวริสติก (เช่นเพื่อตัดสินใจว่าตัวแยกย่อยตัวใด ดูที่stackoverflow.com/a/48174508/2711488
Holger

ฉันทำการทดลองบางอย่างเพื่อพยายามเข้าใจความคิดเห็นของคุณ ฮิวริสติกนั้นดูค่อนข้างเก่าแก่ ดูเหมือนว่าการกลับมาLong.MAX_VALUEทำให้เกิดการแยกมากเกินไปและไม่จำเป็นในขณะที่การประมาณการอื่น ๆ นอกเหนือจากLong.MAX_VALUEสาเหตุอื่น ๆ ที่ทำให้การแยกหยุดการฆ่าขนานกัน การคืนค่าประมาณที่แม่นยำนั้นดูเหมือนจะไม่นำไปสู่การปรับให้เหมาะสมอย่างชาญฉลาด
อเล็กซ์ R

ฉันไม่ได้อ้างว่ากลยุทธ์ของการใช้งานนั้นฉลาดมาก แต่อย่างน้อยก็ใช้ได้กับบางสถานการณ์ที่มีขนาดโดยประมาณ (มิฉะนั้นจะมีรายงานข้อผิดพลาดมากกว่านั้น) ดูเหมือนว่ามีข้อผิดพลาดบางอย่างเกิดขึ้นกับคุณในระหว่างการทดสอบ เช่นในรหัสคำถามของคุณคุณกำลังขยายAbstractSpliteratorแต่เอาชนะtrySplit()ซึ่งเป็นคำสั่งผสมที่ไม่ดีสำหรับสิ่งอื่นนอกเหนือจากที่คุณจะไม่ได้รับการปรับประมาณการขนาดLong.MAX_VALUE trySplit()หลังจากนั้นtrySplit()การประมาณขนาดควรลดลงตามจำนวนองค์ประกอบที่ถูกแยกออก
Holger

คำตอบ:


0

คุณtrySplitควรแยกเอาท์พุทขนาดเท่ากันโดยไม่คำนึงถึงขนาดของไฟล์พื้นฐาน คุณควรถือว่าไฟล์ทั้งหมดเป็นหน่วยเดียวและเติมArrayListspliterator -backed ที่มีวัตถุ JSON จำนวนเท่ากันทุกครั้ง จำนวนวัตถุควรเป็นเช่นนั้นในการประมวลผลหนึ่ง Split ใช้เวลาระหว่าง 1 และ 10 มิลลิวินาที: ต่ำกว่า 1 ms และคุณเริ่มเข้าใกล้ค่าใช้จ่ายในการส่งแบทช์ไปยังเธรดผู้ปฏิบัติงานสูงกว่านั้นและคุณเริ่มเสี่ยง CPU ที่ไม่สม่ำเสมอ งานที่หยาบเกินไป

ตัวแยกสัญญาณไม่จำเป็นต้องรายงานขนาดประมาณการและคุณทำสิ่งนี้อย่างถูกต้องแล้ว: ค่าประมาณของคุณคือLong.MAX_VALUEซึ่งเป็นค่าพิเศษซึ่งหมายถึง "ไม่ จำกัด " อย่างไรก็ตามหากคุณมีไฟล์จำนวนมากที่มีวัตถุ JSON เดียวส่งผลให้มีขนาด 1 ซึ่งจะทำให้ประสิทธิภาพการทำงานของคุณเสียหายสองวิธี: ค่าใช้จ่ายในการเปิด - ปิด - อ่านไฟล์อาจกลายเป็นคอขวดและหากคุณหลบหนี ว่าค่าใช้จ่ายของการแฮนด์ออฟเธรดอาจมีนัยสำคัญเมื่อเทียบกับค่าใช้จ่ายในการประมวลผลหนึ่งรายการทำให้เกิดปัญหาคอขวดอีกครั้ง

ห้าปีที่ผ่านมาผมได้รับการแก้ปัญหาที่คล้ายกันคุณสามารถมีดูที่วิธีการแก้ปัญหาของฉัน


ใช่คุณ "ไม่จำเป็นต้องรายงานขนาดโดยประมาณ" และLong.MAX_VALUEอธิบายขนาดที่ไม่รู้จักอย่างถูกต้อง แต่ไม่ได้ช่วยอะไรเมื่อการใช้งานสตรีมจริงนั้นทำงานได้ไม่ดี แม้การใช้ผลลัพธ์ThreadLocalRandom.current().nextInt(100, 100_000)ตามขนาดที่ประมาณการไว้ก็ให้ผลลัพธ์ที่ดีกว่า
Holger

มันทำงานได้ดีสำหรับกรณีการใช้งานของฉันซึ่งค่าใช้จ่ายในการคำนวณของแต่ละรายการเป็นจำนวนมาก ฉันได้รับการใช้งาน CPU โดยรวม 98% ได้อย่างง่ายดายและปริมาณงานที่ปรับสัดส่วนเกือบจะเป็นเส้นตรงด้วยความขนาน โดยทั่วไปเป็นสิ่งสำคัญที่จะต้องมีขนาดแบทช์ที่ถูกต้องเพื่อให้การประมวลผลใช้เวลาระหว่าง 1 และ 10 มิลลิวินาที นั่นสูงกว่าค่าใช้จ่ายในการส่งเธรดใด ๆ และไม่นานเกินไปที่จะทำให้เกิดปัญหาเกี่ยวกับงานย่อย ฉันได้เผยแพร่ผลลัพธ์มาตรฐานในตอนท้ายของโพสต์นี้แล้ว
Marko Topolnik

วิธีการแก้ปัญหาของคุณแยกออกArraySpliteratorซึ่งมีขนาดประมาณ (แม้ขนาดที่แน่นอน) ดังนั้นการนำไปใช้งานของกระแสจะเห็นขนาดของอาร์เรย์เทียบกับLong.MAX_VALUEพิจารณาสิ่งนี้ไม่สมดุลและแยกตัวแยก "ที่ใหญ่กว่า" (ไม่สนใจนั่นLong.MAX_VALUEหมายความว่า "ไม่ทราบ") จนกว่ามันจะไม่สามารถแยกได้อีก จากนั้นหากมีชิ้นส่วนไม่เพียงพอก็จะแบ่งตัวแยกสัญญาณตามอาร์เรย์โดยใช้ขนาดที่รู้จัก ใช่มันใช้งานได้ดี แต่ไม่ขัดแย้งกับคำแถลงของฉันว่าคุณต้องมีการประมาณขนาดโดยไม่คำนึงว่ามันจะแย่แค่ไหน
Holger

ตกลงดังนั้นดูเหมือนว่าจะเป็นความเข้าใจผิด --- เพราะคุณไม่จำเป็นต้องประเมินขนาดของอินพุต เพียงแค่แยกกันคุณก็สามารถมีได้
Marko Topolnik

ความคิดเห็นแรกของฉันคือ " คุณต้องมีการประมาณขนาดมันอาจเป็นการหลอกลวงโดยสิ้นเชิงตราบใดที่มันสะท้อนให้เห็นถึงอัตราส่วนของการแบ่งไม่สมดุลของคุณ " จุดสำคัญตรงนี้คือรหัสของ OP สร้างตัวแยกสัญญาณที่มีองค์ประกอบเดียว แต่ ยังคงรายงานขนาดที่ไม่รู้จัก นี่คือสิ่งที่ทำให้การใช้งานสตรีมทำอะไรไม่ถูก หมายเลขประมาณการใด ๆ สำหรับตัวแยกสัญญาณใหม่จะเล็กกว่าที่Long.MAX_VALUEควรทำ
Holger

0

หลังจากการทดลองมากมายฉันยังคงไม่สามารถรับความขนานที่เพิ่มขึ้นได้ด้วยการเล่นกับการประมาณขนาด โดยพื้นฐานแล้วค่าอื่นใดนอกจากLong.MAX_VALUEจะมีแนวโน้มที่จะทำให้ spliterator สิ้นสุดเร็วเกินไป (และไม่มีการแยกใด ๆ ) ในขณะที่การ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.