2013-07-09 7 views
17

मेरे शोध के एक हिस्से के रूप में मैं जावा में एक उच्च लोड टीसीपी/आईपी इको सर्वर लिख रहा हूं। मैं लगभग 3-4k ग्राहकों की सेवा करना चाहता हूं और प्रति सेकंड अधिकतम संभव संदेश देख सकता हूं कि मैं इससे बाहर निकल सकता हूं। संदेश का आकार काफी छोटा है - 100 बाइट तक। इस काम में कोई व्यावहारिक उद्देश्य नहीं है - केवल एक शोध।जावा हाई-लोड एनआईओ टीसीपी सर्वर

मैंने देखा है कि कई प्रस्तुतियों (हॉर्नेटक बेंचमार्क, एलएमएक्स विघटन वार्ता, आदि) के अनुसार, असली दुनिया उच्च लोड सिस्टम प्रति सेकंड लाखों लेनदेन की सेवा करते हैं (मुझे विश्वास है कि विघटनकर्ता ने लगभग 6 मिलियन और हॉर्नेट का उल्लेख किया है 8.5)। उदाहरण के लिए, this post बताता है कि 40 एम एमपीएस तक हासिल करना संभव है। इसलिए मैंने इसे आधुनिक हार्डवेयर के सक्षम होने के बारे में अनुमान लगाया।

मैंने सरल एकल थ्रेडेड एनआईओ सर्वर लिखा और एक लोड टेस्ट लॉन्च किया। मुझे आश्चर्य नहीं हुआ कि मैं स्थानीयहोस्ट पर केवल 100k एमपीएस और वास्तविक नेटवर्किंग के साथ 25k प्राप्त कर सकता हूं। संख्या काफी छोटी दिखती है। मैं Win7 x64, कोर i7 पर परीक्षण कर रहा था। सीपीयू लोड को देखते हुए - केवल एक कोर व्यस्त है (जिसे एकल-थ्रेडेड ऐप पर अपेक्षित किया जाता है), जबकि शेष निष्क्रिय रहते हैं। हालांकि अगर मैं सभी 8 कोर (वर्चुअल समेत) लोड करता हूं, तो मेरे पास 800k से अधिक एमपीएस नहीं होंगे - 0 मिलियन के करीब भी नहीं :)

मेरा प्रश्न है: ग्राहकों को भारी मात्रा में संदेश देने के लिए एक सामान्य पैटर्न क्या है ? क्या मुझे एक एकल जेवीएम के अंदर कई अलग-अलग सॉकेट पर नेटवर्किंग लोड वितरित करना चाहिए और कई कोरों को लोड वितरित करने के लिए हैप्रोक्सी जैसे कुछ प्रकार के लोड बैलेंसर का उपयोग करना चाहिए? या मुझे अपने एनआईओ कोड में एकाधिक चयनकर्ताओं का उपयोग करने की ओर देखना चाहिए? या हो सकता है कि कई जेवीएम के बीच लोड भी वितरित करें और उनके बीच एक इंटर-प्रोसेस संचार बनाने के लिए क्रॉनिकल का उपयोग करें? CentOS जैसे उचित सर्वरसाइड ओएस पर परीक्षण करना एक बड़ा अंतर बनाता है (शायद यह विंडोज़ है जो चीजों को धीमा कर देता है)?

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

public class EchoServer { 

private static final int BUFFER_SIZE = 1024; 
private final static int DEFAULT_PORT = 9090; 

// The buffer into which we'll read data when it's available 
private ByteBuffer readBuffer = ByteBuffer.allocate(BUFFER_SIZE); 

private InetAddress hostAddress = null; 

private int port; 
private Selector selector; 

private long loopTime; 
private long numMessages = 0; 

public EchoServer() throws IOException { 
    this(DEFAULT_PORT); 
} 

public EchoServer(int port) throws IOException { 
    this.port = port; 
    selector = initSelector(); 
    loop(); 
} 

private void loop() { 
    while (true) { 
     try{ 
      selector.select(); 
      Iterator<SelectionKey> selectedKeys = selector.selectedKeys().iterator(); 
      while (selectedKeys.hasNext()) { 
       SelectionKey key = selectedKeys.next(); 
       selectedKeys.remove(); 

       if (!key.isValid()) { 
        continue; 
       } 

       // Check what event is available and deal with it 
       if (key.isAcceptable()) { 
        accept(key); 
       } else if (key.isReadable()) { 
        read(key); 
       } else if (key.isWritable()) { 
        write(key); 
       } 
      } 

     } catch (Exception e) { 
      e.printStackTrace(); 
      System.exit(1); 
     } 
    } 
} 

private void accept(SelectionKey key) throws IOException { 
    ServerSocketChannel serverSocketChannel = (ServerSocketChannel) key.channel(); 

    SocketChannel socketChannel = serverSocketChannel.accept(); 
    socketChannel.configureBlocking(false); 
    socketChannel.setOption(StandardSocketOptions.SO_KEEPALIVE, true); 
    socketChannel.setOption(StandardSocketOptions.TCP_NODELAY, true); 
    socketChannel.register(selector, SelectionKey.OP_READ); 

    System.out.println("Client is connected"); 
} 

private void read(SelectionKey key) throws IOException { 
    SocketChannel socketChannel = (SocketChannel) key.channel(); 

    // Clear out our read buffer so it's ready for new data 
    readBuffer.clear(); 

    // Attempt to read off the channel 
    int numRead; 
    try { 
     numRead = socketChannel.read(readBuffer); 
    } catch (IOException e) { 
     key.cancel(); 
     socketChannel.close(); 

     System.out.println("Forceful shutdown"); 
     return; 
    } 

    if (numRead == -1) { 
     System.out.println("Graceful shutdown"); 
     key.channel().close(); 
     key.cancel(); 

     return; 
    } 

    socketChannel.register(selector, SelectionKey.OP_WRITE); 

    numMessages++; 
    if (numMessages%100000 == 0) { 
     long elapsed = System.currentTimeMillis() - loopTime; 
     loopTime = System.currentTimeMillis(); 
     System.out.println(elapsed); 
    } 
} 

private void write(SelectionKey key) throws IOException { 
    SocketChannel socketChannel = (SocketChannel) key.channel(); 
    ByteBuffer dummyResponse = ByteBuffer.wrap("ok".getBytes("UTF-8")); 

    socketChannel.write(dummyResponse); 
    if (dummyResponse.remaining() > 0) { 
     System.err.print("Filled UP"); 
    } 

    key.interestOps(SelectionKey.OP_READ); 
} 

private Selector initSelector() throws IOException { 
    Selector socketSelector = SelectorProvider.provider().openSelector(); 

    ServerSocketChannel serverChannel = ServerSocketChannel.open(); 
    serverChannel.configureBlocking(false); 

    InetSocketAddress isa = new InetSocketAddress(hostAddress, port); 
    serverChannel.socket().bind(isa); 
    serverChannel.register(socketSelector, SelectionKey.OP_ACCEPT); 
    return socketSelector; 
} 

public static void main(String[] args) throws IOException { 
    System.out.println("Starting echo server"); 
    new EchoServer(); 
} 
} 
+4

40 मिलियन लेनदेन प्रति सेकंड ** प्रति सर्वर ** ?! वे एक बाइट के साथ जवाब देना चाहिए। –

+0

मुझे विश्वास है कि व्यापार तर्क के बिना था - केवल संदेशों का एक roundtrips। लेकिन हाँ, मैंने उस पोस्ट में जो देखा है। अद्भुत संख्या – Juriy

+1

