知乎是怎麼運行 tornado web 服務的
You build it, you run it. - Werner Vogels
現在很多互聯網公司採用了微服務架構,將業務拆分,保持代碼倉庫盡量精簡。同時一個小團隊負責開發和維護一個服務,提升了開發和部署效率(軟體本身的組織結構與軟體團隊的組織結構式一致的,即康威定律)。 知乎很多業務後端採用了 python web 框架 tornado,都是開源的、成熟穩定的技術(雖然筆者更喜歡 flask 全家桶)
用 gunicorn + gevent 跑 tornado app
先來寫個無聊的 tornado handler,統計多個網站頁面的 html 長度之和。原始代碼如下,為了簡單起見省去所有異常處理 (tornado_app.py):
import gevent.pywsgiimport requestsimport tornado.wsgifrom tornado.web import Application, RequestHandlerdef get_html_length(url): return len(requests.get(url).text)URLS = ["https://www.baidu.com?page={}".format(i) for i in range(100)]class CurlHandler(RequestHandler): def get(self): length = 0 for url in URLS: length += get_html_length(url) self.write(str(length))def get_tornado_application(): application = Application( [ (r"/", CurlHandler), ], debug=True ) return applicationdef get_wsgi_application(): application = get_tornado_application() return tornado.wsgi.WSGIAdapter(application)app = get_wsgi_application()def run(): server = gevent.pywsgi.WSGIServer(("", 8000), app) server.serve_forever()if __name__ == "__main__": run()
如果安裝了 gevent,可以直接 python tornado_app.py
運行此 app。不過我們一般使用 gunicorn 指定 worker 為 gevent 來運行 tornado app(gunicorn 會 patch_all)
在命令行中用如下命令啟動(實際上就是用的 gevent wsgi):
gunicorn tornado_app:app -b 0.0.0.0:8000 -w 2 -k gevent
一般一個請求的流程如下:http request -> Nginx -> HAProxy -> gunicorn(gevent wsgi) -> tornado app
由於是容器部署,會根據容器的 cpu 和內存資源適當調整 worker 的值,具體需要根據真實的部署環境實測一把。在 tornado 中使用 gevent 並發
mysql 連接上使用了 PyMySQL,用 sqlalchemy core 請求數據
Pure python driver support gevent"s monkey patch, so they support cooperative multitasking using coroutines. That means the main thread won"t be block by MySQL calls when you use PyMySQL
經常在一個 api 介面里需要請求多個數據(多個資料庫請求、rpc 調用、外部網路請求等),比如用戶、文章等,這個時候如果不是並發請求數據,速度將是不可接受的。我們用 gevent 來實現並發請求,具體大家可以參考 gevent 的文檔。(批量、緩存、非同步、並發是幾個非常顯著的提升性能的手段)一個簡單的並發請求的例子如下,實際上很簡單,我們使用 gevent.pool (池(pool)是一個為處理數量變化並且需要限制並發的greenlet而設計的結構。):
class PoolCurlHandler(RequestHandler): def get(self): length = 0 pool = gevent.pool.Pool(20) res = pool.map(get_html_length, URLS) length = sum(res) self.write(str(length))
完整的代碼如下:
import gevent.pywsgiimport requestsimport tornado.wsgifrom tornado.web import Application, RequestHandlerdef get_html_length(url): return len(requests.get(url).text)URLS = ["https://www.baidu.com?page={}".format(i) for i in range(100)]class CurlHandler(RequestHandler): def get(self): length = 0 for url in URLS: length += get_html_length(url) self.write(str(length))class PoolCurlHandler(RequestHandler): def get(self): length = 0 pool = gevent.pool.Pool(20) res = pool.map(get_html_length, URLS) length = sum(res) self.write(str(length))def get_tornado_application(): application = Application( [ (r"/", CurlHandler), (r"/pool", PoolCurlHandler), ], debug=True ) return applicationdef get_wsgi_application(): application = get_tornado_application() return tornado.wsgi.WSGIAdapter(application)app = get_wsgi_application()def run(): server = gevent.pywsgi.WSGIServer(("", 8000), app) server.serve_forever()if __name__ == "__main__": run()
啟動 app 後用我們請求看下差距:
time curl http://localhost:8000/time curl http://localhost:8000/pool
至少能看到數倍的時間差距。
參考:
What is gevent? - gevent 1.3.0.dev0 documentation
Tin/sqlalchemy-gevent-mysql-drivers-comparison
Asynchronous Python and Databases - sqlalchemy 作者的文章
推薦閱讀:
※圖像識別——傳統的驗證碼識別
※有哪些關於 Python 的技術博客?
※有沒有相對比較成熟的python寫的類似jekyll的靜態頁面生成器,可以利用github pages搭建博客的?
※Python 初學者想通過 Django 框架寫一個博客,一個月內完成任務,大致的學習路線怎麼安排?
※Python爬蟲之微打賞爬蟲