Использование Golang для разработки Node.js приложений (Node.js: In Go We Trust)

от автора

Harmonica : [одному из трех мужчин] Ты Фрэнк?
Snaky : Фрэнк послал нас.
Harmonica : Вы привели для меня коня?
Snaky : Кажется… кажется у нас не хватает одного коня.
Harmonica : Вы привели на двух коней больше.

(Once Upon a Time in the West, 1968)

Меня зовут Алексей Новохацкий, я – Software Engineer. Сейчас работаю над архитектурой высоконагруженных систем, провожу технические собеседования, воплощаю в жизнь собственные проекты.

Как известно, Node.js хорошо справляется с I/O intensive задачами. А вот для решения CPU bound мы имеем несколько вариантов – child processes/cluster, worker threads. Также есть возможность использовать другой язык программирования (C, C++, Rust, Golang) в качестве отдельного сервиса/микросервиса или через WebAssembly скрипты.

В данной обзорной статье будут описаны подходы к использованию Golang в разработке Node.js приложений для запуска некоторых CPU intensive задач (простой суммы чисел, последовательности Фибоначчи, а также для таких хеш-функций как md5 и sha256).

Какие у нас есть варианты?

1. Попытаться решить CPU bound задачи только с помощью Node.js

2. Создать отдельный сервис, написанный на Golang и «общаться» с нашим приложением с помощью запросов/брокера сообщений и т.д. (в данной статье будут использованы обычные http запросы)

3. Использовать Golang для создания wasm файла, что позволит использовать дополнительные методы в Node.js

Скорость и деньги

Я фан старых добрых спагетти вестернов, особенно Хороший, плохой, злой. В этой статье 3 подходы к решению задач, а в этом фильме 3 совсем разных героя, которые очень хорошо характеризуют эти подходы.

Так что… давайте погрузимся в атмосферу тех времен, когда скорость и деньги решали все… Дикий Запад

Node.js (The Good)

Достоинства:

1. Один и тот же язык (JavaScript) на фронтенде и бэкенде

2. Мастер I/O операции — имеет супербыстрый event loop

3. Самый большой арсенал оружия — npm

Golang (The Bad)

Достоинства:

1. Разработан в Google

2. Поддерживается почти на всех OS

3. Горутины – специальные функции Golang, которые отрабатывают конкурентно с другими функциями и методами (хорошо справляются с CPU bound задачами)

4. Простой синтаксически – имеет только 25 ключевых слов

nodejs-golang/WebAssembly (The Ugly)

Достоинства:

1. Доступный везде

2. Дополняет JavaScript

3. Дает возможность писать код на разных языках и использовать .wasm скрипты в JavaScript

Немного подробнее о последнем подходе.

Код, написанный на Golang может быть преобразован в .wasm файл с помощью нижеприведенной команды, если установить Operating System как “js” и Architecture как “wasm” (список возможных значений GOOS и GOARCH находится здесь):

GOOS=js GOARCH=wasm go build -o main.wasm

Для запуска скомпилированного кода Go необходимо связать его через специальный интерфейс wasm_exec.js. Содержимое по ссылке:

${GOROOT}/misc/wasm/wasm_exec.js

Для использования WebAssembly я применил @assemblyscript/loader и создал модуль nodejs-golang (кстати, @assemblyscript/loader — это единственная зависимость данного модуля). Этот модуль помогает создавать, билдить и запускать отдельные wasm скрипты или функции, которые могут быть использованы в JavaScript коде

require('./go/misc/wasm/wasm_exec'); const go = new Go(); ... const wasm = fs.readFileSync(wasmPath); const wasmModule = await loader.instantiateStreaming(wasm, go.importObject); go.run(wasmModule.instance);

Кстати, другие языки аналогично могут быть использованы для создания .wasm файла.

C: emcc hello.c -s WASM=1 -o hello.html  C++: em++ hello.cpp -s WASM=1 -o hello.html  Rust: cargo build --target wasm --release

Давайте проверим, кто у нас самый быстрый на Диком Западе

«Вы не можете сказать, насколько хорош

человек или арбуз, пока по нему не постучите.»

— Roy Bean

Для этого мы создадим 2 простых сервера

1. Golang сервер

package main import (     ...     "fmt"     ...     "net/http"     ... )  func main() {     ...     fmt.Print("Golang: Server is running at http://localhost:8090/")     http.ListenAndServe(":8090", nil) }

2. Node.js сервер

const http = require('http'); ... (async () => {   ...   http.createServer((req, res) => {     ...   })   .listen(8080, () => {     console.log('Nodejs: Server is running at http://localhost:8080/');   }); })(); 

Мы будем измерять время выполнения каждой задачи – для Golang сервера это будет время непосредственного выполнения функции + сетевая задержка запроса. В то время как для Node.js и WebAssembly — это будет только время выполнения функции.

Финальная дуэль

1. “ping” (просто проверим сколько времени уйдет на выполнение запроса)

Node.js

const nodejsPingHandler = (req, res) => {   console.time('Nodejs: ping');    const result = 'Pong';    console.timeEnd('Nodejs: ping');    res.statusCode = 200;   res.setHeader('Content-Type', 'application/json');   res.write(JSON.stringify({ result }));   res.end(); };

Golang

// golang/ping.js  const http = require('http');  const golangPingHandler = (req, res) => {   const options = {     hostname: 'localhost',     port: 8090,     path: '/ping',     method: 'GET',   };    let result = '';    console.time('Golang: ping');    const request = http.request(options, (response) => {     response.on('data', (data) => {       result += data;     });     response.on('end', () => {       console.timeEnd('Golang: ping');        res.statusCode = 200;       res.setHeader('Content-Type', 'application/json');       res.write(JSON.stringify({ result }));       res.end();     });   });    request.on('error', (error) => {     console.error(error);   });    request.end(); };
// main.go  func ping(w http.ResponseWriter, req *http.Request) {     fmt.Fprintf(w, "Pong") }

nodejs-golang

// nodejs-golang/ping.js  const nodejsGolangPingHandler = async (req, res) => {   console.time('Nodejs-Golang: ping');    const result = global.GolangPing();    console.timeEnd('Nodejs-Golang: ping');    res.statusCode = 200;   res.setHeader('Content-Type', 'application/json');   res.write(JSON.stringify({ result }));   res.end(); };
// main.go  package main  import (     "syscall/js" )  func GolangPing(this js.Value, p []js.Value) interface{} {     return js.ValueOf("Pong") }  func main() {     c := make(chan struct{}, 0)      js.Global().Set("GolangPing", js.FuncOf(GolangPing))      <-c }

Результат:

2. Следующей задачей будет просто сумма двух чисел

Node.js

const result = p1 + p2;

Golang

func sum(w http.ResponseWriter, req *http.Request) {     p1, _ := strconv.Atoi(req.URL.Query().Get("p1"))     p2, _ := strconv.Atoi(req.URL.Query().Get("p2"))      sum := p1 + p2      fmt.Fprint(w, sum) }

nodejs-golang

func GolangSum(this js.Value, p []js.Value) interface{} {     sum := p[0].Int() + p[1].Int()     return js.ValueOf(sum) }

Результат:

3. Далее последовательность Фибоначчи (получаем 100000-е число)

Node.js

const fibonacci = (num) => {   let a = BigInt(1),     b = BigInt(0),     temp;    while (num > 0) {     temp = a;     a = a + b;     b = temp;     num--;   }    return b; };

Golang

func fibonacci(w http.ResponseWriter, req *http.Request) {     nValue, _ := strconv.Atoi(req.URL.Query().Get("n"))      var n = uint(nValue)      if n <= 1 {         fmt.Fprint(w, big.NewInt(int64(n)))     }      var n2, n1 = big.NewInt(0), big.NewInt(1)      for i := uint(1); i < n; i++ {         n2.Add(n2, n1)         n1, n2 = n2, n1     }      fmt.Fprint(w, n1) }

nodejs-golang

func GolangFibonacci(this js.Value, p []js.Value) interface{} {     var n = uint(p[0].Int())      if n <= 1 {         return big.NewInt(int64(n))     }      var n2, n1 = big.NewInt(0), big.NewInt(1)      for i := uint(1); i < n; i++ {         n2.Add(n2, n1)         n1, n2 = n2, n1     }      return js.ValueOf(n1.String()) }

Результат:

Давайте перейдем к старым добрым хеш-функциям. Сначала – md5 (10k строк)

Node.js

const crypto = require('crypto');  const md5 = (num) => {   for (let i = 0; i < num; i++) {     crypto.createHash('md5').update('nodejs-golang').digest('hex');   }   return num; };

Golang

func md5Worker(c chan string, wg *sync.WaitGroup) {     hash := md5.Sum([]byte("nodejs-golang"))      c <- hex.EncodeToString(hash[:])      wg.Done() }  func md5Array(w http.ResponseWriter, req *http.Request) {     n, _ := strconv.Atoi(req.URL.Query().Get("n"))      c := make(chan string, n)     var wg sync.WaitGroup      for i := 0; i < n; i++ {         wg.Add(1)         go md5Worker(c, &wg)     }      wg.Wait()      fmt.Fprint(w, n) }

