應用層網關調研與基礎測試

背景

故事的開始還是源於現階段各個平臺之間有些許的接口調用,但是各個平臺之間又相對比較獨立,並沒有將各個平臺整合成一組微服務的需求。現在面臨的問題就是各個平臺直接都有互相調用的需求,如果各個平臺有業務升級的情況下,不一定能察覺到是否會影響到對提供給其他系統使用的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也是一個備選方案。由於本人才疏學淺,如有錯誤請批評指正。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章