Bạn có thể cân bằng lại một Spliterator không cân bằng có kích thước không xác định?


12

Tôi muốn sử dụng Streamđể xử lý song song một tập hợp các tệp JSON được lưu trữ từ xa không rõ số lượng (số lượng tệp không được biết trước). Các tệp có thể có kích thước khác nhau, từ 1 bản ghi JSON cho mỗi tệp cho đến 100.000 bản ghi trong một số tệp khác. Một bản ghi JSON trong trường hợp này có nghĩa là một đối tượng JSON độc lập được biểu diễn dưới dạng một dòng trong tệp.

Tôi thực sự muốn sử dụng Luồng cho việc này và vì vậy tôi đã triển khai việc này 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;
            }
        }
    }
}

Vấn đề tôi gặp phải là lúc đầu Stream song song đẹp mắt, cuối cùng tệp lớn nhất lại bị xử lý trong một luồng. Tôi tin rằng nguyên nhân gần nhất được ghi lại rõ ràng: bộ chia là "không cân bằng".

Cụ thể hơn, dường như trySplitphương thức này không được gọi sau một điểm nhất định trong Stream.forEachvòng đời của nó, do đó, logic bổ sung để phân phối các lô nhỏ ở cuối của trySplithiếm khi được thực thi.

Lưu ý cách tất cả các trình phân chia được trả về từ trySplit chia sẻ cùng một pathstrình vòng lặp. Tôi nghĩ rằng đây là một cách thực sự thông minh để cân bằng công việc trên tất cả các bộ chia, nhưng nó không đủ để đạt được sự song song hoàn toàn.

Tôi muốn xử lý song song để tiến hành đầu tiên trên các tệp và sau đó khi một vài tệp lớn vẫn còn bị chia tách, tôi muốn song song trên các đoạn của các tệp còn lại. Đó là ý định của elsekhối ở cuối trySplit.

Có một cách dễ dàng / đơn giản / kinh điển xung quanh vấn đề này?


2
Bạn cần một ước tính kích thước. Nó có thể hoàn toàn không có thật, miễn là nó phản ánh gần đúng tỷ lệ phân chia không cân bằng của bạn. Mặt khác, luồng không biết rằng các phần tách không cân bằng và sẽ dừng lại sau khi một số lượng nhất định được tạo.
Holger

@Holger bạn có thể giải thích thêm về "sẽ dừng lại khi một số lượng nhất định đã được tạo" hoặc chỉ cho tôi nguồn JDK cho việc này không? Số lượng chunk nơi nó dừng lại là gì?
Alex R

Mã này không liên quan, vì nó sẽ hiển thị quá nhiều chi tiết triển khai không liên quan, có thể thay đổi bất cứ lúc nào. Điểm có liên quan là, việc triển khai cố gắng gọi phân tách thường xuyên đủ để mọi luồng công nhân (được điều chỉnh theo số lượng lõi CPU) có việc phải làm. Để bù lại sự khác biệt không thể đoán trước trong thời gian tính toán, nó có thể sẽ tạo ra nhiều khối hơn so với các luồng công nhân để cho phép ăn cắp công việc và sử dụng các kích thước ước tính như heuristic (ví dụ để quyết định bộ chia phụ nào sẽ phân tách hơn nữa). Xem thêm stackoverflow.com/a/48174508/2711488
Holger

Tôi đã làm một số thí nghiệm để cố gắng hiểu nhận xét của bạn. Các heuristic dường như khá nguyên thủy. Có vẻ như, việc trả lại Long.MAX_VALUEgây ra sự chia tách quá mức và không cần thiết, trong khi bất kỳ ước tính nào khác ngoài việc Long.MAX_VALUEgây ra sự chia tách tiếp tục bị đình trệ, giết chết sự song song. Trả lại một hỗn hợp các ước tính chính xác dường như không dẫn đến bất kỳ tối ưu hóa thông minh nào.
Alex R

Tôi không khẳng định rằng chiến lược của việc triển khai là rất thông minh, nhưng ít nhất, nó hoạt động cho một số tình huống với kích thước ước tính (nếu không, có nhiều báo cáo lỗi hơn về điều đó). Vì vậy, có vẻ như, có một số lỗi về phía bạn trong các thí nghiệm. Ví dụ: trong mã câu hỏi của bạn, bạn đang mở rộng AbstractSpliteratornhưng ghi đè trySplit()đó là một kết hợp xấu cho bất cứ điều gì khác Long.MAX_VALUE, vì bạn không điều chỉnh ước tính kích thước trong trySplit(). Sau đó trySplit(), ước tính kích thước nên được giảm theo số lượng phần tử đã được tách ra.
Holger

Câu trả lời:


0

