python的高性能web應用的開發與測試實驗


python的高性能web應用的開發與測試實驗

tornado“同步和異步”網絡IO模型實驗

 

引言

python語言一直以開發效率高著稱,被廣泛地應用於自動化領域:

  • 測試自動化
  • 運維自動化
  • 構建發布自動化

但是因為其也具有如下兩個特征:

  1. 解釋型語言
  2. GIL全局解釋器鎖

前者導致其性能天然就被編譯型語言在性能上落后了許多。而后者則在多核並行計算時代,極大的限制了python的應用場景。

但是通過合理的web框架,則可以使用python揚長避短,仍然能夠在多核並行時代須保持其高效開發的生產力同時,在性能上也有出色表現。例如,tornado框架。

tornado框架主要做了如下幾件事:

  • 使用單線程的方式,避免線程切換的性能開銷,同時避免在使用一些函數接口時出現線程不安全的情況
  • 支持異步非阻塞網絡IO模型,避免主進程阻塞等待

前人實驗

基於python語言的web框架眾多,但是主流的有“Django”和“Tornado”基本上可以代表了它們的實現理念。

因為本文的重點是對 同步 和 異步 進行對比。所以關於不同web框架的性能對比實驗,就引用一位網友的帖子的實驗結果吧。

參考文章 [1]輕量級web server Tornado代碼分析

此文章有些部分寫得比較簡略,但是我們先大膽的做一下假設,作者是使用不同的python的web框架對最基本的 HelloWorld 代碼進行了實現。

參考的Tornado實現如下:

import tornado.ioloop
import tornado.web

class MainHandler(tornado.web.RequestHandler):
    def get(self):
        self.write("Hello, world")

application = tornado.web.Application([
    (r"/", MainHandler),
])

if __name__ == "__main__":
    application.listen(8888)
    tornado.ioloop.IOLoop.instance().start()

最后使用 Apache Benchmark (ab),在另外一台機器上使用了如下指令進行負載測試:

ab -n 100000 -c 25 http://10.0.1.x/

在 AMD Opteron 2.4GHz 的四核機器上,結果如下圖所示:

相較於第二快的服務器,Tornado在數據上的表現也是它的4倍之多。即使只用了一個CPU核的裸跑模式,Tornado也有33%的優勢。

根據引文作者的觀點:tornado是完虐其它的web框架的。

本文點評:此實驗只是暫時讓大伙建立一下宏觀的對不同的web框架的性能的認識,至於可信度是存疑的,因為實驗報告寫得不太規范,細節省略太多。本文的觀點是,如果都是采用同步的的寫法,tornado和django的性能差異應該沒有那么大的。當然這不太重要了,后面提到的 同步 和 異步 才是比較重要的。

下面則是本文的重點,同步和異步網絡IO的性能測試和差異對比。

