2012-10-12 32 views
25

Tôi muốn tạo bộ nhớ cache redis trong python, và như bất kỳ nhà khoa học tự tôn trọng nào tôi đã đánh dấu băng ghế dự bị để kiểm tra hiệu suất.Hiệu suất của Redis vs Disk trong ứng dụng bộ nhớ đệm

Điều thú vị là redis không hoạt động tốt. Hoặc Python đang làm điều gì đó ma thuật (lưu trữ các tập tin) hoặc phiên bản của tôi của redis là stupendously chậm.

Tôi không biết nếu điều này là do cách mã của tôi được cấu trúc, hoặc những gì, nhưng tôi đã mong đợi làm lại để làm tốt hơn so với nó đã làm.

Để tạo bộ nhớ cache redis, tôi đặt dữ liệu nhị phân của mình (trong trường hợp này là trang HTML) thành khóa được lấy từ tên tệp có thời hạn là 5 phút.

Trong mọi trường hợp, xử lý tệp được thực hiện với f.read() (đây là ~ 3x nhanh hơn f.readlines() và tôi cần blob nhị phân).

Có điều gì tôi thiếu trong so sánh của tôi hay Redis thực sự không phù hợp với đĩa? Python có lưu vào bộ nhớ đệm một nơi nào đó và xử lý nó mỗi lần không? Tại sao điều này nhanh hơn rất nhiều so với truy cập vào redis?

Tôi đang sử dụng redis 2.8, python 2.7 và redis-py, tất cả đều trên hệ thống Ubuntu 64 bit.

Tôi không nghĩ rằng Python đang làm bất cứ điều gì đặc biệt huyền diệu, như tôi đã thực hiện một chức năng lưu trữ dữ liệu tập tin trong một đối tượng python và mang lại nó mãi mãi.

Tôi có bốn chức năng cuộc gọi mà tôi theo nhóm:

Đọc file X lần

Một chức năng đó được gọi là để xem nếu đối tượng redis vẫn còn trong bộ nhớ, tải nó, hoặc bộ nhớ cache file mới (đơn và nhiều trường hợp redis).

Một chức năng tạo trình tạo ra kết quả từ cơ sở dữ liệu redis (với một lần và nhiều lần redis).

và cuối cùng, lưu trữ tệp trong bộ nhớ và giữ nó vĩnh viễn.

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) 

Và bây giờ là kết quả:

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) 

Kết quả là thú vị ở chỗ a) máy phát điện là nhanh hơn so với gọi chức năng mỗi lần, b) redis là chậm hơn so với đọc từ đĩa, và c) đọc từ các đối tượng python là ridiculously nhanh.

Tại sao đọc từ đĩa nhanh hơn rất nhiều so với việc đọc từ tệp trong bộ nhớ từ redis?

EDIT: Một số thông tin và thử nghiệm khác.

Tôi thay thế các chức năng để

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

Các kết quả không khác biệt nhiều so

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) 

Tạo một redis dụ mới về mỗi cuộc gọi chức năng thực sự không có một đáng chú ý ảnh hưởng đến tốc độ đọc, sự thay đổi từ thử nghiệm đến kiểm tra lớn hơn mức tăng.

Sripathi Krishnan đề xuất triển khai đọc tệp ngẫu nhiên. Đây là nơi bộ nhớ đệm bắt đầu thực sự hữu ích, như chúng ta có thể thấy từ những kết quả này.

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) 

Có một số lượng lớn thay đổi trong tệp để chênh lệch phần trăm không phải là chỉ báo tốt về tăng tốc.

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) 

Tôi đã sử dụng random.choice (tệpList) để chọn ngẫu nhiên một tệp mới trên mỗi lần truyền qua các hàm.

Các ý chính đầy đủ là ở đây nếu có ai muốn thử nó ra - https://gist.github.com/3885957

Chỉnh sửa biên tập: đã không nhận ra rằng tôi đã gọi một tập tin duy nhất cho máy phát điện (mặc dù việc thực hiện các cuộc gọi chức năng và máy phát điện rất giống nhau). Đây là kết quả của các tập tin khác nhau từ máy phát điện.

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

Tôi không thấy nơi bạn đang tạo một phiên bản redis mới trên mỗi cuộc gọi chức năng. Nó chỉ là điều đối số mặc định? – jdi

+0

