背景

线上有一个相关百科的服务,返回一个query中提及的百科词条。该服务是用python实现的,以前通过thrift接口访问,现要将其改为通过HTTP访问。之前没有搭建HTTPServer的经验,因此想用python的web Framework来做这件事,于是有了下面的工作。第一部分是框架选择,这一部分没有太仔细考虑,只是大概看了一些文章。第二部分是根据所需要的功能,学习及测试在框架上应该如何实现。第三部分是实际的代码。第四部分是下一步的学习。

框架选择

python有很多开源的web framework。从知乎上找了几篇综述型的简介,大体包括:Django、Bottle、Flask、web2py、Tornado。看中了介绍中提及Tornado的速度与并发量,于是打算用tornado来实现。所以按目前的了解,或许Tornado并非实现本工作的最佳方案,只是一个可行方案。

学习与测试

用tornado开发web服务的基本流程

tornado具有web framework的功能,因此用它开发web服务非常方便:

  1. 实现处理请求的Handler,该类继承自tornado.web.RequestHandler,实现用于处理请求的对应方法如:get、post等。返回内容用self.write方法输出。
  2. 实例化一个Application。构造函数的参数是一个Handlers列表,通过正则表达式,将请求与Handler对应起来。通过dict将Handler需要的其他对象以参数的方式传递给Handler的initialize方法。
  3. 初始化一个tornado.httpserver.HTTPServer对象,构造函数的参数是上一步的Application对象。
  4. 为HTTPServer对象绑定一个端口。
  5. 开始IOLoop。

原服务的特点

原服务是一个内存占用大,IO密集,计算量适中的服务。

  1. 内存占用大。需要加载一个比较大的词表,其中每个词对应一个id列表,这一部分是C++实现的,通过boost.python封装为python可调用的so。原服务单进程占用内存超过5G。
  2. IO密集。计算过程中大量访问redis读取term及baikeid的属性信息,用于过滤及rank计算。也访问在线分词服务,获取各term的NLP分析。
  3. 计算量适中。划词匹配、rank计算有一定计算量,但是总体来看计算量不是特别大。python单进程每天500多万的访问量,单CPU利用率也就40%-50%之间。

关于服务的分析:

  1. 内存占用大。内存占用大,但绝大部分是只读的。不适合独立启动多个进程,适合多线程或用子进程。
  2. IO密集。适合将IO操作都变为异步请求,或者用多线程模型。
  3. 计算量适中。由于python解释器使用GIL,多线程只能提高IO的并发能力,不能提高计算的并发能力。因此可以考虑通过子进程的方式,适当增加提供服务的进程数,提高整个系统服务能力的上限。

需要用到的特性

由于tornado的亮点是异步请求,所以这里首先想到的是将所有请求都改造为异步的。但是这里遇到一个问题,就是异步函数内一定不能有阻塞调用出现,否则整个IOLoop都会被卡住。这就要求彻底地去改造服务,将所有IO或是用时较长的请求都改造为异步函数。这个工程量是非常大的,需要去修改已有的代码。因此,我们考虑用线程池的方式去实现。当一个线程阻塞在某个请求或IO时,其他线程或IOLoop会继续执行。

另外一个瓶颈就是GIL限制了CPU的并发数量,因此考虑用子进程的方式增加进程数,提高服务能力上限。

综合上面的分析,大致用以下方案:

  1. 通过子进程的方式复制多个进程,使子进程中的只读页指向同一个物理页。
  2. 线程池。回避异步改造的工作量,增加IO的并发量。

测试代码

首先测试线程池,测试用例为:

对sleep页面同时发出两个请求:

  1. 在线程池中运行的函数(这里是self.block_task)能够同时执行。表现为在控制台交替打印出数字。
  2. 两个get请求几乎同时返回,在浏览器上显示返回的内容。

线程池的测试代码如下:

  1. import os
  2. import sys
  3. import time
  4. import tornado.httpserver
  5. import tornado.ioloop
  6. import tornado.options
  7. import tornado.web
  8. import tornado.gen
  9. from tornado.concurrent import run_on_executor
  10. from concurrent.futures import ThreadPoolExecutor
  11. from tornado.options import define, options
  12. class HasBlockTaskHandler(tornado.web.RequestHandler):
  13. executor = ThreadPoolExecutor(20) #起线程池,由当前RequestHandler持有
  14. @tornado.gen.coroutine
  15. def get(self):
  16. strTime = time.strftime("%Y-%m-%d %H:%M:%S")
  17. print "in get before block_task %s" % strTime
  18. result = yield self.block_task(strTime)
  19. print "in get after block_task"
  20. self.write("%s" % (result))
  21. @run_on_executor
  22. def block_task(self, strTime):
  23. print "in block_task %s" % strTime
  24. for i in range(1, 16):
  25. time.sleep(1)
  26. print "step %d : %s" % (i, strTime)
  27. return "Finish %s" % strTime
  28. if __name__ == "__main__":
  29. tornado.options.parse_command_line()
  30. app = tornado.web.Application(handlers=[(r"/sleep", HasBlockTaskHandler)], autoreload=False, debug=False)
  31. http_server = tornado.httpserver.HTTPServer(app)
  32. http_server.bind(8888)
  33. tornado.ioloop.IOLoop.instance().start()

整个代码里有几个位置值得关注:

  1. executor = ThreadPoolExecutor(20)。这是给Handler类初始化了一个线程池。其中concurrent.futures不属于tornado,是python的一个独立模块,在python3中是内置模块,python2.7需要自己安装。
  2. 修饰符@run_on_executor。这个修饰符将同步函数改造为在executor(这里是线程池)上运行的异步函数,内部实现是将被修饰的函数submit到executor,返回一个Future对象。
  3. 修饰符@tornado.gen.coroutine。被这个修饰符修饰的函数,是一个以同步函数方式编写的异步函数。原本通过callback方式编写的异步代码,有了这个修饰符,可以通过yield一个Future的方式来写。被修饰的函数在yield了一个Future对象后将会被挂起,Future对象的结果返回后继续执行。

运行代码后,在两个不同浏览器上访问sleep页面,得到了想要的效果。这里有一个小插曲,就是如果在同一浏览器的两个tab上进行测试,是无法看到想要的效果。第二个get请求会被block,直到第一个get请求返回,服务端才开始处理第二个get请求。这让我一度觉得多线程没有生效,用了半天时间查了很多资料,才看到是浏览器把相同的第二个请求block了,具体链接参考这里

由于tornado很方便地支持多进程模型,多进程的使用要简单很多,在以上例子中,只需要对启动部分稍作改动即可。具体代码如下所示:

  1. if __name__ == "__main__":
  2. tornado.options.parse_command_line()
  3. app = tornado.web.Application(handlers=[(r"/sleep", HasBlockTaskHandler)], autoreload=False, debug=False)
  4. http_server = tornado.httpserver.HTTPServer(app)
  5. http_server.bind(8888)
  6. print tornado.ioloop.IOLoop.initialized()
  7. http_server.start(5)
  8. tornado.ioloop.IOLoop.instance().start()

需要注意的地方有两点:

  1. app = tornado.web.Application(handlers=[(r"/sleep", HasBlockTaskHandler)], autoreload=False, debug=False),在生成Application对象时,要将autoreload和debug两个参数至为False。也就是需要保证在fork子进程之前IOLoop是未被初始化的。这个可以通过tornado.ioloop.IOLoop.initialized()函数来跟。
  2. http_server.start(5)在启动IOLoop之前通过start函数设置进程数量,如果设置为0表示每个CPU都启动一个进程。

最后的效果是可以看到n+1个进程在运行,且公用同一个端口。

实际代码

