Используя Node.js, у меня появилась задача улучшить созданный код. Этот код выполняет 60 HTTP-запросов и использует для этого библиотеки.
Выполнение всех HTTP-запросов и сохранение каждого в файл занимает 30 секунд!
Говорят, что эти запросы можно выполнить за 3 секунды с помощью:
1. Правильное управление асинхронными промисами
2. Немного умнее кеширование
3. Не использовать кластер
4. Добавляйте накладные расходы только один раз
Я боюсь, что я не уверен, с чего начать, чтобы понять, что я могу сделать точно.
Итак, приведенный ниже код получает массив из 60 элементов, каждый из которых представляет собой один HTTP-запрос:
const exchanges = ccxt.exchangesОни входят в функцию: worker = async и в конце кода: await Promise.all(workers) дождитесь их завершения.
Я не уверен, с чего начать, чтобы на самом деле быть в состоянии сократиться до 3 секунд. Как можно улучшить скорость этого кода?
'use strict';
const ccxt = require ('ccxt')
, log = require ('ololog').noLocate // npm install ololog
, fs = require ('fs')
// the numWorkers constant defines the number of concurrent workers
// those aren't really threads in terms of the async environment
// set this to the number of cores in your CPU * 2
// or play with this number to find a setting that works best for you
, numWorkers = 8
;(async () => {
// make an array of all exchanges
const exchanges = ccxt.exchanges
.filter (id => ![ 'cap1', 'cap2' ].includes (id))
// instantiate each exchange and save it to the exchanges list
.map (id => new ccxt[id] ({
'enableRateLimit': true,
}))
// the worker function for each "async thread"
const worker = async function () {
// while the array of all exchanges is not empty
while (exchanges.length > 0) {
// pop one exchange from the array
const exchange = exchanges.pop()
// check if it has the necessary method implemented
if (exchange.has['fetchTickers']) {
// try to do "the work" and handle errors if any
try {
// fetch the response for all tickers from the exchange
const tickers = await exchange.fetchTickers()
// make a filename from exchange id
const filename = '/myproject/tickers/' + exchange.id + 'Tickers.json'
// save the response to a file
fs.writeFileSync(filename, JSON.stringify({ tickers }));
} catch (e) { } //Error
}
}
}
// create numWorkers "threads" (they aren't really threads)
const workers = [ ... Array (numWorkers) ].map (_ => worker ())
// wait for all of them to execute or fail
await Promise.all (workers)
}) ()Сократите количество запросов, которые вы делаете. Например, найти способ объединить все «fetchTickers» с «бирж» в один запрос.
Кстати, почему вы используете writeFileSync, если производительность имеет значение? Во время выполнения синхронной операции цикл событий узла блокируется.
@James Я полагаю, что в библиотеке невозможно объединить все «fetchTickers» в один запрос, поскольку это совершенно разные URL-адреса.
@ Alberti Buonarroti Да, пожалуй, это единственное, что я могу улучшить. Интересно, можно ли запускать эти запросы в каком-то еще более «параллельном» подходе, чем это закодировано сейчас? Подсказки, которые я получил, заключаются в более разумном использовании async/await/promises.
Итак, посмотрите, как запустить несколько промисов одновременно и поддерживать порядок.
Возможно, вы захотите проверить Как заставить запрос Axios GET ждать?. Использование await внутри цикла означает, что выполнение не будет продолжаться до тех пор, пока обещание не будет разрешено. Это замедляет ваш код. Вы должны отправить свои запросы в массив и использовать Promsie.all, чтобы дождаться, пока все промисы не будут выполнены.
Подумайте о том, чтобы провести тест, неизвестно, что занимает больше всего времени. sync пахнет, вы могли бы обратиться к нему, прежде чем спрашивать, уже есть fs.promises. Скорее всего, вам не нужно ограничиваться 8 работниками, потому что вы не зависите от ядер ЦП. В случае, если запросы могут быть запущены одновременно, вам могут вообще не понадобиться работники. Неизвестно, что нужно кэшировать, вероятно, конкретно для вашего случая (ccxt).
@ Альберти Буонарроти, это было интересно. Спасибо за ссылку и информацию. Я посмотрю ссылку, которую вы прислали. Это может быть что-то, что является узким местом.
@estus Это интересный тест. Я проверю и это. Да, это (ccxt), кажется, требует довольно много времени для инициализации при запуске кода. Возможно, несколько секунд. Возможно ли это преодолеть?