Có, nếu bạn không vượt qua một ví dụ redis cuộc gọi chức năng sẽ lấy một mới def cache_or_get (fp, expiry = 300, r = redis.Redis (db = 5)): – MercuryRising

+2

Thats thực sự không đúng sự thật. Những đối số mặc định này chỉ được đánh giá một lần khi kịch bản được tải và được lưu với định nghĩa hàm. Chúng không được đánh giá mỗi khi bạn gọi nó. Điều đó sẽ giải thích lý do tại sao bạn không thấy bất kỳ sự khác biệt nào giữa việc truyền vào hoặc cho phép nó sử dụng giá trị mặc định. Trên thực tế những gì bạn đang làm là tạo ra một cho mỗi def chức năng, cộng với một cho mỗi khi bạn đã vượt qua nó trong. 2 kết nối không sử dụng – jdi

Trả lời

28

Đây là một quả táo để so sánh cam. Xem http://redis.io/topics/benchmarks

Redis là hiệu quả remote lưu trữ dữ liệu. Mỗi khi một lệnh được thực hiện trên Redis, một thông điệp được gửi đến máy chủ Redis, và nếu máy khách đồng bộ, nó sẽ chờ trả lời. Vì vậy, ngoài chi phí của bản thân lệnh, bạn sẽ trả tiền cho một vòng kết nối mạng hoặc một IPC.

Trên phần cứng hiện đại, mạng vòng hoặc IPC là đáng ngạc nhiên tốn kém so với các hoạt động khác. Điều này là do một số yếu tố:

  • độ trễ liệu của môi trường (chủ yếu cho mạng)
  • độ trễ của scheduler hệ điều hành (không được bảo đảm trên Linux/Unix)
  • bỏ lỡ bộ nhớ cache là đắt và khả năng nhớ cache tăng lên trong khi quy trình máy khách và máy chủ được lên lịch vào/ra.
  • trên hộp cao cấp, NUMA tác dụng phụ

Bây giờ, hãy xem lại kết quả.

So sánh việc triển khai bằng cách sử dụng trình tạo và cuộc gọi hàm sử dụng, chúng không tạo cùng số lượng vòng lặp cho Redis. Với trình tạo, bạn chỉ cần có:

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

Vì vậy, 1 vòng lặp cho mỗi lần lặp lại. Với chức năng này, bạn có:

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

Vì vậy, 2 vòng lặp cho mỗi lần lặp lại. Không có gì lạ khi máy phát nhanh hơn.

Tất nhiên bạn phải sử dụng lại kết nối Redis tương tự để có hiệu suất tối ưu. Không có điểm để chạy một điểm chuẩn mà có hệ thống kết nối/ngắt kết nối.

Cuối cùng, về sự khác biệt hiệu suất giữa các cuộc gọi Redis và tệp đọc, bạn chỉ cần so sánh cuộc gọi nội hạt với cuộc gọi từ xa.Tập tin đọc được lưu trữ bởi hệ thống tập tin hệ điều hành, do đó, họ đang hoạt động chuyển giao bộ nhớ nhanh giữa hạt nhân và Python. Không có đĩa I/O tham gia ở đây. Với Redis, bạn phải trả chi phí cho các chuyến khứ hồi, do đó, nó sẽ chậm hơn nhiều.

+4

Bạn đã đánh bại tôi ở cái này! Tôi sẽ yêu cầu OP chạy các điểm chuẩn SAU a) Xóa kiểm tra tồn tại() đối với Redis, b) Sử dụng kết nối Redis liên tục thay vì tạo lại nó và c) Đọc các tệp ngẫu nhiên thay vì một tệp được mã hóa cứng duy nhất. –

+0

Đã thêm thông tin. Lượt đọc ngẫu nhiên là nơi lưu vào bộ nhớ cache thực sự hữu ích. Nó có vẻ kỳ lạ với tôi rằng có thực sự không phải là nhiều sự khác biệt giữa tái sử dụng một ví dụ redis và tạo ra những cái mới. Không có nhiều chi phí trong việc tạo ra (tôi tự hỏi nó sẽ thay đổi bao nhiêu với xác thực). – MercuryRising

+0

Chi phí xác thực là một vòng bổ sung xảy ra ngay sau khi kết nối. Tạo phiên bản Redis mới chỉ rẻ vì khách hàng của bạn ở cùng một máy chủ so với máy chủ của bạn. –

Các vấn đề liên quan