0x00 Why
爲何做這件事,在去年的一個項目中,算法同學要使用在線模型訓練,不得不使用python的tf框架,這樣我們不得不是python web框架,當時因爲團隊裏面沒人懂python相關的知識,只是簡單的用tornado搭建的一個。但是在後期使用過程中,壓測發現了tornado在低耗時的接口不夠穩定,波動比較大。我們的接口一般20ms,但是經常波動到40ms,併發其實也不大。所以經歷了痛苦過後,爲了滿足後續類似的場景,我們需要打造一款內部使用的python web框架,能夠支持低耗時接口的穩定性,以及稍微性能強點。
0x01 How
基於以上的出發點,開始調研python web相關性能比較高的框架,看到了sanic。說是性能比較高。先看下sanic作者開發sanic的動機,是因爲他看下了下面這篇文章:
uvloop: Blazing fast Python networking
是因爲python3.4推出了asyncio,解決令人詬病的異步io性能問題,3.5後推出了uvloop,基於libuv,libuv是一個使用C語言實現的高性能異步I/O庫,uvloop用來代替asyncio默認事件循環,可以進一步加快異步I/O操作的速度,而tornado在python3中還沒有使用uvloop。
首先來看下uvloop的性能表現:
0x02 tornado vs sanic
口說無憑,直接壓測看數據,我們同時構造兩個框架的一個接口,接口裏面沒有任務邏輯,就是簡單的返回一個helloworld,都是起一個進程,實測的數據
- sanic:18000/s
- tornado:5000/s
就框架本身而言,sanic確實性能高。然後我又對各個耗時階段的接口進行壓測,來判斷不同耗時,兩個框架的表現。
Sanic
Sleep(ms)\Thread | 1 | 10 | 50 | 100 |
---|---|---|---|---|
1 | 1-3之間波動,波動範圍爲0%~300%,週期性的 | 1-6之間波動,波動範圍爲0%~600%,週期性的 | 1-11之間波動,波動範圍爲0%~1100%,週期性的 | 1-7之間波動,波動範圍爲0%~700%,週期性的 |
10 | 11-15之間波動,波動範圍爲10%~50%,週期性的 | 12-17之間波動,波動範圍爲20%~70%,週期性的 | 11-20之間波動,波動範圍爲10%~100%,週期性的 | 11-31之間波動,波動範圍爲10%~310%,週期性的 |
50 | 51-56,週期性的波動 | 54-60,週期性 | 54-71,週期性的 | 51-61,週期的 |
100 | 101-106之間波動,波動範圍爲1%~6%,週期性的 | 103-110之間波動,波動範圍爲3%~10%,週期性的 | 103-120之間波動,波動範圍爲3%~20%,週期性的 | 105-130之間波動,波動範圍爲5%~30%,週期性的 |
Tornado
Sleep(ms)\Thread | 1 | 10 | 50 | 100 |
---|---|---|---|---|
1 | 2-4之間波動,波動範圍爲200%~400%,週期性的 | 6-11之間波動,波動範圍爲600%~1100%,週期性的 | 21-56之間波動,波動範圍爲2100%~5600%,週期性的 | 50-106之間波動,波動範圍爲5000%~10600%,週期性的 |
10 | 11-15之間波動,波動範圍爲110%~150%,週期性的 | 11-18之間波動,波動範圍爲110%~180%,週期性的 | 28-43之間波動,波動範圍爲280%~430%,週期性的 | 62-104之間波動,波動範圍爲620%~1040%,週期性的 |
50 | 52-57之間波動,週期性的 | 54-62,週期性的 | 51-80,週期性的 | 53-75,週期性的 |
100 | 101-108之間波動,波動範圍爲1%~8%,週期性的 | 101-111之間波動,波動範圍爲1%~11%,週期性的 | 101-111之間波動,波動範圍爲1%~11%,週期性的 | 101-140之間波動,波動範圍爲1%~40%,週期性的 |
result
sleep 1ms&100個線程壓的情況
Sanic波動爲1-7ms,而Tornado已經最低到50ms,最高飆到100多ms,顯然在10ms以內的接口,併發超過50的,不適合使用Tornado,推薦使用Sanic,Sanic贏一局,Sanic1:0Tornado
sleep 10ms &100個線程壓的情況
Sanic波動爲11-31ms,而Tornado已經最低到62了,最高到100多毫秒,顯然在100毫秒內,併發超過50的時候,還是Sanic更勝一籌,Sanic再贏一局,Sanic2:0Tornado
sleep 50ms&100個線程壓的情況
Sanic波動爲51-61,Tornado爲53-73,兩個框架之間差別不大,建議使用Sanic,Sanic3:0Tornado。
sleep 100ms & 100個線程壓的情況
Sanic波動爲105到130,Tornado波動爲101-140,選擇誰都可以。這次打個平手Sanic2:0Tornado
綜上所述,sanic性能確實優於tornado。所以新框架採用sanic來開發。
0x04 框架設計
- log庫:傳遞logid和自我生成logid,且能支撐多進程,不丟日誌的情況。
- redis庫:擁有連接池,
- mysql庫:擁有連接池。
- python版本庫管理要簡單,易發佈,易部署。
- 要有利用新框架新建項目,更新框架的工具,不用用戶每次自己下載然後copy,改名。
基於以上的設計,調研了一下用到的庫。
sanic
- python3.7+
- sanic最新版
redis
redis-py:官方推薦,配合hiredis-py會得到性能的極致提升。
mysql
pymysqlpool:mysql連接池,包裝pymysql提供連接池的功能。
還可以使用pymysql+dbutils自己編寫連接池的代碼。
config
配置文件支持ini格式的,保持跟go的一樣。
ConfigParser
官方使用py的配置文件,採用該方式更方便。
logging
- 日誌庫要有logid
- sanic自帶有log庫,使用的就是python自帶的基礎庫logging。
- 解決多進程丟日誌的問題使用concurrent-log-handler,不要使用ConcurrentLogHandler很久不更新了,bug多。
- concurrent-log-handler只支持日誌大小滾動,不支持時間滾動。
- 目前python最吃香的log庫是loguru,我們也集成了,該庫支持多線程,雖然不是進程安全的,但是也有方式支持多進程log。
- 需要對比下這兩個庫在sanic下性能,選擇一個最優的。
- logging(logging.handlers.TimedRotatingFileHandler多進程丟日誌)
- logging(concurrent-log-handler),帶了進程鎖以後嚴重影響了框架性能.
- loguru
- 對於高qps的場景下,可以讓其丟日誌,使用普通的TimedRotatingFileHandler。對於qps要求低的場景,建議使用concurrent-log-handler保證多進程下日誌不丟失。
pipenv
- python依賴包管理
- python項目遷移
click構建命令行工具
利用click開發。一條命令就能新建一個新的項目,直接可運行。
框架性能
目前框架開發已經完成,內部已經發布了第一個版本,對該框架的性能也進行了一個壓測。
sanic空邏輯性能測試(只返回一個json串)
- 1個線程壓1個進程qps:6000
- 1個線程壓2個進程qps:6600
- 2個線程壓1個進程qps:10000
- 4個線程壓1個進程qps:9600,單進程支持4個併發時出現瓶頸。
單進程處理能力比較強,建議優先單進程
- 2個線程壓2個進程qps:11000
- 4個線程壓4個進程qps:13000
- 8個線程壓8個進程qps:14000
- 16個線程壓8個進程qps:13000,瓶頸出現
sanic加入log測試(打印一條log到文件,多進程不丟日誌,啓用進程安全的log)
-
1個線程壓1個進程qps:1500
-
1個線程壓2個進程qps:1300
-
2個線程壓1個進程qps:1900
-
4個線程壓1個進程qps:2100,但是耗時已經上來了。
-
2個線程壓2個進程qps:2000
-
4個線程壓4個進程qps:2400,耗時已經增加許多,瓶頸
-
8個線程壓8個進程qps:2600,耗時增加。
-
16個線程壓8個進程qps:2800,耗時已經飆升了。
sanic加入log測試(打印一條log到文件,多進程丟日誌,不啓用進程安全的log)
- 1個線程壓1個進程qps:3200
- 1個線程壓2個進程qps:2200
-
2個線程壓1個進程qps:4800
-
4個線程壓1個進程qps:4800
-
2個線程壓2個進程qps:4000
-
4個線程壓4個進程qps:5200
-
8個線程壓8個進程qps:4400
sanic加入redis性能測試
- redis(set)操作,1個線程壓1個進程qps:3700
- redis(set)操作,1個線程壓2個進程qps:3200,反而降低了。
-
redis(set)操作,2個線程壓1個進程qps:4700
-
redis(set)操作,4個線程壓1個進程qps:4600,單進程出現瓶頸。
-
redis(set)操作,2個線程壓2個進程qps:5500
- redis(set)操作,4個線程壓4個進程qps:8100
- redis(set)操作,8個線程壓8個進程qps:8000,下降,瓶頸
- redis(set)操作,16個線程壓8個進程qps:8300,瓶頸
Redis穩定性較好,且增加線程或者進程呈線性增長,直到到達瓶頸。且嘗試增加服務能力的時候,最好是起和併發量一樣的進程數,不然來回切進程反而會降低qps。
壓測結果分析
建議採用單進程,開啓普通log,無包裝多進程安全的log。
最後我們測試了下4個線程壓一個進程,打印一條log(普通log),set一次redis的業務,qps:3200
在不要求日誌不丟的,耗時可以放寬的情況下,可以提升進程數來提升性能。
0x05
雖然sanic框架性能很好,但是python自身的性能,以及python庫的一些影響,仍然不能達到go的性能。但是應付一些中小型項目是可以了。後面持續優化,有機會開源出來。