क्या आप अज्ञात आकार के असंतुलित स्प्लिटर को पुन: संतुलित कर सकते हैं?


12

मैं Streamअज्ञात संख्या के दूरस्थ रूप से संग्रहित JSON फ़ाइलों (फ़ाइलों की संख्या ज्ञात नहीं है) के एक विषम सेट के समानांतर प्रसंस्करण का उपयोग करना चाहता हूं । फाइलें आकार में व्यापक रूप से भिन्न हो सकती हैं, 1 JSON रिकॉर्ड प्रति फ़ाइल से कुछ अन्य फाइलों में 100,000 रिकॉर्ड तक। इस मामले में एक JSON रिकॉर्ड का अर्थ है कि फ़ाइल में एक पंक्ति के रूप में प्रतिनिधित्व किया गया एक स्व-निहित JSON ऑब्जेक्ट।

मैं वास्तव में इसके लिए स्ट्रीम का उपयोग करना चाहता हूं और इसलिए मैंने इसे लागू किया 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's' के जीवन चक्र में एक निश्चित बिंदु के बाद नहीं कहा जाता है , इसलिए अंत में छोटे बैचों को वितरित करने के लिए अतिरिक्त तर्क trySplitशायद ही कभी निष्पादित होता है।

ध्यान दें कि trySplit से लौटे सभी स्प्लिटर कैसे एक ही पुनरावृत्त को साझा करते pathsहैं। मैंने सोचा कि यह सभी स्प्लिटरों में काम को संतुलित करने के लिए एक बहुत ही चतुर तरीका था, लेकिन यह पूर्ण समानता प्राप्त करने के लिए पर्याप्त नहीं है।

मैं चाहता हूँ कि समानांतर प्रसंस्करण पहले फाइलों में आगे बढ़े, और फिर जब कुछ बड़ी फ़ाइलों को अभी भी छोड़ दिया जाता है, तो मैं शेष फाइलों के विखंडू में समानांतर करना चाहता हूं। यह elseब्लॉक के इरादे के अंत में था trySplit

क्या इस समस्या के आसपास कोई आसान / सरल / विहित तरीका है?


2
आपको एक आकार अनुमान की आवश्यकता है। यह पूरी तरह से फर्जी हो सकता है, जब तक कि यह आपके असंतुलित विभाजन के अनुपात को दर्शाता है। अन्यथा, स्ट्रीम को पता नहीं है कि विभाजन असंतुलित हैं और एक बार निश्चित संख्या में विखंडू बनने के बाद बंद हो जाएगा।
होल्गर

@ होल्गर क्या आप इस बारे में विस्तार से बता सकते हैं कि "एक बार एक निश्चित संख्या में विखंडू बनने के बाद क्या रुकेगा" या इसके लिए मुझे JDK स्रोत पर इंगित करें? चंक्स की संख्या क्या है जहां वह रुकती है?
एलेक्स आर

कोड अप्रासंगिक है, क्योंकि यह बहुत अधिक अप्रासंगिक कार्यान्वयन विवरण दिखाएगा, जो किसी भी समय बदल सकता है। प्रासंगिक बिंदु यह है कि कार्यान्वयन अक्सर विभाजन को पर्याप्त रूप से कॉल करने की कोशिश करता है, ताकि प्रत्येक श्रमिक थ्रेड (सीपीयू कोर की संख्या के लिए समायोजित) को कुछ करना पड़े। कंप्यूटिंग समय में अप्रत्याशित अंतर की भरपाई करने के लिए, यह संभवत: काम करने की अनुमति देने के लिए कार्यकर्ता थ्रेड्स की तुलना में और भी अधिक विखंडन का उत्पादन करेगा और अनुमानित आकार का उपयोग करने के लिए अनुमानी के रूप में उपयोग करेगा (जैसे यह तय करने के लिए कि उप विभाजक को आगे विभाजित करने के लिए)। यह भी देखें stackoverflow.com/a/48174508/2711488
होल्गर

आपकी टिप्पणी को समझने के लिए मैंने कुछ प्रयोग किए। उत्तराधिकारी काफी आदिम प्रतीत होते हैं। ऐसा लगता है, लौटने Long.MAX_VALUEसे अत्यधिक और अनावश्यक विभाजन हो जाता है, जबकि इसके अलावा कोई भी अनुमान Long.MAX_VALUEआगे रुकने के कारण, समानता की हत्या करता है। सटीक अनुमानों के मिश्रण को लौटाने से कोई बुद्धिमान अनुकूलन नहीं होता है।
एलेक्स आर

मैं यह दावा नहीं कर रहा हूं कि कार्यान्वयन की रणनीति बहुत स्मार्ट थी, लेकिन कम से कम, यह अनुमानित आकारों के साथ कुछ परिदृश्यों के लिए काम करता है (अन्यथा, उस बारे में कहीं अधिक बग रिपोर्ट थे)। तो ऐसा लगता है, प्रयोगों के दौरान आपकी ओर से कुछ त्रुटियां थीं। उदाहरण के लिए, आपके प्रश्न के कोड में, आप विस्तार कर रहे हैं, AbstractSpliteratorलेकिन ओवरराइडिंग trySplit()जो कि इसके अलावा किसी भी चीज़ के लिए एक खराब कॉम्बो है Long.MAX_VALUE, जैसा कि आप आकार के अनुमान को स्वीकार नहीं कर रहे हैं trySplit()। के बाद trySplit(), आकार का अनुमान उन तत्वों की संख्या से कम किया जाना चाहिए जो अलग हो गए हैं।
होल्गर

जवाबों:


0

