2012-10-12 16 views
25

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

दिलचस्प बात यह है कि रेडिस इतनी अच्छी तरह से किराया नहीं था। या तो पायथन कुछ जादू कर रहा है (फ़ाइल संग्रहित) या रेडिस का मेरा संस्करण बेहद धीमा है।

मुझे नहीं पता कि यह मेरे कोड की संरचना के तरीके के कारण है या क्या, लेकिन मैं लालसा से बेहतर करने की अपेक्षा कर रहा था।

रेडिस कैश बनाने के लिए, मैंने 5 मिनट की समाप्ति के साथ फ़ाइल नाम से प्राप्त कुंजी पर अपना बाइनरी डेटा (इस मामले में, एक HTML पृष्ठ) सेट किया है।

सभी मामलों में, फ़ाइल हैंडलिंग f.read() के साथ किया जाता है (यह f.readlines() से ~ 3x तेज है, और मुझे बाइनरी ब्लॉब की आवश्यकता है)।

क्या मेरी तुलना में कुछ याद आ रही है, या क्या रेडिस वास्तव में डिस्क के लिए कोई मिलान नहीं है? क्या पाइथन कहीं फाइल को कैश कर रहा है, और हर बार इसे पुनः प्राप्त कर रहा है? रेडिस तक पहुंच से यह इतना तेज़ क्यों है?

मैं रेडिस 2.8, पायथन 2.7, और रेडिस-पीई का उपयोग कर रहा हूं, सब 64 बिट उबंटू सिस्टम पर।

मुझे नहीं लगता कि पाइथन विशेष रूप से जादुई कुछ भी कर रहा है, क्योंकि मैंने एक ऐसा फ़ंक्शन बनाया है जो फ़ाइल डेटा को एक पायथन ऑब्जेक्ट में संग्रहीत करता है और इसे हमेशा के लिए उत्पन्न करता है।

मैं चार फ़ंक्शन को कॉल मैं वर्गीकृत किया है:

फ़ाइल पढ़ी जा X बार

एक समारोह है कि अगर redis वस्तु स्मृति में अब भी है देखते हैं, इसे लोड, या नई फ़ाइल कैश (एकल के लिए कहा जाता है और कई redis उदाहरण)।

एक ऐसा फ़ंक्शन जो जेनरेटर बनाता है जो रेडिस डेटाबेस (रेडिस के एकल और बहु ​​उदाहरणों के साथ) से परिणाम उत्पन्न करता है।

और अंत में, फ़ाइल को स्मृति में संग्रहीत करना और इसे हमेशा के लिए उपज देना।

import redis 
import time 

def load_file(fp, fpKey, r, expiry): 
    with open(fp, "rb") as f: 
     data = f.read() 
    p = r.pipeline() 
    p.set(fpKey, data) 
    p.expire(fpKey, expiry) 
    p.execute() 
    return data 

def cache_or_get_gen(fp, expiry=300, r=redis.Redis(db=5)): 
    fpKey = "cached:"+fp 

    while True: 
     yield load_file(fp, fpKey, r, expiry) 
     t = time.time() 
     while time.time() - t - expiry < 0: 
      yield r.get(fpKey) 


def cache_or_get(fp, expiry=300, r=redis.Redis(db=5)): 

    fpKey = "cached:"+fp 

    if r.exists(fpKey): 
     return r.get(fpKey) 

    else: 
     with open(fp, "rb") as f: 
      data = f.read() 
     p = r.pipeline() 
     p.set(fpKey, data) 
     p.expire(fpKey, expiry) 
     p.execute() 
     return data 

def mem_cache(fp): 
    with open(fp, "rb") as f: 
     data = f.readlines() 
    while True: 
     yield data 

def stressTest(fp, trials = 10000): 

    # Read the file x number of times 
    a = time.time() 
    for x in range(trials): 
     with open(fp, "rb") as f: 
      data = f.read() 
    b = time.time() 
    readAvg = trials/(b-a) 


    # Generator version 

    # Read the file, cache it, read it with a new instance each time 
    a = time.time() 
    gen = cache_or_get_gen(fp) 
    for x in range(trials): 
     data = next(gen) 
    b = time.time() 
    cachedAvgGen = trials/(b-a) 

    # Read file, cache it, pass in redis instance each time 
    a = time.time() 
    r = redis.Redis(db=6) 
    gen = cache_or_get_gen(fp, r=r) 
    for x in range(trials): 
     data = next(gen) 
    b = time.time() 
    inCachedAvgGen = trials/(b-a) 


    # Non generator version  

    # Read the file, cache it, read it with a new instance each time 
    a = time.time() 
    for x in range(trials): 
     data = cache_or_get(fp) 
    b = time.time() 
    cachedAvg = trials/(b-a) 

    # Read file, cache it, pass in redis instance each time 
    a = time.time() 
    r = redis.Redis(db=6) 
    for x in range(trials): 
     data = cache_or_get(fp, r=r) 
    b = time.time() 
    inCachedAvg = trials/(b-a) 

    # Read file, cache it in python object 
    a = time.time() 
    for x in range(trials): 
     data = mem_cache(fp) 
    b = time.time() 
    memCachedAvg = trials/(b-a) 


    print "\n%s file reads: %.2f reads/second\n" %(trials, readAvg) 
    print "Yielding from generators for data:" 
    print "multi redis instance: %.2f reads/second (%.2f percent)" %(cachedAvgGen, (100*(cachedAvgGen-readAvg)/(readAvg))) 
    print "single redis instance: %.2f reads/second (%.2f percent)" %(inCachedAvgGen, (100*(inCachedAvgGen-readAvg)/(readAvg))) 
    print "Function calls to get data:" 
    print "multi redis instance: %.2f reads/second (%.2f percent)" %(cachedAvg, (100*(cachedAvg-readAvg)/(readAvg))) 
    print "single redis instance: %.2f reads/second (%.2f percent)" %(inCachedAvg, (100*(inCachedAvg-readAvg)/(readAvg))) 
    print "python cached object: %.2f reads/second (%.2f percent)" %(memCachedAvg, (100*(memCachedAvg-readAvg)/(readAvg))) 

if __name__ == "__main__": 
    fileToRead = "templates/index.html" 

    stressTest(fileToRead) 

और अब परिणाम:

10000 file reads: 30971.94 reads/second 

Yielding from generators for data: 
multi redis instance: 8489.28 reads/second (-72.59 percent) 
single redis instance: 8801.73 reads/second (-71.58 percent) 
Function calls to get data: 
multi redis instance: 5396.81 reads/second (-82.58 percent) 
single redis instance: 5419.19 reads/second (-82.50 percent) 
python cached object: 1522765.03 reads/second (4816.60 percent) 

परिणाम है कि एक) जनरेटर में दिलचस्प तेजी से काम करता है हर बार, ख) redis डिस्क से पढ़ने की तुलना में धीमी है बुला से कर रहे हैं, और ग) अजगर वस्तुओं से पढ़ना हास्यास्पद रूप से तेज़ है।

डिस्क से रीडिस में एक मेमोरी फ़ाइल से पढ़ने की तुलना में डिस्क से क्यों पढ़ना इतना तेज़ होगा?

संपादित करें: कुछ और जानकारी और परीक्षण।

मैं

data = r.get(fpKey) 
if data: 
    return r.get(fpKey) 

को समारोह की जगह परिणाम

if r.exists(fpKey): 
    data = r.get(fpKey) 


Function calls to get data using r.exists as test 
multi redis instance: 5320.51 reads/second (-82.34 percent) 
single redis instance: 5308.33 reads/second (-82.38 percent) 
python cached object: 1494123.68 reads/second (5348.17 percent) 


