Как стать автором
Обновить

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

Время на прочтение7 мин
Количество просмотров9.6K

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 файла.
Спасибо всем за плодотворное обсуждение.

Теги:
Хабы:
Всего голосов 5: ↑2 и ↓3-1
Комментарии18

Публикации

Истории

Работа

Go разработчик
125 вакансий
React разработчик
68 вакансий

Ближайшие события