«Используйте правильный инструмент для правильной работы»

Как следует из названия.
На прошлой неделе я экспериментировал с «Связью между несколькими языками» специально для использования на веб-сервере.

Почему?

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

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

Обычно за всем этим стоит микросервис.

Я ничего не знаю о том, как это сделать, но…
Я хочу повторить один, подумал я.

Один с высокой производительностью для обработки входящего запроса, а другой - с неблокирующей асинхронной задачей.

Но какой язык может с этим справиться?
На это довольно легко ответить с моей точки зрения и имеющихся у меня инструментов.

Я выучил несколько языков.

Но специально для этого эксперимента я выбрал Node.js, Go и Rust.

Но почему?

Node.js (JavaScript) сам по себе является «неблокирующим вводом-выводом».

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

Здесь нет Concurrent Lock, Atomic Relation, Null Pointer - всего, что вы нашли при изучении параллелизма на другом языке, здесь просто не существует.

Проще говоря.

Вам не нужно беспокоиться об асинхронности в Node.js

Node.js уже сделает это за вас.

Raw Speed ​​- это не пропускная способность

Выбор языка всегда требует компромисса.

Несмотря на то, что Node.js может легко справляться с асинхронными задачами, его чистая скорость не так хороша, как у других, поскольку это все еще интерпретируемый язык.

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

Следует отметить следующее.

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

Если вас интересует только чистая скорость, конечно, C ++ или Rust - легкий ответ.

Но когда дело доходит до параллельной задачи, такой как запрос нескольких запросов, Just-js, который представляет собой небольшую среду выполнения V8 Engine для Node.js, может просто превзойти C ++, который является одним из самых быстрых веб-серверов.

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

Вы просто не можете сравнивать скорость и параллелизм.
Это другая конструкция

В Node.js. есть много удобных инструментов.

Npm (Node Package Manager) - одно из крупнейших мест на земле, где публикуются пакеты.

Многие разработчики опубликовали множество замечательных инструментов, написанных на Node.js.

ORM, который мы собираемся использовать, - «Prisma».

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

Но проще говоря:

  • Написано на Rust.
  • Автоматически генерировать тип из схемы.
  • Обработка большого количества сложной схемы в простой запрос за сценой.
  • Он поддерживает множество баз данных, использующих тот же синтаксис, даже MongoDB.
  • Даже сложный запрос в Prisma прост и декларативен.
  • Хороший UX как при разработке, так и после развертывания.

В сочетании с неблокирующим вводом-выводом в Node.
Это почти идеальная среда для обработки задач базы данных.

Go известен своей простотой в освоении, высокой производительностью, малым объемом памяти и быстрым временем сборки.

Даже если Go еще не может конкурировать с C ++ по чистой скорости в мире веб-серверов, но он просто достаточно быстр для большинства задач.

Его сообщество довольно велико, в нем есть множество инструментов для создания простого веб-сервера.

Кроме того, Go имеет значительно более низкую строку кода в процессе разработки по сравнению с C ++ и Rust.
В Go не нужно сильно беспокоиться, просто идите и делайте что-нибудь.

Fiber - один из самых популярных веб-фреймворков Go.

Это просто и быстро, особенно если вы раньше использовали Node.js, поскольку его дизайн вдохновлен Express.js, самой популярной веб-платформой в Node.

Rust известен своей скоростью и амбициозностью из-за отсутствия сборщика мусора.

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

Rust работает быстро и может на равных конкурировать с C ++.

Главное то, что в Rust нет сборщика мусора.
Вместо этого у него есть концепция «владения», чтобы гарантировать использование памяти и время ее удаления, что делает его выполнение всегда быстрым, так как он не требует прерывания.

Rust также является самым любимым языком в опросе разработчиков Stack Overflow в течение 6 лет подряд.

Однако у Rust высокая кривая обучения.

Входной барьер Rust высок.

Экосистема Rust все еще мала и недостаточно развита для того, чтобы размещать большинство вещей в производственной среде в контексте веб-сервера.

В нем представлено много новых концепций, и он не подходит для тех, кто никогда не изучает язык низкого уровня (или язык высокого уровня, если считать C ++ высокоуровневым).

Документация по опубликованному пакету в основном создается автоматически, поэтому новичкам ее сложно понять.

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

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

Нет нулевого указателя, ничего, не о чем беспокоиться.

