给redis-py提交pull request引起的思考

起因是这样的,昨天突然发现以前用redis python的时候,从来没注意过他在多线程,多进程下fd复用的情况,直接都是公用一个连接对象。 

对比了多个好项目代码,貌似大家对mysql,mongodb十分的注意,都尽量不要让他有socket fd共享的情况,而对于redis的使用很是粗暴,直接共享。 

那么话说回来? 为什么要避免fd重用? 


该文章写的有些乱,欢迎来喷 ! 另外文章后续不断更新中,请到原文地址查看更新. http://xiaorui.cc/?p=3359

因为redis-py一直都没有报读写异常,我也就没有去测试和验证这个问题,就自以为是的觉得他在socket send or recv做了Lock,使command顺序化.  后来因为有个服务hbase thrift和elasticsearch连接出了问题,就遍历了父子进程的所有连接fd,惊奇的发现他们每个进程连接都不一样。 以前使用mysql的mysqldb模块,我们都是自己用lazy懒惰创建方法解决,而redis帮你实现了这么一个东西。 


注:

    可以发现在测试python脚本的结果里,4个子进程的local port都不一样的. 

#xiaorui.cc
[ruifengyun@xiaorui.cc ~]ps aux f|grep a.py
508      11704  5.0  0.0 421096 11664 pts/11   Sl+  17:50   0:00  |           \_ python a.py
508      11709  0.0  0.0 193760  7464 pts/11   S+   17:50   0:00  |               \_ python a.py
508      11710  0.0  0.0 193760  7468 pts/11   S+   17:50   0:00  |               \_ python a.py
508      11711  0.0  0.0 193760  7468 pts/11   S+   17:50   0:00  |               \_ python a.py
508      11712  0.0  0.0 193760  7476 pts/11   S+   17:50   0:00  |               \_ python a.py
508      11720  0.0  0.0 103248   832 pts/12   S+   17:50   0:00              \_ grep a.py
[ruifengyun@xiaorui.cc ~] sudo lsof -p 11709|grep 6379
python  11709 ruifengyun    4u  IPv4 4173927407      0t0        TCP localhost:51433->localhost:6379 (ESTABLISHED)
[ruifengyun@xiaorui.cc ~]sudo lsof -p 11710|grep 6379
python  11710 ruifengyun    4u  IPv4 4173927417      0t0        TCP localhost:51435->localhost:6379 (ESTABLISHED)
[ruifengyun@xiaorui.cc ~] sudo lsof -p 11711|grep 6379
python  11711 ruifengyun    4u  IPv4 4173927411      0t0        TCP localhost:51434->localhost:6379 (ESTABLISHED)
[ruifengyun@xiaorui.cc ~]$ sudo lsof -p 11712|grep 6379
python  11712 ruifengyun    4u  IPv4 4173927416      0t0        TCP localhost:51436->localhost:6379 (ESTABLISHED)

对于我这么刨根问底的人来说,必须要搞清楚redis是怎么实现的每个进程用不同的连接的。 

redis-py的源代码地址, https://github.com/andymccurdy/redis-py , 作者andymccurdy是个很善于言谈的人,有兴趣的人可以看看youtube有关他的演讲视频,有些风趣. 

redis-py的一些设计巧妙,在该项目里还是可以借鉴一些优点的。 对于多进程连接共享问题你是如何解决的? 他们的实现很取巧 ,第一次初始化连接的时候会记录当前的pid。 当多个进程共用了这个连接去做command操作的时候,redis做了_checkpid()的动作? 这是什么意思? 如果当前的进程跟 第一次初始化的连接进程pid不一样的话,就重新创建一个redis连接。 

注: 我在自己的项目中用python也设计了一套类似redis python这样的connection管理类, 当然是管理MySQLdb的. 多进程下方法跟他类似,但多了单例模式这一步,在类变量里面初始化了pid — > connection. 
多线程下方法就跟他就不一样了,构建单例模式的时候,不仅会对进程pid判断,而且通过ctypes拿到threading id,对线程的id也会进行判断。 

