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).
![](https://habrastorage.org/getpro/habr/upload_files/4bc/d6d/a24/4bcd6da24a4b88a88e79a61aef09e55f.png)
Какие у нас есть варианты?
1. Попытаться решить CPU bound задачи только с помощью Node.js
2. Создать отдельный сервис, написанный на Golang и «общаться» с нашим приложением с помощью запросов/брокера сообщений и т.д. (в данной статье будут использованы обычные http запросы)
3. Использовать Golang для создания wasm файла, что позволит использовать дополнительные методы в Node.js
Скорость и деньги
Я фан старых добрых спагетти вестернов, особенно Хороший, плохой, злой. В этой статье 3 подходы к решению задач, а в этом фильме 3 совсем разных героя, которые очень хорошо характеризуют эти подходы.
Так что… давайте погрузимся в атмосферу тех времен, когда скорость и деньги решали все… Дикий Запад
Node.js (The Good)
![](https://habrastorage.org/getpro/habr/upload_files/567/02f/fb8/56702ffb88b01a03c8c35ace3971173a.gif)
Достоинства:
1. Один и тот же язык (JavaScript) на фронтенде и бэкенде
2. Мастер I/O операции — имеет супербыстрый event loop
3. Самый большой арсенал оружия — npm
Golang (The Bad)
![](https://habrastorage.org/getpro/habr/upload_files/2a0/0a6/f1e/2a00a6f1ef5e5e2e081b0b2ec1c4972c.gif)
Достоинства:
1. Разработан в Google
2. Поддерживается почти на всех OS
3. Горутины – специальные функции Golang, которые отрабатывают конкурентно с другими функциями и методами (хорошо справляются с CPU bound задачами)
4. Простой синтаксически – имеет только 25 ключевых слов
nodejs-golang/WebAssembly (The Ugly)
![](https://habrastorage.org/getpro/habr/upload_files/2ee/73c/e1d/2ee73ce1dbbc5c134a166f2b591abeb6.gif)
Достоинства:
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
![](https://habrastorage.org/getpro/habr/upload_files/2ac/9ad/090/2ac9ad090c2cbe76c7adfc63e2318e01.gif)
Давайте проверим, кто у нас самый быстрый на Диком Западе
«Вы не можете сказать, насколько хорош
человек или арбуз, пока по нему не постучите.»
— Roy Bean
Для этого мы создадим 2 простых сервера
1. Golang сервер
![](https://habrastorage.org/getpro/habr/upload_files/241/ab9/502/241ab9502c016c8248383205eee98802.gif)
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 сервер
![](https://habrastorage.org/getpro/habr/upload_files/35b/3ba/77f/35b3ba77f86faa3e8dca5e0fe327b797.gif)
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 }
Результат:
![](https://habrastorage.org/getpro/habr/upload_files/d60/b03/919/d60b03919abfd71e405687a67f970dd5.png)
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) }
Результат:
![](https://habrastorage.org/getpro/habr/upload_files/ded/5d3/4d2/ded5d34d2ec3deaeceaac6824a8e6e71.png)
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()) }
Результат:
![](https://habrastorage.org/getpro/habr/upload_files/627/52d/e30/62752de302a9e6cfaa903e5da9f5a6ad.png)
Давайте перейдем к старым добрым хеш-функциям. Сначала – 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) }
Результат:
![](https://habrastorage.org/getpro/habr/upload_files/d3c/41c/f76/d3c41cf76e140064fef63301bbcd458d.png)
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) }
Результат:
![](https://habrastorage.org/getpro/habr/upload_files/e5d/ad8/72d/e5dad872d243262338bb2287d262eb7b.png)
Итоговый результат
![](https://habrastorage.org/getpro/habr/upload_files/a5f/340/9f7/a5f3409f7ac4db4ddc0c1af43fb542d0.png)
Что мы сегодня узнали:
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/
Добавить комментарий