![Безумие обратных вызовов в javascript [JS]](https://i.imgur.com/WsjO6zJb.png)


Я пытаюсь понять, можно ли сделать это еще быстрее. Я пытаюсь кэшировать все возможные необходимые воспоминания. Прежде чем выполнять запрос .fetchTickers().
Мне удалось снизиться даже до 9 секунд с 15 секунд, как казалось. Но этот код ниже еще на один шаг вперед, но я получаю ошибки компиляции и не уверен, что делаю неправильно.
Ошибка:
ReferenceError: идентификатор не определен
Разве id не передается в объекте «exchange», который вставляется в «exchangesArray»?
Сначала я пытаюсь поместить объект обмена в массив со всем временем, которое требуется:
var exchangesArray = [];Затем с этим «exchangesArray» я пытаюсь выполнить функцию, которая выполняет fetchTickers:
'use strict';
const ccxt = require('ccxt');
const fs = require('fs');
const path = require('path');
//Cache some memories first
var exchangesArray = [];
(async () => {
const allexchanges = ccxt.exchanges.filter((id) => !['coinmarketcap', 'theocean'].includes(id))
.map(async (id) => {
const Exchange = ccxt[id];
const exchange = new Exchange({ enableRateLimit: true });
if (exchange.has['fetchTickers']) {
exchangesArray.push(exchange);
}
});
await Promise.all(allexchanges);
})();
//Use cached memories to do the "fetchTickers()" as fast as possible
(async () => {
const start = Date.now();
const exchanges = exchangesArray;
while (exchanges.length > 0) {
// pop one exchange from the array
const exchange = exchanges.pop()
try {
const tickers = await exchange.fetchTickers();
const dumpFile = path.join(__dirname, 'exchanges', `${id}-Tickers.json`);
await fs.promises.writeFile(dumpFile, JSON.stringify(tickers));
} catch (e) {
console.error(e);
}
}
await Promise.all(exchanges);
const end = Date.now();
console.info(`Done in ${(end - start) / 1000} seconds`);
})();Я борюсь с: exchangesArray.push(exchange); Кажется, что объект «обмена» не имеет всей информации, когда он вставлен в «обмены».
Я думаю, вы делаете вещи более сложными, чем они должны быть. Вы можете выполнить всю работу в обратном вызове map, а затем использовать Promise.all(promises), чтобы дождаться завершения всех операций. Этот процесс занимает больше времени, чем ожидаемые «3 секунды» (15 секунд в моем случае), и возникает много ошибок (например, отсутствует apiToken или fetchTickers не реализованы), но это может быть проблемой с моей средой (я никогда раньше не использовал ccxt и у меня нет apiTokens).
Это реализация, которую я придумал, надеюсь, она поможет вам удовлетворить ваши потребности:
const ccxt = require('ccxt');
const fs = require('fs');
const path = require('path');
(async () => {
const start = Date.now();
const dumps = ccxt.exchanges
.filter((id) => !['coinmarketcap', 'theocean'].includes(id))
.map(async (id) => {
const Exchange = ccxt[id];
const exchange = new Exchange({enableRateLimit: true});
if (exchange.has['fetchTickers']) {
try {
const tickers = await exchange.fetchTickers();
const dumpFile = path.join(__dirname, 'exchanges', `${id}-Tickers.json`);
await fs.promises.writeFile(dumpFile, JSON.stringify(tickers));
} catch (e) {
console.error(e);
}
}
});
await Promise.all(dumps);
const end = Date.now();
console.info(`Done in ${(end - start) / 1000} seconds`);
})();
Спасибо! Код интересный, но файлы .json пустые? Они содержат только эту строку: "[object Object]" Может быть, это близко, и чего-то не хватает, чего я еще не понял?
Правильно, вам нужно преобразовать объект в строку перед записью в файл. Я внес правку в ответ.
Да, я как раз тоже хотел это опубликовать. Я тоже это узнал :) Спасибо за этот код. Здесь также потребовалось 15 секунд для загрузки, что является большим улучшением. Я попробую и посмотрю, можно ли разделить обмены на разные дампы/асинхронные вызовы, чтобы как-то сделать их параллельными и сделать это еще быстрее :) Спасибо!
Запросы и запись файлов уже выполняются параллельно, я не уверен, насколько еще это можно улучшить с точки зрения производительности.
Да, вы правы, я тоже не уверен. У «гуру» библиотеки, к сожалению, есть способ сделать это за 3 секунды, но он ничего не скажет, а вместо этого позвольте мне попытаться выяснить это.
Кто сказал, что это возможно? Я имею в виду, если вы собираетесь делать это синхронно, чем ждать завершения каждого вызова, прежде чем делать следующий. Похоже, вы не хотели бы делать это синхронно.