大部分逻辑代码是封装好的,服务的代码如下:

  1. import os
  2. import sys
  3. import json
  4. import tornado.httpserver
  5. import tornado.ioloop
  6. import tornado.options
  7. import tornado.httpclient
  8. import tornado.web
  9. import tornado.gen
  10. from tornado.concurrent import run_on_executor
  11. from concurrent.futures import ThreadPoolExecutor
  12. from tornado.options import define, options
  13. import rela_baike_server
  14. from rela_baike_server import RelaBaikeRequest, RelaBaikeResult, RelaBaikeServer
  15. import logging
  16. from logging.handlers import TimedRotatingFileHandler
  17. logging.basicConfig()
  18. import pdb
  19. g_log_prefix = '../log/rela_baike_tornado.'
  20. def getLogger(strPrefixBase):
  21. strPrefix = "%s%d" % (strPrefixBase, os.getpid())
  22. logger = logging.getLogger("RELA_BAIKE")
  23. logger.propagate = False
  24. handler = TimedRotatingFileHandler(strPrefix, 'H', 1)
  25. handler.suffix = "%Y%m%d_%H%M%S.log"
  26. formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
  27. handler.setFormatter(formatter)
  28. logger.addHandler(handler)
  29. logger.setLevel(logging.INFO)
  30. return logger
  31. def makeResponseBody(retCode, errReason, dicSummary):
  32. dicRes = {}
  33. dicRes['retCode'] = retCode
  34. if retCode != 0:
  35. dicRes['error'] = errReason
  36. else:
  37. dicRes['data'] = dicSummary
  38. return json.dumps(dicRes)
  39. class RelaBaikeHandler(tornado.web.RequestHandler):
  40. executor = ThreadPoolExecutor(50)
  41. def initialize(self, relaServer, logger):
  42. self.__serverRelaBaike = relaServer
  43. self.__logger = logger
  44. @tornado.gen.coroutine
  45. def get(self):
  46. lstSummary = []
  47. retCode = 0
  48. errReason = ""
  49. try:
  50. utfQuery = self.get_argument('query').encode('utf8').strip()
  51. except:
  52. errorReason = 'Query encoding not utf-8.'
  53. strRes = makeResponseBody(-1, errorReason, lstSummary)
  54. self.write(strRes)
  55. return
  56. if utfQuery == "":
  57. strRes = makeResponseBody(0, '', lstSummary)
  58. self.write(strRes)
  59. return
  60. error, errReason, lstSummary = yield self.getRelaBaike(utfQuery)
  61. strRes = makeResponseBody(error, errReason, lstSummary)
  62. self.write(strRes)
  63. def __logResponse(self, utfQuery, relaResult):
  64. succ = relaResult.isSuccess()
  65. if succ:
  66. self.__logger.info("%s\tSucc\t%s" % (utfQuery, "|".join([str(item[0]) for item in relaResult])))
  67. else:
  68. self.__logger.info("%s\tError:%d" % (utfQuery, relaResult.getError()))
  69. @run_on_executor
  70. def getRelaBaike(self, utfQuery):
  71. error = 0
  72. lstSummary = []
  73. relaBaikeRequest = RelaBaikeRequest(content=utfQuery)
  74. relaBaikeResult = self.__serverRelaBaike.getRelaBaike(relaBaikeRequest)
  75. self.__logResponse(utfQuery, relaBaikeResult)
  76. if relaBaikeResult.isSuccess():
  77. for item in relaBaikeResult:
  78. baikeid = item[0]
  79. try:
  80. dicSummary = json.loads(item[1])
  81. except:
  82. return -2, 'summary format error' ,lstSummary
  83. lstSummary.append(dicSummary)
  84. else:
  85. return relaBaikeResult.getError(), rela_baike_server.g_dic_error.get(relaBaikeResult.getError(), 'other error') ,lstSumm
  86. ary
  87. return 0, 'success',lstSummary
  88. def start():
  89. port = int(sys.argv[1])
  90. serverRelaBaike = rela_baike_server.getRelaBaikeServer()
  91. logger = getLogger(g_log_prefix)
  92. app = tornado.web.Application(handlers=[(r"/rela_baike", RelaBaikeHandler, dict(relaServer=serverRelaBaike, logger=logger))])
  93. http_server = tornado.httpserver.HTTPServer(app)
  94. http_server.bind(port)
  95. http_server.start(2)
  96. tornado.ioloop.IOLoop.instance().start()
  97. if __name__ == "__main__":
  98. start()

代码所涉及的特性基本上不超过前面的测试例子,除了下两几点:

  1. 在*Handler类里增加了一个def initialize(self, relaServer, logger)函数。这是为了把一些初始化好的对象传到Handler类里。
  2. app = tornado.web.Application(handlers=[(r"/rela_baike", RelaBaikeHandler, dict(relaServer=serverRelaBaike, logger=logger))])。前面handler的initialize函数参数,对应于Application初始化时,每个handler对应的dict。

小结

至此,已经完成将服务通过HTTP服务封装的工作。有很多可以去进一步了解的内容:

  1. 进一步了解tornado的其他特性。
  2. python的修饰符如何使用。
  3. WSGI是什么。

