我们基于tornado搭建了一个Web服务,部署了多台服务器,经由nginx转发请求,近期频繁收到nginx的499日志报警。
nginx的499代码是指客户端调用服务端的服务, 但是服务端未能在限制的时间段内给出回应,于是客户端主动断开请求,nginx打印499日志。
了解了499的问题之后,并确认nginx的配置没有问题,考虑到是我们的服务无法支撑客户端的并发请求,于是我们增加了服务器的部署,nginx的499问题有所缓解但是还是会时不时的出现。
该业务目前只是在简单维护加上手头上有其他优先级更高的项目需要开发,为此499问题暂时缓解后就没再继续深入下去,毕竟该业务每天200W的访问量大概2000多个499问题,服务的可用性应该可以了。
4个9也就是99.99%即一个服务的可用性达到99.99%。我负责的这个服务明显还没有达到,不过这个服务已经有1年多没有更新部署了,99.8%的可用性业务方能接受。
我也说不清楚是什么促使自己再次去查看这个问题,可能是每天的告警邮件让人心里不舒服,也可能是这个周恰好可以抽出点时间,又或者是4个9的这个可用性。
拾起了这个很久没有更新的项目,里面的代码已经很生疏了,我在开发环境进行了服务部署,开始再次查找问题。
首先从压力测试开始,使用的是siege这个压测工具
用100个并发进行压力测试1个小时,查看qps是250左右,平均响应时长是500MS,最长响应时长是2S。
按照这个数值计算单机服务可以处理3600*250个请求,我部署了3台服务器接口每天请求200W,应该完全可以处理的过来(业务方的并发量小于100),不过最大响应时间2S这个不就是超时499的情况吗?(业务方超时限制800MS)。
是不是压力测试的样本中有某些样本特别耗时呢?
打开info日志,tornado框架内web.py会对每个请求打印一条耗时日志,我也在主处理函数进行耗时统计。
再次压力测试,siege显示存在1S以上的情况,查看服务日志却没有发现超过1S的请求日志。从siege的响应中找到1S的哪些样例单独去测试,耗时并没有超过1S,那时间耗在哪里了呢?
查看日志记录是web.py的第1971行打出的
# web.py
def log_request(self, handler):
"""Writes a completed HTTP request to the logs.
By default writes to the python root logger. To change
this behavior either subclass Application and override this method,
or pass a function in the application settings dictionary as
``log_function``.
"""
if "log_function" in self.settings:
self.settings["log_function"](handler)
return
if handler.get_status() < 400:
log_method = access_log.info
elif handler.get_status() < 500:
log_method = access_log.warning
else:
log_method = access_log.error
request_time = 1000.0 * handler.request.request_time()
log_method("%d %s %.2fms", handler.get_status(),
handler._request_summary(), request_time)
查看request_time
# httpuitl.py
def request_time(self):
"""Returns the amount of time it took for this request to execute."""
if self._finish_time is None:
return time.time() - self._start_time
else:
return self._finish_time - self._start_time
定位_start_time
class HTTPServerRequest(object)
是每个请求初始化时定义的,也就是说web.py中的耗时时从内部接受到这个请求到处理结果的时间。
服务处理请求的时间不足1S,但是siege显示耗时超过1S,应该就是请求阻塞了。
最后通过查找tornado文档找到了答案:get/post方法默认是同步处理的。
参考:tornado web应用的结构
Tornado默认会同步处理: 当
get()
/post()
方法返回, 请求被认为结束 并且返回响应. 因为当一个处理程序正在运行的时候其他所有请求都被阻塞, 任何需要长时间运行的处理都应该是异步的, 这样它就可以在非阻塞的方式中调用 它的慢操作了.
主要利用@tornado.web.asynchronous和AsyncHTTPClient,并且手动触发finish。
代码示例如下:
class AsynchronousHandler(tornado.web.RequestHandler):
""" 异步处理
"""
def data_received(self, chunk):
pass
def initialize(self, handler):
self.handler = handler
@tornado.web.asynchronous
def get(self, *args, **kwargs):
http_client = tornado.httpclient.AsyncHTTPClient()
http_client.fetch(self.request, callback=self.process)
@tornado.web.asynchronous
def post(self, *args, **kwargs):
http_client = tornado.httpclient.AsyncHTTPClient()
http_client.fetch(self.request, callback=self.process)
def process(self, response):
start_time = time.time()
try:
params = self.get_argument("params")
pro_type = self.get_argument("type", "time2timestamp")
flag, result = self._process(params, pro_type)
result_str = JsonEncoder.common_response(result, time.time() - start_time)
except Exception as e:
logging.error(traceback.format_exc())
result_str = JsonEncoder.error_response(None, str(e), time.time() - start_time)
self.write(result_str)
# 显示调用finish
self.finish()
def _process(self, params, process_type):
"""
实际处理方法
:param params: 请求参数
:param process_type: 处理类型
:return: True, result
"""
return self.handler.process(params, process_type)
修改代码之后重新部署,100个并发压测3个小时,最长耗时740MS,解决了499问题。
经过这次BUG修复反应出了不少的问题,无论是从技术方面还是做事方面都有很多感触。
总的来说,这次花费时间修复BUG收获颇多,作此文以记录。