背景
故事的開始還是源於現階段各個平臺之間有些許的接口調用,但是各個平臺之間又相對比較獨立,並沒有將各個平臺整合成一組微服務的需求。現在面臨的問題就是各個平臺直接都有互相調用的需求,如果各個平臺有業務升級的情況下,不一定能察覺到是否會影響到對提供給其他系統使用的API,而且還有一個問題就是大家好像都不是太清楚哪些平臺調用了哪些接口,某個平臺對外提供了哪些接口,而且在一些腳本使用的過程中如果部署的系統遷移而需要重新修改訪問的地址,基於這些原因考慮搭建一個openapi相關的服務,主要是來管理與監控各個調用鏈,本次就先測試對比一下openapi的技術模型的選擇。
技術對比
由於本人的技術棧相對比較侷限,針對一些管理性能要求不高的項目用Python,golang通常都是用於不緊急的項目開發,會一丟丟的rust(入門水平)。本次的對比測試主要選用golang,Python。
本次測試的環境是一臺八核,6G內存的虛擬機(虛擬機不要太較真性能)。
服務器端性能測試對比
Flask測試
測試代碼如下;
from flask import Flask, jsonify
import requests
app = Flask(__name__)
@app.route('/api/test_api/')
def hello_world():
return jsonify({"detail": "ok"})
if __name__ == '__main__':
app.run()
雖然代碼簡單了點,但是我們用gunicorn來部署測試;
gunicorn -w 8 -b 0.0.0.0:5001 flask_server:app -k gevent
壓測結果
wrk -t4 -c1000 -d60s -T60s --latency http://127.0.0.1:5001/api/test_api/
Running 1m test @ http://127.0.0.1:5001/api/test_api/
4 threads and 1000 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 225.39ms 89.72ms 864.75ms 71.70%
Req/Sec 1.11k 138.23 2.50k 70.48%
Latency Distribution
50% 199.30ms
75% 269.94ms
90% 358.11ms
99% 453.80ms
265775 requests in 1.00m, 43.85MB read
Requests/sec: 4423.37
Transfer/sec: 747.31KB
可以看出在開啓多進程服務的情況下,qps達到了4423。並且性能響應基本上在毫秒級別。
Tornado測試
tornado測試代碼如下;
from tornado.ioloop import IOLoop
import tornado.web
import tornado.httpserver
import aiohttp
from tornado.platform.asyncio import AsyncIOMainLoop
import asyncio
class MainHandler(tornado.web.RequestHandler):
async def get(self):
self.write({"detail": "ok"})
if __name__ == "__main__":
app = tornado.web.Application([
(r"/api/test_api/", MainHandler),
])
server = tornado.httpserver.HTTPServer(app)
server.bind(5002, '127.0.0.1')
server.start(8)
AsyncIOMainLoop().install()
IOLoop.current().start()
啓動腳本並壓測
wrk -t4 -c1000 -d60s -T60s --latency http://127.0.0.1:5002/api/test_api/
Running 1m test @ http://127.0.0.1:5002/api/test_api/
4 threads and 1000 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 207.38ms 57.48ms 468.47ms 67.41%
Req/Sec 1.20k 298.66 2.13k 68.44%
Latency Distribution
50% 205.08ms
75% 239.01ms
90% 280.76ms
99% 367.11ms
286385 requests in 1.00m, 59.54MB read
Requests/sec: 4764.90
Transfer/sec: 0.99MB
從數據上來看比flask部署之後達到的qps相比還是要高大約三百左右的qps,在本運行的實例代碼中,開啓了八個工作進程來進行處理任務,並且在flask的部署過程中也選用了異步io,在work進程數相同的情況下,qps相差不多也算是情理之中。
golang測試
在golang的測試就不選用現成的web框架了,直接通過http庫來測試。
package main
import (
"fmt"
"encoding/json"
"log"
"net/http"
)
func handler(w http.ResponseWriter, r *http.Request) {
var res map[string]string
res = make(map[string]string)
res["detail"] = "ok"
js, err := json.Marshal(res)
if err != nil {
fmt.Println("erro ", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-type", "application/json")
w.Write(js)
}
func main() {
http.HandleFunc("/api/test_api/", handler)
log.Fatal(http.ListenAndServe(":5003", nil))
}
運行並壓測;
/wrk -t4 -c1000 -d60s -T60s --latency http://127.0.0.1:5003/api/test_api/
Running 1m test @ http://127.0.0.1:5003/api/test_api/
4 threads and 1000 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 20.91ms 19.55ms 316.02ms 83.64%
Req/Sec 13.48k 2.66k 23.70k 69.61%
Latency Distribution
50% 14.44ms
75% 26.04ms
90% 46.21ms
99% 94.57ms
3221474 requests in 1.00m, 377.89MB read
Requests/sec: 53597.81
Transfer/sec: 6.29MB
這樣對比來看qps達到了53597,性能相對比較強悍。不可否認在服務端方面go的性能還是比較厲害的。通過三種服務端的測試來看,在Python中按照常見的部署模式來部署flask和tornado進行壓測,測試數據基本上在四千多左右,在golang中僅用庫來實現服務qps就達到五萬了。
應用層API網關測試
使用的openapi說到底目前也是利用應用層來轉發的,此時我們依然來通過不同的方法來測試。
在本次的測試用,默認測試的API就是剛剛測試的golang作爲後端服務,只不過部署的端口更改爲8084.
Flask測試
測試代碼如下
from flask import Flask, jsonify
import requests
app = Flask(__name__)
@app.route('/api/test_api/')
def hello_world():
resp = requests.get("http://192.168.10.205:8084/api/test_api/")
return jsonify(resp.json())
if __name__ == '__main__':
app.run()
作爲網關服務主要向後端轉發請求並獲取返回請求並將結果返回。此時的部署方式仍然如下,其中192.168.10.205:8084就是剛剛啓動的golang的後端;
gunicorn -w 8 -b 0.0.0.0:5001 flask_gateway:app -k gevent
進行壓測
wrk -t4 -c1000 -d60s -T60s --latency http://127.0.0.1:5001/api/test_api/
Running 1m test @ http://127.0.0.1:5001/api/test_api/
4 threads and 1000 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 1.45s 534.19ms 4.31s 86.27%
Req/Sec 174.11 49.82 356.00 68.96%
Latency Distribution
50% 1.29s
75% 1.48s
90% 2.44s
99% 3.24s
41188 requests in 1.00m, 6.80MB read
Requests/sec: 685.89
Transfer/sec: 115.88KB
通過壓測結果可知,對應的sqs爲675左右。
Tornado測試
tornado測試的時候選用異步編程的方式來進行測試
from tornado.ioloop import IOLoop
import tornado.web
import tornado.httpserver
import aiohttp
from tornado.platform.asyncio import AsyncIOMainLoop
import asyncio
class MainHandler(tornado.web.RequestHandler):
async def get(self):
async with aiohttp.ClientSession() as session:
async with session.get('http://192.168.10.205:8084/api/test_api/') as resp:
text = await resp.text()
self.write(text)
if __name__ == "__main__":
app = tornado.web.Application([
(r"/api/test_api/", MainHandler),
])
server = tornado.httpserver.HTTPServer(app)
server.bind(5002, '127.0.0.1')
server.start(8)
AsyncIOMainLoop().install()
IOLoop.current().start()
此時運行該腳本並壓測
wrk -t4 -c1000 -d60s -T60s --latency http://127.0.0.1:5002/api/test_api/
Running 1m test @ http://127.0.0.1:5002/api/test_api/
4 threads and 1000 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 2.59s 4.26s 21.13s 86.18%
Req/Sec 295.72 209.62 1.00k 59.10%
Latency Distribution
50% 736.92ms
75% 1.19s
90% 9.51s
99% 17.92s
60957 requests in 1.00m, 12.27MB read
Non-2xx or 3xx responses: 1283
Requests/sec: 1014.86
Transfer/sec: 209.11KB
壓測的數據顯示大約的qps在一千左右,但是在測試過程中會出現連接耗盡的情況,主要是session沒有複用導致每次都重新創建。
golang測試
package main
import (
"fmt"
"io/ioutil"
"net/http"
)
func TestHandler(w http.ResponseWriter, r *http.Request){
// 訪問其他api
resp, err := http.Get("http://192.168.10.205:8084/api/test_api/")
if err != nil {
fmt.Println("error ", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
defer resp.Body.Close()
body, _ := ioutil.ReadAll(resp.Body)
w.Header().Set("Content-type", "application/json")
w.Write(body)
}
func main(){
http.HandleFunc("/api/test_api/", TestHandler)
http.ListenAndServe(":5003", nil)
}
壓測結果如下,同樣在壓測過程中也會報連接耗盡的情況。
wrk -t4 -c1000 -d60s -T60s --latency http://127.0.0.1:5003/api/test_api/
Running 1m test @ http://127.0.0.1:5003/api/test_api/
4 threads and 1000 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 2.21s 3.11s 18.58s 81.84%
Req/Sec 705.42 0.99k 3.09k 72.16%
Latency Distribution
50% 181.07ms
75% 3.85s
90% 7.26s
99% 11.84s
89857 requests in 1.00m, 11.21MB read
Non-2xx or 3xx responses: 4312
Requests/sec: 1495.52
Transfer/sec: 191.13KB
在三種情況下的壓測結果表明,flask的網關轉發效果較差,只有六百多,在tornado和golang的壓測過程中,會出現connect: cannot assign requested address的錯誤,就是可用的連接不夠用了,這再一定程度上影響了壓測的結果,不過這個問題不在本次的討論範圍裏面,因爲在這次壓測的代碼裏面就沒有優化每次訪問api時重用連接的情況。以tornado爲例,我們修改成複用連接的情況看一下效果是否會好一些(只是舉例有一定代碼上可以優化的空間)
from tornado.ioloop import IOLoop
import tornado.web
import tornado.httpserver
import aiohttp
from tornado.platform.asyncio import AsyncIOMainLoop
import asyncio
session_global = None
class MainHandler(tornado.web.RequestHandler):
async def get(self):
global session_global
if session_global is None:
session = aiohttp.ClientSession()
session_global = session
resp = await session_global.get("http://192.168.10.205:8084/api/test_api/")
self.write(await resp.text())
if __name__ == "__main__":
app = tornado.web.Application([
(r"/api/test_api/", MainHandler),
])
server = tornado.httpserver.HTTPServer(app)
server.bind(5002, '127.0.0.1')
server.start(8)
AsyncIOMainLoop().install()
IOLoop.current().start()
然後壓測一下
wrk -t4 -c1000 -d60s -T60s --latency http://127.0.0.1:5002/api/test_api/
Running 1m test @ http://127.0.0.1:5002/api/test_api/
4 threads and 1000 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 434.61ms 109.19ms 1.23s 67.93%
Req/Sec 568.01 170.52 1.27k 70.68%
Latency Distribution
50% 434.11ms
75% 506.77ms
90% 572.08ms
99% 718.92ms
135647 requests in 1.00m, 27.17MB read
Requests/sec: 2257.32
Transfer/sec: 462.93KB
可以看出如果優化了連接的話,qps上升了大約一倍。假如再修改一下golang版本的複用問題;
package main
import (
"time"
"fmt"
"io/ioutil"
"net/http"
)
var netClient *http.Client
func TestHandler(w http.ResponseWriter, r *http.Request){
// 訪問其他api
resp, err := netClient.Get("http://192.168.10.205:8084/api/test_api/")
if err != nil {
fmt.Println("error ", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
defer resp.Body.Close()
body, _ := ioutil.ReadAll(resp.Body)
w.Header().Set("Content-type", "application/json")
w.Write(body)
}
func main(){
netClient = &http.Client{Transport: &http.Transport{MaxIdleConnsPerHost: 300},
Timeout: time.Duration(30) * time.Second}
http.HandleFunc("/api/test_api/", TestHandler)
http.ListenAndServe(":5003", nil)
}
此時再壓測;
wrk -t4 -c1000 -d60s -T60s --latency http://127.0.0.1:5003/api/test_api/
Running 1m test @ http://127.0.0.1:5003/api/test_api/
4 threads and 1000 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 78.84ms 34.58ms 430.97ms 76.15%
Req/Sec 3.20k 441.89 4.69k 71.33%
Latency Distribution
50% 87.34ms
75% 99.63ms
90% 110.86ms
99% 155.67ms
763434 requests in 1.00m, 89.55MB read
Requests/sec: 12703.00
Transfer/sec: 1.49MB
可以看到優化之後的qps能夠達到一萬兩千多,當然這個優化只是針對單個後端服務的訪問,並沒有太大的實用意義。
其中這三個版本對應的情況可能不太一樣,Flask在當做應用層網關轉發的時候,主要就是通過阻塞IO去訪問後端的接口的,雖然在Flask接受請求的時候利用的異步IO,但是接受到請求之後又同步阻塞去請求這樣會影響性能,而在Tornado的版本中,則全部都使用了異步IO來進行數據的處理,這樣通過全部異步操作來提升響應速度,從壓測結果來看Tornado的異步版本相比Flask提升了快一倍,最後查看的golang的版本在代碼上無需異步處理,運行的過程中就是全部異步處理,想過也是比較好,響應比Tornado快了好幾倍,如果考慮最後的連接複用的情況下,快了大概有6倍。通過不同版本的測試對比,可以看出如果做網關或者服務在併發要求較高的情況下,選擇golang是比較好的選擇。
總結
本文主要是對比了再服務端,特別是Python中Flask和Tornado的常規的部署情況下的一個壓測情況,並且在應用層網關的壓測過程中對比了一下,API網關設計的過程中的基本情況,對比了Python的阻塞轉發,異步轉發,最後也壓測了golang版本的情況。初步來看的話,如果選擇設計API網關如果性能要求很高可以選擇golang這個方向來繼續調研,如果針對內部系統qps不高、追求開發效率的話,Python也是一個備選方案。由於本人才疏學淺,如有錯誤請批評指正。