Tornado实现多线程、多进程HTTP服务的更多相关文章

  1. python GIL全局解释器锁,多线程多进程效率比较,进程池,协程,TCP服务端实现协程

    GIL全局解释器锁 ''' python解释器: - Cpython C语言 - Jpython java ... 1.GIL: 全局解释器锁 - 翻译: 在同一个进程下开启的多线程,同一时刻只能有一 ...

  2. C++程序员面试题目总结(涉及C++基础、多线程多进程、网络编程、数据结构与算法)

     说明:C++程序员面试题目总结(涉及C++基础知识.多线程多进程.TCP/IP网络编程.Linux操作.数据结构与算法) 内容来自作者看过的帖子或者看过的文章,个人整理自互联网,如有侵权,请联系作者 ...

  3. Python有了asyncio和aiohttp在爬虫这类型IO任务中多线程/多进程还有存在的必要吗?

    最近正在学习Python中的异步编程,看了一些博客后做了一些小测验:对比asyncio+aiohttp的爬虫和asyncio+aiohttp+concurrent.futures(线程池/进程池)在效 ...

  4. WPF 同一窗口内的多线程/多进程 UI(使用 SetParent 嵌入另一个窗口)

    原文 WPF 同一窗口内的多线程/多进程 UI(使用 SetParent 嵌入另一个窗口) WPF 的 UI 逻辑只在同一个线程中,这是学习 WPF 开发中大家几乎都会学习到的经验.如果希望做不同线程 ...

  5. Python多线程多进程那些事儿看这篇就够了~~

    自己以前也写过多线程,发现都是零零碎碎,这篇写写详细点,填一下GIL和Python多线程多进程的坑~ 总结下GIL的坑和python多线程多进程分别应用场景(IO密集.计算密集)以及具体实现的代码模块 ...

  6. 利用多线程使socket服务端可以与多个客户端同时通讯

    利用多线程使socket服务端可以与多个客户端同时通讯 server import socket 1. 符合TCP协议的手机 server = socket.socket(socket.AF_INET ...

  7. python实现并发服务器实现方式(多线程/多进程/select/epoll)

    python实现并发服务器实现方式(多线程/多进程/select/epoll)   并发服务器开发 并发服务器开发,使得一个服务器可以近乎同一时刻为多个客户端提供服务.实现并发的方式有多种,下面以多进 ...

  8. Tornado实现多进程/多线程的HTTP服务

    用tornado web服务的基本流程 实现处理请求的Handler,该类继承自tornado.web.RequestHandler,实现用于处理请求的对应方法如:get.post等.返回内容用sel ...

  9. 爬虫之多线程 多进程 自定义异步IO框架

    什么是进程? 进程是程序运行的实例,是系统进行资源分配和调度的一个独立单位,它包括独立的地址空间,资源以及1个或多个线程. 什么是线程? 线程可以看成是轻量级的进程,是CPU调度和分派的基本单位. 进 ...

随机推荐

  1. 在HTML中为JavaScript传递变量

    在html中为JavaScript传递变量是一个关键步骤,然后就可以通过对JavaScript变量操作,实现想要达到的目的 本节代码主要使用了JavaScript中的document对象中的getEl ...

  2. 2-Twelfth Scrum Meeting20151212

    任务安排 成员 今日完成 明日任务 闫昊 获取视频播放的进度  获取视频播放进度 唐彬 解决handler可能引起的内存泄露问题  阅读IOS代码+阅读上届网络核心代码 史烨轩 下载service开发 ...

  3. 数学口袋精灵app(小学生四则运算app)开发需求

    数学口袋精灵APP,摒除了传统乏味无趣学习数学四则运算的模式,采用游戏的形式,让小朋友在游戏中学习,培养了小朋友对数学的兴趣,让小朋友在游戏中运算能力得到充分提升.快乐学习,成长没烦恼! 项目名字:“ ...

  4. Beta版本冲刺(一)

    目录 组员情况 组员1(组长):胡绪佩 组员3:庄卉 组员4:家灿 组员5:凯琳 组员6:翟丹丹 组员7:何家伟 组员8:政演 组员9:黄鸿杰 组员10:刘一好 组员11:何宇恒 展示组内最新成果 团 ...

  5. 团队伊始——DreamCatcher

    我们的团队,队名是DreamCatcher,中文意思是追梦人,它是一首歌曲,所属专辑是<新世纪的曙光>. 这是一首很好听的歌曲,里面有一句歌词是: I'm a dream catcher ...

  6. nodeJs上传附件

    两种方案: 这两种方案传参还是有区别额 在nodeJs中上传附件调用了 multer 的中间件,采用这个中间件来上传 首先是表单(前端部分): <!DOCTYPE html> <ht ...

  7. [Delphi]实现使用TIdHttp控件向https地址Post请求[转]

    开篇:公司之前一直使用http协议进行交互(比如登录等功能),但是经常被爆安全性不高,所以准备改用https协议.百度了一下资料,其实使用IdHttp控件实现https交互的帖子并不少,鉴于这次成功实 ...

  8. ci test

    下载ci 版本 3.1.9 下载地址 https://www.codeigniter.com/ 怎么查看CI的版本信息?想看某个项目中使用的CI具体是哪个版本,怎么查看?system\core\cod ...

  9. android 权限

    1.开发的apk如果想要使用其他组件提供的功能,需要获取对应的权限.android提供了一些权限,在Manifest.permission类中定义. 比较常用的权限:

  10. Delphi用户登录窗口框架

    经常看到一些新手在CSDN上问登录窗口如何写,也看到N多人form1.show/form1.create/…中做form2.show之类.实在看不下去了.这种写法实在不是很好,于是还是把自己理解的登录 ...