当前位置: 首页 > 工具软件 > Lettuce > 使用案例 >

lettuce MGET性能分析

彭琛
2023-12-01

现象:

根据服务redis响应p99不符合理想,根据redis server端的监控指标发现在系统调用期间出现的超100ms的查询不是由于slowlog导致

 

分析:

1.查看两台机器之间的延时,一个RT延时不超过0.15ms

 

2.查看redis server,在server端设置5ms的延时监控,目前为止也是没有采集到的对应延时时间大的时候的指标

config set latency-monitor-threshold 5

 

3.根据lettuce共享连接的原理来查看代码实现,发现mget在跨slot的时候会经历:1拆分 → 2发送 → 3合并

查看了redis cluster 4的官方文档,发现rediscluster4的mget是不支持跨slot的

@Override

public RedisFuture<List<KeyValue<K, V>>> mget(Iterable<K> keys) {

    //获取分区slot和key的映射关系

    Map<Integer, List<K>> partitioned = SlotHash.partition(codec, keys);

 

    //如果分区数小于2也就是只有一个分区即所有key都落在一个分区就直接获取

    if (partitioned.size() < 2) {

        return super.mget(keys);

    }

 

    //每个key与slot映射关系

    Map<K, Integer> slots = SlotHash.getSlots(partitioned);

    Map<Integer, RedisFuture<List<KeyValue<K, V>>>> executions = new HashMap<>();

 

    //遍历分片信息,逐个发送

    for (Map.Entry<Integer, List<K>> entry : partitioned.entrySet()) {

        RedisFuture<List<KeyValue<K, V>>> mget = super.mget(entry.getValue());

        executions.put(entry.getKey(), mget);

    }

 

    // restore order of key 恢复key的顺序

    return new PipelinedRedisFuture<>(executions, objectPipelinedRedisFuture -> {

        List<KeyValue<K, V>> result = new ArrayList<>();

        for (K opKey : keys) {

            int slot = slots.get(opKey);

 

            int position = partitioned.get(slot).indexOf(opKey);

            RedisFuture<List<KeyValue<K, V>>> listRedisFuture = executions.get(slot);

            result.add(MultiNodeExecution.execute(() -> listRedisFuture.get().get(position)));

        }

 

        return result;

    });

}

那么算的话根据原理,时间应该等于:分发耗时+多个server耗时+多个网络RT+合并耗时

复杂度分析:

get复杂度O(n)

mget复杂度O(slot.size)

解决:

1.首先想lettuce作者怎么面对大家的质疑:

https://gitter.im/lettuce-io/Lobby?at=5bb3bf946e5a401c2d081872

mp911de的回答:

Is there anyway to optimize this ?

@mp911de

Yes: Don't use cross-slot keys.

Everything else requires split/merge/sort to issue multiple commands and correlate the responses back in order.

We probably could add size initializers to allocate maps and lists at the expected size to prevent reallocations.

 

For a batch get from multiple slots is there any better approach ?

@mp911de

 Have you tried using the individual connections that serve the hash slot and dispatching commands in your own code?(意思是让提问者尝试hashtag,这样就到一个slot里了)

这个是来自github的答疑:Question about Redis MGET with Lettuce Client(意思是将大批量的任务拆分成多次的小批量,从server端思考尽可能不要影响其他客户端的请求)

这个mget的量也从其他地方得到验证当一次batch大于1000性能急剧下降:

Redis MGET性能衰减分析

 

2.其他尝试

#既然lettuce不行,那咱们抛弃名媛找新欢jedis呢?发现已经有人帮助踩坑了

https://www.jianshu.com/p/f2681aa319a3

来自中国用户的测评:意思是jedis会在mget的时候遇见同样的跨分区问题,jedis采用的是循环sleep 10ms查看数据有没有到达,后面作者使用线程池get

#那么lettuce有没有优化的空间呢?发现也有人踩过坑了

https://blog.csdn.net/qq_31665011/article/details/100737095

来自中国用户的测评:意思是codis会在mget的时候根据shard进行pipeline请求,lettuce是按照slot进行mget请求(so,要去改源码?单纯了为了快,我觉得作者是不能接收的 作者的目的是实现mget功能,并不是用pipeline混淆mget概念)

复杂度分析:

pipeline技术下的mget复杂度O(node.size)

#那么pipeline一定比mget快吗?

https://stackoverflow.com/questions/56017756/in-lettuce-im-finding-async-mget-on-a-redis-cluster-to-be-considerably-slower-t

StackOverflow上的总结是:慢的根源在于编解码器,这个还没有求证毕竟是别人的结论

另外在lettuce共享连接这个文章,我们强调过pipeline源码问题,建议pipeline使用按照官方文档

也有人抓包看过,文章如下:

Spring Data Redis与Lettuce 使用 pipeline 时注意事项

为什么在Lettuce中不需要pipeline操作

@Override

public RedisFuture<List<K>> keys(K pattern) {

 

    Map<String, CompletableFuture<List<K>>> executions = executeOnMasters(commands -> commands.keys(pattern));

 

    return new PipelinedRedisFuture<>(executions, objectPipelinedRedisFuture -> {

        List<K> result = new ArrayList<>();

        for (CompletableFuture<List<K>> future : executions.values()) {

            result.addAll(MultiNodeExecution.execute(future::get));

        }

        return result;

    });

}

 

结论:

1.lettuce的mget是根据slot多次发送的

2.可以尝试把要mget的keys通过hashtag放到一个slot

3.可以尝试根据shard然后pipeline获取(建议量50~1000

4.减小mget的batch量(建议量小于100),多批次根据业务处理

 

mget性能测试脚本:./redis-mget.sh 1000

1.set数据

redis-set-1024.sh

#!/bin/bash

 

key="testkey"

value="testvalue"

 

int=0

while (($int <= 1024)); do

    echo -e $key$int $value

    redis-cli set $key$int $value

    let "int++"

done

2.根据传入参数mget压测

redis-mget.sh

#!/bin/bash

 

int=0

keys=""

 

while (($int < $1)); do

    keys="$keys","'testkey$int'"

    let "int++"

done

 

redis-benchmark -n 100000 script load "redis.call('mget'$keys)"

 

 

 类似资料: