排查流水账:
  1. 通过平台监控,发现很多偶发的查看推荐列表的接口时延大于0.5s
  2. 写单元测试,不能重现。在测试环境不能重现。只有在正式环境可以偶发重现。
  3. 通过日志埋点,等待重现
  4. 不断地加日志埋点后发现耗时在redis的hmget操作
    1. 这时猜想原因
      1. hmget命令才会有,会不会是hmget命令的问题
      2. 查看redis的慢查询日志,发现没有慢查询。排除是Redis执行慢的原因
      3. 查看当时的负载情况,负载很低,并发也不多。所以排除是Redis的命令等待原因
      4. 多协程下,20条hmget,不会全部都卡,只会卡几条,后面又会正常
      5. 正常hmget的用时是0.01s左右,卡的时候需要0.3-0.9s
      6. 自己写个脚本,不断地用多协程去执行hmget,不能重现。
    2. 猜想解决方案:
      1. 修改多协程池的数量,从20改为3
      2. 获取用户信息改为串行
  5. 继续往hmget命令里面的代码加埋点日志
    1. 由于是修改第三方库,所以要格外小心
    2. 通过阅读源码,发现hmget的底层流程是rediscluster模块里面的client文件里面的execute_command函数
    3. 修改后,测试环境之下单元测试没问题后,部署到正式环境
  6. 最后定位到是在里面的self.connection_pool.nodes.initialize()这行代码耗时
  7. 只有当refresh_table_asap=true才会执行这行代码,所以解决思路
    1. 为什么refresh_table_asap会等于true
      1. 发现只有当连接redis的时候报错ClusterDownError或者MovedError才会设置refresh_table_asap=True
      2. 通过日志埋点。发现是MovedError异常导致的。
      3. 继续增加日志埋点,发现整个触发的流程是:
        1. 触发异常ConnectionError
        2. 设置try_random_node=True
        3. 下一次随机拿一个节点,这时候可能拿到slot不对的节点
        4. 连接节点后,会报异常MovedError,并把目标节点的信息返回来,同时设置refresh_table_asap=True。
        5. 这时会把slot对应的节点设置为返回来的节点信息
        6. 重新连接节点,执行命令成功
        7. 但是这时候已经设置了refresh_table_asap=True,执行下一个命令的时候,就会执行self.connection_pool.nodes.initialize()
        8. 由于使用了多协程,而且self.connection_pool.nodes.initialize()命令没有加锁,所以会导致这个耗时加剧
      4. 通过print traceback,看看为什么会触发ConnectionError异常,发现是redis服务端断开了连接。
      5. 这时候回想到redis有机制,超过一定时间没有命令过来,就会关闭连接。在redis的timeout 配置,一般是300s。所以这样解释了为什么这个是偶发的。
      6. 写单元测试,建立连接后,等待350s再执行命令,稳定重现bug。
    2. 为什么initialize耗时这么慢
      1. 通过单元测试,发现initialize命令并不慢,大于0.04s左右就能完成,但是多协程下是0.5s左右。
      2. 所以考虑是多协程下,因为没有锁,所以多个协程都执行了这条命令,导致最终的用时是原来的10倍
  8. 修改测试环境redis的timeout=5s,写个测试用例,在测试环境可以稳定重现。
  9. 所以定位到rediscluster有问题,解决思路
    1. 不要在多协程执行redis命令(感觉不好)
    2. 升级库,看能不能解决。查看这个库的git地址(https://github.com/Grokzen/redis-py-cluster)的最新版本,问题依然存在。
    3. catchConnectionError异常的时候,区分是否服务端断开连接,如果是,不设置try_random_node=True,重试
    4. init的时候加锁
    5. 参考redis.py的做法,在catch服务端断开连接异常后,重新连接后重试
  10. 最后选用了思路5。
execute_command函数(包含埋点日志,去除不必要的代码段),在rediscluster库的client.py
@clusterdown_wrapper
def execute_command(self, *args, **kwargs):
"""
Send a command to a node in the cluster
"""
import logging
log=logging.getLogger('service.log')
log.error(u'redis execute_command 1 %s ' % str(args)) # If set externally we must update it before calling any commands
if self.refresh_table_asap: #执行self.connection_pool.nodes.initialize()的代码段
log.error(u'redis execute_command 2 %s ' % str(args))
self.connection_pool.nodes.initialize()
log.error(u'redis execute_command 3 %s ' % str(args))
self.refresh_table_asap = False
log.error(u'redis execute_command 4 %s ' % str(args)) redirect_addr = None
asking = False try_random_node = False
log.error(u'redis execute_command 7 %s ' % str(args))
slot = self._determine_slot(*args)
log.error(u'redis execute_command 8 %s ' % str(args))
ttl = int(self.RedisClusterRequestTTL) while ttl > 0:
ttl -= 1 if asking:
node = self.connection_pool.nodes.nodes[redirect_addr]
r = self.connection_pool.get_connection_by_node(node)
elif try_random_node:
r = self.connection_pool.get_random_connection()
try_random_node = False
else:
if self.refresh_table_asap:
# MOVED
node = self.connection_pool.get_master_node_by_slot(slot)
else:
node = self.connection_pool.get_node_by_slot(slot)
r = self.connection_pool.get_connection_by_node(node) try:
r.send_command(*args)
log.error(u'redis execute_command 10 %s ' % str(args))
ret= self.parse_response(r, command, **kwargs)
log.error(u'redis execute_command 11 %s ' % str(args))
return ret except (RedisClusterException, BusyLoadingError):
raise
except (ConnectionError, TimeoutError):
try_random_node = True
log.error(u'redis execute_command 14 %s ' % str(args))
if ttl < self.RedisClusterRequestTTL / 2:
log.error(u'redis execute_command 15 %s ' % str(args))
time.sleep(0.1)
except ClusterDownError as e:
log.error(u'redis execute_command 17 %s ' % str(args))
self.connection_pool.disconnect()
self.connection_pool.reset()
self.refresh_table_asap = True
raise e
except MovedError as e:
# Reinitialize on ever x number of MovedError.
# This counter will increase faster when the same client object
# is shared between multiple threads. To reduce the frequency you
# can set the variable 'reinitialize_steps' in the constructor.
import traceback
print traceback.format_exc()
log.error(u'redis execute_command 16 %s ' % str(args))
self.refresh_table_asap = True
self.connection_pool.nodes.increment_reinitialize_counter() node = self.connection_pool.nodes.set_node(e.host, e.port, server_type='master')
self.connection_pool.nodes.slots[e.slot_id][0] = node

 

优化:

              r.send_command(*args)
ret= self.parse_response(r, command, **kwargs)
return ret

改为

try:
r.send_command(*args)
return self.parse_response(r, command, **kwargs)
except ConnectionError as e:
from redis.connection import SERVER_CLOSED_CONNECTION_ERROR
if SERVER_CLOSED_CONNECTION_ERROR in e.message:
r.disconnect()
r.send_command(*args)
return self.parse_response(r, command, **kwargs)
else:
raise

 未经许可,请不要转载

记一次偶发的bug排查——redis-py-cluster库的bug的更多相关文章

  1. 日常Bug排查-系统失去响应-Redis使用不当

    日常Bug排查-系统失去响应-Redis使用不当 前言 日常Bug排查系列都是一些简单Bug排查,笔者将在这里介绍一些排查Bug的简单技巧,同时顺便积累素材_. Bug现场 开发反应线上系统出现失去响 ...

  2. 日常Bug排查-消息不消费

    日常Bug排查-消息不消费 前言 日常Bug排查系列都是一些简单Bug排查,笔者将在这里介绍一些排查Bug的简单技巧,同时顺便积累素材_. Bug现场 某天下午,在笔者研究某个问题正high的时候.开 ...

  3. 日常Bug排查-抛异常不回滚

    日常Bug排查-抛异常不回滚 前言 日常Bug排查系列都是一些简单Bug排查,笔者将在这里介绍一些排查Bug的简单技巧,同时顺便积累素材_. Bug现场 最近有人反映java应用操作数据库的时候,抛异 ...

  4. 日常Bug排查-Nginx重复请求?

    日常Bug排查-Nginx重复请求? 前言 日常Bug排查系列都是一些简单Bug排查,笔者将在这里介绍一些排查Bug的简单技巧,其中不乏一些看起来很低级但很容易犯的问题. 问题现场 有一天运维突然找到 ...

  5. 解Bug之路-记一次存储故障的排查过程

    解Bug之路-记一次存储故障的排查过程 高可用真是一丝细节都不得马虎.平时跑的好好的系统,在相应硬件出现故障时就会引发出潜在的Bug.偏偏这些故障在应用层的表现稀奇古怪,很难让人联想到是硬件出了问题, ...

  6. 记一次线上bug排查-quartz线程调度相关

    记一次线上bug排查,与各位共同探讨. 概述:使用quartz做的定时任务,正式生产环境有个任务延迟了1小时之久才触发.在这一小时里各种排查找不出问题,直到延迟时间结束了,该任务才珊珊触发.原因主要就 ...

  7. Redis为什么变慢了?透彻解读如何排查Redis性能问题

    Redis 作为优秀的内存数据库,其拥有非常高的性能,单个实例的 OPS 能够达到 10W 左右.但也正因此如此,当我们在使用 Redis 时,如果发现操作延迟变大的情况,就会与我们的预期不符. 你也 ...

  8. 年年出妖事,一例由JSON解析导致的"薛定谔BUG"排查过程记录

    前言 做开发这么多年,也碰到无数的bug了.不过再复杂的bug,只要仔细去研读代码,加上debug,总能找到原因. 但是最近公司内碰到的这一个bug,这个bug初看很简单,但是非常妖孽,在一段时间内我 ...

  9. redis之(十七)自己实现redis的cluster集群环境的搭建

    [一]创建不同节点的配置文件和目录.并将配置文件中的port,cluster-enable,daemonize项做修改. --->port:修改成redis实例对应的端口号 --->clu ...

随机推荐

  1. 201671010406-丁家辉-实验十四 团队项目评审&课程学习总结

    实验十四 团队项目评审&课程学习总结 项目 内容 这个作业属于哪个课程 [教师博客主页链接] 这个作业的要求在哪里 [作业链接地址] 作业学习目标 (1)掌握软件项目评审会流程(2)反思总结课 ...

  2. React Virtual DOM Explained in Simple English

    If you are using React or learning React, you must have heard of the term “Virtual DOM”. Now what is ...

  3. 函数式编程—函数的关系—is-a、has-a、use-a

    is-a:函数的实现与函数类型的关系: has-a:匿名(闭包)函数的创建者与匿名函数的关系:匿名函数与环境和上下文(函数)的关系: use-a:高阶函数与参量函数的关系: 函数式编程的基本功之一就是 ...

  4. 异步编程实现技术:回调、promise、协程序?

    异步编程实现技术:回调.promise.协程序?

  5. cf1158A-The Party and Sweets - (贪心+思维)

    题意:有n个男孩,m个女孩,每个男孩给每个女孩一堆糖果.b数组表示每个男孩给出的最少糖果数,g数组表示每个女孩子收到的最大糖果数.求所有男孩给出的最小糖果总数. 解题: 先对b数组和g数组从小到大排序 ...

  6. 再见Spring Boot 1.x

    记得很早很早之前有过一次面试,面试前端说自己喜欢JavaScript,然后面试官问,你知道当前JavaScript最新标准和规范吗?我无言以对,因为平时没有关注认真对待这些信息,然后就没有然后了. 或 ...

  7. JavaScript 中 call()、apply()、bind() 的用法

    "use strict"; ; var obj = { name:'小李', age:, getInfo(from, to) { console.log(arguments) co ...

  8. A1130 | 中缀表达式、查找根节点

    代码: #include <stdio.h> #include <memory.h> #include <math.h> #include <string&g ...

  9. .NET总结--ASP.NET工作原理

    前言 前前后后写了不少关于某些技术啥的博客,一直在追新求深,而真正使用上的时候才发现了解的太少太少了,从事.net开发三年有余了不是它不行了而是我坚持不住了,如今不得不向生活低头,这个系列作为三年技术 ...

  10. 关于window.getSelection

    window.getSelection(),返回一个Selection对象,表示用户选择的文本范围或光标的当前位置. selection对象先来看下面两个selection结果:selection对象 ...