问题描述
我们对象存储系统用到了老版本的swift mc客户端,memcached切到medis后,swift时 不时的会爆出几个list index out of range 异常,在代码的line[0].upper() != ‘END’
medis :基于redis2.0添加了部分mc命令。 异常复现
开始怀疑我们修改的medis有问题,写了个go测试脚本,把取到的字节打印出来, 和set进去的比较,并没有发现medis有问题。
按照swift mc客户端的思路写了个测试脚本随机抓了1万个key,并没有报异常。 修改代码在swift mc客户端代码抛出异常时打印get的key和获取到的line,异常显示 获取到的line确实为空,telnet到medis上get该key是存在的且返回的数据也正常。 看了python的文档,怀疑应该是超时链接断了,没有及时关闭链接导致读取到EOF 出错。修改了medis timeout为30秒,写了个python脚本,先建立链接get key一次, 然后sleep40秒,保证服务端已经主动发起close,然后再get key一次。异常每次都 复现。
异常分析
通过抓包和结合代码分析,medis服务端timeout超时,会主动close链接,操作系统会 发Fin给客户端,客户端操作系统会回一个Ack,服务端链接状态已经是FIN_WAIT2。因 tcp链接是全双工通信,关闭需要四次挥手,服务端主动close链接,只能说明服务端没 有内容发给客户端了,既然服务端已经关闭了链接,服务端是不会再发内容给客户端 了,客户端理应关闭链接的,但是客户端没有。客户端还是照常发送get key,照常读 取,但是读到的内容肯定为空。通过抓包来看,超时后客户端发送的请求服务端是会 rest的。服务端等待net.ipv4.tcp_fin_timeout秒后或者发完rest后,回收了FIN_WAIT2的 链接。
结合下面swift mc get这个函数看下,最外层的for循环是从链接池取链接,while循环 是处理内容,当链接断掉的时候,调用fp.readline其实返回了空字符串,也就是line这 个数组的长度为0,所以会在line[0].upper() != ‘END’这行抛list index out of range异常。
抛出的异常会被except捕获到,跳过self._return_conn函数不会放到链接池,该链接被 python垃圾回收自动回收掉。最外层for循环再从链接池取一个链接,没有异常发生 return value返回。 其实,从结果来看是没有问题的,最终也会取到正确的返回值。但是从日志看,是非 常严重的问题。让人唏嘘的是释放坏的链接尽然是通过数组越界来完成的。
虽然说这种情况是因medis主动关闭链接发现的,但是链接断掉也会出现。网络断掉没 好会发生阻塞,tcp链接断了会发生list index out of range异常。 所以应该判断读到的内容如果是空,就抛出connect may be closed 异常。
swift mc get:
def get(self, key): """ Gets the object specified by key. It will also unserialize the object before returning if it is serialized in memcache with JSON, or if it is pickled and unpickling is allowed. :param key: key :returns: value of the key in memcache """ key = md5hash(key) value = None for (server, fp, sock) in self._get_conns(key): try: sock.sendall('get %s\r\n' % key) line = fp.readline().strip().split() while line[0].upper() != 'END': if line[0].upper() == 'VALUE' and line[1] == key: size = int(line[3]) value = fp.read(size) if int(line[2]) & PICKLE_FLAG: if self._allow_unpickle: value = pickle.loads(value) else: value = None elif int(line[2]) & JSON_FLAG: value = json.loads(value) fp.readline() line = fp.readline().strip().split() self._return_conn(server, fp, sock) return value except Exception, e: self._exception_occurred(server, e, sock=sock, fp=fp)
测试脚本抓包
11:23:06.063476 IP 127.0.0.1.55574 > 127.0.0.1.11211: Flags [S], seq 15 28032795, win 65535, options [mss 1460,nop,wscale 5,nop,nop,TS val 1590 67830 ecr 0,sackOK,eol], length 0 11:23:06.063506 IP 127.0.0.1.11211 > 127.0.0.1.55574: Flags [S.], seq 8 05908022, ack 1528032796, win 14600, options [mss 1460,nop,nop,sackOK,n op,wscale 9], length 0 11:23:06.065806 IP 127.0.0.1.55574 > 127.0.0.1.11211: Flags [.], ack 1, win 8192, length 0 11:23:06.066458 IP 127.0.0.1.55574 > 127.0.0.1.11211: Flags [P.], seq 1 :11, ack 1, win 8192, length 10 11:23:06.066480 IP 127.0.0.1.11211 > 127.0.0.1.55574: Flags [.], ack 11 , win 29, length 0 11:23:06.066568 IP 127.0.0.1.11211 > 127.0.0.1.55574: Flags [P.], seq 1 :29, ack 11, win 29, length 28 11:23:06.071152 IP 127.0.0.1.55574 > 127.0.0.1.11211: Flags [.], ack 29 , win 8191, length 0 11:23:38.441377 IP 127.0.0.1.11211 > 127.0.0.1.55574: Flags [F.], seq 2 9, ack 11, win 29, length 0 11:23:38.445016 IP 127.0.0.1.55574 > 127.0.0.1.11211: Flags [.], ack 30 , win 8192, length 0 11:23:46.073030 IP 127.0.0.1.55574 > 127.0.0.1.11211: Flags [P.], seq 1 1:21, ack 30, win 8192, length 10 11:23:46.073058 IP 127.0.0.1.11211 > 127.0.0.1.55574: Flags [R], seq 80 5908052, win 0, length 0 11:23:46.073221 IP 127.0.0.1.55574 > 127.0.0.1.11211: Flags [F.], seq 2 1, ack 30, win 8192, length 0 11:23:46.073247 IP 127.0.0.1.11211 > 127.0.0.1.55574: Flags [R], seq 80 5908052, win 0, length 0
测试脚本
go代码:
package main import ( "net" "fmt" "time" ) func main() { b := make([]byte, 32) // set name 123 0 3 // lee conn, err := net.Dial("tcp", "127.0.0.1:11211") if err != nil { fmt.Printf("connect error: %s\n", err.Error()) return } l, e := conn.Write([]byte("get name\r\n")) if e != nil { fmt.Printf("write error: %s\n", e.Error()) conn.Close() return } fmt.Printf("write len: %d\n", l) time.Sleep(40 * time.Second) l, e = conn.Read(b) if e != nil { fmt.Printf("read error: %s\n", e.Error()) conn.Close() return } fmt.Println(b) fmt.Println("========") fmt.Println([]byte("VALUE name 123 3\r\nlee\r\nEND\r\n")) }
import socket import time def getkey(fp, sock): try: n = sock.sendall("get name\r\n") print(n) value = None line = fp.readline().strip().split() print(line) while line[0].upper() != 'END': print(line) if line[0].upper() == 'VALUE' and line[1] == "name": size = int(line[3]) value = fp.read(size) fp.readline() line = fp.readline().strip().split() print(value) except Exception, e: print(e) def main(): # set name 123 0 3 # lee sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) sock.connect(("127.0.0.1", 11211)) sock.settimeout(10) fp = sock.makefile() print("===1===") getkey(fp, sock) time.sleep(40) print("===2===") getkey(fp, sock) if __name__ == '__main__': main()