在實作檔案上傳並加解密的服務時,遇到了 user request 數量增多,造成 lead time 大幅提升的問題。服務本身是使用 Python + FastAPI framework 實作,在排除了網路頻寬問題和 server 效能問題後,懷疑是 Python 或 framework 導致延遲時間拉長,所以就決定從此地方著手進行 benchmark 實驗,來觀察瓶頸是發生在何處。
由於最終目的還是希望能找出改善的方式,而既然認為問題是出在 Python 和 framework 上,這次實驗就會先使用 FastAPI 與 aiohttp 寫的 HTTP API service 進行效能比較;此外也使用 Golang 實作的版本來測量不同語言之間的效能落差會到多少。
While implementing a file upload service with encryption and decryption, we encountered an issue where an increase in user requests significantly increased the lead time. The service is built using Python and the FastAPI framework. After ruling out network bandwidth and server performance issues, we suspect that the delay may be due to Python or the framework. Therefore, we decided to start with a benchmarking experiment in this area to identify where the bottleneck occurs.
Since our ultimate goal is to find a way to improve performance, and we believe the issue lies with Python and the framework, this experiment will initially compare the performance of an HTTP API service written using FastAPI and aiohttp. Additionally, we will use a version implemented in Golang to measure the performance differences between languages.
Experiment Environment
Server
4 core & 8G memory
Libraries
Python 3.10.12
FastAPI 0.109.0
aiohttp 3.9.1
Golang 1.21
Testing Tool
https://github.com/tsenart/vegeta
Experiments
Testing scenarios
- 100MB file 的檔案上傳
- Sync API
- RPS: 10/sec. Duration: 10 sec. Total requests: 100
Python FastAPI vs. aiohttp
Python 是使用 Cpython runtime ,其預設是 single thread 的 runtime 架構。其中使用 FastAPI 和 aiohttp 測量的結果如下:
FastAPI
1Requests [total, rate, throughput] 100, 10.10, 1.99
2Duration [total, attack, wait] 50.208s, 9.9s, 40.308s
3Latencies [min, mean, 50, 90, 95, 99, max] 12.928s, 41.037s, 42.505s, 43.851s, 43.96s, 44.04s, 44.049s
4Bytes Out [total, mean] 10341963800, 103419638.00
5Success [ratio] 100.00%
aiohttp
1Requests [total, rate, throughput] 100, 10.10, 3.80
2Duration [total, attack, wait] 26.297s, 9.9s, 16.398s
3Latencies [min, mean, 50, 90, 95, 99, max] 1.006s, 15.938s, 17.547s, 18.589s, 18.633s, 18.958s, 19.144s
4Bytes Out [total, mean] 10341963800, 103419638.00
5Success [ratio] 100.00%
從量測數據中可以看到, 以 P50 為例,aiohttp 的效能比起 FastAPI 快了約 60%,可知 FastAPI 的實作在處理 form 類型大檔案上傳的效率並不佳。拉近一點看每個 request 的 lead time 趨勢圖,FastAPI 從最初的 12 sec. 快速提升至約 40 sec. 由於 request 數量增加造成延遲時間累積,因此後續的 request 都會受到前面處理 request 的時間影響,這數據也顯示 corouitne 的數量和 CPU 使用比率偏高導致此現象。類似的現象也反應在 aiohttp 實作上,不過可以看到趨勢線較緩,能夠說明 aiohttp 較能有效地處理 parallel file uploading 的 concurrency 議題。
透過 Python profiling tool (pyinstrument) 來查看具體的延遲發生點,可以看到 FastAPI 實際上會呼叫 starlette 的 MultiPartParser 來解析出 Form Data 的欄位內容,而當 Parser 在發現此欄位的內容是 file 型式時,會再呼叫 UploadFile 來讀取資料。其中大多數的 CPU 時間都花費在 await,因此當出現過多的 task (coroutine),就會造成明顯的延遲。
我們從使用不同數量的 core 中來進一步觀察。
從數據中發現,當我們增加 1 個 core 使用時,則在這樣的使用情境之下, FastAPI 處理 request 效率提升 50%,這也反應 request 使用 CPU 的比率高,如果是 I/O bound 的話,效果提升不會這麼明顯。
Summary
根據上述的觀察,重新整理一下論點:在 file upload with FormData 的 use case 下,FastAPI 傾向 CPU bound 實作而 aiohttp 則是傾向 I/O bound 實作,而數據顯示 aiohttp 使用的 asyncio 實作方式的較佳。
Python vs. Golang
考量到 Python 本身的效能限制,如果 Python 無法達到我們對於此服務的目標,那使用其他語言實作也是一個方向,因此就來比較一下效率差異。以下為實驗結果,其中 server 皆使用 2 core。
Python FastAPI
1Requests [total, rate, throughput] 100, 10.10, 3.60
2Duration [total, attack, wait] 27.749s, 9.901s, 17.848s
3Latencies [min, mean, 50, 90, 95, 99, max] 1.535s, 16.763s, 18.373s, 20s, 20.156s, 20.578s, 20.628s
4Bytes Out [total, mean] 10341963800, 103419638.00
5Success [ratio] 100.00%
Golang
1Requests [total, rate, throughput] 100, 10.10, 8.12
2Duration [total, attack, wait] 12.308s, 9.901s, 2.408s
3Latencies [min, mean, 50, 90, 95, 99, max] 702.25ms, 2.103s, 2.037s, 3.402s, 3.504s, 3.971s, 4.147s
4Bytes Out [total, mean] 10341963800, 103419638.00
5Success [ratio] 100.00%
毫無懸念的,Golang 的效率比起原有的 Python FastAPI 版本提高了超過 80%,除了 Golang compiled language 的優勢之外,goroutine 的設計在處理大檔案上傳的 request 上也有不錯的表現。
結語
Python FastAPI 是個很知名也很優秀的 web framework,不過在處理 file uploading with formdata 的效率並不佳。內文中比較了 Python FastAPI 與 aiohttp 的效能差異,其中 aiohttp 的實作方式顯示出較好的效能表現,提升了將近 60% 速度。另外也比較了 Golang 版本的實作,其效能有超過 80% 的改善。