Redis 应用案例 - 在问题中不断成长

GIXRobert 3年前
   <h2>背景</h2>    <p>产品类型:酒店搜索</p>    <p>技术选型:前端 PHP + 后端 Java,都会用到 Redis</p>    <p>Redis 使用场景:缓存、数据持久化前的临时存储</p>    <p>2010年开始应用 Redis,PHP 对其操作时使用的是 Predis 这个客户端库</p>    <p>2013年改用了 phpredis 作为客户端库</p>    <p>2014年开始出现问题</p>    <h2>问题描述</h2>    <p>用户量快速增长,访问量在短时间内翻倍,由于前期容量规划做得比较好,硬件资源可以支撑,可是软件系统方面出现了大问题:</p>    <p>40%   的请求都会返回   <strong>HTTP 500: Internal Server Error</strong></p>    <p>通过查看日志,发现错误是在 PHP <-> Redis 的连接处理上</p>    <h2>调试处理</h2>    <h3>第1次</h3>    <p>刚开始时并没有找到根本原因,只能尝试各种与错误相关的办法,例如:</p>    <ol>     <li> <p>增加 PHP 连接数,并把超时时间从 500ms 增加到 2.5s</p> </li>     <li> <p>禁止掉 PHP 设置中的 default_socket_timeout</p> </li>     <li> <p>在主机系统中禁止掉 SYN cookies</p> </li>     <li> <p>检查 Redis 和 Webservers 的文件描述符数量</p> </li>     <li> <p>增加主机系统的 mbuffer</p> </li>     <li> <p>调整 TCP backlog 数量</p> </li>    </ol>    <p>……</p>    <p>尝试了很多方法,但全部 <strong>无效</strong></p>    <h3>第2次</h3>    <p>想在预发布环境中重现这个问题,可惜,还是没成功,应为流量不够大,无法复现</p>    <h3>第3次</h3>    <p>会不会是代码中没有关闭 Redis 连接呢?</p>    <p>正常来讲,PHP在执行结束时会自动关闭资源连接,但老版本中会有内存泄漏的问题,保险起见,把代码都修改一遍,手动关闭连接</p>    <p>结果还是无效</p>    <h3>第4次</h3>    <p>怀疑目标:phpredis 这个客户端库</p>    <p>做 A/B 测试,替换回 predis 这个库,部署到数据中心中 20% 的用户量上</p>    <p>得益于良好的代码结构,替换工作很快完成</p>    <p>可结果依旧是 <strong>无效</strong> ,但也有好的一面,可以证明 phpredis 没问题嘛</p>    <h3>第5次</h3>    <p>查看了一下 Redis 的版本,是 v2.6,当时最新版本是 v2.8.9</p>    <p>升级 Redis 试一下吧,升完后还是 <strong>不行</strong></p>    <p>没事儿,要保持乐观,这不顺便把 Redis 版本升为最新的了</p>    <h3>第6次</h3>    <p>通过查找大量文档,在官方文档中发现了一个调试好方法 <strong>Redis Software Watchdog</strong> ,打开后执行:</p>    <pre>  $ redis-cli --latency -p 6380 -h 1.2.3.4  min: 0, max: 463, avg: 2.03 (19443 samples)</pre>    <p>查看 Redis 日志:</p>    <pre>  ...  [20398] 22 May 09:20:55.351 * 10000 changes in 60 seconds. Saving...  [20398] 22 May 09:20:55.759 * Background saving started by pid 41941  [41941] 22 May 09:22:48.197 * DB saved on disk  [20398] 22 May 09:22:49.321 * Background saving terminated with success  [20398] 22 May 09:25:23.299 * 10000 changes in 60 seconds. Saving...  [20398] 22 May 09:25:23.644 * Background saving started by pid 42027  ...</pre>    <p>发现了问题:</p>    <p>每隔几分钟就向硬盘保存一次数据,fork 一个后台存储进行为什么需要大概 400ms(通过上面日志的第1条和第2条的时间可以看出来)</p>    <p>到这儿,终于找到 <strong>问题的根源</strong> 了,因为 Redis 实例中有大量的数据,导致每次持久化操作 fork 后台进程时非常耗时,并且在他们的业务中经常修改key,又导致了频繁触发持久化,也就经常产生对 Redis 的阻塞</p>    <p>处理办法:使用单独的 slave 来做持久化</p>    <p>这个 slave 不处理真实的流量请求,唯一的作用就是处理持久化,把之前 Redis 实例上的持久化操作转移到这个 slave 上</p>    <p>效果非常明显,问题基本解决,但有的时候还是会报错</p>    <h3>第7次</h3>    <p>排查可能阻塞 Redis 的慢查询,发现有地方使用了 keys *</p>    <p>因为 Redis 中的数据越来越多,这个命令自然会产生严重阻塞</p>    <p>可以使用 scan   进行替换</p>    <h3>第8次</h3>    <p>经过前面的调整,问题已经解决,随后的几个月,即使流量在不断增长,也都抗住了</p>    <p>但他们意识到了 <strong>新的问题</strong> :</p>    <p>现在的方式是,来一个请求就创建一个 Redis 连接,执行几个命令,然后再断开连接,在请求量很大时,这个方式产生了严重的性能浪费,一半以上的命令是用来处理连接操作的,这都超过了业务逻辑上的处理,也使 Redis 变慢</p>    <p>解决方法:引入 proxy,他们选择了 推ter 的   twemproxy ,只需要在每个 webserver 上安装代理,twemproxy负责与 Redis 实例进行持久连接,这样就大大减少了连接方面的操作</p>    <p>twemproxy还有两个方便的地方:</p>    <ol>     <li> <p>支持 memcached</p> </li>     <li> <p>可以阻止非常耗时或者危险的命令,例如 keys、flushall</p> </li>    </ol>    <p>效果自然很完美,再也不用担心之前的连接错误</p>    <h3>第9次</h3>    <p>通过数据分片来继续优化:</p>    <ol>     <li> <p>对不同上下文的数据拆分隔离</p> </li>     <li> <p>对相同上下文的数据进行一致性哈希分片</p> </li>    </ol>    <p>效果:</p>    <ol>     <li> <p>减少了每台机器上的请求、负载</p> </li>     <li> <p>提升了缓存的可靠性,不担心节点故障</p> </li>    </ol>    <h2>小结</h2>    <p>原文作者写的非常好,详细的描述了他们在 Redis 应用上的成长历程,是很值得参考的实践经验</p>    <p>原文地址</p>    <pre>  http://tech.trivago.com/2017/01/25/learn-redis-the-hard-way-in-production</pre>    <p>点击 “阅读原文” 查看 文章列表</p>    <p> </p>    <p>来自:http://tech.trivago.com/2017/01/25/learn-redis-the-hard-way-in-production</p>    <p> </p>