转自:http://carlosfu.iteye.com/blog/2254154
上上周和同事(龙哥)参加了360组织的互联网技术训练营第三期,美团网的DBA负责人侯军伟给大家介绍了美团网在redis上踩得一些坑,讲的都是干货和坑。
分为5个部分:
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的使用者)
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三次握手:
(2) redis客户端与redis服务器交互的过程(本质就是TCP请求)
(3) I/O 多路复用程序通过队列向文件事件分派器传送套接字的过程
(4) 和redis有什么关系呢?
由于Redis的单线程模型(对命令的处理和连接的处理都是在一个线程中),如果存在慢查询的话,会出现上面的这种情况,造成新的accept的连接进不了队列。
如果上面的图没法理解的话,看看这张图:
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 如何处理客户端连接
一、背景
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();
- }
- }
附图一张:
一、现象:
redis-cluster某个分片内存飙升,明显比其他分片高很多,而且持续增长。并且主从的内存使用量并不一致。
二、分析可能原因:
1. redis-cluster的bug (这个应该不存在)
2. 客户端的hash(key)有问题,造成分配不均。(redis使用的是crc16, 不会出现这么不均的情况)
3. 存在个别大的key-value: 例如一个包含了几百万数据set数据结构(这个有可能)
4. 主从复制出现了问题。
5. 其他原因
三、调查原因:
1. 经查询,上述1-4都不存在
2. 观察info信息,有一点引起了怀疑: client_longes_output_list有些异常。
3. 于是理解想到服务端和客户端交互时,分别为每个客户端设置了输入缓冲区和输出缓冲区,这部分如果很大的话也会占用Redis服务器的内存。
从上面的client_longest_output_list看,应该是输出缓冲区占用内存较大,也就是有大量的数据从Redis服务器向某些客户端输出。
于是使用client list命令(类似于mysql processlist) redis-cli -h host -p port client list | grep -v "omem=0",来查询输出缓冲区不为0的客户端连接,于是查询到祸首monitor,于是豁然开朗.
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