Function calls to get data using if data as test 
multi redis instance: 8540.91 reads/second (-71.25 percent) 
single redis instance: 7888.24 reads/second (-73.45 percent) 
python cached object: 1520226.17 reads/second (5132.01 percent) 

प्रत्येक समारोह कॉल पर एक नया redis उदाहरण बनाना वास्तव में एक उल्लेखनीय पढ़ने की गति पर कोई प्रभाव नहीं है से बहुत अलग नहीं है, परीक्षण से परीक्षण में परिवर्तनशीलता लाभ से बड़ी है।

श्रीपति कृष्णन ने यादृच्छिक फ़ाइल पढ़ने को लागू करने का सुझाव दिया। यह वह जगह है जहां कैशिंग वास्तव में मदद करने लगती है, क्योंकि हम इन परिणामों से देख सकते हैं।

Total number of files: 700 

10000 file reads: 274.28 reads/second 

Yielding from generators for data: 
multi redis instance: 15393.30 reads/second (5512.32 percent) 
single redis instance: 13228.62 reads/second (4723.09 percent) 
Function calls to get data: 
multi redis instance: 11213.54 reads/second (3988.40 percent) 
single redis instance: 14420.15 reads/second (5157.52 percent) 
python cached object: 607649.98 reads/second (221446.26 percent) 

फ़ाइल में परिवर्तनशीलता का एक विशाल राशि पढ़ने इतना प्रतिशत अंतर speedup का एक अच्छा सूचक नहीं है।

Total number of files: 700 

40000 file reads: 1168.23 reads/second 

Yielding from generators for data: 
multi redis instance: 14900.80 reads/second (1175.50 percent) 
single redis instance: 14318.28 reads/second (1125.64 percent) 
Function calls to get data: 
multi redis instance: 13563.36 reads/second (1061.02 percent) 
single redis instance: 13486.05 reads/second (1054.40 percent) 
python cached object: 587785.35 reads/second (50214.25 percent) 

मैंने यादृच्छिक रूप से कार्यों के माध्यम से प्रत्येक पास पर एक नई फ़ाइल का चयन करने के लिए random.choice (fileList) का उपयोग किया।

पूर्ण सार यहाँ है अगर किसी को भी इसे आज़माने के लिए चाहते हैं - https://gist.github.com/3885957

संपादित करें संपादित करें: एहसास नहीं था कि मैं जनरेटर के लिए एक एकल फाइल को बुला रहा था (हालांकि समारोह कॉल और जनरेटर के प्रदर्शन बहुत समान था)। जेनरेटर से अलग-अलग फाइलों का नतीजा यहां दिया गया है।

Total number of files: 700 
10000 file reads: 284.48 reads/second 

Yielding from generators for data: 
single redis instance: 11627.56 reads/second (3987.36 percent) 

Function calls to get data: 
single redis instance: 14615.83 reads/second (5037.81 percent) 

python cached object: 580285.56 reads/second (203884.21 percent) 
+1

मुझे नहीं लगता कि आप प्रत्येक फ़ंक्शन कॉल पर एक नया रेडिस उदाहरण कहां बना रहे थे। क्या यह सिर्फ डिफ़ॉल्ट तर्क बात थी? – jdi

+0

हां, यदि आप रेडिस इंस्टेंस पास नहीं करते हैं तो फ़ंक्शन कॉल एक नया एक def cache_or_get (fp, expiry = 300, r = redis.Redis (db = 5)): – MercuryRising

+2

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

उत्तर

28

यह संतरे की तुलना में एक सेब है। http://redis.io/topics/benchmarks

रेडिस एक कुशल दूरस्थ डेटा स्टोर है। प्रत्येक बार जब Redis पर कमांड निष्पादित किया जाता है, तो Redis सर्वर पर एक संदेश भेजा जाता है, और यदि क्लाइंट तुल्यकालिक है, तो यह उत्तर के लिए प्रतीक्षा कर रहा है। तो कमांड की लागत से परे, आप नेटवर्क राउंडट्रिप या आईपीसी के लिए भुगतान करेंगे।

