背景
故事的开始还是源于现阶段各个平台之间有些许的接口调用,但是各个平台之间又相对比较独立,并没有将各个平台整合成一组微服务的需求。现在面临的问题就是各个平台直接都有互相调用的需求,如果各个平台有业务升级的情况下,不一定能察觉到是否会影响到对提供给其他系统使用的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也是一个备选方案。由于本人才疏学浅,如有错误请批评指正。