环境及问题概述

最近检查服务器的内存使用情况,发现每台服务器的内存剩下的都剩下了个位数,查看各个进程使用内存情况发现redis各个实例占用近16%的内存,每台服务器128G的内存,每个实例有20G的占用。我的天哪怎么这么多?于是开始了redis集群内存占用过高原因的排查和解决之路。

每台服务器上有4个redis实例,版本3.2.8,有3台大数据服务器,6主 6从的rediscluster.但是查看redis进程发现除了redis本身的实例占用16%的内存外,还有2到3个redis的rdb备份进程redis-rdb-bgsave偶尔执行,奇怪的是每个备份进程所占用的内存也是占用和redis实例差不多的内存,这样在交易数据量大的时间段3个备份进程加上4个实例几乎要把服务器的内存全都占用了,于是服务器上的内存剩下0 了各种各样的问题接踵而至。

问题分析

这个rediscluster是之前的同事搭建的,为了把原因查清楚,于是把生产环境上的redis-cluster的搭建方法,配置文件的内容,各个logs和data所放的目录全都看了几遍,发现集群每个实例都是采用redis的rdb的持久化方式,关闭了aof.并且rdb的参数用的是默认的参数设置,即,save 900 1 save 300 10 save 60 10000。而且没有设置redis的最大使用内存。所以在交易量的高峰时段,就会满足60秒10000个key改变的情况,所以会出现rdb备份进程在高峰时段会有至少2个出现。此外还发现一个问题就是集群的负载均衡做的不太好,在6主6从的集群中,每台服务器上的主和从的数量是不一样的,就是在01服务器上可能有3个主1个从,而在02服务器上就是3个从1个主的情况。后来分析这种情况要么是开始搭建的起就不对要么就是后来的实例挂掉后主从切换了。说了这么多,那么内存的占用那么高跟这有没有关系哪?本人认为rdb默认的参数设置是合理的,考虑了交易量高峰和低谷的 情况,并且翻阅资料知道rdb的备份进程每个占用的内存就是要用一个实例的内存,所以他使用的内存会随着实例使用内存的增长而增长,是正常的。

换个角度,redis本身就是内存数据库,所占用的内存跟数据量的增长是密切相关的,于是开始和数据分析人员以及数据引擎的开发人员交流,果然分析人员在管理系统的使用上配置了一系列的没有用到的统计,而核心处置引擎每次上下线都会去mysql里面去加载要累加数据的统计指标。如果这些指标在实际的规则中没有用到的话,那么就不会去把窗口外的数据设置过期,那么就会一直累加数据到redis里面,对于我们的手机银行的渠道每天几百万的交易数据,那可想而知了。

问题解决

由于redis数据量太大,使用普通shell脚本删除还是java程序删除都会出现问题,于是采用各个主节点分页删除的方法写了一个java程序来删除:

程序执行之前注意一下几个事项:

关闭所有节点的rdb和aof持久化,暂停了数据采集程序。

public static void main(String[] args) {
        
            System.out.println("----进入delKeys----");
    

            int count = 5000;
            ScanParams scanParams = new ScanParams().count(count);
            
            String stat = "S100 S200 S300";
            String arrs[] = stat.split(" ");

            int i= 0;
            for (String keyPrefix : arrs) { // 每一条统计
                i ++;

                JedisPoolConfig poolConfig = new JedisPoolConfig();
                // 最大连接数
                poolConfig.setMaxTotal(3);
                // 最大空闲数
                poolConfig.setMaxIdle(1);
                // 最大允许等待时间,如果超过这个时间还未获取到连接,则会报JedisException异常:
                // Could not get a resource from the pool
                poolConfig.setMaxWaitMillis(1000000);
                Set<HostAndPort> nodes = new LinkedHashSet<HostAndPort>();
 

                nodes.add(new HostAndPort("192.168.33.3", 9001));
                nodes.add(new HostAndPort("192.168.33.3", 9002));
                nodes.add(new HostAndPort("192.168.33.4", 9003));
                nodes.add(new HostAndPort("192.168.33.4", 9004));
                nodes.add(new HostAndPort("192.168.33.5", 9005));
                nodes.add(new HostAndPort("192.168.33.5", 9006));
              

                JedisCluster cluster = new JedisCluster(nodes,100000,100000,10,"gskjgfyxgs@2018",poolConfig);

                Map<String, JedisPool> clusterNodes = cluster.getClusterNodes();

                String keysPattern = "*" +keyPrefix + "*";

                long startTime1 = System.currentTimeMillis();
                for (Map.Entry<String, JedisPool> entry : clusterNodes.entrySet()) {
                    Jedis jedis = entry.getValue().getResource();
                    int num = 0;
                    if (!jedis.info("replication").contains("role:slave")) {
                        long dbsize = jedis.dbSize();
                        long scan = dbsize/count;
                        long yushu = dbsize%count;
                        if(yushu>0){
                            scan = scan + 1;
                        }
                        String ip =  jedis.getClient().getHost();
                        int port =  jedis.getClient().getPort();

                        scanParams.match(keysPattern);
                        String cur = ScanParams.SCAN_POINTER_START;
                        long startTime = System.currentTimeMillis();
                        for (int a = 0;a<scan;a++) {
                            ScanResult<String> scanResult = jedis.scan(cur, scanParams);
                            List<String> keyList = scanResult.getResult();
                            cur = scanResult.getCursor();
                            if(keyList.size()>0){
                                Pipeline pip = jedis.pipelined();
                                for(String key :keyList){
                                    pip.del(key);
                                }
                                pip.sync();//同步
                            }

                            num+=keyList.size();
                        }
                        long endTime = System.currentTimeMillis();
                        long time = (endTime - startTime) / 1000;
                        System.out.println("key="+keysPattern+" ip= "+ip+"  port= "+ port +" 总删除条数="+num+" 耗时="+time+"s");
                    }

                }

                long endTime1 = System.currentTimeMillis();
                long time1 = (endTime1 - startTime1) / 1000;
                System.out.println("key="+keysPattern+"总耗时="+time1+"s"+" 未删除的统计个数="+(arrs.length-i));

                cluster.close();

            }

        }
    }
总结:

程序执行完之后,先开启各个节点的rdb持久化,等一会数据备份到dump数据文件中,并且同步数据到从节点中之后,再重启整个redis集群。

重启完集群之后发现,每台机器的内存都释放了30G左右的内存。

通过几个小时的删除,删除了redis集群里将近2亿个key。

由于着急解决问题并没有对代码结构进行优化,后期会在web页面形成功能性的监控和删除机制。还有本人第一次写博客,写的不好,希望各小哥哥小姐姐见谅!