起因是这样的,昨天突然发现以前用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的回答不是我想要的. 我想知道实现原理,这哥们跟我聊原理… 原理谁都知道…
END.
redis-py的关闭操作只是把_sock设置成了None,连接对象还是在连接池里面。再次使用的时候会连接对象会新建socket连接。这是我写的概览https://ficapy.github.io/2016/08/06/redis_py_note/
另外,pop给client的连接会扔到self._in_use_connections.add(connection)里,用完了后会会扔到可用链接池里。
牛逼