图片来自 Pexels
正当我一边喝,一边沉思今晚吃点啥的问题时,还没等我想明白,报警系统把我的黄粱美梦震碎成一地鸡毛。
我急忙去 Sentry 上查看上报缺点日志,创造全都是:

redis.clients.jedis.exceptions.JedisConnectionException:Couldnotgetaresourcefromthepool
我没动过 Redis 啊......
内心激动的我无以言表,但是外表还是得表现沉着,此时我必须的做出选择:回滚or重启。
我也不知道是从哪里来的蜜汁自傲,我坚信这跟我没紧要,我不管,我就要重启。
韶光每一秒对付等待重启过程中的我来说变得无比的慢,就像小时候犯了错,在老师办公室等待父母到来那种觉得。
重启的过程中我连续去看报错日志,猛地创造一条:
什么鬼,谁打日志打成这样?当我点开准备看看是哪位大侠打的日志的时候,我惊奇的创造:
APPLICATION FAILED TO START ...... ......
原来是做事没起来。此刻我的内心是缭乱的,无助的,彷徨不安的。做事没起来,哪里来的 Redis 要求?
能阐明通的便是该当是来自于定时任务刷新数据对 Redis 的要求。这里也解释另一个问题:虽然端口占用,但是做事实在还是发布起来了,不然不可能运行定时任务。
但是还有另一个问题,Redis 为什么报错,且报错的缘故原由还是:
java.lang.IllegalStateException:Poolnotopen
Jedis 线程池未初始化。项目既然能去实行定时任务,为什么不去初始化 Redis 干系配置呢?想想都头疼。这里可以给大家留个坑只管猜。
我们本日的重点不是项目为啥没起来,而是 Redis 那些年都报过哪些错,让你夜不能寐。以下缺点都基于 Jedis 客户端。
忘却添加白名单
之以是把这个放在第一位,是由于上线不规范,亲人不能睡。
上线之前检讨所有的配置项,只假如测试环境做过的操作,一定要拿个小本本记下。
在现如今利用个啥啥都要授权的时期你咋能就忘了白名单这种东西呢!
无法从连接池获取到连接
如果连接池没有可用 Jedis 连接,会等待 maxWaitMillis(毫秒),依然没有获取到可用 Jedis 连接,会抛出如下非常:
redis.clients.jedis.exceptions.JedisException: Could not get a resource from the pool at redis.clients.util.Pool.getResource(Pool.java:51) at redis.clients.jedis.JedisPool.getResource(JedisPool.java:226) at com.yy.cs.base.redis.RedisClient.zrangeByScoreWithScores(RedisClient.java:2258) ...... java.util.NoSuchElementException: Timeout waiting for idle object at org.apache.commons.pool2.impl.GenericObjectPool.borrowObject(GenericObjectPool.java:448) at org.apache.commons.pool2.impl.GenericObjectPool.borrowObject(GenericObjectPool.java:362) at redis.clients.util.Pool.getResource(Pool.java:49) at redis.clients.jedis.JedisPool.getResource(JedisPool.java:226) ......
实在涌现这个问题,我们从两个方面来探测一下缘故原由:
连接池配置有问题连接池没问题,利用有问题连接池配置
Jedis Pool 有如下参数可以配置:
①如何确定 maxTotal 呢?
最大连接数肯定不是越大越好,首先 Redis 做事端已经配置了许可所有客户端连接的最大连接数,那么当前连接 Redis 的所有节点的连接池加起来总数不能超过做事真个配置。
其次对付单个节点来说,须要考虑单机的 Redis QPS,假设同机房 Redis 90% 的操作耗时都在 1ms,那么 QPS 大约是 1000。
而业务系统期望 QPS 能达到 10000,那么理论上须要的连接数=10000/1000=10。
考虑到网络抖动不可能每次操作都这么定时,以是实际配置值该当比当前预估值大一些。
②maxIdle 和 minIdle 如何确定?
maxIdle 从默认值来看是即是 maxTotal。这么做的缘故原由在于既然已经分配了 maxTotal 个连接,如果 maxIdle
如果你的系统只是在高峰期才会达到 maxTotal 的量,那么你可以通过 minIdle 来掌握低峰期最低有多少个连接的存活。
以是连接池参数的配置直接决定了你能否获取连接以及获取连接效率问题。
利用有问题
说到利用,真的便是仁者见仁智者也会犯错,谁都不能担保他写的代码一次性考虑全面。
比如有这么一段代码:
是不是没有问题。再好好想想,这里从线程池中获取了 Jedis 连接,用完了是不是要归还?不然这个连接一贯被某个人占用着,线程池逐步连接数就被花费完。
以是精确的写法:
多个线程利用同一个 Jedis 连接
这种缺点一样平常发生在新手身上会多一些。
这段代码乍看是不是觉得良好,不过你跑起来了之后就知道有多痛楚:
redis.clients.jedis.exceptions.JedisConnectionException: Unexpected end of stream. at redis.clients.util.RedisInputStream.ensureFill(RedisInputStream.java:199) at redis.clients.util.RedisInputStream.readByte(RedisInputStream.java:40) at redis.clients.jedis.Protocol.process(Protocol.java:151) ......
这个报错是不是让你一头雾水,不知所措。涌现这种报错是做事端无法分辨出一条完全的从哪里结束,正常情形下一个连接被一个线程利用,上面这种情形多个线程同时利用一个连接发送,那做事端可能就无法区分到底现在发送的是哪一条的。
类型转换缺点
这种缺点虽然很低级,但是涌现的几率还不低。
java.lang.ClassCastException: com.test.User cannot be cast to com.test.User at redis.clients.jedis.Connection.getBinaryMultiBulkReply(Connection.java:199) at redis.clients.jedis.Jedis.hgetAll(Jedis.java:851) at redis.clients.jedis.ShardedJedis.hgetAll(ShardedJedis.java:198)
上面这个错乍一看是不是很吃惊,为啥同一个类无法反序列化。由于开拓这个功能的同学用了一个序列化框架 Kryo 先将 User 工具序列化后存储到 Redis。
后来 User 工具增加了一个字段,而反序列化的 User 与新的 User 工具对不上导致无法反序列化。
客户端读写超时
涌现客户端读超时的缘故原由很多,这种情形就要综合来判断。
redis.clients.jedis.exceptions.JedisConnectionException: java.net.SocketTimeoutException: Read timed out ......
涌现这种情形的缘故原由我们可以综合剖析:
首先检讨读写超时时间是否设置的过短,如果确定设置的很短,调大一点不雅观察一下效果。其次检讨涌现超时的命名是否本身实行较大的存储或者拉数据任务。如果数据量过大,那么就要考虑做业务拆分。前面这两项如果还不能确定,那么就要检讨一下网络问题,确定当前业务主机和 Redis 做事器主机是否在同机房,机房质量怎么样。机房质量如果还是没问题,那能做的便是检讨当前业务中 Redis 读写是否发生有可能发生壅塞,是否业务量大到这种程度,是否须要扩容。大 Key 造成的 CPU 飙升
我们有个新项目中 Redis 紧张存储西席真个讲义数据(浓缩讲义非全部), QPS 达到了15k,但是通过监控查看命中率特殊低,仅 15% 旁边。这解释有很多讲义是没有被看的,Cache 这样利用是对内存的极大摧残浪费蹂躏。
项目在上线中期就频繁涌现 Redis 所在机器 CPU 利用率频频报警,单看这么低的命中率也很难想象到底是什么导致 CPU 超。后面不雅观察到报警时候的 response 数据基本都在 15k-30 k 旁边。
不雅观察了 Redis 的缺点日志,有一些页交流缺点的日志。联系起来看可以得出结论:Redis 获取大工具时该工具首先被序列化到通信缓冲区中,然后写入客户端套接字,这个序列化是有本钱的,涉及到随机 I/O 读写。
其余 Redis 官方也不建议利用 Redis 存储大数据,虽然官方建议值是一个 value 最大值不能超过 512M,试想真的存储一个 512M 的数据到缓存和到关系型数据库的差异该当不大,但是本钱就完备不一样。
Too Many Cluster Redirections
这个缺点信息一样平常在 cluster 环境中涌现,紧张缘故原由还是单机涌现了命令堆积。
redis.clients.jedis.exceptions.JedisClusterMaxRedirectionsException: Too many Cluster redirections? at redis.clients.jedis.JedisClusterCommand.runWithRetries(JedisClusterCommand.java:97) at redis.clients.jedis.JedisClusterCommand.runWithRetries(JedisClusterCommand.java:152) at redis.clients.jedis.JedisClusterCommand.runWithRetries(JedisClusterCommand.java:131) at redis.clients.jedis.JedisClusterCommand.runWithRetries(JedisClusterCommand.java:152) at redis.clients.jedis.JedisClusterCommand.runWithRetries(JedisClusterCommand.java:131) at redis.clients.jedis.JedisClusterCommand.runWithRetries(JedisClusterCommand.java:152) at redis.clients.jedis.JedisClusterCommand.runWithRetries(JedisClusterCommand.java:131) at redis.clients.jedis.JedisClusterCommand.run(JedisClusterCommand.java:30) at redis.clients.jedis.JedisCluster.get(JedisCluster.java:81)
Redis 是单线程处理要求的,如果一条命令实行的特殊慢(可能是网络壅塞,可能是获取数据量大),那么新到来的要求就会放在 TCP 行列步队中等待实行。但是等待实行的命令数如果超过了设置的行列步队大小,后面的要求就会被丢弃。
涌现上面这个缺点的缘故原由是:
集议论况中 client 先通过key 打算 slot,然后查询 slot 对应到哪个做事器,假设这个 slot 对应到 server1,那么就去要求 server1。此时如果 server1 整由于实行慢命令而被壅塞且 TCP 行列步队也已满,那么新来的要求就会直接被谢绝。client 以为是 server1不可用,随即要求另一个做事器 server2。server2 检讨到该 slot 由 server1 卖力且 server1 心跳检讨正常,以是见告 client 你还是去找 server1 吧。client 又来要求 server1,但是 server1 此时还是壅塞中,又回到 3。当要求的次数超过谢绝做事次数之后,就会抛出非常。再次解释,大命令要不得。对付这种缺点,最紧张的便是要优化存储构造或者获取数据办法。其次,增加 TCP 行列步队长度。再次,扩容也是可以办理的。
集群扩容之后找不到 Key
现在有如下集群,6 台主节点,6 台从节点:
redis-master001~redis-master006redis-slave001~redis-slave006之前 Redis 集群的 16384 个槽均匀分配在 6 台主节点中,每个节点 2730 个槽。
现在线上主节点数已经涌现到达容量阈值,须要增加 3 主 3 从。
为担保扩容后,槽依然均匀分布,须要将之前 6 台的每台机器上迁移出 910 个槽,方案如下:
分配完之后,每台节点 1820 个 slot。迁移完数据之后,开始报如下非常:
Exception in thread "main" redis.clients.jedis.exceptions.JedisMovedDataException: MOVED 1539 34.55.8.12:6379 at redis.clients.jedis.Protocol.processError(Protocol.java:93) at redis.clients.jedis.Protocol.process(Protocol.java:122) at redis.clients.jedis.Protocol.read(Protocol.java:191) at redis.clients.jedis.Connection.getOne(Connection.java:258) at redis.clients.jedis.ShardedJedisPipeline.sync(ShardedJedisPipeline.java:44) at org.hu.e63.MovieLens21MPipeline.push(MovieLens21MPipeline.java:47) at org.hu.e63.MovieLens21MPipeline.main(MovieLens21MPipeline.java:53
报这种缺点肯定便是 slot 迁移之后找不到了。
我们看一下代码:
之以是这种办法会出问题还是在于我们没有明白 Redis Cluster 的事情事理。
Key 通过 Hash 被均匀的分配到 16384 个槽中,不同的机器被分配了不同的槽,那么我们利用的 API 是不是也要支持去打算当前 Key 要被落地到哪个槽。
你可以去看看 Pipelined 的源码它支持打算槽吗。动脑筋想想 Pipelined 这种批量操作也不太适宜集群事情。
以是我们用错了 API。如果在集群模式下要利用 JedisCluster API,示例代码如下:
JedisPoolConfig config = new JedisPoolConfig(); //可用连接实例的最大数目,默认为8; //如果赋值为-1,则表示不限定,如果pool已经分配了maxActive个jedis实例,则此时pool的状态为exhausted(耗尽) private Integer MAX_TOTAL = 1024; //掌握一个pool最多有多少个状态为idle(空闲)的jedis实例,默认值是8 private Integer MAX_IDLE = 200; //等待可用连接的最大韶光,单位是毫秒,默认值为-1,表示永不超时。 //如果超过等待韶光,则直接抛出JedisConnectionException private Integer MAX_WAIT_MILLIS = 10000; //在borrow(用)一个jedis实例时,是否提提高行validate(验证)操作; //如果为true,则得到的jedis实例均是可用的 private Boolean TEST_ON_BORROW = true; //在空闲时检讨有效性, 默认false private Boolean TEST_WHILE_IDLE = true; //是否进行有效性检讨 private Boolean TEST_ON_RETURN = true; config.setMaxTotal(MAX_TOTAL); config.setMaxIdle(MAX_IDLE); config.setMaxWaitMillis(MAX_WAIT_MILLIS); config.setTestOnBorrow(TEST_ON_BORROW); config.setTestWhileIdle(TEST_WHILE_IDLE); config.setTestOnReturn(TEST_ON_RETURN); Set<HostAndPort> jedisClusterNode = new HashSet<HostAndPort>(); jedisClusterNode.add(new HostAndPort("192.168.0.31", 6380)); jedisClusterNode.add(new HostAndPort("192.168.0.32", 6380)); jedisClusterNode.add(new HostAndPort("192.168.0.33", 6380)); jedisClusterNode.add(new HostAndPort("192.168.0.34", 6380)); jedisClusterNode.add(new HostAndPort("192.168.0.35", 6380)); JedisCluster jedis = new JedisCluster(jedisClusterNode, 1000, 1000, 5, config);
以上先容了看似平常实则在日常开拓中只要一不把稳就会发生的缺点。出错了先别慌,保留日志现场,如果一眼能看出问题就修复,如果不能就赶紧回滚,不然再过一会便是一级事件你的年终奖估计就没了。
Redis 精确利用小技巧
①精确设置过期韶光
把这个放在第一位是由于这里实在是有太多坑。
如果你不设置过期韶光,那么你的 Redis 就成了垃圾堆,假以时日你领导看到了告警,再看一下你的代码,估计你可能就 “没了”!
如果你设置了过期韶光,但是又设置了特殊长,比如两个月,那么带来的问题便是极有可能你的数据不一致问题会变得特殊棘手。
我就碰着过这种,用户信息缓存中包含了除基本信息外的各种附加属性,这些属性又是随时会变的,在有变革的时候关照缓存进行更新,但是这些附加信息是在各个微做事中,做事之间调用总会有失落败的时候,只要发生那便是缓存与数据不一致之日。
但是缓存又是 2 个月过期一次,碰着这种情形你能怎么办,只好手动删除缓存,重新去拉数据。
以是过期韶光设置是很有技巧性的。
②批量操作利用 Pipeline 或者 Lua 脚本
利用 Pipeline 或 Lua 脚本可以在一次要求中发送多条命令,通过分摊一次要求的网络及系统延迟,从而可以极大的提高性能。
③大工具只管即便利用序列化或者先压缩再存储
如果存储的值是工具类型,可以选择利用序列化工具比如 protobuf,Kyro。对付比较大的文本存储,如果真的有这种需求,可以考虑先压缩再存储,比如利用 snappy 或者 lzf 算法。
④Redis 做事器支配只管即便与业务机器同机房
如果你的业务对延迟比较敏感,那么只管即便申请与当前业务机房同地区的 Redis 机器。同机房 Ping 值可能在 0.02ms,而跨机房能达到 20ms。当然如果业务量小或者对延迟的哀求没有那么高这个问题可以忽略。
Redis 做事器内存分配策略的选择:
首先我们利用 info 命令来查看一下当前内存分配中都有哪些指标:
info $2962 # Memory used_memory:325288168 used_memory_human:310.22M #数据利用内存 used_memory_rss:337371136 used_memory_rss_human:321.74M #总占用内存 used_memory_peak:327635032 used_memory_peak_human:312.46M #峰值内存 used_memory_peak_perc:99.28% used_memory_overhead:293842654 used_memory_startup:765712 used_memory_dataset:31445514 used_memory_dataset_perc:9.69% total_system_memory:67551408128 total_system_memory_human:62.91G # 操作系统内存 used_memory_lua:43008 used_memory_lua_human:42.00K maxmemory:2147483648 maxmemory_human:2.00G maxmemory_policy:allkeys-lru # 内存超限时的开释空间策略 mem_fragmentation_ratio:1.04 # 内存碎片率(used_memory_rss / used_memory) mem_allocator:jemalloc-4.0.3 # 内存分配器 active_defrag_running:0 lazyfree_pending_objects:0
上面我截取了 Memory 信息。根据参数:mem_allocator 能看到当前利用的内存分配器是 jemalloc。
Redis 支持三种内存分配器:tcmalloc,jemalloc 和 libc(ptmalloc)。
在存储小数据的场景下,利用 jemalloc 与 tcmalloc 可以显著的降落内存的碎片率。
根据这里的评测:
https://matt.sh/redis-quicklist
保存 200 个列表,每个列表有 100 万的数字,利用 jemalloc 的碎片率为 3%,共利用 12.1GB 内存,而利用 libc 时,碎片率为 33%,利用了 17.7GB 内存。
但是保存大工具时 libc 分配器要稍有上风,例如保存 3000 个列表,每个列表里保存 800 个大小为 2.5k 的条款,jemalloc 的碎片率为 3%,占用 8.4G,而 libc 为 1%,占用 8GB。
现在有一个问题:当我们从 Redis 中删除数据的时候,这一部分被开释的内存空间会急速还给操作系统吗?
比如有一个占用内存空间(used_memory_rss)10G 的 Redis 实例,我们有一个大 Key 现在不该用须要删除数据,大约删了 2G 的空间。那么理论上占用内存空间该当是 8G。
如果你利用 libc 内存分配器的话,这时候的占用空间还是 10G。这是由于 malloc() 方法的实现机制问题,由于删除掉的数据可能与其他正常数据在同一个内存分页中,因此这些分页就无法被开释掉。
当然这些内存并不会摧残浪费蹂躏掉,当有新数据写入的时候,Redis 会重用这部分空闲空间。
如果此时不雅观察 Redis 的内存利用情形,就会创造 used_memory_rss 基本保持不变,但是 used_memory 会不断增长。
小结
本日给大家分享 Redis 利用过程中可能会碰着的问题,也是我们稍不留神就会碰着的坑。
很多问题在测试环境我们就能碰着并办理,也有一些问题是上了生产之后才发生的,须要你临时判断该怎么做。
总之别慌,你碰着的这些问题都是古人曾经走过的路,只要仔细看日志都是有办理方案的。
作者:杨越
简介:目前就职广州欢聚时期,专注音视频做事端技能,对音视频编解码技能有深入研究。日常紧张研究怎么造轮子和掩护已经造过的轮子,深耕直播类 APP 多年,对垂直直播玩法和运用有广泛的运用履历,学习技能不局限于技能,欢迎大家一起互换。
【51CTO原创稿件,互助站点转载请注明原文作者和出处为51CTO.com】