最新消息:

【案例】美团在Redis上踩过的一些坑

企业案例 摘星怪 2274浏览 0评论

【写在前面】

网上很不错的一篇介绍美团redis在实际线上应用中遇到的问题和解决办法,文章的初始来源是哪个博客不确定,只知道是作者参加了360组织的互联网技术训练营第三期,总结了美团网的DBA负责人侯军伟给大家介绍的美团网在redis上踩得一些坑,讲的都是干货和坑,很具有参考学习价值,转载记录。

【文章内容】

分为5个部分:
一、周期性出现connect timeout
二、redis bgrewriteaof问题
三、redis内存占用飙升
四、redis内存使用优化
五、redis cluster遇到的一些问题

一、周期性出现connect timeout

1. 背景:
大部分互联网公司都会有Mysql或者Oracle的DBA,但是在Nosql方面一般不会设置专门的DBA。不过对于一些知名的互联网公司来说,Nosql的使用量是巨大的,所以通常让Mysql的DBA或者单独聘请工程师来维护一些Nosql数据库,

比如:
Redis, Hbase, Memcache(其实严格讲不是nosql), Mongodb, Cassandra。从讲座看美团网应该是有专职的Redis DBA。所以作为业务开发人员不需要自己安装、配置、运维Redis,只需要找Redis DBA来申请就可以了。

这里为了简化说明:Redis DBA提供的服务叫做Redis云,业务开发人员叫做业务端(redis的使用者)

20170703110123

2. 现象:

业务端在使用redis云提供的redis服务后,经常出现connect timeout:

redis.clients.jedis.exceptions.JedisConnectionException  
java.net.SocketException  
java.net.SocketTimeoutException:connect time out 

3. 分析和怀疑:

业务端一般认为redis出现问题,就是redis云有问题,人的“正常”思维:看别人错误容易,发现自己难,扯多了, 出现这个有很多原因:
(1). 网络原因:比如是否存在跨机房、网络割接等等。
(2). 慢查询,因为redis是单线程,如果有慢查询的话,会阻塞住之后的操作。
(3). value值过大?比如value几十兆,当然这种情况比较少,其实也可以看做是慢查询的一种
(4). aof重写/rdb fork发生?瞬间会堵一下Redis服务器。
(5). 其他………………

4. 查询原因

演讲者一开始怀疑是网络问题,但是并未发现问题,观察各种对比图表,tcp listenOverFlow和timeout经常周期出现。(赞一下这个监控,我们监控现在还没有这个层面的)

有关listenOverFlow:

  • 查看现有的连接数是否大于设置的backlog,如果大于就丢弃,并相应的参数值加1。其中backlog是由程序和系统参数net.core.somaxconn共同设置,当backlog的值大于系统设置的net.core.somaxconn时则取net.core.somaxconn的值,否则取程序设置的backlog值。这种出错的方式也被记录在TcpListenOverflows中(其只记录了连接个数不足而产生溢出错误的次数!)。

觉得可能和TCP相关,于是分析了Tcp三次握手:最后一次握手客户端的请求会进入服务器端的一个队列(可以认为是下三图)中,如果这个队列满了,就会发生上面的异常。(accept)

(1) TCP三次握手: 

20170703111225

 (2) redis客户端与redis服务器交互的过程(本质就是TCP请求)

20170703111312

(3) I/O 多路复用程序通过队列向文件事件分派器传送套接字的过程

20170703111349

(4) 和redis有什么关系呢?
 
由于Redis的单线程模型(对命令的处理和连接的处理都是在一个线程中),如果存在慢查询的话,会出现上面的这种情况,造成新的accept的连接进不了队列。
20170703111548
 
如果上面的图没法理解的话,看看这张图:
20170703111629
(5). 解决方法:
1) 对慢查询进行持久化,比如定时存放到mysql之类。(redis的慢查询只是一个list,超过list设置的最大值,会清除掉之前的数据,也就是看不到历史)
2) 对慢查询进行报警(频率、数量、时间)等等因素
3) 打屁股,哈哈:
4) 其实应该做的是:对业务端进行培训,告诉他们一下redis开发的坑,redis不是万金油,这个和Mysql DBA要培训Mysql使用者一样,否则防不胜防。比如他执行了 monitor, keys *, flushall, drop table, update table set a=1; 这种也是防不胜防的(当然也可以做限制,利用rename-command一个随机数),但是提高工程师的水平才是关键。

二、redis bgrewriteaof问题

(一)、背景
1. AOF:
Redis的AOF机制有点类似于MySQL binlog,是redis的提供的一种持久化方式(另一种是RDB),它会将所有的写命令按照一定频率(no, always, every seconds)写入到日志文件中,当Redis停机重启后恢复数据库。
20170703112609
2. AOF重写:
(1) 随着AOF文件越来越大,里面会有大部分是重复命令或者可以合并的命令(100次incr = set key 100)
(2) 重写的好处:减少AOF日志尺寸,减少内存占用,加快数据库恢复时间。
20170703112710
(二)、单机多实例可能存在Swap和OOM的隐患:
由于Redis的单线程模型,理论上每个redis实例只会用到一个CPU, 也就是说可以在一台多核的服务器上部署多个实例(实际就是这么做的)。但是Redis的AOF重写是通过fork出一个Redis进程来实现的,所以有经验的Redis开发和运维人员会告诉你,在一台服务器上要预留一半的内存(防止出现AOF重写集中发生,出现swap和OOM)。
20170703113123
(三)、最佳实践

1. meta信息:作为一个redis云系统,需要记录各个维度的数据,比如:业务组、机器、实例、应用、负责人多个维度的数据,相信每个Redis的运维人员都应该有这样的持久化数据(例如mysql),一般来说还有一些运维界面,为自动化和运维提供依据

例如如下:

20170703113229

20170703113310

2. AOF的管理方式:
(1) 自动:让每个redis决定是否做AOF重写操作(根据auto-aof-rewrite-percentage和auto-aof-rewrite-min-size两个参数):
(2) crontab: 定时任务,可能仍然会出现多个redis实例,属于一种折中方案。

(3) remote集中式:

  • 最终目标是一台机器一个时刻,只有一个redis实例进行AOF重写。
  • 具体做法其实很简单,以机器为单位,轮询每个机器的实例,如果满足条件就运行(比如currentSize和baseSize满足什么关系)bgrewriteaof命令。
  • 期间可以监控发生时间、耗时、频率、尺寸的前后变化

20170703113536

 

策略 优点 缺点
自动 无需开发

1. 有可能出现(无法预知)上面提到的Swap和OOM

2. 出了问题,处理起来其实更费时间。

AOF控制中心(remote集中式)

1. 防止上面提到Swap和OOM。

2. 能够收集更多的数据(aof重写的发生时间、耗时、频率、尺寸的前后变化),更加有利于运维和定位问题(是否有些机器的实例需要拆分)。

控制中心需要开发。

一台机器轮询执行bgRewriteAof代码示例:

package com.sohu.cache.inspect.impl;  
  
import com.sohu.cache.alert.impl.BaseAlertService;  
import com.sohu.cache.entity.InstanceInfo;  
import com.sohu.cache.inspect.InspectParamEnum;  
import com.sohu.cache.inspect.Inspector;  
import com.sohu.cache.util.IdempotentConfirmer;  
import com.sohu.cache.util.TypeUtil;  
import org.apache.commons.collections.MapUtils;  
import org.apache.commons.lang.StringUtils;  
import redis.clients.jedis.Jedis;  
  
import java.util.Collections;  
import java.util.LinkedHashMap;  
import java.util.List;  
import java.util.Map;  
import java.util.concurrent.TimeUnit;  
  
  
public class RedisIsolationPersistenceInspector extends BaseAlertService implements Inspector {  
  
    public static final int REDIS_DEFAULT_TIME = 5000;  
  