लिखने से पहले आपको OP_WRITE की प्रतीक्षा करने की आवश्यकता नहीं है। शून्य लंबाई लिखने के बाद आपको केवल ऐसा करने की आवश्यकता है। चैनल बंद करने से पहले या बाद में आपको कुंजी को रद्द करने की आवश्यकता नहीं है। – EJP

उत्तर

4

लेखन के आसपास आपका तर्क दोषपूर्ण है। लिखने के लिए आपको तत्काल लिखने का प्रयास करना चाहिए। यदि write() शून्य लौटाता है तो यह है तो OP_WRITE के लिए पंजीकरण करने के लिए समय, चैनल लिखने पर लिखने का प्रयास करें, और लिखने के बाद OP_WRITE के लिए पंजीकरण रद्द करें। आप यहां बड़ी मात्रा में विलंबता जोड़ रहे हैं। आप OP_READ के लिए पंजीकरण के दौरान और भी अधिक विलंब जोड़ रहे हैं जबकि आप यह सब कर रहे हैं।

+0

धन्यवाद @EJP। क्या आप कृपया मुझे कुछ उदाहरण दे सकते हैं? एनआईओ का उपयोग कर अधिकतम प्रदर्शन प्राप्त करने का सही तरीका क्या है? – FaNaJ

+0

मैंने कुछ उदाहरण प्रदान करने से बेहतर किया है। मैंने आपको एक सामान्य सिद्धांत प्रदान किया है। विलंबता से बचें अधिकतम अधिकतम प्राप्त करने के तरीकों में से एक है। – EJP

+0

ईजेपी, क्या आप यह जानने का एक तरीका सुझा सकते हैं कि OP_WRITE मोड में एक चैनल क्यों छोड़ना है जब लिखने की कोई जरूरी आवश्यकता नहीं है? मैं कल्पना कर सकता हूं कि प्रोसेसर को पढ़ने या लिखने के लिए तैयार होना चाहिए, लेकिन उम्मीद नहीं थी कि यह प्रदर्शन को प्रभावित करे। यह चुनने में इतनी धीमी क्यों है कि चयनकर्ता लिखने के लिए तैयार है या नहीं? –

19
what is a typical pattern for serving massive amounts of messages to clients? 

कई संभावित पैटर्न होते हैं: कई JVMs माध्यम से जा रहा बिना सभी कोर का उपयोग करने का एक आसान तरीका है:

  1. किसी एकल थ्रेड कनेक्शन स्वीकार और एक चयनकर्ता का उपयोग करके पढ़ी है।
  2. एक बार आपके पास एक संदेश बनाने के लिए पर्याप्त बाइट्स हो जाने के बाद, इसे रिंग बफर जैसे निर्माण का उपयोग करके किसी अन्य कोर पर पास कर दें। विघटनकर्ता जावा ढांचा इसके लिए एक अच्छा मैच है। यह एक अच्छा पैटर्न है यदि प्रसंस्करण की आवश्यकता है कि एक पूर्ण संदेश क्या है हल्का वजन। उदाहरण के लिए यदि आपके पास लम्बाई प्रीफ़िक्स्ड प्रोटोकॉल है तो आप तब तक प्रतीक्षा कर सकते हैं जब तक आप बाइट्स की अपेक्षित संख्या प्राप्त न करें और फिर इसे किसी अन्य थ्रेड पर भेजें। यदि प्रोटोकॉल का पार्सिंग बहुत भारी है तो आप कनेक्शन को स्वीकार करने या नेटवर्क के बाइट पढ़ने से रोकने के लिए इस एकल थ्रेड को दबा सकते हैं।
  3. अपने कार्यकर्ता थ्रेड पर, जो एक अंगूठी बफर से डेटा प्राप्त करता है, वास्तविक प्रसंस्करण करें।
  4. आप या तो अपने कार्यकर्ता धागे पर या किसी अन्य एग्रीगेटर थ्रेड के माध्यम से प्रतिक्रियाएं लिखते हैं।

