알 수없는 크기의 불균형 분배기를 재조정 할 수 있습니까?


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;
            }
        }
    }
}

내가 겪고있는 문제는 Stream이 처음에 아름답게 병렬화되는 동안 결국 가장 큰 파일이 단일 스레드에서 처리되는 것입니다. 근위 원인은 잘 문서화되어 있다고 생각합니다. 스플리터는 "불균형"입니다.

보다 구체적으로, 수명주기 trySplit의 특정 시점 이후에이 메소드가 호출되지 않는 것으로 보이 Stream.forEach므로 마지막에 작은 배치를 분배하는 추가 로직 trySplit이 거의 실행되지 않습니다.

trySplit에서 반환 된 모든 스플리터가 어떻게 동일한 paths반복자를 공유하는지 확인하십시오 . 나는 이것이 모든 스플리터에서 작업의 균형을 잡는 정말 영리한 방법이라고 생각했지만 완전한 병렬 처리를 달성하기에는 충분하지 않았습니다.

병렬 처리가 파일 전체에서 먼저 진행된 다음 큰 파일이 여전히 분할되지 않은 경우 나머지 파일의 청크로 병렬 처리하고 싶습니다. 그것 else의 끝에서 블록 의 의도 였다 trySplit.

이 문제를 해결하기 쉬운 / 단순 / 정식 방법이 있습니까?


2
크기 추정이 필요합니다. 불균형 스플릿의 비율을 대략적으로 반영하는 한 완전히 가짜 일 수 있습니다. 그렇지 않으면 스트림은 분할이 불균형하다는 것을 알지 못하며 특정 청크 수를 만든 후에 중지됩니다.
Holger

@Holger 당신은 "특정 청크가 생성되면 멈추게 될 것"에 대해 정교하게 설명 할 수 있습니까? 멈추는 덩어리의 수는 얼마입니까?
Alex R

코드는 관련이없는 구현 세부 사항을 너무 많이 표시하므로 언제든지 변경 될 수 있으므로 관련이 없습니다. 관련 요점은 구현이 split 호출을 충분히 자주 시도하여 모든 작업자 스레드 (CPU 코어 수에 맞게 조정 됨)에 수행 할 작업이 있다는 것입니다. 컴퓨팅 시간의 예측할 수없는 차이를 보상하기 위해 워커 스레드보다 훨씬 많은 청크를 생성하여 워크 스털링을 허용하고 추정 된 크기를 휴리스틱으로 사용합니다 (예 : 분할 할 하위 분할기를 결정하는 경우). 참조 stackoverflow.com/a/48174508/2711488
홀거

귀하의 의견을 이해하기 위해 몇 가지 실험을했습니다. 휴리스틱은 매우 원시적 인 것처럼 보입니다. 리턴 Long.MAX_VALUE은 과도하고 불필요한 분할을 야기하는 반면, Long.MAX_VALUE추가 분할을 중지시키는 것 이외의 다른 추정 은 병렬 처리를 중단시키는 것처럼 보입니다 . 정확한 추정치가 혼합 되어도 지능적인 최적화가 이루어지지 않는 것 같습니다.
Alex R

구현 전략이 매우 똑똑하다고 주장하지는 않지만 적어도 예상 크기의 일부 시나리오에서는 작동합니다 (그렇지 않으면 버그 보고서가 훨씬 많았습니다). 실험 도중에 약간의 오차가있는 것 같습니다. 예를 들면, 당신의 질문의 코드에서, 당신은 확장하고 AbstractSpliterator있지만, 오버라이드 (override) trySplit()이외의 대한 나쁜 콤보 인 Long.MAX_VALUE당신이 크기 추정치를 채택하지 않는 한, trySplit(). 이후 trySplit()에는 분할 된 요소 수만큼 크기 추정치를 줄여야합니다.
Holger

답변:


0

귀하는 trySplit관계없이 기본 파일의 크기의 동일한 크기의 출력 분할,해야한다. 모든 파일을 단일 단위로 취급하고 ArrayList매번 같은 수의 JSON 객체로 백업 된 스플리터를 채워야합니다 . 하나의 스플릿을 처리하는 데 1 ~ 10 밀리 초 (1ms 미만)가 걸리고 배치를 작업자 스레드에 전달하는 비용에 접근하기 시작하는 것보다 높은 객체에 도달해야합니다. 너무 거친 작업.

스플리터는 크기 추정을보고 할 의무가 없으며 이미이 작업을 올바르게 수행하고 있습니다. 추정값은 Long.MAX_VALUE"제한 없음"을 의미하는 특수한 값입니다. 그러나 단일 JSON 객체가있는 파일이 많은 경우 크기가 1 인 배치가 발생하면 두 가지 방식으로 성능이 저하됩니다. 파일을 열고 닫는 오버 헤드로 인해 병목 현상이 발생할 수 있으며 즉, 스레드 핸드 오프 비용은 하나의 항목을 처리하는 비용과 비교하여 중요 할 수 있으며 병목 현상이 다시 발생합니다.

5 년 전 비슷한 문제를 해결하고 있었고 내 솔루션을 살펴볼 수 있습니다 .


예. "크기 추정치를보고 할 의무는 없으며" Long.MAX_VALUE알 수없는 크기를 정확하게 설명하고 있지만 실제 스트림 구현이 제대로 수행되지 않는 경우에는 도움이되지 않습니다. ThreadLocalRandom.current().nextInt(100, 100_000)예상 크기 의 결과를 사용하더라도 더 나은 결과를 얻을 수 있습니다.
Holger

각 항목의 계산 비용이 상당했던 사용 사례에서 잘 수행되었습니다. 병렬 처리로 거의 선형으로 확장 된 총 CPU 사용량과 처리량을 98 % 쉽게 달성 할 수있었습니다. 기본적으로 배치 크기가 1에서 10 밀리 초 사이가되도록 배치 크기를 확보하는 것이 중요합니다. 이는 스레드 핸드 오프 비용보다 훨씬 높으며 작업 세분성 문제를 일으키기에는 너무 오래 걸리지 않습니다. 나는 이 포스트 의 끝을 향해 벤치 마크 결과를 발표했다 .
Marko Topolnik

솔루션은 예상 크기 (정확한 크기) ArraySpliterator가진 솔루션을 분리합니다 . 따라서 스트림 구현은 배열 크기 vs Long.MAX_VALUE를 볼 수 있습니다.이 불균형을 고려하고 더 큰 Long.MAX_VALUE분할 할 수 없을 때까지 "알 수없는"의 의미를 무시하고 "더 큰"분할기를 분할하십시오. 그런 다음 청크가 충분하지 않으면 알려진 크기를 사용하여 어레이 기반 스플리터를 분할합니다. 예, 이것은 잘 작동하지만 크기에 관계없이 크기 추정이 필요하다는 내 진술과 모순되지 않습니다.
Holger

입력에 대한 크기 추정이 필요하지 않기 때문에 오해 인 것 같습니다. 개별 스플릿에서만 가능합니다.
Marko Topolnik

음, 내 첫 코멘트 "이었다 . 그것은 완전히 가짜만큼이 대략 당신의 불균형 분할의 비율을 반영으로 할 수 있습니다. 당신은 크기 추정치를해야 할 핵심은 여기에"영업의 코드는 하나의 요소를 포함하는 다른 spliterator를 만드는했지만 여전히 알 수없는 크기를보고합니다. 이것이 스트림 구현을 무력하게 만드는 것입니다. 새로운 스플리터의 예상 수치는 훨씬 작을 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.