Actix Web - один из самых популярных веб-фреймворков на Rust.

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

Он познакомит вас с моделью актера, когда вещь станет достаточно большой.

Actix также не блокирует ввод-вывод благодаря модели Actor и модифицированной среде выполнения Tokio, известной как «Actix Runtime».

А теперь вернемся к вопросу.
Как создать многоязычный веб-сервер?

Однажды я слышал, как мой коллега и мой младший говорили о RabbitMQ и Kafka.
Итак, я провел свое исследование.

Эти две вещи являются «очередью сообщений».
Она может отправлять сообщения в другое место.

В этот момент я знаю, что делать дальше.

Мне просто нужно найти «промежуточное ПО», которое представляет собой очередь сообщений для связи между службами.

Апач Кафка

Платформа потоковой передачи событий произошла от очереди сообщений.

Он предлагает высокую пропускную способность, а не только ограничение на отправку сообщений.

Сообщение никогда не теряется, поскольку все представляет собой журнал, в котором можно легко обработать миллион сообщений в секунду.

RabbitMQ

Есть возможность гибкой очереди сообщений.

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

RabbitMQ поддерживает приоритет сообщений, при котором вы можете сначала обработать важное сообщение, даже если есть очередь.

Он поддерживает различные шаблоны, особенно при маршрутизации, где вы можете легко реализовать, где должно проходить сообщение.

Сообщение может быть долговечным, уникальным, практически все, что вы можете придумать, может быть реализовано.

Компромисс в том, что производительность далека от Кафки.

Ну, так как мой случай - это просто простая связь между сервисами.
Я просто выберу Kafka.

Позже я узнал, что то, что я пытаюсь реализовать, называется шаблоном «RPC».

Ну, у меня есть кое-какие эксперименты с Kafka, но если не сказать кратко.

Кафке не нравится RPC на веб-сервере

Не поймите меня неправильно, вы можете реализовать его, и это быстро.
Он просто не предназначен для выполнения RPC.

Но это не лучший шаблон для реализации RPC в Kafka.

Вы не должны мне верить, просто погуглите "Kafka RPC",
Вы увидите много проблем с его внедрением, особенно для веб-сервера.

А как насчет RabbitMQ?

Транзакции базы данных не требуется миллион сообщений в секунду, верно?
Базе данных всегда нужно время для обработки запроса перед его возвратом.

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

Кроме того, я также могу установить приоритет сообщения для важного запроса.

RabbitMQ также имеет реализацию RPC в учебнике.

Итак, я реализую один с RabbitMQ, и он работает.
Связь между Node.js и Rust теперь, работает впервые.

Однако мне не нравится, как библиотека RabbitMQ в JavaScript использует много шаблонов и обещаю, когда я думаю, что дизайн должен быть в состоянии справиться с этим более элегантно.

Так что я просто написал обертку для нее и опубликовал ее под названием Usagi.

Usagi означает кролик на японском языке, поэтому UsagiMQ - это переведенный RabbitMQ.

У японцев есть своя особенность в том, что внимание к деталям »разработано и высоко принято во всем мире.

Я хочу убедиться, что библиотека будет более удобной для разработчиков, поэтому я назвал ее Усаги, что в переводе с японского.

Я также встраиваю высокоуровневые функции, такие как sendRPC и getRPC, где я могу запускать только 1 функцию и обрабатывать задачу RPC за меня.

Сначала я реализую один в Node.js для связи Node.js и реализую его в Node.js и Go.

Я думал, что все сделано, и я достиг своей цели, думая, что это простая задача, но ...

Я не прав. Я совершенно ошибаюсь.

Еще далеко ... далеко до конца.

Я тестирую эталонный тест между связью Node.js и Go, где служба будет отложена на 300 миллисекунд и отправит сообщение «Hello World» обратно.

Используя wrk, я реплицирую 2000 одновременных соединений с 5 потоками и выясняю.

700–800 запросов

Ха, это странно.

Я ожидал, что это будет не менее 3000–4000 запросов в секунду, поскольку я отправляю 10 000 активных запросов, и каждый запрос должен обрабатываться менее чем за секунду, даже если он имеет задержку.

Глядя на журнал, среднее время отправки сообщения составляет 3–4 секунды, если я правильно помню.
Даже если я немного отрегулировал, но производительность все равно не увеличилась.

У меня есть гипотеза.

