python urllib2.urlopen hang 住 的bug 追踪

结论

先说结论, python urllib2 的urlopen 函数没有设置timeout 时间 ,导致 这个函数有概率读取网络请求的时候,hang住进程中断了。


起因

起因 是有一个crontab 的任务 ,部署在一个内部的k8s 容器上, 定时每小时去获取队列的数据。但是发现定时脚本生成的数据没有入库里,首先怀疑是脚本问题,粗略看了下代码, 就是一个简单的遍历去队列里面拿数据。然后看了下打印的业务日志,发现日志打印的最后一行 在代码里是请求网络请求的一条日志,(然后后续请求成功会打成功的日志,请求失败则有失败的。 )具体代码如下:

def http_post_request(self, req_url, req_data, req_header=''):
        """
            http请求-post
        """
        try:
        		# 最后一条日志
            self.info('http_post_request: req_url is ' + str(req_url))
            req_data = urllib.urlencode(req_data)
            req = urllib2.Request(url = req_url, data = req_data, headers = req_header)
            response_js = urllib2.urlopen(req).read()
            response_js = response_js.strip()
            response = json.loads(response_js)
            if response.get('error_code') == "CantGetMsg":
               # 无
                self.error('http_post_request fail, req_url is ' + str(req_url) + ' , req_data is ' + str(req_data)
                        + ", ret_code is " + str(response['status']) + " ret_msg is " + str(response['messages']))
                return False
             #无
            self.info('http_post_request suc, req_url is ' + str(req_url) + ' , req_data is ' + str(req_data))
            return response
        except Exception as e:
           #无
            self.error('http_post_request EXECUTE FAIL, MSG [' + str(e) + "]")
            return False

所以初步怀疑是Python 脚本挂起了。

果然查询一下 Python urllib2 urlopen hangs 一大堆提示

img

大概看了一个方案:感觉就是这个了,

结果这段bug的同事还翻Python 源码的 表示 就算urlopen没有timeout 参数也会有默认值的。

img

img

看来不翻个底儿 是没办法了。

找到一篇Stack Overflow上的贴子5faa347dly1gqh20vi0xkj215y0ygtec.jpeg

表示 default_timeout 在不同的Python模块中实现是有问题,的,至少在他贴的cpython 源码中可以看到 在首次 new sockets 的时候 默认defaulttimeout 是 -1. 换句话说就是没有。但是为啥会这样的,首次连接的时候对于一个套接字sockets 会有defaulttimeout -1 的情况

然后看到这篇,哇 https://github.com/mesonbuild/meson/issues/4087 真的佩服这哥们,学习。写的通篇文章值得看一遍,他是在一个构建工具提的一个issue :

我摘重点:

check the socket python-module documentation [ https://docs.python.org/3/library/socket.html#socket.getdefaulttimeout ]:

socket.getdefaulttimeout()

Return the default timeout in seconds (float) for new socket objects. A value of None indicates that new socket objects have no timeout. When the socket module is first imported, the default is None.

socket.setdefaulttimeout(timeout)

Set the default timeout in seconds (float) for new socket objects. When the socket module is first imported, the default is None. See settimeout() for possible values and their respective meanings.

他截取了在python module 里 socket.getdefaulttimeout的注释。 其实上面就已经表明了 第一次 默认值为none, 但是后面他介绍了为啥这里是none 的原因 ,很重要。

in Linux kernel (with all default parameters and default parameters like in all popular linux-distros) – socket object’s behaviour assumes next situations:

  • the socket is in connection phase. in this case there is some default timeout (not large) for the operation. socket operation is NOT able to hangup forever during this phase.
  • the connected socket is in sending data phase. in this case also there is some default timeout (also not large) for acknowledging of sending. and socket operation is also NOT able to hangup forever during this phase .
  • the connected socket is in the middle of receiving data phase. in this case also there is some default timeout (can be pretty large) for making acknowledging and continuing functionality. and socket operation is also (ALSO!) NOT able to hangup forever during this phase.
  • the connected socket is NOT sending anything AND is NOT in the middle of receiving data, but the socket is waiting of receiving new sequence of data. the client’s socket is waiting new data, but the server’s socket is not sending anything yet, for example the server has to do some own work before starting new sending data to the client . YES! this phase IS able to hangup FOREVER. if socket connection had broken down during that phase, we wouldn’t know when we should stop the waiting, and timeout for this phase is infinite by default.

就是在流行的Linux 内容 一个socket 对象的行为大致有一下几种。

  • socket 在建立连接阶段。这个情况下 是有默认超时时间的 对于连接操作(时间不长 RTT ) socket 也不允许 在这里被一直挂起。
  • 建立连接的socket 在发送 数据阶段, 在这种情况下 也有默认时间和ac 确认发送操作,同时也不允许被挂起。forever
  • socket 接收数据阶段也和发送阶段一样。
  • 现在还有一个阶段 就是 连接了的socket 在没有发送任何数据 和没有接收数据的,而且这个socket 是正在等待接收一个新的队列数据,换句话说就是 客户端的socket 正在等待新数据,但是服务端还没有发送任何数据。举个例子 就是服务器端在发送数据前 做一些其他的自己操作, 然后这个阶段 没错 ,就是会一直等待。 如果套接字的连接在这个阶段挂了,我们不知道我们应该停止等待, 然而 我们的timeout 在这个阶段是默认 none 就是infinite 无限的。。。

而这个阶段 对应我们的python 就是 在 我们发送了 http header 和body 然后在我们接收数据请求头之前。(这段是这个老哥的说法, 我感觉不是呀 根据tcp 协议 ,socket 是在 三次握手之后,如果你发送了http header .应该是在三次握手 socket 建立 成功之后发送数据 阶段了。应该再早一步,在tcp 阶段 还没有发送数据的时候。 )

it is after sending of http-headers and http-body, but before receiving http-headers from a server.

其实有系统 TCP 有一个 配置 SO_KEEPALIVE 可以检测 不活跃的连接 关掉 但是默认是false

SO_KEEPALIVE 选项为 true 时, 表示底层的TCP 实现会监视该连接是否有效. 当连接处于空闲状态(连接的两端没有互相传送数据) 超过了 2 小时时, 本地的TCP 实现会发送一个数据包给远程的 Socket. 如果远程Socket 没有发回响应, TCP实现就会持续尝试 11 分钟, 直到接收到响应为止. 如果在 12 分钟内未收到响应, TCP 实现就会自动关闭本地Socket, 断开连接. 在不同的网络平台上, TCP实现尝试与远程Socket 对话的时限有所差别.


后续

https://medium.com/pipedrive-engineering/socket-timeout-an-important-but-not-simple-issue-with-python-4bb3c58386b4

底层协议实现在Python里