tornado异步HTTP坑~
一、基于tornado的异步http在高qps下返回599错误
基于tornado协程实现的异步http,在高qps情况下会返回599错误码。原因就在于ioloop计算超时时间的时候,是从request请求放入queue中时开始计算的。也就是说超时时间= queue等待时间 + min(request建立连接时间 + request请求时间)。如果queue太长,实际上请求大部分时间都卡在queue中。
SimpleAsyncHTTPClient中fetch_impl实现源码:
可以看到超时时间为:self.io_loop.time() + min(request.connect_timeout,request.request_timeout)
def fetch_impl(self, request, callback):
key = object()
self.queue.append((key, request, callback))
if not len(self.active) < self.max_clients:
timeout_handle = self.io_loop.add_timeout(
self.io_loop.time() + min(request.connect_timeout,
request.request_timeout),
functools.partial(self._on_timeout, key, "in request queue"))
else:
timeout_handle = None
self.waiting[key] = (request, callback, timeout_handle)
self._process_queue()
if self.queue:
gen_log.debug("max_clients limit reached, request queued. "
"%d active, %d queued requests." % (
len(self.active), len(self.queue))) 想要修改也很简单,自己定义一个HTTPClient,继承SimpleAsyncHTTPClient,然后重写fetch_impl跟_connection_class方法。
如何重写呢?
对于异步http,完全可以根据await或者yield关键字等待超时来判断,所以可以直接删掉add_timeout。
response = await http_client.fetch("http://www.google.com") 具体实现如下:
class NoQueueTimeoutHTTPClient(SimpleAsyncHTTPClient):
# 队列
def fetch_impl(self, request, callback):
key = object()
self.queue.append((key, request, callback))
self.waiting[key] = (request, callback, None)
self._process_queue()
if self.queue:
gen_log.debug("max_clients limit reached, request queued. %d active, %d queued requests." % (
len(self.active), len(self.queue)))
# 重写连接
def _connection_class(self):
return _HTTPConnection1 二、tornado 204响应码强校验
背景
项目在开发http相关的某个功能的时候,没有按http规范的要求,返回了204的响应码,但是携带content内容。
在用request库发请求的时候,能够正常收到响应,但是在tornado中使用异步http却只收到了599错误码
引言
设计http相关功能的时候,务必要按照http规范进行!!!不然好多坑啊
tornado 204响应码强校验
HTTP1Connection下的_read_body方法中,有一段源码是这样的:
if code == 204:
# This response code is not allowed to have a non-empty body,
# and has an implicit length of zero instead of read-until-close.
# http://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.3
if ("Transfer-Encoding" in headers or
content_length not in (None, 0)):
raise httputil.HTTPInputError(
"Response with code %d should not have body" % code)
content_length = 0 当响应码是204的时候,会强制校验content_length是否是None或者0,不是的话会抛异常。
tornado在捕获异常之后,_HTTPConnection下_handle_exception处理异常时会返回599的错误码。
def _handle_exception(self, typ, value, tb):
if self.final_callback:
self._remove_timeout()
if isinstance(value, StreamClosedError):
if value.real_error is None:
value = HTTPStreamClosedError("Stream closed")
else:
value = value.real_error
self._run_callback(HTTPResponse(self.request, 599, error=value,
request_time=self.io_loop.time() - self.start_time,
start_time=self.start_wall_time,
))
if hasattr(self, "stream"):
# TODO: this may cause a StreamClosedError to be raised
# by the connection's Future. Should we cancel the
# connection more gracefully?
self.stream.close()
return True
else:
# If our callback has already been called, we are probably
# catching an exception that is not caused by us but rather
# some child of our callback. Rather than drop it on the floor,
# pass it along, unless it's just the stream being closed.
return isinstance(value, StreamClosedError) 适配方案:
自定义_HTTPConnection1,继承_HTTPConnection,重写headers_received方法,如果收到204响应码,将headers中的content-length强制改为0
class _HTTPConnection1(_HTTPConnection):
# 添加204content_length处理
def headers_received(self, first_line, headers):
if self.request.expect_100_continue and first_line.code == 100:
self._write_body(False)
return
self.code = first_line.code
self.reason = first_line.reason
self.headers = headers
# 设置204 content_length为0
if self.code == 204:
length = self.headers.get('content-length', 0)
self.headers["Content-Length"] = "0"
gen_log.info("turn headers content-length from {} to 0".format(length))
if self._should_follow_redirect():
return
if self.request.header_callback is not None:
# Reassemble the start line.
self.request.header_callback('%s %s %s\r\n' % first_line)
for k, v in self.headers.get_all():
self.request.header_callback("%s: %s\r\n" % (k, v))
self.request.header_callback('\r\n') #HTTP#1
