redis LRU test分析

Redis LRU caches解析

LRU简介

LRU(least recently used)最近最少使用算法,页面置换算法。根据数据的历史访问记录来进行淘汰数据,其核心思想是:如果数据最近被访问过,那么将来被访问的几率也更高。

redis配置文件中的参数项


    # 限定redis最大内存使用量
    # maxmemory <bytes>

    # 置换策略
    # 当redis内存使用达到maxmemory时,需要选择设置好的maxmemory-policy对旧数据进行置换
    # 1) noeviction:不进行置换,表示即使内存达到上限也不进行置换,所有能引起内存增加的命令都返回error
    # 2) allkeys-lru:优先删除最近最不经常使用的key,用以保存新数据
    # 3) allkeys-random:只从设置失效的key中选择最近最不经常使用的key进行删除,用以保存新数据
    # 4) volatile-random:只从设置失效的key中,选择一些key进行删除,用以保存新数据
    # 5) volatile-ttl:只从设置失效的key中,选出存活时间最短的key进行删除,用以保存新数据
    # maxmemory-policy volatile-lru

    # redis的LRU不是严格意义上的LRU算法实现,是一种近似的LRU现实,主要是为了节约内存占用提升性能
    # redis的LRU取出maxmemory-samples数量的key,然后从中选择一个最近最不经常使用的key进行置换
    # maxmemory-samples 3

置换策略选择

一般的经验规则:

  • 使用allkeys-lru策略:当你希望你的请求符合一个幂定律分布,也就是说,你希望部分的子集元素将比其它其它元素被访问的更多。如果你不确定选择什么,这是个很好的选择。
  • 使用allkeys-random:如果你是循环访问,所有的键被连续的扫描,或者你希望请求分布正常(所有元素被访问的概率都差不多)。
  • 使用volatile-ttl:如果你想要通过创建缓存对象时设置TTL值,来决定哪些对象应该被过期。
  • allkeys-lru 和 volatile-random策略对于当你想要单一的实例实现缓存及持久化一些键时很有用。不过一般运行两个实例是解决这个问题的更好方法。

为了键设置过期时间也是需要消耗内存的,所以使用allkeys-lru这种策略更加高效,因为没有必要为键取设置过期时间当内存有压力时。

LRU test

    # <keys>表示key的分布范围
    --lru-test <keys>

用于测试redis置换策略的性能以及分析设置的maxmemory项是否合理。

测试

置换策略是allkeys-lru

设置配置文件maxmemory项为100MB

    maxmemory 104857600
    $ ./redis-cli --lru-test 10000000
    156000 Gets/sec | Hits: 4552 (2.92%) | Misses: 151448 (97.08%)
    153750 Gets/sec | Hits: 12906 (8.39%) | Misses: 140844 (91.61%)
    159250 Gets/sec | Hits: 21811 (13.70%) | Misses: 137439 (86.30%)
    151000 Gets/sec | Hits: 27615 (18.29%) | Misses: 123385 (81.71%)
    145000 Gets/sec | Hits: 32791 (22.61%) | Misses: 112209 (77.39%)
    157750 Gets/sec | Hits: 42178 (26.74%) | Misses: 115572 (73.26%)
    154500 Gets/sec | Hits: 47418 (30.69%) | Misses: 107082 (69.31%)
    151250 Gets/sec | Hits: 51636 (34.14%) | Misses: 99614 (65.86%)

N Gets/sec 表示每秒执行了多少次get
Hits: M(m%) 表示M次命中,命中率m%
Misses: P(p%) 表示P次没有命中,命中率p%
N = M + P

随着时间的增加,命中率会增加,但是当内存使用量超过设置的最大内存时,命中率就会在一个范围内波动。

内部实现

实现位于redis-cli.c

    #define LRU_CYCLE_PERIOD 1000 /* 1000 milliseconds. */
    #define LRU_CYCLE_PIPELINE_SIZE 250
    static void LRUTestMode(void) {
        redisReply *reply;
        char key[128];
        PORT_LONGLONG start_cycle;
        int j;

        srand((unsigned int)(time(NULL)^getpid()));                                   WIN_PORT_FIX /* cast (unsigned int) */
        while(1) {
            /* Perform cycles of 1 second with 50% writes and 50% reads.
             * We use pipelining batching writes / reads N times per cycle in order
             * to fill the target instance easily. */
            start_cycle = mstime();
            PORT_LONGLONG hits = 0, misses = 0;
            while(mstime() - start_cycle < 1000) {
                /* Write cycle. */
                for (j = 0; j < LRU_CYCLE_PIPELINE_SIZE; j++) {
                    LRUTestGenKey(key,sizeof(key));
                    redisAppendCommand(context, "SET %s val",key);
                }
                for (j = 0; j < LRU_CYCLE_PIPELINE_SIZE; j++)
                    redisGetReply(context, (void**)&reply);

                /* Read cycle. */
                for (j = 0; j < LRU_CYCLE_PIPELINE_SIZE; j++) {
                    LRUTestGenKey(key,sizeof(key));
                    redisAppendCommand(context, "GET %s",key);
                }
                /* 统计命中次数 */
                for (j = 0; j < LRU_CYCLE_PIPELINE_SIZE; j++) {
                    if (redisGetReply(context, (void**)&reply) == REDIS_OK) {
                        switch(reply->type) {
                            case REDIS_REPLY_ERROR:
                                printf("%s\n", reply->str);
                                break;
                            case REDIS_REPLY_NIL:
                                misses++;
                                break;
                            default:
                                hits++;
                                break;
                        }
                    }
                }

                if (context->err) {
                    fprintf(stderr,"I/O error during LRU test\n");
                    exit(1);
                }
            }
            /* 计算命中率 */
            printf(
                "%lld Gets/sec | Hits: %lld (%.2f%%) | Misses: %lld (%.2f%%)\n",
                hits+misses,
                hits, (double)hits/(hits+misses)*100,
                misses, (double)misses/(hits+misses)*100);
        }
        exit(0);
    }

可以看出命中率的计算分为以下5步:
1. 首先通过pipeline机制 SET 250次,SET的内容:key->”lru:<%d>” value->”val”,<%d>根据传入参数项keys按照2-8分布计算得到,具体可查看powerLowRand函数
2. 然后通过pipeline机制 GET 250次,GET的key使用上述相同的方法生成
3. 接着统计这250次GET中的命中次数
4. 重复1,2,3步,统计不超过1秒间隔内GET的命中次数,并计算命中率
5. 重复前四步,不断计算GET的命中率

可以看出,刚开始测试时,由于数据库中插入的key-value较少,命中率会很低,随着时间的增加,插入的key-value越来越多,GET的命中率会提高,但是当内存使用量超过最大内存设置量时,会出现页面交换的情况,此时命令率会在一个范围内波动。

影响因素

  • key-value大小,m
  • key-value数量,n
  • maxmemory设置大小,p

如果m*n>p,就发生页面置换,会影响Redis性能。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值