Налаштування Bull для обробки асинхронних завдань в Node.js (Частина 1)

текст перекладу

pic

Фото від Aadil Imam Hussain на Unsplash

Нещодавно я повернувся до своїх старих проєктів, щоб виправити недоліки в коді та оптимізувати їхню продуктивність з точки зору часу запиту та відповіді.

Цей проєкт був бекендом для системи управління навчанням (LMS), хоча він був незавершений, я спроектував та протестував API для його функцій автентифікації (вхід, реєстрація, підтвердження електронної пошти, скидання паролю тощо) та функцій управління курсами (створення, оновлення, отримання та видалення). Тому я почав вимірювати час, необхідний для відповіді на маршрути автентифікації, і результати були досить непогані згідно з даними chatGPT і з огляду на середовище, в якому працює сервер.

pic

вхід

  • Середній час відповіді: 636 мс

pic

Вимірюючи час відповіді на маршрут реєстрації, я отримав жахливий результат, який не піддавався вимірюванню.

pic

реєстрація

Час відповіді був майже 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 (або будь-які інші дані, необхідні для авторизації).
  • Для подальших запитів:
  1. Перевірка та декодування токена для отримання userId і role.
  2. Використовувати 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)

Leave a Reply

Your email address will not be published. Required fields are marked *