    @Override  
    public boolean inspect(Map<InspectParamEnum, Object> paramMap) {  
        // 某台机器和机器下所有redis实例  
        final String host = MapUtils.getString(paramMap, InspectParamEnum.SPLIT_KEY);  
        List<InstanceInfo> list = (List<InstanceInfo>) paramMap.get(InspectParamEnum.INSTANCE_LIST);  
        // 遍历所有的redis实例  
        for (InstanceInfo info : list) {  
            final int port = info.getPort();  
            final int type = info.getType();  
            int status = info.getStatus();  
            // 非正常节点  
            if (status != 1) {  
                continue;  
            }  
            if (TypeUtil.isRedisDataType(type)) {  
                Jedis jedis = new Jedis(host, port, REDIS_DEFAULT_TIME);  
                try {  
                    // 从redis info中索取持久化信息  
                    Map<String, String> persistenceMap = parseMap(jedis);  
                    if (persistenceMap.isEmpty()) {  
                        logger.error("{}:{} get persistenceMap failed", host, port);  
                        continue;  
                    }  
                    // 如果正在进行aof就不做任何操作,理论上要等待它完毕,否则  
                    if (!isAofEnabled(persistenceMap)) {  
                        continue;  
                    }  
                    // 上一次aof重写后的尺寸和当前aof的尺寸  
                    long aofCurrentSize = MapUtils.getLongValue(persistenceMap, "aof_current_size");  
                    long aofBaseSize = MapUtils.getLongValue(persistenceMap, "aof_base_size");  
                    // 阀值大于60%  
                    long aofThresholdSize = (long) (aofBaseSize * 1.6);  
                    double percentage = getPercentage(aofCurrentSize, aofBaseSize);  
                    // 大于60%且超过60M  
                    if (aofCurrentSize >= aofThresholdSize && aofCurrentSize > (64 * 1024 * 1024)) {  
                        // bgRewriteAof 异步操作。  
                        boolean isInvoke = invokeBgRewriteAof(jedis);  
                        if (!isInvoke) {  
                            logger.error("{}:{} invokeBgRewriteAof failed", host, port);  
                            continue;  
                        } else {  
                            logger.warn("{}:{} invokeBgRewriteAof started percentage={}", host, port, percentage);  
                        }  
                        // 等待Aof重写成功(bgRewriteAof是异步操作)  
                        while (true) {  
                            try {  
                                // before wait 1s  
                                TimeUnit.SECONDS.sleep(1);  
                                Map<String, String> loopMap = parseMap(jedis);  
                                Integer aofRewriteInProgress = MapUtils.getInteger(loopMap, "aof_rewrite_in_progress", null);  
                                if (aofRewriteInProgress == null) {  
                                    logger.error("loop watch:{}:{} return failed", host, port);  
                                    break;  
                                } else if (aofRewriteInProgress <= 0) {  
                                    // bgrewriteaof Done  
                                    logger.warn("{}:{} bgrewriteaof Done lastSize:{}Mb,currentSize:{}Mb", host, port,  
                                            getMb(aofCurrentSize),  
                                            getMb(MapUtils.getLongValue(loopMap, "aof_current_size")));  
                                    break;  
                                } else {  
                                    // wait 1s  
                                    TimeUnit.SECONDS.sleep(1);  
                                }  
                            } catch (Exception e) {  
                                logger.error(e.getMessage(), e);  
                            }  
                        }  
                    } else {  
                        if (percentage > 50D) {  
                            long currentSize = getMb(aofCurrentSize);  
                            logger.info("checked {}:{} aof increase percentage:{}% currentSize:{}Mb", host, port,  
                                    percentage, currentSize > 0 ? currentSize : "<1");  
                        }  
                    }  
                } finally {  
                    jedis.close();  
                }  
            }  
        }  
        return true;  
    }  
  
    private long getMb(long bytes) {  
        return (long) (bytes / 1024 / 1024);  
    }  
  
    private boolean isAofEnabled(Map<String, String> infoMap) {  
        Integer aofEnabled = MapUtils.getInteger(infoMap, "aof_enabled", null);  
        return aofEnabled != null && aofEnabled == 1;  
    }  
  
    private double getPercentage(long aofCurrentSize, long aofBaseSize) {  
        if (aofBaseSize == 0) {  
            return 0.0D;  
        }  
        String format = String.format("%.2f", (Double.valueOf(aofCurrentSize - aofBaseSize) * 100 / aofBaseSize));  
        return Double.parseDouble(format);  
    }  
  
