上上周和同事(龙哥)参加了360组织的互联网技术训练营第三期,美团网的DBA负责人侯军伟给大家介绍了美团网在redis上踩得一些坑,讲的都是干货和坑。
- redis.clients.jedis.exceptions.JedisConnectionException
- java.net.SocketException
- java.net.SocketTimeoutException:connect time out
一、背景
1. AOF:
Redis的AOF机制有点类似于Mysql binlog,是Redis的提供的一种持久化方式(另一种是RDB),它会将所有的写命令按照一定频率(no, always, every seconds)写入到日志文件中,当Redis停机重启后恢复数据库。
2. AOF重写:
(1) 随着AOF文件越来越大,里面会有大部分是重复命令或者可以合并的命令(100次incr = set key 100)
(2) 重写的好处:减少AOF日志尺寸,减少内存占用,加快数据库恢复时间。
二、单机多实例可能存在Swap和OOM的隐患:
由于Redis的单线程模型,理论上每个redis实例只会用到一个CPU, 也就是说可以在一台多核的服务器上部署多个实例(实际就是这么做的)。但是Redis的AOF重写是通过fork出一个Redis进程来实现的,所以有经验的Redis开发和运维人员会告诉你,在一台服务器上要预留一半的内存(防止出现AOF重写集中发生,出现swap和OOM)。
三、最佳实践
1. meta信息:作为一个redis云系统,需要记录各个维度的数据,比如:业务组、机器、实例、应用、负责人多个维度的数据,相信每个Redis的运维人员都应该有这样的持久化数据(例如Mysql),一般来说还有一些运维界面,为自动化和运维提供依据
例如如下:
2. AOF的管理方式:
(1) 自动:让每个redis决定是否做AOF重写操作(根据auto-aof-rewrite-percentage和auto-aof-rewrite-min-size两个参数):
(2) crontab: 定时任务,可能仍然会出现多个redis实例,属于一种折中方案。
(3) remote集中式:
最终目标是一台机器一个时刻,只有一个redis实例进行AOF重写。
具体做法其实很简单,以机器为单位,轮询每个机器的实例,如果满足条件就运行(比如currentSize和baseSize满足什么关系)bgrewriteaof命令。
期间可以监控发生时间、耗时、频率、尺寸的前后变化
策略 | 优点 | 缺点 |
自动 | 无需开发 | 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();
- }
- }
附图一张:
- client-output-buffer-limit normal 0 0 0
- rename-command FLUSHALL "随机数"
- rename-command FLUSHDB "随机数"
- rename-command KEYS "随机数"
- 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
- # Clients
- connected_clients:1
- client_longest_output_list:0
- client_biggest_input_buf:0
- blocked_clients:0
- redis-cli -h 127.0.0.1 -p 6379 monitor
- redis-benchmark -h 127.0.0.1 -p 6379 -c 500 -n 200000
- 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
(磁带已死,磁盘是新磁带,闪存是新磁盘,随机存储器局部性是为王道)
userId(用户id) | weiboCount(微博数) |
1 | 2000 |
2 | 10 |
3 | 288 |
.... | ... |
1000000 | 1000 |
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
内存使用量:
- 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();
- }
- }
- }
- }
方案 | 优点 | 缺点 |
string | 直观、容易理解 |
|
hash | 直观、容易理解、整合整体 |
|
segment-hash | 内存占用量小,虽然理解不够直观,但是总体上是最优的。 | 理解不够直观。 |
由于演讲时间有限,有关Redis-Cluster,演讲者没做太多介绍,简单的介绍了一些Redis-Cluster概念作用和遇到的两个问题,我们在Redis-Cluster也有很多运维经验,将来的文章会介绍。
但是讲演者反复强调,不要听信网上对于Redis-Cluster的毁谤(实践出真知),对于这一点我很赞同,我们从Redis-Cluster beta版 RC1~4 到现在的3.0-release均没有遇到什么大问题(线上维护600个实例)。
一、Redis-Cluster
有关Redis-Cluster的详细介绍有很多这里就不多说了,可以参考:
3. 本博客的一些Redis-Cluster的介绍(未更新完毕)
4. Redis设计与实现那本书(作者:黄建宏):非常的推荐看这本书。
总之Redis-Cluster是一个无中心的分布式Redis存储架构,解决了Redis高可用、可扩展等问题。
二、两个问题:
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秒完全够用。
三、未来要介绍的问题:
1. Redis-Cluster客户端实现Mget操作。
2. Redis-Cluster--Too many Cluster redirections异常。
3. Redis-Cluster无底洞问题解析。
4. 两个Redis-Cluster集群,meet操作问题后的恶果。
5. Redis-Cluster配置之full converage问题。
7. Redis-Cluster常用运维技巧。
8. Redis-Cluster一键开通。
9. Redis-Cluster客户端jedis详解。
四、附赠一些不错的资料:
- Redis-Cluster的FailOver失败案例分析
- Redis Cluster 迁移遇到的各种坑及解决方案
- Redis Cluster架构优化
- Redis常见集群方案、Codis实践及与Twemproxy比较
- Redis Cluster架构优化
- 【运维实践】鱼与熊掌:使用redis-cluster需要注意些什么?
- Docker及和Redis Cluster的化学反应(上)By 芒果TV
- Docker及和Redis Cluster的化学反应(下)By 芒果TV
- Redis cluster使用经验——网易有道
- Redis Cluster浅析和Bada对比
- 互联网Redis应用场景探讨
- Redis集群技术及Codis实践
- 谈Twitter的百TB级Redis缓存实践
- Hadoop、Spark、HBase与Redis的适用性讨论
- Codis作者黄东旭细说分布式Redis架构设计和踩过的那些坑们