यह इसका सारांश है। यहां कई और संभावनाएं हैं और उत्तर वास्तव में आपके द्वारा लिखे गए एप्लिकेशन के प्रकार पर निर्भर करता है। कुछ उदाहरण हैं:

  1. एक सीपीयू भारी राज्यविहीन आवेदन कहना एक छवि प्रसंस्करण आवेदन। प्रति अनुरोध किए गए सीपीयू/जीपीयू काम की मात्रा शायद एक बहुत ही निष्क्रिय इंटर-थ्रेड संचार समाधान द्वारा उत्पन्न ओवरहेड की तुलना में काफी अधिक होगी। इस मामले में एक आसान समाधान एक कतार से काम खींचने वाले कार्यकर्ता धागे का एक गुच्छा है। ध्यान दें कि प्रति कार्य एक कतार के बजाय यह एक कतार कैसे है। लाभ यह है कि यह स्वाभाविक रूप से संतुलित लोड होता है। प्रत्येक कार्यकर्ता इसके काम को पूरा करता है और फिर एकल-निर्माता एकाधिक उपभोक्ता कतार चुनाव करता है। भले ही यह विवाद का स्रोत है, छवि-प्रसंस्करण कार्य (सेकंड?) किसी भी सिंक्रनाइज़ेशन विकल्प से कहीं अधिक महंगा होना चाहिए।
  2. एक शुद्ध आईओ एप्लिकेशन उदा। एक आंकड़े सर्वर जो अनुरोध के लिए कुछ काउंटरों को बढ़ाता है: यहां आप लगभग कोई CPU भारी काम नहीं करते हैं। अधिकांश काम केवल बाइट्स पढ़ना और बाइट लिखना है। एक बहु थ्रेडेड एप्लिकेशन आपको यहां महत्वपूर्ण लाभ नहीं दे सकता है। वास्तव में यह चीजों को धीमा कर सकता है अगर कतार वस्तुओं में जो समय लगता है वह उन्हें संसाधित करने के समय से अधिक होता है। एक थ्रेडेड जावा सर्वर आसानी से 1 जी लिंक को संतृप्त करने में सक्षम होना चाहिए।
  3. राज्यव्यापी अनुप्रयोग जिसके लिए मध्यम मात्रा में प्रसंस्करण की आवश्यकता होती है उदा। एक सामान्य व्यावसायिक अनुप्रयोग: यहां प्रत्येक ग्राहक के पास कुछ राज्य होता है जो यह निर्धारित करता है कि प्रत्येक अनुरोध को कैसे प्रबंधित किया जाता है। मान लीजिए कि प्रसंस्करण गैर-तुच्छ है क्योंकि हम बहु-थ्रेडेड जाते हैं, हम ग्राहकों को कुछ धागे से जोड़ सकते हैं। यह अभिनेता वास्तुकला का एक रूप है:

    i) जब कोई ग्राहक पहले किसी कार्यकर्ता को हैश जोड़ता है। हो सकता है कि आप इसे कुछ क्लाइंट आईडी के साथ करना चाहें, ताकि अगर यह डिस्कनेक्ट हो और फिर से कनेक्ट हो जाए तो यह अभी भी एक ही कार्यकर्ता/अभिनेता को सौंपा गया है।

    ii) जब पाठक धागा एक पूर्ण अनुरोध पढ़ता है तो उसे सही कार्यकर्ता/अभिनेता के लिए रिंग-बफर पर रखा जाता है। चूंकि एक ही कार्यकर्ता हमेशा एक विशेष ग्राहक को संसाधित करता है, इसलिए सभी राज्य थ्रेड स्थानीय होना चाहिए, सभी प्रसंस्करण तर्क सरल और एकल-थ्रेडेड बनाना।

    iii) कार्यकर्ता धागा अनुरोध लिख सकता है। हमेशा लिखने का प्रयास करें()। यदि आपका सभी डेटा केवल तभी लिखा नहीं जा सकता है, तो क्या आप OP_WRITE के लिए पंजीकरण करते हैं। वास्तव में कुछ बकाया होने पर कार्यकर्ता थ्रेड को केवल चयन कॉल करने की आवश्यकता होती है। अधिकांश लिखना सिर्फ इस अनावश्यक बनाने में सफल होना चाहिए। यहां की चाल चुनिंदा कॉल के बीच संतुलन और रिंग बफर को अधिक अनुरोधों के लिए मतदान कर रही है। आप एक लेखक धागे को भी नियोजित कर सकते हैं जिसका अनुरोध केवल अनुरोध लिखना है। प्रत्येक कार्यकर्ता धागा इसे एक अंगूठी बफर पर इस प्रतिक्रिया को अपने एकल लेखक धागे से जोड़ सकता है। एकल लेखक थ्रेड राउंड-रॉबिन प्रत्येक आने वाली रिंग-बफर चुनाव करता है और ग्राहकों को डेटा लिखता है। चयन से पहले लिखने की कोशिश करने के बारे में फिर से चेतावनी लागू होती है जैसे कि कई अंगूठी बफर और कॉल का चयन करने के बीच संतुलन की चाल होती है।

