Это произошло в начале 2017 года. Я разрабатывал эту функцию около 2 недель, прежде чем смог объединить ее с мастером. После нескольких конфликтов, двух чашек кофе, сброса настроек, большого исправления и еще двух эспрессо тесты, наконец, прошли. Наконец, я смог совершить это и мне нужно немного отдохнуть. Ведь это была суббота.

Наступил понедельник, и я с улыбкой на лице поехал в офис, думая о том, что я собирался сказать своим коллегам на стендапе. За две недели до этого я начал работать над этой функцией, и с тех пор каждое утро я повторял одно и то же: «вчера я использовал эту функцию, а сегодня все еще работаю над ней». Наконец, я могу переключить его на «готово». Вам может быть интересно, в чем заключалась эта функция? - ну хоть надеюсь 🙈. Что ж, я не хочу раздражать вас аналитической математикой. Чтобы не усложнять: одним из наших клиентов был банк, и я отвечал за кодирование работы по анализу данных, которая должна выполняться на счетах. Чтобы упростить цель этой статьи, давайте представим, что я пытался вычислить Пи (π) быстрее.

Моя «функция Pi» была развернута на той неделе, и производство шло хорошо… Поначалу. Но в какой-то момент один производственный сервер застрял с одной учетной записью, в которой было гораздо больше транзакций, чем на тех, на которых я проводил свои тесты. Этот сервер буквально заморозил и создал пробку в цепочке аналитических заданий.

Меня назначили решать вопрос: снова в аду функции Пи. Извините, но я не могу поделиться исходным исходным кодом функции, поскольку это не проект с открытым исходным кодом, но вот как выглядел мой код: я заменил свой фактический код алгоритмом оценки Pi, известным как Монте-Карло. Метод, вдохновленный примером Napa.js.

Идея состоит в том, чтобы сгенерировать множество случайных точек в квадрате [0,0] [0,1] [1,1] [0,1] и подсчитать, сколько точек находится в 1 радиусном диске с центром в [0,0]. Это не предназначено для идеального вычисления Пи, поскольку мы знаем, что это действительно зависит от точности случайного метода. В этом примере все же стоит продемонстрировать некоторые задачи с интенсивным использованием ЦП, как это было в моей реальной функции.

Метод countInsidePoints генерирует количество точек, указанное в параметре, и возвращает количество точек, сгенерированных внутри круга.

Теперь давайте посмотрим на основной сценарий: я вызвал эту функцию 48 раз с 25 миллионами точек каждый, чтобы получить справедливую оценку числа Пи. В моем реальном случае цифры были действительно выше, но этого достаточно для демонстрации. Почему я не звоню сразу 48 × 25M? Потому что, как и вы, я научился в Node.js 101: никогда не связывайтесь с циклом событий. Когда я чувствую, что вычисления могут занять некоторое время, я предпочитаю писать свой код асинхронным способом - в основном с помощью формы Promise async await - чтобы разделить обработку и позволить циклу событий дышать. Здесь я разделил его на 48 частей.

Обратите внимание, что последний «await Promise.all» ожидает выполнения всех созданных обещаний. Мне нравится этот способ написания асинхронного кода, потому что он не нарушает логику алгоритма с помощью метода обратного вызова.

Вернемся к моей производственной среде: когда я просмотрел журналы, я увидел, что в некоторых случаях мне нужно было выполнить так много вычислений, что обработка одной учетной записи могла занять до часа. Я наконец понял предел однопоточности: одного процессора иногда действительно недостаточно.

Чтобы решить эту проблему, я рассмотрел несколько вариантов: 🐱‍💻

Во-первых, кластерный режим Node.js, но я быстро понял, что он предназначен не для ускорения одного процесса на нескольких процессорах, а для параллельного обслуживания нескольких запросов: это не было моей проблемой.

Затем я прочитал статью о дочернем процессе. Это было именно то, что мне нужно для запуска части моего скрипта в разных потоках, следовательно, для параллельного использования других процессоров. Теоретически я мог бы работать в 4 раза быстрее на 4 процессорах, в 8 раз на 8… только теоретически.

