Python против Go: Сравнение производительности при проверке JWT и запросе MySQL
Введение
После публикации рекордного количества статей о сравнении производительности различных технологий, таких как Node.js, Deno, Bun, Rust, Go, Spring, Python и т.д. для простого примера hello world, я постоянно получал комментарии о том, что статьи были хороши, но не были применимы непосредственно к реальным случаям использования. Меня попросили сделать то же самое для более “реальных” случаев. Статьи также (и до сих пор) собирали рекордное количество просмотров. Тем не менее, суть была понятна. Hello world был лучшей отправной точкой, но определенно не “реальным” примером.
Реальный пример использования
Этой статьей я начинаю новый цикл статей, в котором я буду сравнивать несколько технологий для реального случая:
- Получение JWT из заголовка авторизации
- Проверьте JWT и получите электронную почту из утверждений
- Выполните запрос MySQL с электронной почтой
- Верните запись пользователя
Это очень распространенный случай в реальном мире. Для случая “Hello world” я видел технологии, предлагающие от 70K до 200K RPS. RPS был высоким, потому что все, что делало приложение, это возвращало простую строку. Конечно, мы не будем ожидать 200K RPS для случая использования JWT + MySQL. Сколько мы получим, еще предстоит увидеть.
В этой статье сравниваются Python и Go для этого случая использования. Это интересное сравнение, потому что Python интерпретируется, а Go компилируется в машинный код. Кроме того, проверка JWT – это операция, требующая больших затрат процессора. Компилируемый язык должен быть быстрее интерпретируемого? Не так ли? Мы узнаем это очень скоро.
Из-за большого количества статей, которые будут опубликованы, я также создам статью для индексации всех реальных случаев. В конце этой статьи вы найдете ссылку на нее. Давайте начнем.
Настройка тестов
Все тесты выполняются на MacBook Pro M1 с 16 Гб оперативной памяти.
Версии программного обеспечения следующие:
- Python v3.11.3
- Go 1.20.4
На стороне Python я использую FastAPI (конечно же). Другие фреймворки на стороне Python – это jwt для проверки и декодирования JWT и mysql-connector для выполнения запросов к MySQL.
На стороне Go я использую веб-фреймворк Gin. Другие фреймворки, которые я использую: golang-jwt для проверки и декодирования JWT и go-sql-driver для выполнения запросов к MySQL.
Тестер нагрузки HTTP построен на основе libcurl. Имеется предварительно созданный список из 100K JWT. Тестер выбирает случайные JWT и отправляет их в заголовке Authorization HTTP-запроса.
База данных MySQL содержит таблицу users, которая имеет 6 столбцов:
mysql> desc users;
+--------+--------------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+--------+--------------+------+-----+---------+-------+
| email | varchar(255) | NO | PRI | NULL | |
| first | varchar(255) | YES | | NULL | |
| last | varchar(255) | YES | | NULL | |
| city | varchar(255) | YES | | NULL | |
| county | varchar(255) | YES | | NULL | |
| age | int | YES | | NULL | |
+--------+--------------+------+-----+---------+-------+
6 rows in set (0.00 sec)
Таблица users предварительно заполнена 100 тыс. записей:
mysql> select count(*) from users;
+----------+
| count(*) |
+----------+
| 99999 |
+----------+
1 row in set (0.01 sec)
Для каждого электронного адреса, присутствующего в JWT, существует соответствующая запись пользователя в базе данных MySQL.
Код
Python
from fastapi import FastAPI, Response, Request, HTTPException
from fastapi.responses import JSONResponse
from fastapi.encoders import jsonable_encoder
import jwt
import mysql.connector
import os
app = FastAPI()
conn = mysql.connector.connect(
host="127.0.0.1",
port=3306,
user="dbuser",
password="dbpwd",
database="testdb")
PREFIX = 'Bearer '
jwtSecret = os.getenv('JWT_SECRET')
def get_token(headers):
hdr = headers.get('authorization')
if not hdr:
raise ValueError('Invalid token')
if not hdr.startswith(PREFIX):
raise ValueError('Invalid token')
return hdr[len(PREFIX):]
@app.get("/")
async def root(request: Request):
auth_hdr = get_token(request.headers)
email = ""
try:
payload = jwt.decode(auth_hdr, jwtSecret, algorithms="HS256")
email = payload['email']
except:
raise HTTPException(status_code=401)
cur = conn.cursor()
sql_query = """SELECT * from USERS where email = %s"""
params = (email,)
cur.execute(sql_query, params)
row = cur.fetchone()
user = {
"email": email,
"first": row[1],
"last": row[2],
"city": row[3],
"county": row[4],
"age": row[5]
}
cur.close()
return JSONResponse(content=jsonable_encoder(user))
Go
package main
import (
"net/http"
"os"
"strings"
"database/sql"
"github.com/gin-gonic/gin"
_ "github.com/go-sql-driver/mysql"
"github.com/golang-jwt/jwt"
)
type MyCustomClaims struct {
Email string `json:"email"`
jwt.RegisteredClaims
}
type User struct {
Email string
First string
Last string
City string
County string
Age int
}
var jwtSecret = os.Getenv("JWT_SECRET")
func getToken(req *http.Request) string {
hdr := req.Header.Get("authorization")
if hdr == "" {
return ""
}
token := strings.Split(hdr, "Bearer ")[1]
return token
}
func main() {
r := gin.New()
db, err := sql.Open("mysql", "dbuser:dbpwd@/testdb")
if err != nil {
return
}
r.GET("/", func(c *gin.Context) {
tokenString := getToken(c.Request)
if tokenString == "" {
c.AbortWithStatus(http.StatusUnauthorized)
return
}
token, err := jwt.ParseWithClaims(tokenString, &MyCustomClaims{}, func(token *jwt.Token) (interface{}, error) {
return []byte(jwtSecret), nil
})
if err != nil {
c.AbortWithStatus(http.StatusUnauthorized)
return
}
claims := token.Claims.(*MyCustomClaims)
query := "SELECT * FROM USERS WHERE EMAIL = ?"
row := db.QueryRow(query, claims.Email)
var user User
err2 := row.Scan(&user.Email, &user.First, &user.Last, &user.City, &user.County, &user.Age)
if err2 != nil {
c.AbortWithStatus(http.StatusNotFound)
return
}
c.JSON(http.StatusOK, user)
})
r.Run(":3000")
}
Результаты
В каждом тесте выполняется в общей сложности 500K запросов. Уровни параллелизма – 10, 50 и 100 соединений. Перед проведением измерений дается разогрев в 1K запросов.
Ниже приведены графики с результатами:
Анализ
Во-первых, мы больше не видим таких высоких показателей RPS, которые мы наблюдали в простом примере hello world (Python предлагал ~35K RPS, а Go gin ~120K RPS). В этом реальном примере использования Python предлагает около 6K RPS, а Go – всего ~4K RPS.
Во-вторых, в коде Go, который я использовал, определенно что-то не так. При 50 и 100 одновременных соединениях ~100K запросов отработали по таймеру. Это большое число. Go потерпел неудачу для этого случая использования.