这周终于解决了Redis访问经常超时的问题,终于可以踏实睡觉了。从上周就开始纠结在这个问题上,可以用寝食难安来形容,感觉这个问题就像个定时炸弹一样,虽然根据手搜的访问量,极少的Timeout Error对用户基本不会造成影响,但是这种问题如果不及时遏制导致Redis整体被拖慢造成的危害是相当严重滴!毕竟在手搜的架构中,Redis是极其重要的一环。

先来讲个故事(说说问题上下文),有兴趣来手搜工作的Python程序员认真听:

前段时间我们新上了个业务,为了满足需求我们对进程级的cache做了些改动,根据业务对cache key进行了拆分,导致的情况是同一份数据会被缓存多份,只因为展示类型不同(可简单理解为不同的展示用的key不同,数据相同)。有经验的同学应该能够想到这么做的结果会是怎么样的——同一份数据会请求两次Redis(因为cache key不同)。(可以看的出来,对于多种展示需求的业务在展示层做cache并不合适) 功能测试没问题,so,上线。晚高峰来了。运维同学和值班同学被报警短信“轰炸”了,大量应用服务器端口访问超时(Nagios监控)。因为那天只有我们的改动上线了,所以可以定位为我们代码的问题。遂,备机上。 次日看日志发现大量的Redis的Timeout Error(timeout=1s),虽然日志中在我们这个系统上线之前就已经存在很多这种日志,显然我们这次改动的上线加剧了这一状况。

从直观的感觉上,我们的功能导致Redis请求增多,因此这时就聚焦在如何较少Redis的请求上。 在分析了一个页面的加载到底会请求多少次Redis之后发现一个很大的问题,因为针对具体业务的设计,一个页面在加载时访问Redis的次数跟页面上展示的内容成正比。因此页面上的内容越多,对Redis的请求会越频繁,因此一次正常的页面访问很可能会产生100次甚至更多的Redis请求。 问题已经很明显了,因为这种设计,必须完全依赖本地缓存,无缓存情况访问页面的速度是不可接受的。

知道问题在哪就好办了,经过不断的思考和尝试,找到一种方案。减少页面上的内容肯定是不行的,那么就只能合并请求了。

说了一大堆,其实问题的本质在于要把之前串行的Redis请求换成并行。另外值得一说的是Redis本身的响应是非常快的,另外还有一些时间消耗在网络上。 串行换成并行的方案可以简单理解为把之前的: GET 1 , GET 2 换成: MGET 1 2 这样。改完之后在无缓存的情况下页面加载时间几乎提升了一倍。 OK,既然可行,那就测试,在测试,上线。Yeah!没人被短信轰炸。

你以为故事到这里就结束了,那就太Naive了。Nagios监控只是用来看端口访问是否符合预期的(1s内响应),并不能发现所有的问题。

上线几天之后发现状态码为500的情况增多了,且波动很大。So,继续看日志(有人问,你们不是用Sentry吗?为毛看日志?呃,内个,内个,业务调整...)。 咱们还是来看日志吧,依然是大量的Timeout Error,还是Redis的问题。依据上面的方案我们已经完全减少了Redis请求次数,为何还是慢。

有兴趣的同学可以思考下,为何还是慢?之前的方案会造成哪种影响。

喝口水,继续讲故事:(终于明白知乎上为何经常有人讲故事了,讲故事原来比写正经文章要容易)

为何还是慢呢?之前的改动相当于是把100次请求合并成1次,造成的影响是,虽然减少了网络上的耗时,但是会增加Redis的耗时。假设之前每次请求读10个key(mget),合并成1次请求就变成一次读取1000个key。这块可能是问题原因,那么继续定位吧。 要怎么确定呢?Redis并不熟,但是MySQL有Slowlog,Redis应该也有。于是找到了几个命令:

CONFIG GET slowlog-log-slower-than  (10000us,Redis中的执行单位是微秒,相当于10ms)
CONFIG GET slowlog-max-len (最多存储128条慢日志,应该是默认的吧?)
SLOWLOG LEN (128, 靠,存满了)
SLOWLOG GET (默认去10条)

然后看到了慢日志出现的频率非常高!并且MGET命令中key的数量相当大——1w多(不是上面那个方案导致的),执行耗时在50ms左右。 好了,再次Get到问题所在,解决吧。 然后发现无法从这个日志上去找是哪个项目产生的,因为Redis不记录这个日志产生的client ip。现在回忆知道是哪台服务器发的请求也定位不到。因为这种错误的产生就不是正常业务逻辑代码产生的。