nodejs-golang

func md5Worker(c chan string, wg *sync.WaitGroup) {     hash := md5.Sum([]byte("nodejs-golang"))      c <- hex.EncodeToString(hash[:])      wg.Done() }  func GolangMd5(this js.Value, p []js.Value) interface{} {     n := p[0].Int()      c := make(chan string, n)     var wg sync.WaitGroup      for i := 0; i < n; i++ {         wg.Add(1)         go md5Worker(c, &wg)     }      wg.Wait()      return js.ValueOf(n) }

Результат:

5. … и наконец sha256 (10k строк)

Node.js

const crypto = require('crypto');  const sha256 = (num) => {   for (let i = 0; i < num; i++) {     crypto.createHash('sha256').update('nodejs-golang').digest('hex');   }   return num; };

Golang

func sha256Worker(c chan string, wg *sync.WaitGroup) {     h := sha256.New()     h.Write([]byte("nodejs-golang"))     sha256_hash := hex.EncodeToString(h.Sum(nil))      c <- sha256_hash      wg.Done() }  func sha256Array(w http.ResponseWriter, req *http.Request) {     n, _ := strconv.Atoi(req.URL.Query().Get("n"))      c := make(chan string, n)     var wg sync.WaitGroup      for i := 0; i < n; i++ {         wg.Add(1)         go sha256Worker(c, &wg)     }      wg.Wait()      fmt.Fprint(w, n) }

nodejs-golang

func sha256Worker(c chan string, wg *sync.WaitGroup) {     h := sha256.New()     h.Write([]byte("nodejs-golang"))     sha256_hash := hex.EncodeToString(h.Sum(nil))      c <- sha256_hash      wg.Done() }  func GolangSha256(this js.Value, p []js.Value) interface{} {     n := p[0].Int()      c := make(chan string, n)     var wg sync.WaitGroup      for i := 0; i < n; i++ {         wg.Add(1)         go sha256Worker(c, &wg)     }      wg.Wait()      return js.ValueOf(n) }

Результат:

Итоговый результат

Что мы сегодня узнали:

1. Существует Node.js, который хорошо выполняет свою работу

2. Существует Golang, который хорошо выполняет свою работу

3. Существует WebAssembly (а теперь и мой модуль nodejs-golang), который хорошо выполняет свою работу

4. Golang можно использовать как: самостоятельное приложение, сервис/микросервис, источник для wasm скрипта (который затем можно использовать в JavaScript)

5. Node.js и Golang имеют готовые механизмы использования WebAssembly в JavaScript

Выводы:

“Скорость – это хорошо, но точность – это все.” — Wyatt Earp

1. Не запускать CPU-bound задачи с Node.js (если есть возможность)

2. На самом деле лучше не делать никакой задачи, если это возможно

3. Если вам нужно запустить CPU-bound задачу, в Node.js приложении – попробуйте сделать это с помощью Node.js. (будет не так плохо)

4. Между производительностью (с использованием других языков) и удобством чтения (продолжая сохранять только код JavaScript в команде, поддерживающей только JavaScript), лучше выбрать читабельность

5. «Хороший архитектор отталкивается от того, что решение еще не принято, и формирует систему таким образом, что эти решения все еще могут быть изменены или отложены как можно дольше. Хороший архитектор максимизирует количество не принятых решений» — Clean Architecture by Robert C. Martin

6. Лучше «держать отдельно отдельно». Создайте сервис/микросервис для тяжелых вычислений – при необходимости будет легко масштабироваться

7. WebAssembly полезен прежде всего для браузеров. Бинарник Wasm меньше и проще для парсинга, чем код JS и т.д.


Спасибо за прочтение. Надеюсь, вам понравилось.

Для получения дополнительной информации и возможности проверить результаты, пожалуйста, посетите мой github.

Модуль, написанный специально для статьи — nodejs-golang

До новых приключений!

*Примечание:
Благодаря внимательным читателям в тексте было обнаружено несколько неточностей. В частности, исправлен вызов синхронных функций (md5/sha256) с помощью async/await (Node.js). Прошу прощения за допущенную ошибку в написании кода. Результаты замеров проверены — отклонения не имеют значительного влияния на исследование в целом и составляют ±5-10% для вышеупомянутых функций. Полная статистическая оценка не была проведена, так как это выходило за рамки цели данной статьи — показать разницу между использованием Golang для написания отдельного сервиса и для создания wasm файла.
Спасибо всем за плодотворное обсуждение.


ссылка на оригинал статьи https://habr.com/ru/articles/593537/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *