текст перекладу
Фото від Aadil Imam Hussain на Unsplash
Нещодавно я повернувся до своїх старих проєктів, щоб виправити недоліки в коді та оптимізувати їхню продуктивність з точки зору часу запиту та відповіді.
Цей проєкт був бекендом для системи управління навчанням (LMS), хоча він був незавершений, я спроектував та протестував API для його функцій автентифікації (вхід, реєстрація, підтвердження електронної пошти, скидання паролю тощо) та функцій управління курсами (створення, оновлення, отримання та видалення). Тому я почав вимірювати час, необхідний для відповіді на маршрути автентифікації, і результати були досить непогані згідно з даними chatGPT і з огляду на середовище, в якому працює сервер.
вхід
- Середній час відповіді: 636 мс
Вимірюючи час відповіді на маршрут реєстрації, я отримав жахливий результат, який не піддавався вимірюванню.
реєстрація
Час відповіді був майже 7 секунд, при цьому створення та збереження користувача в базі даних займало в середньому 652 мс. У цей момент я зрозумів, що цей проєкт підходить лише як навчальний матеріал, а не як готовий до використання в продукції додаток.
Я намагаюся створювати та архітектурувати бекенд-системи, готові до використання в продуктивному середовищі, слідуючи принципам чистого коду та найкращим практикам.
Далі я виміряв час відповіді з кінцевих точок API для управління курсами, і результати були також поганими. Я спостерігав і побачив дві проблеми в моєму коді, які сприяють затримці відповіді:
- Безвідповідальні практики кодування, включаючи непотрібні читання з бази даних і надмірне відкриття деталей користувача
- Поганий дизайн бекенду
Давайте почнемо з першого, моє застосування використовує токенну автентифікацію для автентифікації користувачів при наступних запитах після входу.
текст перекладу
Точніше кажучи, коли користувач входить в систему, його дані шифруються і підписуються за допомогою jwt і надсилаються йому як токен, а при кожному наступному запиті цей токен декодується, і проміжний обробник перевіряє дійсність цієї інформації, перш ніж надати користувачеві доступ до різних ресурсів додатка.
Поганий код
При вході в систему лише ідентифікатор користувача підписується (за допомогою JWT) і повертається, а для подальших запитів відбувається наступне:
- Перевірка та декодування токена для отримання
userId
. - Запит до бази даних за допомогою
userId
для отримання деталей користувача (наприклад, роль, дозволи). - Перевірка властивості
role
користувача, щоб дізнатися, чи має він право доступу до ресурсу.
// middlewares.auth.ts
export async function isLoggedIn(req:Request, res:Response, next:NextFunction){
const token = req.headers.authorization.split(" ")[1];
if(!token){
return res.status(401).json({message: "Unauthorized, please login to continue"})
}
try{
const decode: any = jwt.verify(token, config.JWT_SECRET);
const user = await User.findById(decode?.userId).select("-password")
if(!user){
return res.status(401).json({message: "Invalid Token"})
}
// у мене виникла помилка з ts, і я змушений був змінити тип express Request, щоб врахувати користувача
req.user = user;
next()
}catch (error) {
if (error.name === 'TokenExpiredError') {
return res.status(401).json({ message: "Token has expired, please login again" });
} else if (error.name === 'JsonWebTokenError') {
return res.status(401).json({ message: "Invalid token, please login again" });
} else {
return res.status(500).json({ message: "Internal server error" });
}
}
}
Проблеми:
- Збільшене навантаження на базу даних: Кожен запит вимагає запиту до бази даних, що може стати вузьким місцем при високому навантаженні.
- Потенційна затримка: Пошук у базі даних додає навантаження до кожного запиту.
Для мого застосування цей підхід є поганим і серйозно впливає на продуктивність.
Лікування: Токен містить userId
і role
- При вході в систему, відповідь повинна містити підписаний JWT, що містить
userId
іrole
(або будь-які інші дані, необхідні для авторизації). - Для подальших запитів:
- Перевірка та декодування токена для отримання
userId
іrole
. - Використовувати
role
з токена для авторизації без запиту до бази даних.
// Запит був розширений, щоб включити userId і role - AuthenticatedRequest
export async function isLoggedIn(req:AuthenticatedRequest, res:Response, next:NextFunction){
const token = req.headers.authorization.split(" ")[1];
if(!token) return res.status(401).json({message: "Unauthorized, please login to continue"})
try{
const decode= jwt.verify(token, config.JWT_SECRET) as {userId, role};
req.userId = decode.userId;
req.role = decode.role
next()
}catch (error) {
if (error.name === 'TokenExpiredError') {
return res.status(401).json({ message: "Token has expired, please login again" });
} else if (error.name === 'JsonWebTokenError') {
return res.status(401).json({ message: "Invalid token, please login again" });
} else {
return res.status(500).json({ message: "Internal server error" });
}
}
}
Переваги:
- Швидші запити: Для кожного запиту не потрібен запит до бази даних, оскільки токен містить необхідну інформацію.
- Зменшене навантаження на базу даних: Корисно для мого застосування, де критично важливо мінімізувати запити до бази даних.
Поганий дизайн бекенду / архітектура
Маршрут управління курсами
Спочатку основною причиною часу відповіді в 7 секунд на кінцеву точку create/
API був поганий дизайн.
текст перекладу
Ось як обробляється запит до кінцевої точки create/
- Проміжний обробник
isLoggedin
, який перевіряє, чи увійшов користувач. - Проміжний обробник
isInstructor
, який перевіряє, чи є користувач інструктором (тільки інструктори можуть створювати курси). - Проміжний обробник
uploadCourseThumbnail
, який обробляє файл ескізу курсу в запиті, завантажує ескіз до Cloudinary та чекає публічне посилання. - Контролер
createCourse
, який отримує публічне посилання на файл ескізу, що зберігається в Cloudinary, а потім за допомогою інших деталей курсу зreq.body
створює та зберігає курс у базі даних.
Висока затримка зумовлена наступним:
- Послідовне виконання проміжних обробників: хоча кожен проміжний обробник асинхронний, кожен з них впливає на загальний час виконання запиту. Точніше, вони виконуються по черзі.
- Третій проміжний обробник
uploadCourseThumbnail
, який обробляє завантаження медіафайлів до Cloudinary, є основним чинником затримки, оскільки, хоча завантаження в Cloudinary відбувається асинхронно, проміжний обробник має чекати завершення цього процесу, перш ніж перейти до наступного проміжного обробника.
Маршрут автентифікації
У маршруті автентифікації при реєстрації час відповіді склав 5 секунд. Ось як обробляється запит до кінцевої точки register/
:
- Контролер
signup
: парсить дані користувача для реєстрації (ім'я, електронну пошту, телефон), потім перевіряє базу даних, чи є користувач з такою електронною поштою (електронна пошта є унікальним полем), після чого створює та зберігає користувача в базі даних. signup Email
: після того, як користувач збережений, відправляється електронний лист користувачеві з підтвердженням реєстрації за допомогою Nodemailer, реалізованого для цього додатка.
Затримка в цьому маршруті виникає через те, що лист відправляється одразу після збереження користувача.
Кращий дизайн
Залишайтеся на зв'язку. Дякую за читання!
Перекладено з: Setting up Bull to handle Asynchronous tasks in Node.js (Part 1)