下面是redis-py/connection.py 源码. 

#xiaorui.cc
class ConnectionPool(object):
    def __init__(self, connection_class=Connection, max_connections=None,
         max_connections = max_connections or 2 ** 31   #连接数
         if not isinstance(max_connections, (int, long)) or max_connections < 0:
             raise ValueError('"max_connections" must be a positive integer')

         self.connection_class = connection_class
         self.connection_kwargs = connection_kwargs
         self.max_connections = max_connections

         self.reset()

    def _checkpid(self):
        #检查pid, 这里需要用lock控制,不然新进程里多个线程操作的化,会线程不安全. 
        if self.pid != os.getpid():
            with self._check_lock:
                if self.pid == os.getpid():
                    # another thread already did the work while we waited
                    # on the lock.
                    return
                self.disconnect()
                self.reset()

    def get_connection(self, command_name, *keys, **options):
        #从_available_connections获取可用的连接
        self._checkpid()
        try:
            connection = self._available_connections.pop()
        except IndexError:
            connection = self.make_connection()
        self._in_use_connections.add(connection)
        return connection

    def make_connection(self):
        #创建一个新连接
        if self._created_connections >= self.max_connections:   #连接的数目是有限制的
            raise ConnectionError("Too many connections")
        self._created_connections += 1
        return self.connection_class(**self.connection_kwargs)

令人尴尬的事情来了 !

到这里为止 redis python模块解决了多进程共用socket fd问题,但是我翻弄了redis-py的代码,没有看到redis-py对于多线程threading的处理.
我就fork了一个版本,提交了一段多线程下lazy mode的代码,逻辑是通过ctypes获取thread id,但是针对Class Connection加一层实例判断,测试通过后就提交了pull request merge请求。 

但提交了merge请求后,心有疑虑,作者连多进程的情况都考虑了,为什么不会考虑到多线程的场景? 不应该呀… 我后自己临时写了个脚本验证threading redis-py 共享连接问题… 验证的结果,每个线程都不同的连接…     果然呀,这20个线程都有自己的连接。  既然事情已经明了,没必要再去让别人merge代码了,带着羞愧,我把提交及fork都给删了。 太丢鸡了呀…. 

#xiaorui.cc
[root@iZ25wvd7l9xZ redis]# ps aux|grep q2.py
root     24193  2.2  1.3 1231500 14060 pts/4   Sl+  11:14   0:00 python q2.py
[root@iZ25wvd7l9xZ redis]# sudo lsof -p 24193|grep 6379
python  24193 root    3u  IPv4 74065317      0t0     TCP localhost:51767->localhost:6379 (ESTABLISHED)
python  24193 root    4u  IPv4 74065320      0t0     TCP localhost:51768->localhost:6379 (ESTABLISHED)
python  24193 root    6u  IPv4 74065322      0t0     TCP localhost:51769->localhost:6379 (ESTABLISHED)
python  24193 root    7u  IPv4 74065324      0t0     TCP localhost:51770->localhost:6379 (ESTABLISHED)
python  24193 root    8u  IPv4 74065326      0t0     TCP localhost:51771->localhost:6379 (ESTABLISHED)
python  24193 root    9u  IPv4 74065328      0t0     TCP localhost:51772->localhost:6379 (ESTABLISHED)
python  24193 root   10u  IPv4 74065330      0t0     TCP localhost:51773->localhost:6379 (ESTABLISHED)
python  24193 root   11u  IPv4 74065332      0t0     TCP localhost:51774->localhost:6379 (ESTABLISHED)
python  24193 root   12u  IPv4 74065334      0t0     TCP localhost:51775->localhost:6379 (ESTABLISHED)
python  24193 root   13u  IPv4 74065344      0t0     TCP localhost:51776->localhost:6379 (ESTABLISHED)
python  24193 root   14u  IPv4 74065346      0t0     TCP localhost:51777->localhost:6379 (ESTABLISHED)
python  24193 root   15u  IPv4 74065348      0t0     TCP localhost:51778->localhost:6379 (ESTABLISHED)
python  24193 root   16u  IPv4 74065350      0t0     TCP localhost:51779->localhost:6379 (ESTABLISHED)
python  24193 root   17u  IPv4 74065352      0t0     TCP localhost:51780->localhost:6379 (ESTABLISHED)
python  24193 root   18u  IPv4 74065354      0t0     TCP localhost:51781->localhost:6379 (ESTABLISHED)
python  24193 root   19u  IPv4 74065356      0t0     TCP localhost:51782->localhost:6379 (ESTABLISHED)
python  24193 root   20u  IPv4 74065358      0t0     TCP localhost:51783->localhost:6379 (ESTABLISHED)
python  24193 root   21u  IPv4 74065360      0t0     TCP localhost:51784->localhost:6379 (ESTABLISHED)
python  24193 root   22u  IPv4 74065362      0t0     TCP localhost:51785->localhost:6379 (ESTABLISHED)
python  24193 root   23u  IPv4 74065364      0t0     TCP localhost:51786->localhost:6379 (ESTABLISHED)
python  24193 root   24u  IPv4 74065366      0t0     TCP localhost:51787->localhost:6379 (ESTABLISHED)
python  24193 root   25u  IPv4 74065368      0t0     TCP localhost:51788->localhost:6379 (ESTABLISHED)

其实期初有关共享连接的问题,我给redis-py github提交过issue。  当然作者关注回复了我的问题,但回复内容只是不痛不痒的基础理论,估摸是自己的问题需求他没看懂。 

回到这个问题,redis-py是怎么解决这问题的。  就是这段代码,你可以照着redis-py反推,你会发现很多有趣的东西.  当你使用redis-py构建连接对象的时候,她不管你用不用Pool池,他都会给你实例化一个Pool池的,当然默认是给你一个只有一个连接的线程池。  他跟有max_connections的Pool区别在于, max_connections = max_connections or 2 ** 31

#xiaorui.cc
def get_connection(self, command_name, *keys, **options):
    #从_available_connections获取可用的连接
    self._checkpid()
    try:
        connection = self._available_connections.pop()
    except IndexError:
        connection = self.make_connection()
    self._in_use_connections.add(connection)
    return connection

那么怎么关闭连接, del redis_conn_object

#xiaorui.cc
def __del__(self):
    try:
        self.disconnect()
    except Exception:
        pass

def disconnect(self):
    "Disconnects all connections in the pool"
    all_conns = chain(self._available_connections,
                      self._in_use_connections)
    for connection in all_conns:
        connection.disconnect()

redis-py会主动回收redis连接么? 不会.  redis-py没有这段代码.  

andymccurdy的回答不是我想要的.  我想知道实现原理,这哥们跟我聊原理… 原理谁都知道…  


http://xiaorui.cc/?p=3359

END.


大家觉得文章对你有些作用! 如果想赏钱,可以用微信扫描下面的二维码,感谢!
另外再次标注博客原地址  xiaorui.cc

3 Responses

  1. Ficapy 2016年8月7日 / 上午10:02

    redis-py的关闭操作只是把_sock设置成了None,连接对象还是在连接池里面。再次使用的时候会连接对象会新建socket连接。这是我写的概览https://ficapy.github.io/2016/08/06/redis_py_note/

    • 峰云就她了 2016年8月9日 / 上午10:23

      另外,pop给client的连接会扔到self._in_use_connections.add(connection)里,用完了后会会扔到可用链接池里。

  2. 白宇 2016年5月17日 / 下午4:39

    牛逼

发表评论

邮箱地址不会被公开。 必填项已用*标注