    private Map<String, String> parseMap(final Jedis jedis) {  
        final StringBuilder builder = new StringBuilder();  
        boolean isInfo = new IdempotentConfirmer() {  
            @Override  
            public boolean execute() {  
                String persistenceInfo = null;  
                try {  
                    persistenceInfo = jedis.info("Persistence");  
                } catch (Exception e) {  
                    logger.warn(e.getMessage() + "-{}:{}", jedis.getClient().getHost(), jedis.getClient().getPort(),  
                            e.getMessage());  
                }  
                boolean isOk = StringUtils.isNotBlank(persistenceInfo);  
                if (isOk) {  
                    builder.append(persistenceInfo);  
                }  
                return isOk;  
            }  
        }.run();  
        if (!isInfo) {  
            logger.error("{}:{} info Persistence failed", jedis.getClient().getHost(), jedis.getClient().getPort());  
            return Collections.emptyMap();  
        }  
        String persistenceInfo = builder.toString();  
        if (StringUtils.isBlank(persistenceInfo)) {  
            return Collections.emptyMap();  
        }  
        Map<String, String> map = new LinkedHashMap<String, String>();  
        String[] array = persistenceInfo.split("\r\n");  
        for (String line : array) {  
            String[] cells = line.split(":");  
            if (cells.length > 1) {  
                map.put(cells[0], cells[1]);  
            }  
        }  
  
        return map;  
    }  
  
    public boolean invokeBgRewriteAof(final Jedis jedis) {  
        return new IdempotentConfirmer() {  
            @Override  
            public boolean execute() {  
                try {  
                    String response = jedis.bgrewriteaof();  
                    if (response != null && response.contains("rewriting started")) {  
                        return true;  
                    }  
                } catch (Exception e) {  
                    String message = e.getMessage();  
                    if (message.contains("rewriting already")) {  
                        return true;  
                    }  
                    logger.error(message, e);  
                }  
                return false;  
            }  
        }.run();  
    }  
}  

 三、redis内存占用飙升

 (一)、现象:
redis-cluster某个分片内存飙升,明显比其他分片高很多,而且持续增长。并且主从的内存使用量并不一致。
20170703113939
(二)、分析可能原因:

1. redis-cluster的bug (这个应该不存在)
2. 客户端的hash(key)有问题,造成分配不均。(redis使用的是crc16, 不会出现这么不均的情况)
3. 存在个别大的key-value: 例如一个包含了几百万数据set数据结构(这个有可能)
4. 主从复制出现了问题。
5. 其他原因

(三)、调查原因:
1. 经查询,上述1-4都不存在
2. 观察info信息,有一点引起了怀疑: client_longes_output_list有些异常。
20170703114136
3. 于是理解想到服务端和客户端交互时,分别为每个客户端设置了输入缓冲区和输出缓冲区,这部分如果很大的话也会占用Redis服务器的内存。
20170703114208
从上面的client_longest_output_list看,应该是输出缓冲区占用内存较大,也就是有大量的数据从Redis服务器向某些客户端输出。于是使用client list命令(类似于mysql processlist) redis-cli -h host -p port client list | grep -v “omem=0″,来查询输出缓冲区不为0的客户端连接,于是查询到祸首monitor,于是豁然开朗.
20170703114250
monitor的模型是这样的,它会将所有在Redis服务器执行的命令进行输出,通常来讲Redis服务器的QPS是很高的,也就是如果执行了monitor命令,Redis服务器在Monitor这个客户端的输出缓冲区又会有大量“存货”,也就占用了大量Redis内存。
(四)、紧急处理和解决方法
进行主从切换(主从内存使用量不一致),也就是redis-cluster的fail-over操作,继续观察新的Master是否有异常,通过观察未出现异常。
查找到真正的原因后,也就是monitor,关闭掉monitor命令的进程后,内存很快就降下来了。
(五)、 预防办法:
1. 为什么会有monitor这个命令发生,我想原因有两个:
(1). 工程师想看看究竟有哪些命令在执行,就用了monitor
(2). 工程师对于redis学习的目的,因为进行了redis的托管,工程师只要会用redis就可以了,但是作为技术人员都有学习的好奇心和欲望。
2. 预防方法:
(1) 对工程师培训,讲一讲redis使用过程中的坑和禁忌
(2) 对redis云进行介绍,甚至可以让有兴趣的同学参与进来
(3) 针对client做限制,但是官方也不建议这么做,官方的默认配置中对于输出缓冲区没有限制。
client-output-buffer-limit normal 0 0 0  
(4) 密码:redis的密码功能较弱,同时多了一次IO
(5) 修改客户端源代码,禁止掉一些危险的命令(shutdown, flushall, monitor, keys *),当然还是可以通过redis-cli来完成
(6) 添加command-rename配置,将一些危险的命令(flushall, monitor, keys * , flushdb)做rename,如果有需要的话,找到redis的运维人员处理
rename-command FLUSHALL "随机数"  
rename-command FLUSHDB "随机数"  
rename-command KEYS "随机数"  
(六)、模拟实验:
1.  开启一个空的Redis(最简,直接redis-server)
redis-server  

初始化内存使用量如下:

# Memory  
used_memory:815072  
used_memory_human:795.97K  
used_memory_rss:7946240  
used_memory_peak:815912  
used_memory_peak_human:796.79K  
used_memory_lua:36864  
mem_fragmentation_ratio:9.75  
mem_allocator:jemalloc-3.6.0 

 client缓冲区:

# Clients  
connected_clients:1  
client_longest_output_list:0  
client_biggest_input_buf:0  
blocked_clients:0  

 2. 开启一个monitor:

redis-cli -h 127.0.0.1 -p 6379 monitor  

 3. 使用redis-benchmark:

redis-benchmark -h 127.0.0.1 -p 6379 -c 500 -n 200000  
4. 观察
(1) info memory:内存一直增加,直到benchmark结束,monitor输出完毕,但是used_memory_peak_human(历史峰值)依然很高–观察附件中日志
(2)info clients: client_longest_output_list: 一直在增加,直到benchmark结束,monitor输出完毕,才变为0–观察附件中日志
(3)redis-cli -h host -p port client list | grep “monitor” omem一直很高,直到benchmark结束,monitor输出完毕,才变为0–观察附件中日志
监控脚本:
while [ 1 == 1 ]  
do  
now=$(date "+%Y-%m-%d_%H:%M:%S")  
echo "=========================${now}==============================="  
echo " #Client-Monitor"  
redis-cli -h 127.0.0.1 -p 6379 client list | grep monitor  
redis-cli -h 127.0.0.1 -p 6379 info clients  
redis-cli -h 127.0.0.1 -p 6379 info memory  
#休息100毫秒  
usleep 100000  
done  
 完整的日志文件:
 

四、redis内存使用优化

(一)、背景: 选择合适的使用场景

很多时候Redis被误解并乱用了,造成的Redis印象:耗内存、价格成本很高:
1. 为了“赶时髦”或者对于Mysql的“误解”在一个并发量很低的系统使用Redis,将原来放在Mysql数据全部放在Redis中。—-(Redis比较适用于高并发系统,如果是一些复杂Mis系统,用Redis反而麻烦,因为单从功能讲Mysql要更为强大,而且Mysql的性能其实已经足够了。)
2. 觉得Redis就是个KV缓存—–(Redis支持多数据结构,并且具有很多其他丰富的功能)
3. 喜欢做各种对比,比如Mysql, Hbase, Redis等等—–(每种数据库都有自己的使用场景,比如Hbase吧,我们系统的个性化数据有1T,此时放在Redis根本就不合适,而是将一些热点数据放在Redis)
 
总之就是在合适的场景,选择合适的数据库产品。
 

(二)、一次string转化为hash的优化

1. 场景:
用户id: userId,
用户微博数量:weiboCount    
 
userId(用户id) weiboCount(微博数)
1 2000
2

10

3

288

….
1000000 1000
 
2. 实现方法:
(1) 使用Redis字符串数据结构, userId为key, weiboCount作为Value
(2) 使用Redis哈希结构,hashkey只有一个, key=”allUserWeiboCount”,field=userId,fieldValue= weiboCount
(3) 使用Redis哈希结构,  hashkey为多个, key=userId/100, field=userId%100, fieldValue= weiboCount
 
前两种比较容易理解,第三种方案解释一下:每个hashKey存放100个hash-kv,field=userId%100,也就是
 
userId hashKey field
1 0 1
2 0

2

3 0

3

