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 запросов.

Ниже приведены графики с результатами:

Python против Go: Сравнение производительности при проверке JWT и запросе MySQL
Python против Go: Сравнение производительности при проверке JWT и запросе MySQL
Python против Go: Сравнение производительности при проверке JWT и запросе MySQL
Python против Go: Сравнение производительности при проверке JWT и запросе MySQL
Python против Go: Сравнение производительности при проверке JWT и запросе MySQL
Python против Go: Сравнение производительности при проверке JWT и запросе MySQL
Python против Go: Сравнение производительности при проверке JWT и запросе MySQL
Python против Go: Сравнение производительности при проверке JWT и запросе MySQL
Python против Go: Сравнение производительности при проверке JWT и запросе MySQL

Анализ

Во-первых, мы больше не видим таких высоких показателей RPS, которые мы наблюдали в простом примере hello world (Python предлагал ~35K RPS, а Go gin ~120K RPS). В этом реальном примере использования Python предлагает около 6K RPS, а Go – всего ~4K RPS.

Во-вторых, в коде Go, который я использовал, определенно что-то не так. При 50 и 100 одновременных соединениях ~100K запросов отработали по таймеру. Это большое число. Go потерпел неудачу для этого случая использования.

+1
0
+1
0
+1
0
+1
0
+1
0

Ответить

Ваш адрес email не будет опубликован. Обязательные поля помечены *