FastAPI (Python) vs Fiber (Go) vs Spring Webflux (Java)
FastAPI (Python), Fiber (Go), Spring Webflux (Java) 각 언어별로 동일한 기능을 구현하고 각 언어의 스레드 관리와 이벤트처리에 대한 성능을 비교하고 싶었다. 그러기에 초당 몇건의 요청을 얼마나 안정적이고 빠르게 응답 하는지를 벤치마킹 해보았다.
테스트를 하면서 제일 크게 고민 했던 점이 이 테스트가 각 언어별 DB 드라이버에 대한 성능 테스트로 이어지지는 않을까 하는 생각이 들어서 고심을 했지만 결론적으로는 언어별 스레드 관리와 이벤트 처리에 대한 성능을 기준으로 본다면, 언어별 DB 드라이버만의 성능을 테스트 하는 것은 아니라는 생각이 들었다.
Go 1.21.1
github.com/go-sql-driver/mysql v1.6.0
github.com/gofiber/fiber/v2 v2.49.2
2) 서버 소스
package main
import (
"fmt"
"log"
"strconv"
"time"
"database/sql"
_ "github.com/go-sql-driver/mysql"
"github.com/gofiber/fiber/v2"
)
type (
App struct {
Idx int `json:"idx"`
Regdate string `json:"regdate"`
Title string `json:"title"`
Body string `json:"body"`
}
)
func main() {
dsn := "root:root@tcp(127.0.0.1:3306)/test"
db, err := sql.Open("mysql", dsn)
if err != nil {
fmt.Println(err.Error())
} else {
fmt.Println("db is connected")
}
defer db.Close()
db.SetConnMaxLifetime(time.Second * 1)
db.SetMaxIdleConns(50)
db.SetMaxOpenConns(50)
//
app := fiber.New(fiber.Config{})
app.Post("/memo", func(c *fiber.Ctx) error {
var d App
c.BodyParser(&d)
res, err := db.Exec("INSERT INTO memo(title, body) VALUES(?, ?)", d.Title, d.Body)
if err != nil {
panic(err.Error())
}
id, err := res.LastInsertId()
if err != nil {
panic(err.Error())
}
d.Idx = int(id)
return c.Status(200).JSON(d)
})
app.Get("/memo/list/:page", func(c *fiber.Ctx) error {
page := c.Params("page")
limit := 10
pint, _ := strconv.Atoi(page)
offset := (pint - 1) * limit
results, err := db.Query("SELECT * FROM memo LIMIT ?, ?", offset, limit)
if err != nil {
panic(err.Error())
}
defer results.Close()
var list = []App{}
for results.Next() {
var app App
err = results.Scan(
&app.Idx,
&app.Title,
&app.Body,
&app.Regdate,
)
if err != nil {
panic(err.Error())
}
list = append(list, app)
}
return c.Status(200).JSON(list)
})
log.Fatal(app.Listen(":8000"))
}
3. Java (Spring Boot, Webflux) 서버 구성
1) 주요 라이브러리 버전
java 17
spring framework boot 3.1.4
2) 서버 소스
B. 스트레스 테스트 방법
스트레스 테스트는 Locust 라는 도구를 사용하기로 했고, 10만건의 데이터를 미리 DB에 적재하고 서버를 띄워 Select 를 하는 테스트 하나, 초기화된 데이터 테이블에 Insert 를 하는 테스트 로 나누어 스트레스 테스트를 진행했다.
1. Locust, 테스트 소스 작성
1) Insert 소스
from locust import FastHttpUser, task, between
import random
from faker import Faker
# 임의의 데이터를 생성하기 위한 라이브러리
fake = Faker('ko_kr')
class MyTest(FastHttpUser):
# 아래 task 를 진행한 후에 대기 시간없이 바로 실행하도록 한다
wait_time = between(0, 0)
@task
def index(self):
# 임의의 값을 post api 에 요청한다
self.client.post("/memo", json={
"title": fake.name(),
"body": fake.sentence()
})
2) Select 소스
from locust import FastHttpUser, task, between
import random
class MyTest(FastHttpUser):
wait_time = between(0, 0)
@task
def index(self):
# 10 만 건이라 10개씩 1페이지를 구성하는 페이지를 1페이지 부터 10000페이지까지 임의로 호출
r = random.randint(1,10000)
self.client.get(f"/memo/list/{r}")
2. Locust, 테스트 실행
테스트는 아래와 같은 명령어로 실행 하며, 20명씩 증가 시키며 사용자 100명이 동시에 호출 한다. 그리고 시간은 60초간 실행하고 멈추도록 한다.
로컬호스트의 8089 포트로 접근하면 위와 같은 웹페이지로 성능 및 진행사항을 확인 할 수 있는데 크게 아래 두 지표로 성능을 확인하도록 한다.
RPS : 초당 요청수 (Failure가 있는지 확인)
95th Percentile : 대부분(95%)의 응답 시간 (노란색 그래프 확인)
C. 언어별 서버 성능
1. Python (FastAPI) 서버 성능
1) Select
2) Insert
2. Go (Fiber) 서버 성능
1) Select
2) Insert
3. Java (Spring Boot, Webflux) 서버 성능
1) Select
2) Insert
C. 스트레스 테스트 결과
비동기 방식의 서버들을 가지고 테스트를 했고 성능상 큰 차이는 없다고 본다. 특히 Go 와 Reactor 기반의 Java의 Spring Webflux 는 유사한 성능을 보여주고 있다. (insert 가 select 보다 빠른 이유는 select 의 경우 페이징을 겸비한 아이템 목록을 던져주기 때문이다)
특정 언어가 더 빠르고 좋다 라는 논쟁은 의미가 없어 보인다. 어떤 언어를 다루든 비즈니스를 더 효율적으로 빨리 만들수 있는가 유지보수는 어떠한가의 기준으로 접근해야 할 것이다.