Первоначально опубликовано на coreycleary.me. Это перепост из моего блога о контенте. Я публикую новые материалы каждую неделю или две, и вы можете подписаться на мою рассылку, если хотите получать мои статьи прямо на свой почтовый ящик! Я также регулярно высылаю шпаргалки и другие бесплатные материалы.
При работе с Node и JavaScript одним из преимуществ является то, что мы можем сделать код асинхронным, будь то через обратные вызовы или Promises. Вместо того, чтобы ждать окончания выполнения строки кода, мы можем продолжить работу, если не будем await
или .then()
Promise, или не вложим обратные вызовы, если они используются.
Вы также, вероятно, знаете об очередях задач, где вместо выполнения кода в вашем «основном» сервисе вы создаете задание/задачу в очереди, а потребитель следит за очередью и выполняет работу вместо «основного» сервиса. Вместо того, чтобы быть встроенной асинхронной вещью Node/JS, это асинхронный паттерн на уровне архитектуры.
Обычно очередь задач используется, когда вы хотите разгрузить долго выполняющийся блок кода, и вам не нужны результаты этого кода в остальной части вашего кода.
Но если мы можем пропустить ожидание завершения асинхронного кода JavaScript и таким образом сохранить «быстроту» кода, разве это не достигает той же цели?
Зачем вообще нужна очередь?
Это важная концепция, которую необходимо понять, особенно когда вы становитесь более «старшим» и принимаете архитектурные решения. Поэтому давайте изучим оба варианта и поймем, в чем разница и почему вы захотите использовать один вариант, а не другой.
Обработка кода
Когда вы не ждете разрешения Promise, самое важное, что нужно помнить, это то, что Node все еще обрабатывает Promise из цикла событий. Это не так, как если бы оно исчезло или было отправлено на какую-то волшебную фабрику, которая делает работу бесплатно.
Так что даже если вы не ждете разрешения, ваш сервер все еще выполняет этот код. Это важно отметить, потому что у вас может быть сценарий, в котором это выполнение требует больших вычислительных затрат (использование большого количества CPU и/или памяти).
Поэтому даже если вы не будете ждать завершения, производительность сервера будет тем, что вам нужно учитывать.
Представьте, что у вас есть задача с интенсивными вычислениями, например, обработка изображений, и когда она выполняется в цикле событий Node, это приводит к загрузке вашего сервера.
Это главный кандидат на то, что следует перенести в очередь задач. Вы перегружаете эту дорогостоящую вычислительную работу куда-то еще, и опять же вы не можете этого избежать. Но эта работа больше не находится в основном сервисе, загромождая его, и вместо этого вы можете более мгновенно вернуть ответ пользователю. И теперь вы можете увеличивать или уменьшать количество потребителей («сервисов», выполняющих код), чтобы, по сути, сбалансировать нагрузку.
Обработка ошибок при отсутствии ожидания разрешения Promise
Сейчас, вероятно, самое время обсудить еще один важный момент, когда вы не ждете разрешения Promise.
Если Promise отклоняется, вы все равно должны его поймать. Если этого не сделать, вы получите ошибку Unhandled promise rejection
.
Самый «локальный» способ сделать это — использовать .catch()
, например, так:
async function test() {
// artificial rejection just to demonstrate
return Promise.reject('this is a rejection')
}
// notice, NO .then() or await
test().catch((err) => {
// handle Promise rejection here
console.error(err)
})
Обратите внимание, что здесь нельзя использовать try/catch, например, так:
try {
test()
} catch (err) {
console.error(err)
}
В try/catch
даже без await
это приведет к не пойманной ошибке Promise. Другого способа сделать это с помощью try/catch
, о котором я знаю, не существует.
Можно также использовать обработчик ошибок «верхнего уровня», а не «локальный», например:
process.on('unhandledRejection', (reason, promise) => {
console.log('Unhandled Rejection at:', promise, 'reason:', reason)
// Application specific logging, throwing an error, or other logic here
})
Но независимо от этого, ее нужно обрабатывать. Особенно если вы используете более новую версию Node. В зависимости от версии, более новые версии не просто выдадут предупреждение, они убьют сервер. И если вы пойдете по пути «верхнего уровня», вы можете потерять возможность дополнить ошибку другими переменными или информацией, находящейся в области действия функции.
Повторное выполнение невыполненных обещаний
Еще одна вещь, которую следует учитывать, если вы думаете о том, чтобы не ждать разрешения Promise, заключается в том, что если оно не сработает/откажет, вам нужно будет добавить код для обработки повторного выполнения Promise (если вы действительно хотите повторить его). Что-то вроде:
const retry = (fn, ms) => new Promise(resolve => {
fn()
.then(resolve)
.catch(() => {
setTimeout(() => {
console.log('retrying...')
retry(fn, ms).then(resolve)
}, ms)
})
})
retry(someFnThatReturnsPromise, 2000)
Конечно, если вас не волнует отказ функции/обещания, и вы можете с этим смириться, то вам не нужно этого делать. Но обычно вы, вероятно, хотите, чтобы код выполнялся успешно.
Приведенный выше код обеспечивает нам повторные попытки выполнения функции Promise, но что если приведенная выше someFnThatReturnsPromise
продолжает терпеть неудачу? Возможно, где-то в определении функции есть логическая ошибка или TypeError. Никакое количество повторных попыток не приведет к ее успешному завершению.
Мы можем реализовать maxNumberRetries
в функции retry()
, и это остановит повторные попытки после X количества раз. Но мы все еще возвращаемся к проблеме, что код не завершается успешно.
И те повторные попытки, которые происходят, все еще находятся в цикле событий, используя вычислительную мощность сервера (вернемся к пункту №1). А что если вам абсолютно необходимо, чтобы эти функции завершились, и это критически важно для вашего приложения?
Повторные попытки при таких «постоянных» сбоях становятся более сложными.
Кроме того, чтобы отслеживать эти сбои, мы должны использовать код для регистрации повторных попыток, количества попыток и т.д. Опять же, это выполнимо, но это означает больше кода для реализации.
И если у вас не настроено что-то пользовательское, например, пользовательский счетчик с помощью statsd
, Splunk и т.д. для инструментария и мониторинга отказов в какой-нибудь приборной панели, вы, вероятно, будете просто регистрировать отказы. А это означает, что нужно просматривать журналы, чтобы найти сбои, или, возможно, настроить запрос CloudWatch для отслеживания этих сбоев.
Может быть, очередь упростит некоторые из этих задач? С меньшим количеством пользовательской работы, которую вам придется выполнять на своей стороне?
В зависимости от того, какое решение очереди вы используете, вы обычно получаете следующее из коробки:
- настраиваемые повторные попытки
- очередь мертвых писем («DLQ»)
- мониторинг/наблюдаемость очереди
Вместо добавления пользовательского кода повторных попыток вы обычно получаете настраиваемые «автоматические» повторные попытки из коробки с решением для очередей задач.
В сценарии, в котором вы получаете постоянные сбои, эта задача может быть автоматически перемещена в DLQ, где она будет находиться до тех пор, пока вы не предпримите соответствующие действия. Это поможет вам избежать бесконечного цикла повторных попыток.
Представьте, что у вас есть асинхронный код, в котором пользователь регистрируется в вашем приложении, ваш код отправляет приветственное письмо, создает для него учетные данные и запускает некоторую маркетинговую последовательность. Возможно, это не очень трудоемкая обработка, но что-то, чего вы решили не ждать (например, ваш почтовый провайдер немного медленный).
Что, если вы запустили плохой код обработки (т.е. в вашем коде отправки электронной почты была ошибка)? С помощью очереди вы можете исправить ситуацию, а затем повторить все эти попытки с исправленным кодом, используя элементы из DLQ.
И вы также получите наблюдаемость не только DLQ — вы хотите знать, когда код просто не может успешно выполниться — но и других задач. Такие вещи, как сколько их в настоящее время в очереди, сколько из них обрабатывается, завершено и т.д.
Главное здесь то, что вы получаете эти вещи из коробки (опять же, большинство решений должны иметь эти функции, но всегда проверяйте).
Если очередь еще не установлена, требуется настройка инфраструктуры
Если у вас еще не создана инфраструктура для очереди задач, это «накладная» работа, о которой придется позаботиться вам или кому-то из вашей команды. Очевидно, что с увеличением инфраструктуры увеличивается и стоимость, так что это то, что нужно учитывать при определении цены/биллинга.
Если вы создаете MVP или можете смириться с некоторыми сбоями в выполнении кода и меньшей наблюдаемостью за его выполнением, возможно, инфраструктурная настройка не стоит того.
Если вы выберете вариант просто не ждать разрешения Promise, хорошо то, что это решение — просто код приложения. Нет настройки очереди, настройки рабочих и т.д.
Замечание о ламбдах
Стоит отметить, что если вы используете AWS Lambdas и не await
или .then()
Promise, вы рискуете тем, что код «перехватит» и завершит его разрешение в другом запросе Lambda. Я не эксперт по лямбдам, но я лично видел, как это происходит. Одна Lambda выполняла два разных запроса, причем часть одного запроса, которая не была await
‘ed, завершалась в этом Lambda-запросе.
Поэтому вышеприведенное обсуждение Promises необходимо взвесить с учетом нюансов Lambda.
Резюме
Я рассмотрел все возможные соображения при определении того, следует ли вам использовать очередь задач или просто пропустить разрешение Promise и продолжить выполнение кода.
Но в конце я привожу псевдоматрицу решений, когда вы, скорее всего, будете использовать ту или иную очередь:
- Если обработка (например, обработка изображений) займет несколько секунд или минут, вам, вероятно, следует использовать очередь. Это, скорее всего, слишком интенсивная обработка для сервера, и вы можете столкнуться с дополнительными проблемами производительности, даже если вы пропускаете разрешение и переходите к следующему фрагменту кода.
- Если задача не является критически важной и не требует больших затрат на обработку, и вы можете иметь дело с некоторыми сбоями то тут, то там, отказ от ожидания разрешения Promise, вероятно, является некоторымFnThatReturnsPromise.
- То же самое относится и к тому, можете ли вы жить с постоянными сбоями (в случае ошибки программирования, связанной с задачей).
- Если задача критически важна, даже если она не требует интенсивной обработки, вам, вероятно, следует использовать очередь, чтобы получить наблюдаемость, повторные попытки и DLQ (что опять же очень полезно в случае ошибки программирования).
- Если настройка инфраструктуры для вас слишком трудоемка, даже с учетом вышеизложенных соображений, просто не ждите разрешения Promise и не используйте очередь.
- Это может показаться очевидным, но если вы либо не можете создать инфраструктуру очереди, либо это слишком трудоемко, у вас все равно не будет очереди, поэтому вы не сможете использовать это решение.
- Если с учетом ваших нефункциональных требований и технических соображений вы решите, что очередь задач подходит для вашего приложения, я бы порекомендовал вам попробовать и создать инфраструктуру.
Возможность работы с асинхронным кодом в Node и JavaScript великолепна и, очевидно, является основной частью языка, но она также может привести к некоторой путанице. Надеюсь, это обсуждение и объяснение различий даст вам более глубокое понимание разницы между двумя подходами и поможет вам решить, когда какой из них использовать.
Любите JavaScript, но все еще путаетесь в локальных разработках, архитектуре, тестировании и т.д.? Я публикую статьи по JavaScript и Node каждые 1-2 недели, так что если вы хотите получать все новые статьи прямо на свой почтовый ящик, вот ссылка, чтобы подписаться на мою рассылку!