Это может быть связано с тем, что сообщение все еще находится в очереди из-за шаблона RPC.
«Канал сообщений» должен быть активен до тех пор, пока сообщение не будет возвращено из RPC, в противном случае оно не может быть возвращено.

«Ябэ» - вот что я имею в виду, узнав его.

Я не знаю, так ли это на самом деле, и не предполагаю, что моя гипотеза верна, но отсутствие результатов вполне реально.

«Я не могу поставить это в производство», - подумал я,
«Думаю, тогда оба варианта мне не подходят».

Я резюмирую свой вариант использования RabbitMQ как «процесс, в котором производительность не является требованием, и он имеет сложную маршрутизацию».

Для RabbitMQ подходит все, что угодно, кроме скорости.
Без сложной маршрутизации и условий Kafka более подходит, поскольку он является журналом и имеет гораздо лучшую производительность.

Не думайте, что RabbitMQ - плохой инструмент.

Это хороший инструмент, но не для этого, не для высокопроизводительного веб-сервера RPCing.

«Ну, у меня кончился вариант», - подумал я,
«Тогда поищи еще».

Поиск в Интернете очереди сообщений и RPC показал, что есть много вариантов, чем только эти два.

gRPC, ActiveMQ, а потом остановлюсь на одном блоге.

В блоге экспериментировали между несколькими очередями сообщений и библиотекой RPC, и я увидел график.

«ZeroMQ имеет лучшую производительность в тесте».

Ноль- что, почему я никогда об этом не слышал.

«ZeroMQ, использует собственный реализованный протокол под названием zmq», - блог начинает более подробно рассматривать 0mq.

zmq? Затем я вспоминаю день, когда устанавливал ноутбук Jupyter.

Протокол zmq не поддерживается.

«Ах да, я слышал это раньше», во времена изучения Python.

Что ж, тогда я просто узнаю больше о ZeroMQ.

Документация по 0mq довольно длинная, но ее можно подвести.

ZeroMQ, 0mq или ØMQ, или как вы хотите это называть, - это библиотека с открытым исходным кодом для высокопроизводительной связи с использованием собственного протокола «zmq» для обмена сообщениями.

Его производительность превосходит Kafka и поддерживает несколько протоколов, рекомендуется TCP, но вы также можете использовать другие, например. UDP, IPC, inproc (сообщение между потоками).

Имея несколько типов услуг, таких как REQ-RES, ROUTER-DEALER, PUSH-PULL, каждая услуга имеет свой собственный сценарий использования.

Поддержка нескольких языков благодаря активному сообществу.

Недостатком является то, что сообщение недолговечно, а это означает, что сообщение будет потеряно, если все подключенные клиенты умрут.

Подождите, разве это не идеальный предмет для моего дела?

Да, определенно это.

Я хочу сначала заставить его работать, а затем понять, почему это работает.

Я погружаюсь в документ и читаю до конца первой главы.
В главе был представлен шаблон "запрос-ответ".

«Хорошо, вот и все», - подумал я.

Я быстро реализовал один простой веб-сервер, обменивающийся данными на Go и Node.js, оставив Rust позади, потому что я обнаружил, что ZeroMQ в Rust еще не поддерживает асинхронность.

Так почему же он не поддерживает асинхронный режим?

Это связано с тем, что Rust - относительно новый язык, официальная среда выполнения и стандартная библиотека еще не поддерживают асинхронный режим.

Самое близкое, что у нас есть, - это сторонняя библиотека для обработки асинхронного кода, которая появилась недавно.

Самый популярный - между Tokio и async-std.
К тому времени библиотека Zeromq в Rust еще не поддерживает обе среды выполнения.
Что заставляет меня вернуться к Go, так как он достаточно быстр.

После реализации сервера с использованием шаблона «запрос-ответ», возвращающего строку «Hello World» без каких-либо задержек, я снова запускаю тест.

600–1200 запросов в секунду.

Здесь происходит что-то подозрительное.
Я знаю, что мой MacBook Pro 1,4 ГГц работает медленно, но не ожидал, что он будет таким медленным.

Сервер Pure Fiber отвечает на 100 000 запросов в секунду, но при обмене данными с узлом с использованием 0mq он составляет всего 2–3 000 запросов в секунду?

Определенно происходит что-то подозрительное.

Я тестирую тот же код на своем ПК: Ryzen 3 - 3500 3,6 ГГц, 16 ГБ оперативной памяти DDR4, 1 ТБ Samsung 970 Evo SSD с 3,500/2 500 МБ / с для чтения-записи и гигабитным интернет-адаптером, а KDE Neon - это ОС. .

1600–2600 запросов в секунду.

Я ожидал, что это MacBook Pro из-за проблемы с дроссельной заслонкой аккумулятора, но похоже, что это не так.

На ПК все еще медленно.

Может дело в конфиге или проблема с библиотекой Linux, я снова обыскал весь интернет.

После поиска в Интернете в течение нескольких дней, копания в репозитории библиотеки Golang и прочтения, переустановки libzmq на несколько раз, потому что у меня проблемы с его использованием с библиотекой Go.

0mq в Go не имеет официальной библиотеки.
На самом деле есть одна, реализующая ее на чистом Go, но она все еще находится в разработке, автор пока не рекомендует разработчикам использовать ее.

Я использовал pebbe / zmq4 и является наиболее активным.
Библиотека использует CGO (C FFI в Go) для использования библиотеки C, libzmq для использования в Go.

По какой-то странной причине я вообще не могу использовать его в Linux, пока в шутку не подумал, может быть, понижение версии решит проблему.

Итак, я понизил версию с 1.2.6 до 1.2.5, и это почему-то просто работает.
Позже выпущена версия 1.2.7, и она тоже работает, только 1.2.6 не работает.

«Чувак, я потратил на это 10 часов», - подумал я.
«Я попытался собрать его из исходников, несколько версий, и это не работает, но когда я понижаю версию библиотеки Go, и она просто работает? Может, бог меня ненавидит ». как я заснул от усталости.

На следующее утро я тестирую 0mq, добавляя задержку ответа до 3 секунд.

Я повторяю 3 запроса, но по какой-то причине третий запрос отвечает спустя 9 секунд.

Я был в шоке.

Но в документации об этом не упоминалось.

Чтобы подтвердить свою гипотезу, я снова поискал в Google и кое-что нашел.
Просматривая конверсию на каком-то веб-сайте, я натыкаюсь на сообщение.

«Запрос-ответ синхронный»

«Вместо этого вам следует использовать шаблон« Маршрутизатор-Дилер »».

Я атеист, учился в католической школе, но
«Бог определенно меня ненавидит», - подумал я.

Ну, по крайней мере, теперь я знаю проблему.

Теперь, чтобы убедиться, что я больше не повторю эту ошибку, я прочитаю документацию, прежде чем реализовывать ее повторно.

Документация охватывает многое.

И теперь, зная, что шаблон запрос-ответ является асинхронным, я наконец могу снова реализовать этот шаблон.

Еще одна причина, по которой я выбираю шаблон «Маршрутизатор-Дилер», также из-за его идентичности.

Сообщение в ZeroMQ называется «фрейм».

К шаблону «Маршрутизатор-дилер» добавлен заголовок для автоматически сгенерированной «идентичности».

Идентификатор используется для точного возврата данных отправителю.

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

Внедрить это в Node.js не проблема, а в Go?

Голанг не любит одновременных

Все, что нужно делать вне контекста go, особенно отправка RPC в другую службу, как то, что я делаю, довольно… сложно, чем обычно.

Goroutine (обработчик потоков параллелизма) в Go обычно пользуется тем, что он может контролировать, но не в случае ZeroMq, поскольку Go не может его контролировать.

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

Проще говоря, я разделяю дилера на 2 разных потока, которые будут блокировать другие, если один из них активен, поэтому у него не будет одновременных проблем.

Создание 2-х дилеров не решит проблему, потому что сообщение вернется только к отправителю-дилеру.

Я часто сталкивался с ошибкой Go ZeroMQ из-за Go Routine, так как сокет Go ZeroMQ - это значение указателя, которое не является потокобезопасным.
Я должен обернуть его с помощью sync.RwMutex, для меня нет проблем, так как я привык к обработка параллельных задач в Rust там, где дело хуже, чем в Go.

Но дело в том, что я не могу все это контролировать.

Что-то, процедура go просто ... по какой-то причине застряла.
И при попытке я не обнаружила никаких ошибок.
Она просто застряла.

У вас никогда не будет таких проблем с Node.js.

Поэтому я возвращаюсь к созданию Socket для каждого запроса на веб-сервере Go.

Работает.

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

Каждый запрос должен подключаться к другой стороне (Node.js в нашем контексте) и должен выполнить рукопожатие TCP (я не знаю, что он называется в 0mq, но процесс, который 0mq устанавливает тестовое соединение с сервером) перед подключением к моему узлу .js сервер.

Запрос выполняется очень медленно после развертывания тестового сервера в Google Cloud с помощью Kubernetes Engine, даже если сокет размещается в том же развертывании.

Обычно это задержка 0,9–2 секунды для каждого запроса, и я знаю, что это будет проблемой для реального продукта.

Я работаю несколько дней, ищу ответ, чтобы решить проблему.

Если я создам сокет для каждого запроса, запрос будет медленным и будет использовать много памяти.
Но если я использую один запрос, это будет либо способ блокировки, либо очень параллельный запрос, просто застрявший.

Я разделяю дилера на 2 разных потока…

Что, если…

Вместо этого я использую другую службу для полного разделения читателя и отправителя?
Значит, у меня не будет проблем с параллелизмом ?!

Когда эта мысль приходит мне в голову, когда я сплю, я вскакиваю с кровати, охлаждая осенний ветерок.

Шучу, здесь, в Юго-Восточной Азии, нет осени, но свежий воздух на заднем дворе приятный.

Не задумываясь, я запрыгнул в свой MacBook и подключил внешний монитор для написания кода.

Вместо этого я переписываю реализацию с Router-Dealer на Push-Pull.

Шаблон «Push-Pull» используется для отправки асинхронного сообщения на другую сторону.
Вы можете представить, что сообщение помещается в пул с помощью PUSH, а сообщение извлекается из пула с помощью PULL.

Достаточно просто, я закончил переписывание всего за 10 минут.

Использование простого экземпляра Push, Pull на каждом сервере для обработки одновременных сообщений без паники на небезопасном для потоков сокете ZeroMQ.

Но мне все еще нужен sync.RwMutex потокобезопасный ответ на карту для чтения и записи для отображения HTTP-ответа с использованием канала.

«Третий раз оберег», - подумал я.

Итак, я провожу тест для проверки правильности моей гипотезы.

3 Запрос завершен за 3,02 секунды.
Выполнен без блокировки.

Запрос нагрузочного теста 2000 с 5 потоками
Выполнен без одновременных проблем и блокировок.

10 000 запросов в секунду

Наконец-то это работает !!

Несмотря на то, что пропускная способность увеличилась в 5 раз, на моем MacBook Pro с тактовой частотой 1,4 ГГц она меня вполне удовлетворила.

Как я упоминал ранее, иногда скорость и одновременность - не одно и то же.
Видимая пропускная способность здесь составляет 10 000 запросов в секунду, но это не значит, что он не может обрабатывать запросы более 10 000 запросов в секунду.

Я раскручиваю до 5000 с 5 потоками, результат все равно 10000 запросов в секунду.

Но…

Среднее время отклика почти не увеличивается.
Оно возвращается всего за 0,2–0,4 секунды, как бы я ни создавал к нему HTTP-запрос.

Сервер просто не ломается, не тормозит, нет ошибки состояния гонки. Крепкий, как камень.

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

Кроме того, он работает на ЦП 1,4 ГГц на однопоточном сервере.

Есть много способов улучшить производительность, не создавая узких мест со стороны самого сервера.

Я могу в одиночку увеличить производительность, распределив сервер для использования многопоточности и развернув больше экземпляров PUSH-PULL в зависимости от доступного оставшегося потока.
Или переключиться на Kubernetes Horizontal Pod Auto Scaler, применив Nginx Ingress Chart для распределенных балансировка нагрузки.
Не говоря уже о добавлении кеша Redis на стороне Go, чтобы не запрашивать каждый запрос через ZeroMQ.

Убедившись, что все работает, я развернул образ в Google Cloud Kubernetes Engine, и его производительность позволяет использовать Proof of Concept в будущем.

Послесловие

За последние несколько недель произошло много событий, пытающихся добиться экспериментального результата.

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

Я, напротив, хочу вспомнить решение, которое я принял, и путешествие, которое я совершил.

Я хочу вспоминать как хорошее, так и плохое время, которое у меня было, я хочу ценить все время, которое у меня есть.

Даже если меня здесь больше нет, кто-нибудь прочитает мою историю и признает это.

Моя история может быть кому-то полезна в начале моего пути.

Это доказательство моего существования.
В этом смысл жизни.