आधुनिक हार्डवेयर पर, नेटवर्क राउंडट्रिप्स या आईपीसी अन्य परिचालनों की तुलना में आश्चर्यजनक रूप से महंगा हैं। यह कई कारकों के कारण है:

  • मध्यम (मुख्य रूप से नेटवर्क के लिए)
  • ऑपरेटिंग सिस्टम अनुसूचक (लिनक्स/यूनिक्स पर इसकी गारंटी नहीं)
  • मेमोरी कैश छूट जाए महंगे हैं की विलंबता के कच्चे विलंबता , और कैश की संभावना बढ़ जाती है जबकि क्लाइंट और सर्वर प्रक्रियाओं को निर्धारित/आउट किया जाता है।
  • उच्च अंत बक्से पर, NUMA दुष्प्रभाव

अब, चलो परिणामों की समीक्षा करें।

जनरेटर का उपयोग करके कार्यान्वयन की तुलना करना और फ़ंक्शन कॉल का उपयोग करने वाले व्यक्ति की तुलना में, वे रेडिस के समान राउंडट्रिप्स उत्पन्न नहीं करते हैं। जेनरेटर के साथ आपके पास बस है:

while time.time() - t - expiry < 0: 
     yield r.get(fpKey) 

तो प्रति पुनरावृत्ति प्रति 1 राउंडट्रिप। फ़ंक्शन के साथ, आपके पास है:

if r.exists(fpKey): 
    return r.get(fpKey) 

तो प्रति पुनरावृत्ति प्रति 2 राउंडट्रिप्स। कोई आश्चर्य नहीं कि जनरेटर तेज़ है।

बेशक आपको इष्टतम प्रदर्शन के लिए एक ही रेडिस कनेक्शन का पुन: उपयोग करना होगा। बेंचमार्क चलाने का कोई मतलब नहीं है जो व्यवस्थित रूप से कनेक्ट/डिस्कनेक्ट हो।

अंत में, रेडिस कॉल और फ़ाइल के बीच प्रदर्शन अंतर के संबंध में, आप बस एक दूरस्थ कॉल को एक दूरस्थ कॉल की तुलना कर रहे हैं।फाइल पढ़ने ओएस फाइल सिस्टम द्वारा कैश किए जाते हैं, इसलिए वे कर्नेल और पायथन के बीच तेज़ मेमोरी ट्रांसफर ऑपरेशन हैं। यहां कोई डिस्क I/O शामिल नहीं है। रेडिस के साथ, आपको राउंडट्रिप्स की लागत के लिए भुगतान करना होगा, इसलिए यह बहुत धीमी है।

+4

आपने मुझे इस पर हराया! मैं ओपी से बेंचमार्क चलाने के लिए कहूंगा ए) रेडिस के लिए मौजूदा() जांच को हटा रहा है, बी) इसे पुन: बनाने के बजाए लगातार रेडिस कनेक्शन का उपयोग करें, और सी) एक हार्ड कोड वाली फ़ाइल के बजाय यादृच्छिक फाइलें पढ़ना। –

+0

अधिक जानकारी जोड़ा गया। यादृच्छिक पढ़ता है जहां कैशिंग वास्तव में मदद करता है। ऐसा लगता है कि मेरे लिए अजीब लगता है कि रेडिस इंस्टेंस का पुन: उपयोग करने और नए बनाने के बीच वास्तव में इतना अंतर नहीं है। सृजन में बहुत अधिक उपर नहीं होना चाहिए (मुझे आश्चर्य है कि यह प्रमाणीकरण के साथ कितना बदल जाएगा)। – MercuryRising

+0

प्रमाणीकरण लागत कनेक्शन के ठीक बाद एक अतिरिक्त राउंडट्रिप होती है। नया रेडिस इंस्टेंस बनाना केवल सस्ता है क्योंकि आपका क्लाइंट आपके सर्वर की तुलना में एक ही होस्ट पर है। –

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