….
99 0 99
100 1 0
101 1 1
….
9999 99 99
100000 1000 0

3. 获取方法:

#获取userId=5003用户的微博数  
(1) get 5003  
(2) hget allUserWeiboCount 5003  
(3) hget 50 3  

 4. 内存占用量对比(100万用户 userId:1~1000000) 

#方法一 Memory  
used_memory:85999592  
used_memory_human:82.02M  
used_memory_rss:96043008  
used_memory_peak:85999592  
used_memory_peak_human:82.02M  
used_memory_lua:36864  
mem_fragmentation_ratio:1.12  
mem_allocator:jemalloc-3.6.0  
  
#方法二 Memory  
used_memory:101665632  
used_memory_human:96.96M  
used_memory_rss:110702592  
used_memory_peak:101665632  
used_memory_peak_human:96.96M  
used_memory_lua:36864  
mem_fragmentation_ratio:1.09  
mem_allocator:jemalloc-3.6.0  
  
#方法三 Memory  
used_memory:9574136  
used_memory_human:9.13M  
used_memory_rss:17285120  
used_memory_peak:101665632  
used_memory_peak_human:96.96M  
used_memory_lua:36864  
mem_fragmentation_ratio:1.81  
mem_allocator:jemalloc-3.6.0  

 内存使用量:

20170704104650

5. 导入数据代码(不考虑代码优雅性,单纯为了测试,勿喷)

package com.carlosfu.redis;  
  
import java.util.ArrayList;  
import java.util.HashMap;  
import java.util.List;  
import java.util.Map;  
import java.util.Random;  
  
import org.junit.Test;  
  
import redis.clients.jedis.Jedis;  
  
/** 
 * 一次string-hash优化 
 * @author carlosfu 
 * @Date 2015-11-8 
 * @Time 下午7:27:45 
 */  
public class TestRedisMemoryOptimize {  
      
    private final static int TOTAL_USER_COUNT = 1000000;  
  
    /** 
     * 纯字符串 
     */  
    @Test  
    public void testString() {  
        Jedis jedis = null;  
        try {  
            jedis = new Jedis("127.0.0.1", 6379);  
            List<String> kvsList = new ArrayList<String>(200);  
            for (int i = 1; i <= TOTAL_USER_COUNT; i++) {  
                String userId = String.valueOf(i);  
                kvsList.add(userId);  
                String weiboCount = String.valueOf(new Random().nextInt(100000));  
                kvsList.add(weiboCount);  
                if (i % 2000 == 0) {  
                    System.out.println(i);  
                    jedis.mset(kvsList.toArray(new String[kvsList.size()]));  
                    kvsList = new ArrayList<String>(200);  
                }  
            }  
        } catch (Exception e) {  
            e.printStackTrace();  
        } finally {  
            if (jedis != null) {  
                jedis.close();  
            }  
        }  
    }  
      
      
      
    /** 
     * 纯hash 
     */  
    @Test  
    public void testHash() {  
        String hashKey = "allUserWeiboCount";  
          
        Jedis jedis = null;  
        try {  
            jedis = new Jedis("127.0.0.1", 6379);  
            Map<String,String> kvMap = new HashMap<String, String>();  
            for (int i = 1; i <= TOTAL_USER_COUNT; i++) {  
                String userId = String.valueOf(i);  
                String weiboCount = String.valueOf(new Random().nextInt(100000));  
                kvMap.put(userId, weiboCount);  
                if (i % 2000 == 0) {  
                    System.out.println(i);  
                    jedis.hmset(hashKey, kvMap);  
                    kvMap = new HashMap<String, String>();  
                }  
            }  
        } catch (Exception e) {  
            e.printStackTrace();  
        } finally {  
            if (jedis != null) {  
                jedis.close();  
            }  
        }  
    }  
      