[1] 輕量級web server Tornado代碼分析(http://blog.csdn.net/goldlevi/article/details/7047726)

測試環境

環境

  • CPU:core i3
  • 操作系統:Ubuntu 14.0
  • Python框架:py2.7
  • Web服務器:Tornado 4.2.0,服務器只啟用一核心

內容

使用同步和異步的方式來寫一段延時代碼,然后再使用 apachebench進行壓力測試:

  • 並發量 40
  • 總請求量 200

由於本文只是做性能對比,而不是性能的上限對比,所以都使用的是比較少的壓力。

同步和異步代碼

class SyncSleepHandler(RequestHandler):
    """
    同步的方式,一個延時1s的接口
    """
    def get(self):
        time.sleep(1)
        self.write("when i sleep 5s")


class SleepHandler(RequestHandler):
    """
    異步的延時1秒的接口
    """
    @tornado.gen.coroutine
    def get(self):
        yield tornado.gen.Task(
            tornado.ioloop.IOLoop.instance().add_timeout,
            time.time() + 1
        )
        self.write("when i sleep 5s")

同步測試結果

➜  /  ab -n 200 -c 40 http://localhost:8009/demo/syncsleep-handler/
This is ApacheBench, Version 2.3 <$Revision: 1528965 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/

Benchmarking localhost (be patient)
Completed 100 requests
Completed 200 requests
Finished 200 requests


Server Software:        TornadoServer/4.2.1
Server Hostname:        localhost
Server Port:            8009

Document Path:          /demo/syncsleep-handler/
Document Length:        15 bytes

Concurrency Level:      40
Time taken for tests:   200.746 seconds
Complete requests:      200
Failed requests:        0
Total transferred:      42000 bytes
HTML transferred:       3000 bytes
Requests per second:    1.00 [#/sec] (mean)
Time per request:       40149.159 [ms] (mean)
Time per request:       1003.729 [ms] (mean, across all concurrent requests)
Transfer rate:          0.20 [Kbytes/sec] received

Connection Times (ms)
            min  mean[+/-sd] median   max
Connect:        0    0   0.2      0       1
Processing:  1005 36235 18692.2  38133  200745
Waiting:     1005 36234 18692.2  38133  200745
Total:       1006 36235 18692.2  38133  200746

Percentage of the requests served within a certain time (ms)
50%  38133
66%  38137
75%  38142
80%  38161
90%  38171
95%  38176
98%  38179
99%  199742
100%  200746 (longest request)

異步測試結果

➜  /  ab -n 200 -c 40 http://localhost:8009/demo/sleep-handler/
This is ApacheBench, Version 2.3 <$Revision: 1528965 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/

Benchmarking localhost (be patient)
Completed 100 requests
Completed 200 requests
Finished 200 requests


Server Software:        TornadoServer/4.2.1
Server Hostname:        localhost
Server Port:            8009

Document Path:          /demo/sleep-handler/
Document Length:        15 bytes

Concurrency Level:      40
Time taken for tests:   5.083 seconds
Complete requests:      200
Failed requests:        0
Total transferred:      42000 bytes
HTML transferred:       3000 bytes
Requests per second:    39.35 [#/sec] (mean)
Time per request:       1016.611 [ms] (mean)
Time per request:       25.415 [ms] (mean, across all concurrent requests)
Transfer rate:          8.07 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    0   0.4      0       2
Processing:  1001 1010  12.0   1005    1053
Waiting:     1001 1010  12.0   1005    1053
Total:       1001 1010  12.3   1005    1055

Percentage of the requests served within a certain time (ms)
  50%   1005
  66%   1009
  75%   1011
  80%   1015
  90%   1032
  95%   1044
  98%   1045
  99%   1054
 100%   1055 (longest request)

結果對比

在並發量為40,總請求量為200的簡單的壓力測試里面,兩種網絡IO模型的編程方式的性能對比如下:

同步和異步性能對比
性能指標 同步阻塞式 異步非阻塞式
每秒處理請求數(Requests per second) 1 39
請求平均等待時間-ms(Time per request,mean) 40149 1017
請求平均處理時間-ms(Time per request,across all ) 1003 25

測試的結果比較符合被測試程序的理論預期,因為被測試程序就功能就是:一個1s的延時等待。

顯然:異步非阻塞式 和性能是遠高於 同步阻塞式 的。

在上表中的 同步IO模型 數據里:只要是進入了單個請求的處理環節,進入到睡眠等待的 內核態 操作時,就會將整個進程給 阻塞,別的程序就只能進入 等待 狀態了,這樣本質上還是使用的 串行 的處理方式,所以 請求平均處理時間 大概是1000ms(1秒)左右,然后完成一個並發度為40的請求平均等待時間為40149ms。

關於上面參數的理解可以進行簡單的類比解釋。

以如下場景為例子:客戶去銀行處理業務的窗口辦理業務。

  • 並行度:銀行開設的服務窗口數和前台服務員

    對應CPU,窗口數對應着核心數,即真正的實現並行的能力,即不是在時間分片后交錯進行的 “假象並行”

  • 並發度:大廳里面所有服務窗口等待服務的人數

    對應着單次的並發度,即本次作業需要處理的任務量

  • 總請求量:從銀行大廳外面陸續過來加入到大廳隊伍的客戶的累計人數

  • 內核態操作:銀行業務中必須只能由前台服務員處理的操作

  • 用戶態操作:客戶自己要處理的工作,比如:准備好自己的身份證,到外面復印證件,打電話和公司同事確認信息等等。

那么關於 同步 和 異步 的概念類比如下:

  • 同步阻塞系統:銀行 沒有 排隊叫號系統 ,客戶(Web服務器進程) 只能 在隊伍人群里面傻等輪到自己,沒有在排隊時間干其它事的機會。隨着外面的人不斷地進入大廳,新請求的每個人都要等前面的隊伍的全部處理完畢后( 40149ms)才能等到業務員(CPU)花1003ms 來處理自己的業務
  • 異步非阻塞系統:銀行  排隊叫號系統 ,客戶有可以 不用 在擁擠的人群中傻等,旁邊的休息區打開處理其它事情。客戶直接領取叫號單據,花掉 5ms 遞交准備材料(發起內核態操作請求) 要么收發郵件,要么看下小電影,然后等叫號系統叫自己后,立刻上去 20ms的時間解決掉問題。客戶實際浪費在這上面的時間為 25ms ,當然銀行業務員(CPU)還是要花 1000ms 去處理這個任務的

在這個假設的場景里面,不管是同步還是異步,業務員(CPU)都是 滿負荷 的工作,但是卻極大的節省了客戶(web服務器進程) 的時間。這樣客戶自身可以把等待業務員響應的時間都利用起來做一些其它工作,這樣就極大地提高了整體的工作效率。

眾所周知,python有GIL,所以多線程其實是偽多線程。tornado於是就單進程只用單線程,不做線程切換,但是又要實現並行的方式,就全部使用異步了。只要是某個請求進入了內核態的耗時的IO操作,tornado的主進程在發起內核IO初始化之后就做不管它了,立刻回到web的監控中來去響應別的請求。等內核態的IO完成之后,再回調到用戶態的主進程處理結果。如果是用同步模型,如果是使用單進程多線程,則會造成線程切換的開銷,如果使用單進程單線程(像django一樣),如果有一個請求比較耗時,第二個人的請求只會排隊等候的,Web服務進程絕大多數情況都是被阻塞狀態,性能就極大地降低了。

最后結合前面的延時1s的例子,再加一個即時響應的接口示例:

class JustNowHandler(tornado.web.RequestHandler):
    def get(self):
        self.write("i hope just now see you")

有興趣的同學可以自己做實驗。 事先約定:

  • 同步延時1s的接口為:A
  • 異步延時1s的接口為:B
  • 即時響應的接口為:C

使用單核模式運行web服務器。

然后在瀏覽器中以不同的順序組合運行程序請求接口:

  • 先即時再延時
    • 先C再A:總共是1s后響應完畢C和A,C立刻響應
    • 先C再B:總共是1s后響應完畢C和B,C立刻響應
  • 先延時再即時
    • 先A再C:總共是1s后響應完畢C和A,C必須等A處理完畢后,才能在1s后響應
    • 先B再C:總共是1s后響應完畢C和B,C能立刻響應

同步模型中,一旦進程被阻塞掉,那么程序的效率就被等待的時間給嚴重降低了。

總結

有興趣的同學,可以更深入的研究一下 《Unix網絡編程-卷1,套接字聯網API》(W.Richard Stevens) 的第6章第2節 I/O模型

在python的web框架里面,tornado就是采用的最高效的異步非阻塞框架,可以在python語言下提供高性能的web應用服務。


作者: Harmo哈莫
作者介紹: https://zhengwh.github.io
技術博客: http://www.cnblogs.com/beer
Email: dreamzsm@gmail.com
QQ: 1295351490
時間: 2015-10
版權聲明: 歡迎以學習交流為目的讀者隨意轉載,但是請 【注明出處】
支持本文: 如果文章對您有啟發,可以點擊博客右下角的按鈕進行 【推薦】


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM