0
点赞
收藏
分享

微信扫一扫

Redis--无底洞--含义/原因/解决方案


简介

        本文介绍Redis的无底洞,包括:含义、原因、解决方案。

含义

        2010年,Facebook的Memcache节点已经达到了3000个,承载着TB级别的缓存数据。但开发和运维人员发现了一个问题,为了满足业务要求添加了大量新Memcache节点,但是发现性能不但没有好转反而下降了,当时将这种现象称为缓存的“无底洞”现象。

无底洞的原因

        通常来说添加节点使得Memcache集群性能应该更强了,但事实并非如此。键值数据库由于通常采用哈希函数将key映射到各个节点上,造成key的分布与业务无关,但是由于数据量和访问量的持续增长,造成需要添加大量节点做水平扩容,导致键值分布到更多的节点上,所以无论是Memcache还是Redis的分布式,批量操作通常需要从不同节点上获取,相比于单机批量操作只涉及一次网络操作,分布式批量操作会涉及多次网络时间。

        用一句通俗的话总结就是,更多的节点不代表更高的性能,所谓“无底洞”就是说投入越多不一定产出越多。但是分布式又是不可以避免的,因为访问量和数据量越来越大,一个节点根本抗不住,所以如何高效地在分布式缓存中批量操作是一个难点。

        图1展示了在分布式条件下,一次mget操作需要访问多个Redis节点,需要多次网络时间。而图2由于所有键值都集中在一个节点上,所以一次批量操作只需要一次网络时间。


Redis--无底洞--含义/原因/解决方案_批量操作

图1 分布式存储批量操作多次网络时间


Redis--无底洞--含义/原因/解决方案_数据库_02

图2 当一个节点存储批量操作只需一次网络时间

无底洞问题分析:


  1. 客户端一次批量操作会涉及多次网络操作,也就意味着批量操作会随着节点的增多,耗时会不断增大。
  2. 网络连接数变多,对节点的性能也有一定影响。

优化思路


  1. 命令本身的优化,例如优化Redis语句等。
  2. 减少网络通信次数。
  3. 降低接入成本,例如客户端使用长连/连接池、NIO等。

这里我们假设命令、客户端连接已经为最优,重点讨论减少网络操作次数。

以Redis批量获取n个字符串为例,有4种实现方法, 如图3(第4种待完善)所示:


  1. 客户端n次get:n次网络 + n次get命令本身。
  2. 客户端1次pipeline get:1次网络 + n次get命令本身。
  3. 客户端1次mget:1次网络 + 1次mget命令本身。
  4. 客户端1次get:1次网络时间 + n次命令时间
     

Redis--无底洞--含义/原因/解决方案_database_03

图3 客户端批量操作的4种实现

初始方案:串行命令

        由于n个key是比较均匀地分布在Redis Cluster的各个节点上,因此无法使用mget命令一次性获取,所以通常来讲要获取n个key的值,最简单的方法就是逐次执行n个get命令,这种操作时间复杂度较高,它的操作时间 = n次网络时间 + n次命令时间,网络次数是n。很显然这种方案不是最优的,但是实现起来比较简单,如图4所示。


Redis--无底洞--含义/原因/解决方案_批量操作_04

图4 客户端串行n次命令

 代码实现(Jedis)

List<String> serialMGet(List<String> keys) 
{
// 结果集
List<String> values = new ArrayList<String>();

// n次串行get
for (String key : keys) {
String value = jedisCluster.get(key);
values.add(value);
}
return values;
}

解决方案

1.串行IO

        Redis Cluster使用CRC16算法计算出散列值,再取对16383的余数就可以算出slot值。Smart客户端会保存slot和节点的对应关系,有了这两个数据就可以将属于同一个节点的key进行归档, 得到每个节点的key子列表,之后对每个节点执行mget或者Pipeline操作,它的操作时间 = node次网络时间 + n次命令时间,网络次数是node的个数,整个过程如图5所示。


Redis--无底洞--含义/原因/解决方案_客户端_05

图5 客户端串行node次网络IO

        很明显这种方案比初始方案要好很多,但是如果节点数太多,还是有一定的性能问题。

代码实现(Jedis)

Map<String, String> serialIOMget(List<String> keys) {
// 结果集
Map<String, String> keyValueMap = new HashMap<String, String>();
// 属于各个节点的key列表,JedisPool要提供基于ip和port的hashcode方法
Map<JedisPool, List<String>> nodeKeyListMap = new HashMap<JedisPool, List<String>>()
// 遍历所有的key
for (String key : keys) {
// 使用CRC16本地计算每个key的slot
int slot = JedisClusterCRC16.getSlot(key);
// 通过jedisCluster本地slot->node映射获取slot对应的node
JedisPool jedisPool = jedisCluster.getConnectionHandler().getJedisPoolFrom
Slot(slot);
// 归档
if (nodeKeyListMap.containsKey(jedisPool)) {
nodeKeyListMap.get(jedisPool).add(key);
} else {
List<String> list = new ArrayList<String>();
list.add(key);
nodeKeyListMap.put(jedisPool, list);
}
}
//从每个节点上批量获取,这里使用mget也可以使用pipeline
for (Entry<JedisPool, List<String>> entry : nodeKeyListMap.entrySet()) {
JedisPool jedisPool = entry.getKey();
List<String> nodeKeyList = entry.getValue();
// 列表变为数组
String[] nodeKeyArray = nodeKeyList.toArray(new String[nodeKeyList.size()]);
// 批量获取, 可以使用mget或者Pipeline
List<String> nodeValueList = jedisPool.getResource().mget(nodeKeyArray);
// 归档
for (int i = 0; i < nodeKeyList.size(); i++) {
keyValueMap.put(nodeKeyList.get(i), nodeValueList.get(i));
}
}
return keyValueMap;
}

2.并行IO

        此方案是将方案1中的最后一步改为多线程执行,网络次数虽然还是节点个数,但由于使用多线程网络时间变为O(1) ,这种方案会增加编程的复杂度。它的操作时间为:max_slow(node网络时间) + n次命令时间

        整个过程如图6所示:


Redis--无底洞--含义/原因/解决方案_redis_06

图6 客户端并行node次网络IO

代码实现(Jedis)

Map<String, String> parallelIOMget(List<String> keys) {
// 结果集
Map<String, String> keyValueMap = new HashMap<String, String>();

// 属于各个节点的key列表
Map<JedisPool, List<String>> nodeKeyListMap = new HashMap<JedisPool, List<String>>()
// 和前面一样

// 多线程mget, 最终汇总结果
for (Entry<JedisPool, List<String>> entry : nodeKeyListMap.entrySet()) {
// 多线程实现
}
return keyValueMap;
}

3.hash_tag

说明

        Redis Cluster有hash_tag功能,它可以将多个key强制分配到一个节点上,它的操作时间 = 1次网络时间 + n次命令时间。


Redis--无底洞--含义/原因/解决方案_客户端_07

图7 hash_tag将多个key分配到一个节点

如图8所示,读取时,因为所有key属于node2节点,所以只去node2读即可:


Redis--无底洞--含义/原因/解决方案_database_08

图8 hash_tag只需要1次网络时间

代码实现(Jedis示例)

List<String> hashTagMget(String[] hashTagKeys) {
return jedisCluster.mget(hashTagKeys);
}

方案对比

 Redis--无底洞--含义/原因/解决方案_数据库_09

其他网址

《Redis开发与运维》=> 无底洞优化


举报

相关推荐

0 条评论