摘要:通過以上三點解決方案,epoll技術的效率相比select、poll技術效率大大提升了,Tornado自然也採用了epoll技術,通過這種技術也就解決了著名的C10k問題,實現了用一個進程/線程來同時處理若干個連接的想法,減少了硬件資源的浪費。所以爲了減輕我們身體的負擔,是不是放學的時候只帶幾本今天需要做家庭作業的幾本書就很輕鬆了,同樣的爲了減少重複初始化過程中用戶空間和內核空間發生不必要的拷貝帶來的資源浪費,epoll技術提供了epoll_ctl函數,在用epoll_ctl函數進行事件註冊的時候,會將文件句柄都複製到內核中,所以不用每次都複製一遍,當有新的文件句柄時採用的也是增量往內核拷貝,確保了每個文件句柄只會被拷貝一次。

本文章主要目的是介紹tornado的工作原理及延伸的關鍵技術,爲了便於讀者理解,會通過幾個簡單易懂的例子,再配合原理圖進行講解。由於本文主要是分析tornado的工作原理,所以牽扯到一些操作系統的細節會簡單略過,希望讀者把握文章重點,不要迷失在理解各種操作系統名詞的“深淵”裏。當你真正瞭解了tornado的工作原理後,相應的應用場景也就能舉一反三了。


本文主要通過以下三點進行講解:

  • Tornado的背景及技術介紹

  • 簡單代碼後鮮爲人知的祕密

  • Tornado的應用場景

一、Tornado的背景及技術介紹

與傳統框架的區別


首先談一下傳統的一些web服務器框架吧,沒有對比就沒有“傷害”嘛,這裏主要和Django爲代表的傳統框架進行比較,這一類的Python web應用部署的時候一般是採用WSGI協議與服務器對接的,而這類服務器通常是基於多線程/多進程的,也就是說每有一個網絡請求,服務器都會有一個線程/進程進行處理。

這裏要重點介紹一下WSGI協議。

WSGI協議的由來:

例子:

中國有三家有名的通信運營商,分別是移動、聯通和電信,這三家通信商的手機號是可以跨平臺撥打的,假設三家通信商負責通信的協議不同且無法互通,用移動的手機號就無法給聯通電信的手機打電話,爲了方便通信就需要一個統一的規範。


WSGI協議的角色就是這個統一的規範,是描述web server如何與web application通信的規範,要實現WSGI協議,就必須同時實現web server和web application,目前常見的有Tornado、Flask和Django。

WSGI協議具體做了什麼:

1. 定義調用方式:讓Web服務器知道如何調用python應用程序,並把用戶的請求告訴應用程序。

2. 定義接收方式:讓python應用程序知道用戶的請求是什麼,以及如何返回結果給web服務器。

application對象形式:
#application是定義的應用端的調用方式
defsimple_app(environ, start_response):
# environ解釋了第一點,將客戶的需求告訴應用端
# start_response解釋了第二點,回調函數
pass

3. 定義以上兩點後,WSGI還去充當了服務器和應用程序間的中間件,即充當應用程序又充當服務器,可以形象的用下圖表示。

其中大致流程是這樣的:


1. Server收到客戶端的HTTP請求後,生成了environ_s,並且已經定義了start_response_s。

2. Server調用Middleware的application對象,傳遞的參數是environ_s和start_response_s。

3. Middleware會根據environ執行業務邏輯,生成environ_m,並且已經定義了start_response_m。

4. Middleware決定調用Application的application對象,傳遞參數是environ_m和start_response_m。Application的application對象處理完成後,會調用start_response_m並且返回結果給Middleware,存放在result_m中。

5. Middleware處理result_m,然後生成result_s,接着調用start_response_s,並返回結果result_s給Server端。Server端獲取到result_s後就可以發送結果給客戶端了。

WSGI是個同步模型,不支持非阻塞的請求方式,Tornado默認是不推薦使用WSGI的,如果在Tornado中使用WSGI,將無法使用Tornado的異步非阻塞的處理方式,相應的異步接口也就無法使用,性能方面也就大打折扣,這個也是Tornado性能如此優越的原因。

二、C10K問題的提出

考慮兩種高併發場景:


1. 用戶量大,高併發。如秒殺搶購,雙十一,618和春節搶票。

2. 大量的HTTP持久連接。


對於以上兩種熟悉的場景,通常基於多進程/線程的服務器是很難應付的。

基於上述高併發場景,引出了C10k問題,是由一名叫DanKegel的軟件工程師提出的,即當同時的連接數以萬計的時候,服務器性能會出現急劇下降甚至直接崩潰的情況,這就是著名的C10k問題。

值得一提的,騰訊QQ就遇到過C10k問題,當時採用了udp的方式避開了這個問題,當然過程是相當痛苦的,後來也就專用了tcp,主要是當時還沒有epoll技術。

簡要地分析下C10k的本質問題,其實無外乎是操作系的問題,連接多了,創建的進程線程就多了,數據拷貝也就變得頻繁(緩存I/O、內核將數據拷貝到用戶進程空間、阻塞),進程/線程上下文切換消耗又大,直接就導致操作系統奔潰。


