找回密码
 会员注册
查看: 29|回复: 0

RedisCluster基于客户端对mget的性能优化

[复制链接]

2万

主题

0

回帖

6万

积分

超级版主

积分
64437
发表于 2024-9-19 17:20:55 | 显示全部楼层 |阅读模式
1背景2分析原因2.1现象2.2定位问题3解决问题3.1使用hashtag3.2客户端改造4效果展示4.1性能测试4.2结论5总结1背景Redis是知名的、应用广泛的NoSQL数据库,在转转也是作为主要的非关系型数据库使用。我们主要使用Codis来管理Redis分布式集群,但随着Codis官方停止更新和RedisCluster的日益完善,转转也开始尝试使用RedisCluster,并选择Lettuce作为客户端使用。但是在业务接入过程中发现,使用Lettuce访问RedisCluster的mget、mset等Multi-Key命令时,性能表现不佳。2分析原因2.1现象业务在从Codis迁移到RedisCluster的过程中,在RedisCluster和Codis双写了相同的数据。结果Codis在比RedisCluster多一次连接proxy节点的耗时下,同样是mget获取相同的数据,使用Lettuce访问RedisCluster还是比使用Jeds访问Codis耗时要高,于是我们开始定位性能差异的原因。2.2定位问题2.2.1RedisCluster的架构设计导致RedisCluster的mget性能不佳的根本原因,是RedisCluster在架构上的设计导致的。RedisCluster基于smartclient和无中心的设计,按照槽位将数据存储在不同的节点上如上图所示,每个主节点管理不同部分的槽位,并且下面挂了多个从节点。槽位是RedisCluster管理数据的基本单位,集群的伸缩就是槽和数据在节点之间的移动。通过CRC16(key)%16384来计算key属于哪个槽位和哪个Redis节点。而且RedisCluster的Multi-Key操作受槽位限制,例如我们执行mget,获取不同槽位的数据,是限制执行的:2.2.2Lettuce的mget实现方式lettuce对Multi-Key进行了支持,当我们调用mget方法,涉及跨槽位时,Lettuce对mget进行了拆分执行和结果合并,代码如下:public RedisFuture>> mget(Iterable keys) {    //将key按照槽位拆分    Map> partitioned = SlotHash.partition(codec, keys);    if (partitioned.size()  slots = SlotHash.getSlots(partitioned);    Map>>> executions = new HashMap();    //对不同槽位的keys分别执行mget    for (Map.Entry> entry : partitioned.entrySet()) {        RedisFuture>> mget = super.mget(entry.getValue());        executions.put(entry.getKey(), mget);    }    // 获取、合并、排序结果    return new ipelinedRedisFuture(executions, objectPipelinedRedisFuture -> {        List> result = new ArrayList();        for (K opKey : keys) {            int slot = slots.get(opKey);            int position = partitioned.get(slot).indexOf(opKey);            RedisFuture>> listRedisFuture = executions.get(slot);            result.add(MultiNodeExecution.execute(() -> listRedisFuture.get().get(position)));        }        return result;    });}mget涉及多个key的时候,主要有三个步骤:1、按照槽位将key进行拆分;2、分别对相同槽位的key去对应的槽位mget获取数据;3、将所有执行的结果按照传参的key顺序排序返回。所以Lettuce客户端,执行mget获取跨槽位的数据,是通过槽位分发执行mget,并合并结果实现的。而Lettuce基于Netty的NIO框架实现,发送命令不会阻塞IO,但是处理请求是单连接串行发送命令:所以Lettuce的mget的key数量越多,涉及的槽位数量越多,性能就会越差。Codis也是拆分执行mget,不过是并发发送命令,并使用pipeline提高性能,进而减少了网络的开销。3解决问题3.1使用hashtag我们首先想到的是客户端分别执行分到不同槽位的请求,导致耗时增加。我们可以将我们需要同时操作到的key,放到同一个槽位里去。我们是可以通过hashtag来实现hashtag用于RedisCluster中。hashtag规定以key里{}里的内容来做hash,比如user:{a}:zhangsan和user:{a}:lisi就会用a去hash,保证带{a}的key都落到同一个slot里利用hashtag对key进行规划,使得我们mget的值都在同一个槽位里。但是这种方式需要业务方感知到RedisCluster的分片的存在,需要对RedisCluster的各节点存储做规划,保证数据平均的分布在不同的Redis节点上,对业务方使用上太不友好,所以舍弃了这种方案。3.2客户端改造另一种方案是在客户端做改造,这样做成本较低。不需要业务方感知和维护hashtag。我们利用pipeline对Redis节点批量发送get命令,相对于Lettuce串行发送mget命令来说,减少了多次跨槽位mget发送命令的网络耗时。具体步骤如下:1、把所有key按照所在的Redis节点拆分;2、通过pipeline对每个Redis节点批量发送get命令;3、获取所有命令执行结果,排序、合并结果,并返回。这样改造,使用pipeline一次发送批量的命令,减少了串行批量发送命令的网络耗时。3.2.1改造JedisCluster由于Lettuce没有原生支持pipeline批量提交命令,而JedisCluster原生支持pipeline,并且JedisCluster没有对Multi-Key进行支持,我们对JedisCluster的mget进行了改造,代码如下:public List mget(String... keys) {        List  pipelineList = new ArrayList();        List jedisList = new ArrayList();        try {            //按照key的hash计算key位于哪一个redis节点            Map> pooling = new HashMap();            for (String key : keys) {                JedisPool pool = connectionHandler.getConnectionPoolFromSlot(JedisClusterCRC16.getSlot(key));                pooling.computeIfAbsent(pool, k -> new ArrayList()).add(key);            }            //分别对每个redis 执行pipeline get操作            Map> resultMap = new HashMap();            for (Map.Entry> entry : pooling.entrySet()) {                Jedis jedis = entry.getKey().getResource();                ipeline pipelined = jedis.pipelined();                for (String key : entry.getValue()) {                    Response response = pipelined.get(key);                    resultMap.put(key, response);                }                pipelined.flush();                //保存所有连接和pipeline 最后进行close                pipelineList.add(pipelined);                jedisList.add(jedis);            }            //同步所有请求结果            for (Pipeline pipeline : pipelineList) {                pipeline.returnAll();            }            //合并、排序结果            List list = new ArrayList();            for (String key : keys) {                Response response = resultMap.get(key);                String o = response.get();                list.add(o);            }            return list;        }finally {            //关闭所有pipeline和jedis连接            pipelineList.forEach(Pipeline::close);            jedisList.forEach(Jedis::close);        }    }3.2.2处理异常case上面的代码还不足以覆盖所有场景,我们还需要处理一些异常caseRedisCluster扩缩容导致的数据迁移数据迁移会造成两种错误1、MOVED错误代表数据所在的槽位已经迁移到另一个redis节点上了,服务端会告诉客户端对应的槽的目标节点信息。此时我们需要做的是更新客户端缓存的槽位信息,并尝试重新获取数据。2、ASKING错误代表槽位正在迁移中,且数据不在源节点中,我们需要先向目标Redis节点执行ASKING命令,才能获取迁移的槽位的数据。List list = new ArrayList();for (String key : keys) {    Response response = resultMap.get(key);    String o;    try {        o = response.get();        list.add(o);    } catch (JedisRedirectionException jre) {        if (jre instanceof JedisMovedDataException) {            //此槽位已经迁移 更新客户端的槽位信息            this.connectionHandler.renewSlotCache(null);        }        boolean asking = false;        if (jre instanceof JedisAskDataException) {            //获取槽位目标redis节点的连接 设置asking标识,以便在重试前执行asking命令            asking = true; askConnection.set(this.connectionHandler.getConnectionFromNode(jre.getTargetNode()));        } else {            throw new JedisClusterException(jre);        }        //重试获取这个key的结果        o = runWithRetries(this.maxAttempts, asking, true, key);        list.add(o);    }}数据迁移导致的两种异常,会进行重试。重试会导致耗时增加,并且如果达到最大重试次数,还没有获取到数据,则抛出异常。pipeline的某个命令执行失败不捕获执行失败的异常,抛出异常让业务服务感知到异常发生。4效果展示4.1性能测试在改造完客户端之后,我们对客户端的mget进行了性能测试,测试了下面三种类型的耗时1、使用Jedis访问Codis2、使用改造的JedisCluster访问RedisCluster3、使用Lettuce同步方式访问RedisCluster4.1.1mget100keyCodisJedisCluster(改造)Lettuceavg0.411ms0.224ms0.61mstp990.528ms0.35ms1.53mstp9990.745ms1.58ms3.87ms4.1.2mget500keyCodisJedisCluster(改造)Lettuceavg0.96ms0.511ms2.14mstp991.15ms0.723ms3.99mstp9991.81ms1.86ms6.88ms4.1.3mget1000keyCodisJedisCluster(改造)Lettuceavg1.56ms0.92ms5.04mstp991.83ms1.22ms8.91mstp9993.15ms3.88ms32ms4.2结论使用改造的客户端访问RedisCluster,比使用Lettuce访问RedisCluster要快1倍以上;改造的客户端比使用codis稍微快一点,tp999不如codis性能好。但是改造的客户端相对于Lettuce也有缺点,JedisCluster是基于复杂的连接池实现,连接池的配置会影响客户端的性能。而Lettuce是基于Netty的NIO框架实现,对于大多数的Redis操作,只需要维持单一的连接即可高效支持并发请求,不需要业务考虑连接池的配置。5总结RedisCluster在架构设计上对Multi-Key进行的限制,导致无法跨槽位执行mget等命令。我们对客户端JedisCluster的Multi-Key命令进行改造,通过分别对Redis节点执行pipeline操作,提升了mget命令的性能。关于作者赵浩,转转架构部后台开发工程师想了解更多转转公司的业务实践,欢迎点击关注下方公众号:
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 会员注册

本版积分规则

QQ|手机版|心飞设计-版权所有:微度网络信息技术服务中心 ( 鲁ICP备17032091号-12 )|网站地图

GMT+8, 2024-12-26 23:37 , Processed in 0.734911 second(s), 26 queries .

Powered by Discuz! X3.5

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表