Bạn trySplitnên xuất các phần tách có kích thước bằng nhau, bất kể kích thước của các tệp bên dưới. Bạn nên coi tất cả các tệp là một đơn vị và điền vào trình ArrayListphân tách được sao lưu với cùng số lượng đối tượng JSON mỗi lần. Số lượng đối tượng phải sao cho việc xử lý một lần phân tách mất từ ​​1 đến 10 mili giây: thấp hơn 1 ms và bạn bắt đầu tiếp cận chi phí chuyển giao lô cho luồng công nhân, cao hơn mức đó và bạn bắt đầu gặp rủi ro khi tải CPU không đồng đều do nhiệm vụ quá thô.

Bộ chia không bắt buộc phải báo cáo ước tính kích thước và bạn đã làm điều này một cách chính xác: ước tính của bạn là Long.MAX_VALUE là một giá trị đặc biệt có nghĩa là "không giới hạn". Tuy nhiên, nếu bạn có nhiều tệp với một đối tượng JSON, dẫn đến các lô có kích thước 1, điều này sẽ ảnh hưởng đến hiệu suất của bạn theo hai cách: chi phí mở-đọc-đóng tệp có thể trở thành nút cổ chai và nếu bạn thoát được rằng, chi phí chuyển giao luồng có thể là đáng kể so với chi phí xử lý một mặt hàng, một lần nữa gây ra tắc nghẽn.

Năm năm trước tôi đã giải quyết một vấn đề tương tự, bạn có thể xem xét giải pháp của tôi .


Có, bạn "không bắt buộc phải báo cáo ước tính kích thước" và Long.MAX_VALUEmô tả chính xác một kích thước không xác định, nhưng điều đó không giúp ích gì khi việc triển khai Luồng thực tế hoạt động kém sau đó. Ngay cả việc sử dụng kết quả của ThreadLocalRandom.current().nextInt(100, 100_000)kích thước ước tính cũng mang lại kết quả tốt hơn.
Holger

Nó hoạt động tốt cho các trường hợp sử dụng của tôi, trong đó chi phí tính toán của từng mặt hàng là đáng kể. Tôi đã dễ dàng đạt được 98% tổng mức sử dụng CPU và thông lượng được thu nhỏ gần như tuyến tính với sự song song. Về cơ bản, điều quan trọng là phải có kích thước lô đúng để quá trình xử lý mất từ ​​1 đến 10 mili giây. Đó là cao hơn bất kỳ chi phí chuyển giao chủ đề và không quá lâu để gây ra các vấn đề chi tiết nhiệm vụ. Tôi đã công bố kết quả điểm chuẩn vào cuối bài này .
Marko Topolnik

Giải pháp của bạn tách ra một ArraySpliteratorcái kích thước ước tính (thậm chí là kích thước chính xác). Vì vậy, việc triển khai Luồng sẽ thấy kích thước mảng so với Long.MAX_VALUE, xem xét điều này không cân bằng và phân tách bộ chia "lớn hơn" (bỏ qua điều đó Long.MAX_VALUEcó nghĩa là "không xác định"), cho đến khi nó không thể phân tách thêm. Sau đó, nếu không có đủ khối, nó sẽ phân chia các bộ chia dựa trên mảng sử dụng các kích thước đã biết của chúng. Vâng, điều này hoạt động rất tốt, nhưng không mâu thuẫn với tuyên bố của tôi rằng bạn cần một ước tính kích thước, bất kể nó nghèo đến mức nào.
Holger

OK, do đó, nó có vẻ là một sự hiểu lầm --- bởi vì bạn không cần một ước tính kích thước trên đầu vào. Chỉ trên các phần tách riêng lẻ, và bạn luôn có thể có điều đó.
Marko Topolnik

Chà, nhận xét đầu tiên của tôi là " Bạn cần một ước tính kích thước. Nó có thể hoàn toàn không có thật, miễn là nó phản ánh gần đúng tỷ lệ phân chia không cân bằng của bạn. " Điểm mấu chốt ở đây là mã OP tạo ra một trình phân tách khác chứa một phần tử duy nhất nhưng vẫn báo cáo một kích thước không xác định. Đây là những gì làm cho việc thực hiện Stream bất lực. Bất kỳ số ước tính nào cho bộ chia mới sẽ nhỏ hơn đáng kể Long.MAX_VALUE.
Holger

0

Sau nhiều thử nghiệm, tôi vẫn không thể có được bất kỳ sự song song nào bằng cách chơi với các ước tính kích thước. Về cơ bản, bất kỳ giá trị nào khác ngoài Long.MAX_VALUEsẽ có xu hướng khiến bộ chia kết thúc quá sớm (và không có bất kỳ sự phân tách nào), mặt khác, một Long.MAX_VALUEước tính sẽ gây ra trySplitđược gọi là không ngừng cho đến khi nó quay trở lại null.

Giải pháp tôi tìm thấy là chia sẻ nội bộ giữa các bộ chia và để chúng tự cân bằng lại.

Mã làm việc:

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;
            }
        }
    }
}
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.