可見,解決C10k問題的關鍵就是減少這些CPU等核心計算資源消耗,從而榨乾單臺機器的性能,突破C10k描述的瓶頸;那麼常規的解決思路有哪些呢,其實無外乎下面兩種方法:

解決一、對於每個連接處理分配一個獨立的進程/線程。提升單臺機器的能力,儘可能多提供進程/線程,一臺機器不夠就增加多臺機器。

解決二、用一個進程/線程來同時處理若干個連接。


針對方法一,假設每臺機器都達到了一萬連接,同時有一億個請求,那麼就需要一萬臺機器,所以這種解決方法不太實際。

針對方法二,需要有新的技術支持這種方案,實際上是可行的,也是現在普遍採取的方法,針對這種方法,其實有過多種技術支持,接下來重點介紹下。

三、epoll技術的引入

接下來我們根據技術的迭代發展來引入epoll技術。

實現方式一:傳統的循環遍歷的方式處理多個連接

這種方式明顯的缺點就是,當其中任何一個socket的文件數據不ready的時候,線程/進程會一直等待,進而導致後面要處理的連接都被阻塞,整個應用也就阻塞了。

實現方式二:select技術

首先解釋下select,它是個系統調用函數,格式如下:


int select(int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, const struct timeval *timeout);

nfds:select監視的文件句柄數

readfds:select監視的可讀文件句柄集合,就是個long類型的數組。

writefds: select監視的可寫文件句柄集合。

exceptfds:select監視的異常文件句柄集合。

timeout:本次select的超時結束時間。


還有幾個對fds集合操作的宏:


FD_ZERO(fd_set *fdset):清空fdset與所有文件句柄的聯繫。

FD_SET(int fd,fd_set *fdset):建立文件句柄fd與fdset的聯繫。

FD_CLR(int fd,fd_set *fdset):清除文件句柄fd與fdset的聯繫。FD_ISSET(int fd,fd_set *fdset):檢查fdset聯繫的文件句柄fd是否

可讀寫,當>0表示可讀寫。

實現過程:


1. 首先fdset集合裏需要監控的文件句柄由程序員來添加,當前連接需要監控哪些文件句柄,那麼通過FD_SET宏來進行添加。

2. 然後調用select函數將fd_set從用戶空間拷貝到內核空間。

3. 註冊一個回調函數。

4. 內核對文件句柄進行監控。

5. 當有滿足可讀寫等條件時/超時調用回調函數並將文件句柄集合拷貝回用戶 空間。

6. 應用通過輪詢的方式查找所有文件句柄,用FD_ISSET宏來判讀具體是哪個文件句柄可操作。

7. 當再次有新連接處理需要監控,再次重複以上步驟往內核拷貝fdset。

用圖表示整個流程如下:


缺點分析:

1. 句柄上限:單個進程通過輪詢的方式監控所有的文件句柄,當文件句柄越多,處理的效率越低,爲了保證效率,文件句柄也就設置了上限,這個上限和內存也是又一定關係的,32位機默認是1024。

2. 重複初始化:每次監控都重複將fdset從用戶空間拷貝到內核空間,然後又從內核空間拷貝到用戶空間,這個過程重複比較耗費系統資源。

3. 逐個排查文件效率不高: 檢測哪些文件句柄可操作時,採用的是輪詢遍歷所有的文件句柄,用FD_ISSET宏來判斷文件句柄是否可操作,然而實際情況,大部分文件句柄是不可操作的,這種逐個排查的方式效率太低。


實現方式三、poll技術

poll技術與select 技術本質上是沒有區別的,只是文件句柄的存儲結構變更了,變成了鏈表,所以沒有了文件句柄的上限,但是其他缺點依舊存在。

實現方式四、epoll技術

epoll技術整個流程其實和select、poll技術大體上是一樣的,主要是針對造成效率低下的點進行優化,可以說是將select和poll技術的缺點一一解決才達到現在的高效率,接下來我們一一道來:


1. 句柄上限


句柄上限的問題poll技術已經解決,就不用多說了。

2. 重複初始化


這個問題就像中學時候讀書書包帶課本一樣(中學的課程數量和書本數量之多大家應該都懂的),每天上學把所有幾十本課本從家裏背到學校,放學了再從學校將所有書揹回家,但你今天家庭作業實際需要帶的書可能就個別課程的個別幾本書而已。


所以爲了減輕我們身體的負擔,是不是放學的時候只帶幾本今天需要做家庭作業的幾本書就很輕鬆了,同樣的爲了減少重複初始化過程中用戶空間和內核空間發生不必要的拷貝帶來的資源浪費,epoll技術提供了epoll_ctl函數,在用epoll_ctl函數進行事件註冊的時候,會將文件句柄都複製到內核中,所以不用每次都複製一遍,當有新的文件句柄時採用的也是增量往內核拷貝,確保了每個文件句柄只會被拷貝一次。


3. 逐個排查文件效率不高


epoll會用epoll_ctl爲每個文件句柄註冊一個回調函數,同時會在內核中通過epoll_create創建一個專用鏈表(還有包含存儲fd的專用內存空間),當有文件句柄狀態發生變更,通過回調函數會將狀態發生變更的文件句柄加入該鏈表,epoll技術還提供了epoll_wait函數,來查看鏈表中有沒有就緒的文件句柄,然後只將該鏈表中的就緒文件句柄從內核空間拷貝到用戶空間,這樣一來就不用遍歷每個文件句柄,只處理狀態發生變更的,效率自然就提升上去了。

總結一下,epoll技術提供了三個系統調用函數:

- epoll_create:用於創建和初始化一些內部使用的數據結構。

- epoll_crl: 用於註冊時間、添加、刪除和修改指定的df及其期待的事件。

- epoll_wait: 用於等待先前指定的fd事件,即就緒的fd。


通過以上三點解決方案,epoll技術的效率相比select、poll技術效率大大提升了,Tornado自然也採用了epoll技術,通過這種技術也就解決了著名的C10k問題,實現了用一個進程/線程來同時處理若干個連接的想法,減少了硬件資源的浪費。

五、Tornado是如何發揮優勢的(背後不爲人知的處理邏輯)

我們先看一段簡單代碼的demo:

如果你經常用Tornado,那麼對這段代碼一定非常熟悉了,那麼我們今天的關注點就放在最後一句Torando.ioloop.IOLoop.current.start代碼上,先簡單的分析下這句代碼,前面一部分Torando.ioloop是Tornado的核心模塊ioloop模塊,IOLoop是ioloop模塊的一個類,current是IOLoop類的一個方法,結果是返回一個當前線程的IOLoop的實例,start也是IOLoop的方法,調用後開啓循環。

先看一張流程圖:

然後我們分析下Tornado這段代碼後的整個邏輯流程:


1. 首先Tornado需要建立監聽,會創建一個socket用於監聽,如果有客戶端A請求建立連接之後,Tornado會基於原先的socket新創建一個包含客戶端A連接的有關信息的socket(分配新的監聽端口),用於監聽和客戶端A的請求。此時對Tornado來說就有兩個socket需要進行監控,原先的socket繼續用來監聽建立新連接,新的socket用於和客戶端A進行通信,假如沒有epoll技術的話,Tornado需要自己去循環詢問哪個socket有新的請求。

2. 有了epoll技術,Tornado只需要把所有的socket丟給epoll,epoll作爲管家幫忙監控,然後Torando.ioloop.IOLoop.current.start開啓循環,不斷的去詢問epoll是否有請求需要處理,這就是ioloop所做的工作,也是Tornado的核心部分。

3. 當有客戶端進行請求,epoll就發現有socket可處理,當ioloop再次詢問epoll時,epoll就把需要處理的socket交由Tornado處理

4. Tornado對請求進行處理,取出報文,從報文中獲取請求路徑,然後從tornado.web.Applcation裏配置的路由映射中把請求路徑映射成對應的處理類,如上圖IndexHandler就是處理類。

5. 處理類處理完成後,生成響應,將響應內容封裝成http報文,通過請求時建立的連接(尚未中斷)將響應內容返回給客戶端。

6. 當有多個請求同時發生,Tornado會按順序挨個處理。

看了上面的流程,假如Tornado在處理一個非常耗時的請求時,後面的請求是不是就會被卡死呢?答案是肯定的,所以提到了Tornado的另一個特性—異步處理,當一個請求特別耗時,Tornado就把它丟在那處理,然後繼續處理下一個請求,確保後面的請求不會被卡死。

Tornado異步:原生Tornado框架提供異步網絡庫IOLoop和IOStream以及異步協程庫tornado.gen(必須使用Tornado的web框架和HTTP服務器,否則異步接口可能無法使用),方便用戶通過更直接的方法實現異步編程,而不是回調的方式,官方推薦yield協程方式完成異步。(異步是Tornado重要且核心部分,期待下篇技術好文重點介紹)。

通過上面所講,基本上已經對Tornado的整個處理流程瞭解了,總結一下Tornado之所以能同時處理大量連接的原因:

1. 利用高效的epoll技術處理請求,單線程/單進程同時處理大量連接。

2. 沒用使用傳統的wsgi協議,而是利用Tornado自己的web框架和http服務形成了一整套WSGI方案進行處理。

3. 異步處理方式,Tornado提供了異步接口可供調用。

六、Tornado的應用場景

要性能,Tornado 首選;要開發速度,Django 和Flask 都行,區別是Flask 把許多功能交給第三方庫去完成了,因此Flask 更爲靈活。Django適合初學者或者小團隊的快速開發,適合做管理類、博客類網站、或者功能十分複雜需求十分多的網站,Tornado適合高度定製,適合訪問量大,異步情況多的網站。

以上便是本文是本人學習收集整理後的文章,如有錯誤,請多多留言指教。

ABOUT

關於作者

吳俊傑:達觀數據後端開發工程師,負責達觀數據產品後端開發、產品落地、客戶定製化產品需求等設計。對後端開發使用到的web及服務器框架、http協議及相關應用方面有比較深入的瞭解。


相關文章