具体调试过程简单概括为:在所有可能产生巨量MGET请求前添加判断,打印log到sentry(没错,我们又上了Sentry服务),主要还是在获取列表的部分(其实最后查到问题的根源完全跟列表没关系),依然没有捕获到任何日志。于是开始到Redis服务器tcpdump所有请求,定位到一台服务器,至少知道了是哪个项目产生的。然后就是运气了,不断的查看正常访问日志。终于发现了问题。太TMD走运了,那一刻都要哭了。这个结果完全不在所有的预料情况之中。

从MGET的key上可以发现所有的新闻都是属于某一个频道,所以我们的预期是这些请求一定是在访问这些新闻所属频道列表页面时产生的。最终发现跟列表页面没半毛钱关系。

看吧,这就是“大型”系统了,充满了不可预测。这样真的不好!

这个问题产生的原因是某个页面中有一部分内容是可以指定获取某频道数据的,但是这部分内容又是存在Redis中的。好吧,之前完全没考虑到会有这种间接的请求。

知道具体问题了,顺藤摸瓜,找到了对应的存在Redis中的内容,奇怪的是这部分内容没有设置要从某个频道获取多少条数据。这个内容的模型大致是这样的:

class Content:
channel = int()
data_size = int()

设置了频道,没设置要获取的数量,默认是0。

故事结束,来看代码。

假设我的这个blog系统用的是Redis,那么怎么处理某个频道的分页呢?因为是非关系型数据库。因此在存储数据的时候,我得这样:

import redis

cache = redis.StrictRedis(host='localhost', port=6379)

for post in posts:
cache.set('blog:post:%s:title' % post.id, post.title) cache.zadd('blog:channel:1',time.time(), post.id)

这样存下去如果我要从这个频道(假设id为1)下获取十条数据:

start = 0
count = 10
post_ids = cache.zrange('blog:channel:1', start, start + count - 1)
post_tiltes = cache.mget(map(labda _id: 'blog:post:%s:title' % _id, post_ids))

这样的代码有问题吗?

咋看之下没有问题,因为redis的zset是从0开始计数的,获取10个元素,那就是0到9。那么看下这个描述:

Both start and stop are zero-based indexes, where 0 is the first element, 1 is the next element and so on. They can also be negative numbers indicating offsets from the end of the sorted set, with -1 being the last element of the sorted set, -2 the penultimate element and so on.

大意是,索引从0开始,-1表示最后的一个位置。因此如果你想获取某个频道(id为1)的所有文章id就是这样:

post_ids = cache.zrange('blog:channel:1', 0, -1)

因为-1表示最后一个元素的位置。

再回到上面故事的结尾,那块内容要获取的频道新闻数量为0,而我们的代码和我写的类似,没对这种-1可能性做判断,因此一次就取出来频道下的所有新闻,so,产生了那个结果。

总结时间

在复杂的业务下的复杂的系统结构中,要找到一个隐藏的问题确实不容易,因为写代码的人不断的变化,负责业务的人也在不断变化,最后都不知道这个业务为何存在,是否还在使用,也不知道这段代码是否正常,毕竟每个人还在负责新的业务,不断的添加新的代码。这样的结果就是系统进一步复杂,这样下去的结果就是再经过几年人员的变迁之后,任何一个人想要理解这个系统(这些系统)中的业务逻辑,代码逻辑都将变的十分困难。

代码应该是历史的结果,不应该把历史平铺到代码中,git log才是历史应该存在的地方。 说人话就是:业务的增加不仅仅是增加代码,有时也是要对现有结构进行抽象和重构。

REDIS线上问题的更多相关文章

  1. Redis线上环境做Keys匹配操作!你可以离职了!

    转自:https://blog.csdn.net/bntx2jsqfehy7/article/details/84207884一.一个新闻 新闻内容如下:php工程师执行redis keys * 导致 ...

  2. 今天,你遇到redis线上连接超时了吗?

    一封报警邮件,大量服务节点 redis 响应超时. 又来,好烦. redis 响应变慢,查看日志,发现大量 TimeoutException. 大量TimeoutException,说明当前redis ...

  3. Redis持久化磁盘IO方式及其带来的问题   有Redis线上运维经验的人会发现Redis在物理内存使用比较多,但还没有超过实际物理内存总容量时就会发生不稳定甚至崩溃的问题,有人认为是基于快照方式持

    转自:http://blog.csdn.net/kaosini/article/details/9176961 一.对Redis持久化的探讨与理解    redis是一个支持持久化的内存数据库,也就是 ...

  4. Redis 集群缓存测试要点--关于 线上 token 失效 BUG 的总结

    在测试账户系统过程中遇到了线上大面积用户登录态失效的严重问题,事后对于其原因及测试盲点做了一些总结记录以便以后查阅,总结分为以下7点,其中原理性的解释有些摘自网络. 1.账户系统token失效问题复盘 ...

  5. 线上Redis偶发性链接失败排查记

    问题过程 输入法业务于12月12日上线了词库接受业务,对部分用户根据用户uuid判断进行回传,在12月17日早上8点多开始出现大量的php报错(Redis went away),报错导致了大量的链接积 ...

  6. Jedis线上的一个小坑:Redis有并发访问的数据错乱的问题

    问题现象: 业务数据有错乱,A的一些数据有好几个都是B的数据 这些业务数据在保存在Redis缓存中,怀疑是并发情况下Jedis错乱的问题 原因分析: JedisUtil里面在使用完Jedis 后释放资 ...

  7. 一次线上Redis类转换异常排查引发的思考

    之前同事反馈说线上遇到Redis反序列化异常问题,异常如下: XxxClass1 cannot be cast to XxxClass2 已知信息如下: 该异常不是必现的,偶尔才会出现: 出现该异常后 ...

  8. redis扫描特定keys脚本,可避免阻塞,不影响线上业务

    #!/bin/sh ## 该脚本用来查询redis集群中,各个实例当中特定前缀的key,对应只需要修改redis的其中一个实例的 host和port## 脚本会自动识别出该集群的所有实例,并查出对应实 ...

  9. 获取客户端IP地址-----以及--------线上开启redis扩展

    /** * 获取客户端IP地址 * @param integer $type 返回类型 0 返回IP地址 1 返回IPV4地址数字 * @return mixed */ function get_cl ...

随机推荐

  1. [javaSE] 数组(获取最值)

    数组的常见操作(获取最值) 1.获取最值需要进行比较,每一次比较都会有一个较大的值,因为该值不确定,通过一个变量进行存储 2.让数组中的每一个元素都和这个变量中的值进行比较,如果大于了变量中的值,就用 ...

  2. JS基础(四)之jQuery

    31.jQuery(http://jquery.com/)是一个快速.简洁的JavaScript框架. 它封装了JavaScript常用的功能代码,提供一种便捷的JavaScript设计模式,优化HT ...

  3. FineReport中JS如何自定义按钮导出

    FineReport支持多种不同的导出方式,直接使用FineReport内置导出按钮可以非常快捷方便的来对各种格式的输出,但是我们在web页面集成中的时候,往往只想将报表内容嵌入到iframe中,而工 ...

  4. ifup / ifdown eth0 / eno1 reports unknown interface when it exists!

    li {list-style-type:decimal;}.wiz-editor-body ol.wiz-list-level2 > li {list-style-type:lower-lati ...

  5. Andoid多语言国际化策略

    目前手上的项目,为了普及覆盖更多的用户群,也已经开始实现了多语言设置这样的功能,不过今天我要说的不是微信,而是我们自己项目中的实现策略. 直接附上关键代码: package com.huolonglu ...

  6. git远程仓库问题

    1:下载下来的仓库,可能变更远程仓库 git remote rm origin (origin默认的远程仓库名) 可以在.git文件夹下的config文件查看remote的信息. 同时也可以查看bra ...

  7. 解决PJA错误

     现场有WAS5.6,均安装在AIX下,且都已经设置PJA,在WAS5上报表正常,但是在WAS6下报表会报错,两个应用下的配置均相同. 反馈的报错日志: [11-4-25 14:20:42:758 ...

  8. java 内存分析之方法返回值二

    package Demo; class Point { private double x, y; public Point(double x, double y) { this.x = x; this ...

  9. Android8.0适配那点事(一)

    最近有小伙伴说,7.0适配整了一波,现在又要来适配8.0,真是一波未平一波又起 但是作为开发者来说,学无止境,不跟上时代的步伐,肯定会被时代所淘汰... 话说Android P已经在路上了,你准备好了 ...

  10. Html 表单标签 Form

    Html表单 #转载请留言联系 表单用于搜集不同类型的用户输入,表单由不同类型的标签组成,相关标签及属性用法如下: 1.<form>标签 定义整体的表单区域 action属性 定义表单数据 ...