В последнее время я много читал о рабочих потоках, которые довольно близки к дочернему процессу, за исключением того, что теперь есть способ разделять определенные объекты между потоками. В то время я реализовал дочерний процесс, который был наиболее распространенным способом разделения моей обработки между процессорами.

Дочерний процесс и рабочие потоки отлично справляются со своей задачей, но то, что раздражает в этих методах, - это рефакторинг. Мне в значительной степени пришлось создать другую службу, полностью независимую от основной, просто для того, чтобы запускать эту часть параллельно. Мне потребовалось 2 дня, включая тесты, чтобы убедиться, что он работает правильно.

В прошлом году я услышал о компании под названием ScaleDynamics, которая специализируется на оптимизации Node.js и javascript. Он был основан Гилбертом Кабилликом, который, вероятно, является одним из лучших экспертов в области компиляции и виртуальных машин на планете. Среди прочего он известен тем, что продал свой бывший стартап Google после разработки на оптимизированной JVM для Android.

Когда я понял, что Гилберт на самом деле работал над средой выполнения, чтобы разработчикам было проще запускать javascript параллельно, я сразу вспомнил эту болезненную «функцию Pi» и мой мучительный рефакторинг. Я сказал себе, что если бы я знал их технологию раньше, это сэкономило бы мне много времени и проблем. Потом я познакомился с Гилбертом, и теперь я горжусь его командой :). Хорошо, теперь вы думаете, что я пытаюсь продать вам технологию, потому что мне платят. На самом деле, я присоединился к команде в первую очередь потому, что искренне верю, что то, что создала команда R&D, просто великолепно.

Мы назвали пакет Node.js Starnode, а динамический компилятор javascript - warp - - да, это точно так же, как warp drive в Star Trek. Его задача - позволить вам деформировать любую функцию javascript, чтобы заставить ее работать параллельно. Он не только снимает нагрузку со стороны основного потока и цикла событий, но также немедленно увеличивает скорость выполнения, если вы используете его в цикле!

Вернемся к предыдущему сценарию, чтобы посмотреть, как он работает. Я изменил только одну строку кода: я заменил только New PromisesetImmediate… на warp.callAsPromise . Это позволяет моему сценарию выполнять вычисления не только асинхронно, но и параллельно.

Мне потребовался пакет warp, и я изменил первую строку цикла.

Итак, warp.callAsPromise теперь вызывает мою функцию параллельно 48 раз. Но, конечно, он не запускает 48 функций параллельно одновременно: он не порождает 48 процессов.

Warp помещает это в очередь для выполнения в параллельных потоках. По умолчанию количество потоков равно количеству ЦП сервера. Мой компьютер разработки имеет 8 ядер ЦП, поэтому я оставил количество потоков по умолчанию равным 8. Каждый раз, когда функция завершается потоком, следующая деформированная функция в строке принимается за обработанный. Это немного похоже на то, если бы деформация имела свой собственный цикл событий, за исключением того, что обработка выполняется на всех процессорах. Более подробно о его магии вы можете прочитать в документации по деформации.

Этот новый скрипт запускается за 3,7 секунды против 15,5 секунды в начальной версии: это коэффициент ускорения 4,2. Неплохо, а? Я изменил только одну строчку кода, чтобы превратить мою обработку из «асинхронной» в «асинхронную и параллельную» быстрее, сохранив при этом цикл обработки событий полностью свободным. И, самое главное, код легче писать и читать, чем исходную версию.

Если вы заинтересованы в ускорении работы вашего сервера Node.js или заботитесь о своем цикле событий, вам, вероятно, стоит взглянуть на Starnode: это бесплатно для разработчиков, бесплатная пробная версия для предприятий, и это потрясающе! Я обещаю. 😘

Спасибо за чтение! Если вам понравилась эта статья (или нет), не стесняйтесь оставлять комментарий. Я сделаю все возможное, чтобы ответить и соответствующим образом обновить эту статью.