排查流水账:
  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
  1. @clusterdown_wrapper
  2. def execute_command(self, *args, **kwargs):
  3. """
  4. Send a command to a node in the cluster
  5. """
  6. import logging
  7. log=logging.getLogger('service.log')
  8. log.error(u'redis execute_command 1 %s ' % str(args))
  9.  
  10. # If set externally we must update it before calling any commands
  11. if self.refresh_table_asap: #执行self.connection_pool.nodes.initialize()的代码段
  12. log.error(u'redis execute_command 2 %s ' % str(args))
  13. self.connection_pool.nodes.initialize()
  14. log.error(u'redis execute_command 3 %s ' % str(args))
  15. self.refresh_table_asap = False
  16. log.error(u'redis execute_command 4 %s ' % str(args))
  17.  
  18. redirect_addr = None
  19. asking = False
  20.  
  21. try_random_node = False
  22. log.error(u'redis execute_command 7 %s ' % str(args))
  23. slot = self._determine_slot(*args)
  24. log.error(u'redis execute_command 8 %s ' % str(args))
  25. ttl = int(self.RedisClusterRequestTTL)
  26.  
  27. while ttl > 0:
  28. ttl -= 1
  29.  
  30. if asking:
  31. node = self.connection_pool.nodes.nodes[redirect_addr]
  32. r = self.connection_pool.get_connection_by_node(node)
  33. elif try_random_node:
  34. r = self.connection_pool.get_random_connection()
  35. try_random_node = False
  36. else:
  37. if self.refresh_table_asap:
  38. # MOVED
  39. node = self.connection_pool.get_master_node_by_slot(slot)
  40. else:
  41. node = self.connection_pool.get_node_by_slot(slot)
  42. r = self.connection_pool.get_connection_by_node(node)
  43.  
  44. try:
  45. r.send_command(*args)
  46. log.error(u'redis execute_command 10 %s ' % str(args))
  47. ret= self.parse_response(r, command, **kwargs)
  48. log.error(u'redis execute_command 11 %s ' % str(args))
  49. return ret
  50.  
  51. except (RedisClusterException, BusyLoadingError):
  52. raise
  53. except (ConnectionError, TimeoutError):
  54. try_random_node = True
  55. log.error(u'redis execute_command 14 %s ' % str(args))
  56. if ttl < self.RedisClusterRequestTTL / 2:
  57. log.error(u'redis execute_command 15 %s ' % str(args))
  58. time.sleep(0.1)
  59. except ClusterDownError as e:
  60. log.error(u'redis execute_command 17 %s ' % str(args))
  61. self.connection_pool.disconnect()
  62. self.connection_pool.reset()
  63. self.refresh_table_asap = True
  64. raise e
  65. except MovedError as e:
  66. # Reinitialize on ever x number of MovedError.
  67. # This counter will increase faster when the same client object
  68. # is shared between multiple threads. To reduce the frequency you
  69. # can set the variable 'reinitialize_steps' in the constructor.
  70. import traceback
  71. print traceback.format_exc()
  72. log.error(u'redis execute_command 16 %s ' % str(args))
  73. self.refresh_table_asap = True
  74. self.connection_pool.nodes.increment_reinitialize_counter()
  75.  
  76. node = self.connection_pool.nodes.set_node(e.host, e.port, server_type='master')
  77. self.connection_pool.nodes.slots[e.slot_id][0] = node

 

优化:

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

改为

  1. try:
  2. r.send_command(*args)
  3. return self.parse_response(r, command, **kwargs)
  4. except ConnectionError as e:
  5. from redis.connection import SERVER_CLOSED_CONNECTION_ERROR
  6. if SERVER_CLOSED_CONNECTION_ERROR in e.message:
  7. r.disconnect()
  8. r.send_command(*args)
  9. return self.parse_response(r, command, **kwargs)
  10. else:
  11. 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. Cause: com.mysql.cj.jdbc.exceptions.MysqlDataTruncation: Data truncation: Incorrect date value

    Cause: com.mysql.cj.jdbc.exceptions.MysqlDataTruncation: Data truncation: Incorrect date value: '154 ...

  2. python应用-21根火柴游戏

    """ 21跟火柴 """ from random import randint def main(): total=21 while to ...

  3. 17、Python面向对象高级

    一.isinstance和issubclass type():不会认为子类实例是一种父类类型: isinstance():认为子类实例是一种父类类型. issubclass():判断是否为其子类. c ...

  4. python-自动登录禅道

    from bs4 import BeautifulSoup import hashlib import requests import re from tool.request_data_change ...

  5. A* 第k短路

    #include <cstdio> #include <algorithm> #include <queue> #include <cstring> # ...

  6. java 调度框架quartz

    核心代码如下: public class SchedulerTest { public static void main(String[] args) { //创建schedulerFactory类 ...

  7. memoryDiary

    What did you accomplish today? , did you exercise today? Do you care about the people around you tod ...

  8. LeetCode 865. Smallest Subtree with all the Deepest Nodes

    原题链接在这里:https://leetcode.com/problems/smallest-subtree-with-all-the-deepest-nodes/ 题目: Given a binar ...

  9. yugabyte 安装pg extention

    前段时间在学习yugabyte 发现yugabyte 是直接复用了pg server的源码,所以当时就觉得大部分pg extension 也是可用. 今天看到了官方文档中有关于如何安装的,发现还得多看 ...

  10. cube.js 通过presto-gateway 进行连接

    cube.js 对于presto 的支持是通过presto-client 刚好简单修改了一个可以支持presto-gateway 连接的 以下是一个简单的集成,以及关于集成中原有的一些修改 环境准备 ...