    /** 
     * segment hash 
     */  
    @Test  
    public void testSegmentHash() {  
        int segment = 100;  
        Jedis jedis = null;  
        try {  
            jedis = new Jedis("127.0.0.1", 6379);  
            Map<String,String> kvMap = new HashMap<String, String>();  
            for (int i = 1; i <= TOTAL_USER_COUNT; i++) {  
                String userId = String.valueOf(i % segment);  
                String weiboCount = String.valueOf(new Random().nextInt(100000));  
                kvMap.put(userId, weiboCount);  
                if (i % segment == 0) {  
                    System.out.println(i);  
                    int hash = (i-1) / segment;  
                    jedis.hmset(String.valueOf(hash), kvMap);  
                    kvMap = new HashMap<String, String>();  
                }  
            }  
        } catch (Exception e) {  
            e.printStackTrace();  
        } finally {  
            if (jedis != null) {  
                jedis.close();  
            }  
        }  
    }  
  
}  

 (三)、结果对比

redis核心对象 数据类型 + 编码方式 + ptr  分段hash也不会造成drift
 
方案 优点 缺点
string

直观、容易理解

  1. 内存占用较大
  2. key值分散、不变于计算整体
hash

直观、容易理解、整合整体

  1. 内存占用大
  2. 一个key占用过大内存,如果是redis-cluster会出 现data drift

 

segment-hash

内存占用量小,虽然理解不够直观,但是总体上是最优的。

理解不够直观。

 

(四)、结论:

在使用Redis时,要选择合理的数据结构解决实际问题,那样既可以提高效率又可以节省内存。所以此次优化方案三为最佳。

五、redis cluster遇到的一些问题 

由于演讲时间有限,有关Redis-Cluster,演讲者没做太多介绍,简单的介绍了一些Redis-Cluster概念作用和遇到的两个问题,我们在Redis-Cluster也有很多运维经验,将来的文章会介绍。

但是讲演者反复强调,不要听信网上对于Redis-Cluster的毁谤(实践出真知),对于这一点我很赞同,我们从Redis-Cluster beta版 RC1~4 到现在的3.0-release均没有遇到什么大问题(线上维护600个实例)。

(一)、Redis-Cluster

有关Redis-Cluster的详细介绍有很多这里就不多说了,可以参考:
1. redis-cluster研究和使用
2. Redis Cluster 3.0.5集群实践
3. 本博客的一些Redis-Cluster的介绍(未更新完毕)
4. Redis设计与实现那本书(作者:黄建宏):非常的推荐看这本书。

总之Redis-Cluster是一个无中心的分布式Redis存储架构,解决了Redis高可用、可扩展等问题。

20170704105102

(二)、两个问题:

1. Redis-Cluster主从节点不要在同一个机器部署

(1) 以我们的经验看redis实例本身基本不会挂掉,通常是机器出了问题(断电、机器故障)、甚至是机架、机柜出了问题,造成Redis挂掉。

(2) 如果Redis-Cluster的主从都在一个机器上,那么如果这台机器挂了,主从全部挂掉,高可用就无法实现。(如果full converage=true,也就意味着整个集群挂掉)

(3) 通常来讲一对主从所在机器:不跨机房、要跨机架、可以在一个机柜。

2. Redis-Cluster误判节点fail进行切换

(1) Redis-Cluster是无中心的架构,判断节点失败是通过仲裁的方式来进行(gossip和raft),也就是大部分节点认为一个节点挂掉了,就会做fail判定。

(2) 如果某个节点在执行比较重的操作(flushall, slaveof等等)(可能短时间redis客户端连接会阻塞(redis单线程))或者由于网络原因,造成其他节点认为它挂掉了,会做fail判定。

(3) Redis-Cluster提供了cluster-node-timeout这个参数(默认15秒),作为fail依据(如果超过15秒还是没反应,就认为是挂掉了),具体可以参考这篇文章:Redis-Cluster的FailOver失败案例分析

以我们的经验看15秒完全够用。

20170704105354

三、未来要介绍的问题:

 1. Redis-Cluster客户端实现Mget操作。

2. Redis-Cluster–Too many Cluster redirections异常

3. Redis-Cluster无底洞问题解析。

4. 两个Redis-Cluster集群,meet操作问题后的恶果。

5. Redis-Cluster配置之full converage问题。

6. Redis-Cluster故障转移测试

7. Redis-Cluster常用运维技巧。

8. Redis-Cluster一键开通。

9. Redis-Cluster客户端jedis详解。

转载请注明:架构迷 » 【案例】美团在Redis上踩过的一些坑

发表我的评论
取消评论

表情

Hi,您需要填写昵称和邮箱!

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址