За последние два дня для Node-проекта была проведена волна оптимизации на уровне кода.С точки зрения времени отклика это очень значительное улучшение.
Сервис, который просто предоставляет интерфейс клиенту, не связанный с отрисовкой страницы.
задний план
Во-первых, этот проект был несколько лет назад, за это время добавились новые требования, что усложнило логику кода, а также увеличилось время отклика интерфейса.
До этого была оптимизация под серверную среду(обновление версии узла), производительность действительно значительно улучшилась, но в соответствии с концепцией «молодость — это смерть», на этот раз мы снова оптимизируем ее на уровне кода.
Связанная среда
Поскольку это проект нескольколетней давности, я используюExpress
+co
Такой.
из-за раннегоNode.js
Версия4.x
, то асинхронная обработка используетyield
+generator
сделано таким образом.
действительно по сравнению с некоторыми более раннимиasync.waterfall
Другими словами, читаемость кода уже очень высока.
Что касается хранения данных, поскольку это некоторые данные с высокими требованиями к реальному времени, все данные взяты изRedis
.
Node.js
Из-за обновления некоторое время назад версия теперь8.11.1
, что делает разумным использование нового синтаксиса для упрощения кода.
Поскольку трафик увеличивается, некоторые коды, которые не имели проблем в первые годы, также станут одной из причин замедления работы программы после того, как запрос достигнет определенного уровня.Эта оптимизация в основном предназначена для заполнения этой части ямы.
несколько советов
В этом примечании по оптимизации не будет ничегоprofile
Отображение файлов.
В этот раз я не стал полагаться на анализ производительности для оптимизации, а просто добавил время отклика интерфейса, и сравнил полученные результаты после подведения итогов. (асинхронная запись в файлappendFile
метки времени начала и окончания)
в соответствии сprofile
Оптимизацию можно проводить в три этапа.
profile
Он в основном используется для поиска таких проблем, как утечка памяти и размер памяти стека вызовов функций, поэтому эта оптимизация не рассматривается.profile
использование
И я лично не думаю, что имеет смысл публиковать так мало снимков памяти (в этой оптимизации), лучше придумать какое-то реальное сравнение кода до и после оптимизации.
Несколько оптимизаций
Вот список мест, задействованных в этой оптимизации:
- Некоторые неразумные структуры данных (используемая поза проблематична)
- Последовательный асинхронный код (что-то вроде
callback
адский формат)
Оптимизация, связанная со структурой данных
Упомянутые здесь структурыRedis
Связанный, в основном относится к реализации частичной фильтрации данных
Фильтрация в основном отражена в некоторых интерфейсах данных списка, потому что некоторые операции, такие как фильтрация, должны выполняться в соответствии с бизнес-логикой:
- Отфильтрованная ссылка взята из другого сгенерированного набора данных.
- Отфильтрованная ссылка исходит от Redis
На самом деле, первые данные такжеRedis
Сгенерировано. :)
Отфильтровать оптимизации из другого источника данных
Как и в первом случае, в коде это может выглядеть так:
let data1 = getData1()
// [{id: XXX, name: XXX}, ...]
let data2 = getData2()
// [{id: XXX, name: XXX}, ...]
data2 = data2.filter(item => {
for (let target of data1) {
if (target.id === item.id) {
return false
}
}
return true
})
Есть два списка, чтобы данные из первого списка не отображались во втором списке.
Конечно, оптимальным решением должно быть то, что сервер не обрабатывает его, а клиент выполняет фильтрацию, но это теряет гибкость и затрудняет совместимость со старыми версиями.
Приведенный выше код проходитdata2
попытается перебрать каждый элемент вdata1
, а затем сравните их.
Недостатком этого является то, что итератор будет каждый раз регенерироваться, и поскольку суждениеid
свойства, мы будем каждый раз искать свойства объекта, поэтому оптимизируем код следующим образом:
// 在外层创建一个用于过滤的数组
let filterData = data1.map(item => item.id)
data2 = data2.filter(item =>
filterData.includes(item.id)
)
Таким образом, мы пересекаемdata2
в самый разfilterData
объект называетсяincludes
Выполняйте поиск вместо создания каждый раз нового итератора.
Конечно, на самом деле в этой области еще есть место для дальнейшей оптимизации, потому что мы создали вышеfilterData
На самом делеArray
,ЭтоList
,использоватьincludes
, можно считать, что его временная сложность равнаO(N)
сейчас,N
заlength
.
Таким образом, мы можем попытаться поставить вышеуказанноеArray
переключиться наObject
илиMap
объект.
Поскольку последние два принадлежатhash
структуры, поиск этой структуры можно рассматривать как временную сложностьO(1)
сейчас,да или нет.
let filterData = new Map()
data.forEach(item =>
filterData.set(item.id, null) // 填充null占位,我们并不需要它的实际值
)
data2 = data2.filter(item =>
filterData.has(item.id)
)
P.S. Я обсудил этот вопрос с коллегами и провел тестовый скрипт-эксперимент, который доказал, что при оценке существования элемента на большом количестве данныхSet
а такжеArray
производительность является худшей, в то время какMap
а такжеObject
Плоский.
О фильтрации из Redis
Что касается этой фильтрации, необходимо рассмотреть возможность оптимизацииRedis
Структура данных, как правило,Set
,SortedSet
.
НапримерSet
перечислитьsismember
судитьitem
он существует,
илиSortedSet
перечислитьzscore
судитьitem
он существует(Есть ли соответствующийscore
ценность)
Вот где возникает компромисс, если мы используем два вышеуказанных метода в цикле.
заключается в том, что вы должны получить всеitem
, непосредственно в памяти, чтобы определить, существует ли элемент
Или вызовите его последовательно в циклеRedis
чтобы получитьitem
Он существует?
Вот несколько небольших предложений для справки
- если
SortedSet
, рекомендуется использовать в циклеzscore
Суждение (на этот раз сложностьO(1)
) - если
Set
, если известноSet
Мощность в основном больше, чем количество циклов, ее рекомендуется использовать в циклеsismember
судить
Если код будет повторяться много раз, иSet
Мощность невелика и может быть вынесена и использована вне цикла (smembers
Временная сложностьO(N)
,N
мощность множества)И, еще один момент, затраты на передачу по сети также должны быть включены в наши компромиссы, потому что такие вещи, какsismbers
Возвращаемое значение просто1|0
,а такжеsmembers
передам всю коллекцию
Два практических сценария о Set
- Если теперь есть список данных, некоторые данные необходимо отфильтровать для некоторых провинций.
Мы можем выбрать извлекать все значения из коллекции во внешний слой цикла, а затем судить и фильтровать непосредственно по объектам в памяти внутри цикла. - Если данные этого списка должны быть занесены в черный список для пользователей, учитывая, что некоторые пользователи могут заблокировать многих людей, это
Set
Сложно оценить мощность , поэтому рекомендуется использовать метод внутрициклового суждения.
Снижение затрат на передачу по сети
Положите конец злоупотреблению гашишем
Действительно, используйтеhgetall
Это очень беззаботная вещь, несмотря ни на чтоRedis
этоHash
Что бы ни было в нем, я получу это.
Однако это вызывает некоторые проблемы с производительностью.
Например, у меня естьHash
, структура данных следующая:
{
name: 'Niko',
age: 18,
sex: 1,
...
}
Теперь в интерфейсе списка нам нужно использовать этоhash
серединаname
а такжеage
поле.
Самый простой способ:
let info = {}
let results = await redisClient.hgetall('hash')
return {
...info,
name: results.name,
age: results.age
}
существуетhash
В малых случаях,hgetall
не повлияет на производительность.
Но когда нашhash
Когда число большое, такоеhgetall
окажет большое влияние.
-
hgetall
Временная сложностьO(N)
,N
заhash
размер - Не говоря уже о временной сложности выше, мы на самом деле использовали только
name
а такжеage
, и прочие значения, передаваемые по сети, на самом деле пустая трата
Итак, нам нужно изменить аналогичный код:
let results = await redisClient.hgetall('hash')
// == >
let [name, age] = await redisClient.hmget('hash', 'name', 'age')
P.S. Еслиhash
Количество предметов будет меняться после определенной суммыhash
структура хранения,
использовать в это времяhgetall
производительность будет лучше, чемhmget
, может быть просто понято как 20 или меньшеhmget
это все хорошо
Оптимизация, связанная с асинхронным кодом
отco
начиная с сейчасasync
,await
,существуетNode.js
Асинхронное программирование становится очень понятным в , мы можем писать асинхронные функции в следующем формате:
async function func () {
let data1 = await getData1()
let data2 = await getData2()
return data1.concat(data2)
}
await func()
Выглядит очень удобно, правда?
Когда вам удобно, программа тоже удобна.Программу можно толькоgetData1
Он будет выполнен после получения возвращаемого значенияgetData2
, а затем застрял в процессе ожидания обратного вызова.
Это очень распространенное место для злоупотребления асинхронными функциями. Изменил асинхронный на последовательный, потерялNode.js
Преимущества в качестве асинхронного потока событий.
Для несвязанных асинхронных запросов, подобных этому, предложение:
Объединяйте, если можете. Это слияние не означает, что вам нужно изменить логику поставщика данных, но чтобы лучше использовать преимущества асинхронного потока событий и одновременно регистрировать несколько асинхронных событий.
async function func () {
let [
data1,
data2
] = await Promise.all([
getData1(),
getData2()
])
}
Такой подход позволил быgetData1
а такжеgetData2
Запросы отправляются одновременно, а результаты обратного вызова обрабатываются единообразно.
В идеале мы должны отправлять все асинхронные запросы вместе и ждать результатов.
Однако добиться этого, как правило, невозможно, так как в приведенных выше примерах нам, возможно, придется вызывать в циклеsismember
или один из наших наборов данных зависит от фильтрации другого набора данных.
Вот еще один компромисс.Как и в примере этой оптимизации, есть два набора данных, один с данными фиксированной длины (одиночные цифры), а второй с данными нефиксированной длины.
Первый набор данных будет обрезан после создания данных, чтобы гарантировать, что длина является фиксированным числом.
Второй набор данных имеет переменную длину и должен быть отфильтрован на основе элементов первого набора.
В это время асинхронный вызов первой коллекции займет много времени, и если мы не отфильтруем по первым данным при сборе данных второй коллекции, это вызовет некоторые недопустимые запросы (дублирование сбора данных).
Но после сравнения я все еще чувствую, что более рентабельно заменить два на одновременные.
Как упоминалось выше, номер первого набора примерно однозначный, то есть, даже если второй набор повторяется, он не будет повторять много данных Сравнивая два, мы решительно выбираем параллелизм.
Получив два набора данных, возьмите первый набор для фильтрации данных второго набора.
Если время асинхронного выполнения двух не сильно отличается, такая оптимизация может в основном сэкономить40%
стоимость времени (Конечно, недостаток в том, что давление на поставщика данных увеличится вдвое.).
Дополнительные преимущества замены последовательного на параллельный
Если несколько асинхронных операций выполняются последовательно, медлительность любой из них приведет к увеличению общего времени.
И если вы решите распараллелить несколько асинхронных кодов, одна из операций займет слишком много времени, но она может быть не самой длинной во всей очереди, поэтому это не повлияет на общее время.
постскриптум
В целом эта оптимизация заключается в следующих моментах:
- Разумное использование структур данных (хорошее использование
hash
структура для замены некоторыхlist
) - уменьшить ненужные сетевые запросы (
hgetall
tohmget
) - Измените последовательный на параллельный (включая асинхронные события)
И только что выпущенная свежая сравнительная таблица времени отклика интерфейса: