天天看點

python close_wait_redis-py 連接配接池不能處理空閑的 CLOSE_WAIT 連接配接

距離上次排查 epoll 與 CLOSE_WAIT 連接配接 的問題,已經過去了将近一年。最近在看 《UNIX 網絡程式設計》,看到 “TCP 狀态轉換圖” 中提到 CLOSE_WAIT 狀态時,突然又想起來上次還有一個 遺留問題,于是決定再次嘗試分析一下。

一、問題現象

上次的遺留問題,歸納起來就是:(由于 Redis 的 server 端主動關閉逾時連接配接)在 client 端産生的 CLOSE_WAIT 連接配接,一直無法被 redis-py 連接配接池複用,進而無法被正常 close。

二、分析 redis-py 連接配接池機制

以目前最新的 redis-py 2.10.6 為例,從連接配接池擷取連接配接 的源碼:

1

2

3

4

5

6

7

8

9def get_connection(self, command_name, *keys, **options):

"Get a connection from the pool"

self._checkpid()

try:

connection = self._available_connections.pop()

except IndexError:

connection = self.make_connection()

self._in_use_connections.add(connection)

return connection

1

2

3

4

5

6

7def release(self, connection):

"Releases the connection back to the pool"

self._checkpid()

if connection.pid != self.pid:

return

self._in_use_connections.remove(connection)

self._available_connections.append(connection)

可以看出,redis-py 使用 _available_connections 來維護 “空閑可用的連接配接清單”,擷取連接配接時 pop 出清單末尾的連接配接,釋放連接配接時 append 連接配接到清單末尾。是以 “空閑可用的連接配接清單” 其實是個 後進先出的棧。

很顯然,基于這種 “後進先出的棧” 的資料結構,redis-py 連接配接池對連接配接的擷取和釋放都發生在 “棧頂”。至此,原因就很明顯了:如果某段時間内由于突發流量産生了大量連接配接,一旦流量趨于平穩(減少)後,位于 “棧底” 的部分連接配接就會一直無法被複用,于是這些連接配接被 Redis 的 server 端逾時關閉後,就會一直處于 CLOSE_WAIT 狀态。

三、解決方案

為了讓 redis-py 連接配接池能夠更均衡地複用各個連接配接,很容易想到的一個方案是:将資料結構從 “後進先出的棧” 改成 “先進先出的隊列”。

通過修改 get_connection 的實作可以很容易做到這一點:

1

2# connection = self._available_connections.pop()

connection = self._available_connections.pop(0) # 擷取連接配接時,從隊列首部 pop 出來

關于這個方案,其實在 GitHub 上也有一個 pull request:Connection management improvements,然而還是沒有得到響應 :-( 不得不手動尴尬一下…

四、複現和驗證

為了簡化場景,便于問題的複現和方案的驗證,這裡有一段輔助代碼:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30# example.py

import select

import redis

def main():

import os; print('pid: %s' % os.getpid())

r = redis.StrictRedis(host='localhost', port=6379, db=0)

pool = r.connection_pool

epoll = select.epoll()

for conn in (pool.get_connection(''), pool.get_connection('')):

conn.connect()

epoll.register(conn._sock, select.POLLIN)

pool.release(conn)

command_args = ('SET', 'foo', 'bar')

while True:

conn = pool.get_connection('')

conn.send_command(*command_args)

epoll.poll()

r.parse_response(conn, command_args[0])

pool.release(conn)

if __name__ == '__main__':

main()

操作步驟提示:

設定 Redis 的 server 端的 timeout 參數(比如 10 秒)

運作代碼(python example.py)

一段時間後,觀察程序的 CPU 占用率(top)

觀察程序是否有 CLOSE_WAIT 連接配接(lsof -p PID)