आप बाहर बिंदु के रूप में देखते हैं कई अन्य विकल्प:

Should I distribute networking load over several different sockets inside a single JVM and use some sort of load balancer like HAProxy to distribute load to multiple cores?

आप ऐसा कर सकते हैं, लेकिन IMHO इस एक लोड संतुलन के लिए सबसे अच्छा उपयोग नहीं है। यह आपको स्वतंत्र जेवीएम खरीदता है जो स्वयं पर असफल हो सकता है लेकिन शायद एक एकल जेवीएम ऐप लिखने से धीमा हो जाएगा जो बहु-थ्रेडेड है।एप्लिकेशन को लिखना आसान हो सकता है हालांकि यह सिंगल थ्रेडेड होगा।

Or I should look towards using multiple Selectors in my NIO code? 

आप यह भी कर सकते हैं। इस बारे में कुछ संकेतों के लिए नग्निक्स आर्किटेक्चर को देखें।

Or maybe even distribute the load between multiple JVMs and use Chronicle to build an inter-process communication between them? यह भी एक विकल्प है। क्रॉनिकल आपको एक लाभ देता है कि स्मृति मैप की गई फ़ाइलें मध्य में छोड़ने वाली प्रक्रिया के लिए अधिक लचीला होती हैं। आपको अभी भी बहुत सारे प्रदर्शन मिलते हैं क्योंकि साझा संचार के माध्यम से सभी संचार किया जाता है।

Will testing on a proper serverside OS like CentOS make a big difference (maybe it is Windows that slows things down)? 

मुझे इस बारे में पता नहीं है। संभावना नहीं है। यदि जावा पूरी तरह से मूल विंडोज एपीआई का उपयोग करता है, तो इससे कोई फर्क नहीं पड़ता। मैं 40 मिलियन लेनदेन/सेक आकृति (बिना उपयोगकर्ता स्पेस नेटवर्किंग स्टैक + यूडीपी के) के बारे में बहुत संदिग्ध हूं लेकिन सूचीबद्ध आर्किटेक्चर को बहुत अच्छा करना चाहिए।

ये आर्किटेक्चर अच्छी तरह से काम करते हैं क्योंकि वे सिंगल-लेखक आर्किटेक्चर हैं जो इंटर थ्रेड संचार के लिए बाध्य सरणी आधारित डेटा संरचनाओं का उपयोग करते हैं। निर्धारित करें कि बहु-थ्रेडेड भी जवाब है या नहीं। कई मामलों में इसकी आवश्यकता नहीं है और मंदी हो सकती है।

मेमोरी आवंटन योजनाओं में देखने के लिए एक और क्षेत्र है। विशिष्ट रूप से बफर आवंटित करने और पुन: उपयोग करने की रणनीति महत्वपूर्ण लाभ का कारण बन सकती है। सही बफर पुन: उपयोग रणनीति आवेदन पर निर्भर है। यह देखने के लिए कि क्या वे आपको लाभ पहुंचा सकते हैं, यह देखने के लिए दोस्त-स्मृति आवंटन, क्षेत्र आवंटन आदि जैसी योजनाएं देखें। जेवीएम जीसी अधिकांश काम भारों के लिए बहुत बढ़िया काम करता है, हालांकि इस मार्ग से नीचे जाने से पहले हमेशा उपाय करें।

प्रोटोकॉल डिज़ाइन का प्रदर्शन पर भी बड़ा प्रभाव पड़ता है। मैं लंबाई prefixed प्रोटोकॉल पसंद करते हैं क्योंकि वे आपको बफर और/या बफर विलय की सूचियों से परहेज से सही आकार के बफर आवंटित करने देते हैं। लम्बाई प्रीफिक्स्ड प्रोटोकॉल यह भी तय करना आसान बनाता है कि अनुरोध कब सौंपना है - बस num bytes == expected देखें। श्रमिक धागे द्वारा वास्तविक पार्सिंग किया जा सकता है। सीरियलाइजेशन और deserialization लंबाई पूर्ववर्ती प्रोटोकॉल से परे फैली हुई है। आवंटन के बजाए बफर पर फ्लाईवेट पैटर्न जैसे पैटर्न यहां मदद करते हैं। इन सिद्धांतों में से कुछ के लिए SBE देखें।

जैसा कि आप कल्पना कर सकते हैं कि एक संपूर्ण ग्रंथ यहां लिखा जा सकता है। यह आपको सही दिशा में स्थापित करना चाहिए। चेतावनी: हमेशा मापें और सुनिश्चित करें कि आपको सबसे सरल विकल्प की तुलना में अधिक प्रदर्शन की आवश्यकता है। प्रदर्शन सुधारों के कभी खत्म होने वाले काले-छेद में चूसना आसान नहीं है।

2

आप नियमित हार्डवेयर के साथ प्रति सेकंड कुछ सौ हजार अनुरोधों को शीर्ष पर पहुंचेंगे। कम से कम यही मेरा अनुभव समान समाधान बनाने की कोशिश कर रहा है, और the Tech Empower Web Frameworks Benchmark भी सहमत है।

सबसे अच्छा तरीका, आमतौर पर, इस पर निर्भर करता है कि आपके पास आईओ-बाउंड या सीपीयू-बाध्य भार है या नहीं।

आईओ-बाउंड लोड (उच्च विलंबता) के लिए, आपको कई धागे के साथ async io करना होगा। सर्वोत्तम प्रदर्शन के लिए आपको जितना संभव हो सके धागे के बीच हैंडऑफ को रद्द करने का प्रयास करना चाहिए। इसलिए एक समर्पित चयनकर्ता धागा और प्रसंस्करण के लिए एक और थ्रेडपूल होने से थ्रेडपूल होने की तुलना में धीमी गति होती है जहां प्रत्येक थ्रेड या तो चयन या प्रसंस्करण करता है, ताकि एक अनुरोध को एक ही थ्रेड द्वारा सर्वोत्तम मामले में संभाला जा सके (यदि io तुरंत उपलब्ध हो)। इस प्रकार का सेटअप कोड के लिए अधिक जटिल है लेकिन तेज़ है और मुझे विश्वास नहीं है कि कोई भी एसिंक वेब फ्रेमवर्क इसका पूरी तरह से शोषण करता है।

सीपीयू-बाध्य भार के लिए प्रति अनुरोध एक थ्रेड आमतौर पर सबसे तेज़ होता है, क्योंकि आप संदर्भ स्विच से बचते हैं।

संबंधित मुद्दे