आपका trySplitबराबर आकार के उत्पादन विभाजन, अंतर्निहित फ़ाइलों का आकार की परवाह किए बिना ऐसा करना चाहिए। आपको सभी फाइलों को एक ही इकाई के रूप में ArrayListमानना ​​चाहिए और प्रत्येक बार JSON ऑब्जेक्ट्स की समान संख्या के साथ-बैकवर्ड स्प्लिटर को भरना चाहिए । वस्तुओं की संख्या ऐसी होनी चाहिए कि एक विभाजन की प्रक्रिया 1 और 10 मिली सेकेंड के बीच हो: 1 एमएस से कम हो और आप बैच को श्रमिक धागे को सौंपने की लागत के करीब पहुंचना शुरू कर देते हैं, इससे अधिक और आप असमान सीपीयू लोड को जोखिम में डालना शुरू कर देते हैं। ऐसे कार्य जो बहुत मोटे होते हैं।

स्प्लिटेटर एक आकार के अनुमान की रिपोर्ट करने के लिए बाध्य नहीं है, और आप पहले से ही यह सही ढंग से कर रहे हैं: आपका अनुमान है Long.MAX_VALUE, जो एक विशेष मूल्य का अर्थ है "अनबाउंड"। हालाँकि, यदि आपके पास एकल JSON ऑब्जेक्ट के साथ कई फ़ाइलें हैं, तो आकार 1 के बैचों के परिणामस्वरूप, यह आपके प्रदर्शन को दो तरह से नुकसान पहुंचाएगा: फ़ाइल को खोलने-पढ़ने-बंद करने का ओवरहेड एक अड़चन बन सकता है और, यदि आप भागने का प्रबंधन करते हैं एक वस्तु के प्रसंस्करण की लागत की तुलना में थ्रेड हैंडऑफ की लागत महत्वपूर्ण हो सकती है, जिससे फिर से अड़चन पैदा हो सकती है।

पांच साल पहले मैं इसी तरह की समस्या को हल कर रहा था, आप मेरे समाधान पर एक नज़र डाल सकते हैं


हां, आप "एक आकार के अनुमान की रिपोर्ट करने के लिए बाध्य नहीं हैं" और Long.MAX_VALUEएक अज्ञात आकार का सही वर्णन कर रहे हैं, लेकिन यह तब मदद नहीं करता है जब वास्तविक स्ट्रीम कार्यान्वयन तब खराब प्रदर्शन करता है। यहां तक ​​कि ThreadLocalRandom.current().nextInt(100, 100_000)अनुमानित आकार के परिणाम का उपयोग करने से बेहतर परिणाम मिलते हैं।
होल्गर

इसने मेरे उपयोग के मामलों के लिए अच्छा प्रदर्शन किया, जहां प्रत्येक वस्तु की कम्प्यूटेशनल लागत काफी थी। मैं आसानी से 98% कुल सीपीयू उपयोग को प्राप्त कर रहा था और थ्रूपुट लगभग रेखीय रूप से समानता के साथ स्केल किया गया था। मूल रूप से, बैच का आकार सही होना महत्वपूर्ण है ताकि यह प्रसंस्करण 1 और 10 मिलीसेकंड के बीच हो। यह किसी भी थ्रेड हैंडऑफ की लागत से अधिक है और कार्य ग्रैन्युलैरिटी मुद्दों के कारण बहुत लंबा नहीं है। मैंने इस पोस्ट के अंत में बेंचमार्क परिणाम प्रकाशित किए हैं ।
मार्को टोपोलनिक

आपका समाधान बंद विभाजन एक ArraySpliteratorजो है एक अनुमान के अनुसार आकार (यहां तक कि एक सटीक आकार)। इसलिए स्ट्रीम कार्यान्वयन सरणी आकार बनाम को देखेगा Long.MAX_VALUE, इस असंतुलित पर विचार करें और "बड़ा" स्प्लिटर (जो Long.MAX_VALUE"अज्ञात" का अर्थ है) को विभाजित करें, जब तक कि यह आगे विभाजित न हो। फिर, यदि पर्याप्त विखंडू नहीं हैं, तो यह उनके ज्ञात आकारों का उपयोग करते हुए सरणी आधारित स्प्लिटेटर को विभाजित करेगा। हां, यह बहुत अच्छी तरह से काम करता है, लेकिन मेरे कथन का खंडन नहीं करता है कि आपको आकार के अनुमान की आवश्यकता है, चाहे वह कितना भी गरीब क्यों न हो।
होल्गर

ठीक है, तो यह एक गलतफहमी प्रतीत होती है --- क्योंकि आपको इनपुट पर आकार के अनुमान की आवश्यकता नहीं है। बस व्यक्तिगत विभाजन पर, और आप हमेशा ऐसा कर सकते हैं।
मार्को टोपोलनिक

ठीक है, मेरी पहली टिप्पणी थी, " आपको एक आकार अनुमान की आवश्यकता है। यह पूरी तरह से फर्जी हो सकता है, जब तक कि यह आपके असंतुलित विभाजन के अनुपात को दर्शाता है। " यहां महत्वपूर्ण बिंदु यह था कि ओपी का कोड एक अन्य विभाजक बनाता है, जिसमें एक ही होता है लेकिन अभी भी एक अज्ञात आकार की रिपोर्टिंग। यही वह है जो स्ट्रीम कार्यान्वयन को असहाय बनाता है। नए स्प्लिटर के लिए कोई अनुमान संख्या काफी कम Long.MAX_VALUEहोगी।
होल्गर

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;
            }
        }
    }
}
हमारी साइट का प्रयोग करके, आप स्वीकार करते हैं कि आपने हमारी Cookie Policy और निजता नीति को पढ़ और समझा लिया है।
Licensed under cc by-sa 3.0 with attribution required.