Время чтения: 16 минут
Я занимаюсь разработкой на Python и Rails (временно заменяя Ruby). Оба языка имеют свои преимущества и широко используются, но у них есть и недостатки, и самая заметная проблема — производительность. Следовательно, до появления высокой параллелизма необходимо найти в качестве замены высокопроизводительный язык, в основном используемый для замены роли Rails. Golang был первой мыслью, но в сообществе Rails можно сказать, что Elixir является синонимом высокой производительности. Я нашел эту статью, когда искал сравнение двух языков.Оригинальная ссылка
И Elixir, и Go за последние несколько лет приобрели огромную популярность. Эти два языка обычно удовлетворяют сценариям с высокой степенью параллелизма, которые ищут разработчики. Они следуют многим схожим принципам, но оба идут на компромиссы для некоторых особых сценариев.
Давайте сравним два языка по их предыстории, стилю программирования и обработке параллелизма.
источник
Go/Golang разрабатывается Google с 2009 года и работает как двоичный файл (скомпилированный) на развернутых платформах. Это началось как попытка создать новый язык программирования, который устранил бы основные недостатки других языков программирования, сохранив при этом их сильные стороны.
Go отлично справляется с достижением баланса между скоростью разработки, параллелизмом, производительностью, стабильностью, переносимостью и ремонтопригодностью. Таким образом, и Docker, и InfluxDB встроены в Go, и многие крупные компании, включая Google, Netflix, Uber, Dropbox, SendGrid и SoundCloud, используют его в качестве инструмента классификации.
Эликсир с 2011 года Хосе ВалимPlataformatecОн был разработан во время работы на виртуальной машине BEAM, которая называется Erlang VM.
Erlang разрабатывается компанией Ericsson с 1986 года для высокодоступных распределенных телефонных систем. Он расширился во многие другие области, такие как веб-серверы, и достиг доступности 9 девяток (время простоя 31 мс в год).
Elixir предназначен для повышения масштабируемости и производительности виртуальной машины Erlang при сохранении совместимости с экосистемой Erlang. Это достигается за счет использования библиотек Erlang в коде Elixir и наоборот.
Чтобы избежать повторения, мы будем называть Elixir/Erlang/BEAM вместе «Эликсир».
много компанийЭликсир использовался в производстве, включая Discord, Bleacher Report, Teachers Pay Teachers, Puppet Labs, Seneca Systems и FarmBot. И многие другие проекты также создаются с использованием Erlang, включая WhatsApp, службу чата Facebook, Amazon CloudFront CDN, Incapsula, уровень маршрутизации и ведения журналов Heroku, CouchDB, Riak, RabbitMQ и около половины телефонных систем мира.
стиль программирования
Понимание основных принципов каждой среды выполнения позволяет провести надежное сравнение Elixir и Go, поскольку эти строительные блоки являются основой языка.
Go — это язык, который более знаком людям с традиционным опытом программирования на языке C, хотя в нем есть некоторый дизайн в пользу функционального программирования. Статическая типизация Go, указатели и структуры дадут вам ощущение дежавю.
Функции могут быть присоединены к типам структур, что способствует расширению проекта с течением времени. Функции можно создавать где угодно и присоединять к структуре, а не встраивать ее в объект, который необходимо расширить.
Если метод должен вызываться несколькими типами структур, для этого метода можно определить интерфейс, чтобы обеспечить большую гибкость. В отличие от типичных объектно-ориентированных языков программирования, где объект должен быть сначала определен для реализации определенного интерфейса, интерфейс в Go будет автоматически применяться ко всему, что ему соответствует.Экземпляр интерфейса Go
Эликсир больше склоняется к функциональному стилю, но включает в себя некоторые принципы объектно-ориентированного языка, чтобы сделать этот переход менее противоречивым.
Переменные неизменяемы, и, поскольку используется передача сообщений, нет необходимости передавать указатели, что означает, что вызовы функций очень просты. Передать параметры, вернуть результаты и не иметь никакого другого эффекта. Это упрощает такие вопросы, как тестирование и читаемость кода.
Поскольку данные неизменяемы, стандартные операции, такие как циклы for, недоступны, поскольку нет возможности создать увеличивающийся счетчик. Хотя библиотека Enum предоставляет простые шаблоны итерации, вместо таких операций часто используется рекурсия.
Из-за частого использования рекурсии Elixir также специально оптимизирует хвостовые вызовы. Это предотвращает рост стека вызовов, если последним вызовом функции был сам вызов, что позволяет избежать ошибок переполнения стека.
Elixir широко использует сопоставление с образцом, что очень похоже на то, как Go использует интерфейсы. Используя Эликсир, функции могут быть определены как:
def example_pattern(%{ "data" => %{ "nifty" => "bob", "other_thing" => other}}) do
IO.puts(other)
end
Используя режим карты в качестве параметра функции, только когда входящая карта содержит ключ как данные и значение как вложенную карту, а значение также содержит ключ как nift как значение bob и другой ключ как other_thing, функция будет изменена. В это время переменной other будет присвоено значение.
Сопоставление с образцом используется во всем, от параметров функции до присваивания переменных, особенно в рекурсии. вот некоторыепример, вы это почувствуете. Структуры могут быть определены как типы, которые затем также можно использовать при сопоставлении с образцом.
В принципе, эти два метода очень похожи. Как отдельные структуры данных, так и манипулирование данными. В то же время вызовы функций определяются путем сопоставления, Go через интерфейсы и Elixir через сопоставление с образцом.
Хотя Go позволяет вызывать функции с определенным типом, g.area() , на самом деле это то же самое, что и area(g). Единственная разница между ними заключается в том, что в Elixir функция area() должна возвращать результат, а Go реализует ссылку в памяти.
Благодаря такому подходу два языка становятся очень компонуемыми, что устраняет необходимость контролировать, расширять, внедрять и перестраивать большие деревья наследования на протяжении всего жизненного цикла проекта. Это большой плюс для крупных проектов.
Большая разница здесь в том, что при использовании Go этот шаблон определяется вне функций для повторного использования, но если он плохо организован, это может привести к созданию множества дублирующихся интерфейсов. Elixir не может просто повторно использовать шаблоны, но шаблоны всегда определяются там, где они используются.
Эликсир использует «строгую» типизацию вместо статической, и в основном это логический вывод. В Эликсире нет перегрузки символов, поэтому вы не можете использовать+
Соединение двух персонажей может сбить вас с толку. В Эликсире,<>
Может использоваться для объединения строк.
Если вы не понимаете причину этого, вы можете задохнуться. Компилятор может сделать вывод из явного оператора, что знак плюс должен иметь числа с обеих сторон. такой же,<>
Любая сторона должна быть строкой.
Строгая типизация по существу относится к динамической типизации, когда компилятор может фиксировать каждый тип через диализатор, за исключением неоднозначных параметров при сопоставлении с образцом (избегайте, как_
Выделить память для обращения к переменным или параметрам, которые не будут использоваться, как и в Go, только для заполнителей). Комментарии к коду можно использовать для определения типов этих исключений. Преимущество этого заключается в том, что вы получаете большинство преимуществ статической типизации, не теряя гибкости и привилегий метапрограммирования, которые дает динамическая типизация.
Файлы Elixir могут использовать .ex в качестве суффикса для скомпилированного кода или .exs в качестве суффикса для сценариев, скомпилированных во время выполнения, таких как сценарии оболочки. Go всегда компилируется, но компилятор Go настолько быстр, что огромные куски кода могут быть скомпилированы за одно мгновение.
параллелизм
Параллелизм является ключом к этому сравнению. Теперь, когда у вас есть базовое понимание языкового стиля, остальное будет иметь больше смысла.
Традиционно параллелизм, связанный с потоками, является относительно тяжелым. В последнее время некоторые языки программирования начали использовать «легкие потоки» или «зеленые потоки», которые фактически используют планировщик внутри одного потока для управления поочередным выполнением различной логики.
Этот тип модели параллелизма эффективно использует память, но опирается на поток выполнения, указанный во время выполнения. JavaScript использует этот стиль уже много лет. Для простого 🌰, когда вы слышите «неблокирующий ввод-вывод» в JavaScript, это означает, что когда код, выполняющийся в потоке, должен выполнить операцию ввода-вывода, он возвращает управление планировщику.
Совместное планирование против упреждающего планирования
И Elixir, и Go используют планировщики для реализации своих моделей параллелизма, и оба языка изначально поддерживают многоядерность, а JavaScript — нет.
Elixir и Go реализуют планирование по-разному. Go использует совместное планирование, что означает, что код среды выполнения должен вернуть управление планировщику, чтобы другие операции могли выполняться по очереди. Эликсир использует упреждающее планирование, которое устанавливает предустановленное окно выполнения для каждой операции.
Совместное планирование более эффективно при сравнительном анализе, а упреждающее планирование влечет за собой дополнительные накладные расходы из-за начального применения. Но упреждающее планирование является более последовательным, гарантируя, что миллионы небольших операций не будут остановлены одной большой операцией, которая не отказывается от привилегий выполнения.
Программисты Go могут вставлять в кодruntime.Goshed()
, заставляет планировщик выполнять больше проверок в качестве меры предосторожности против потенциально проблемного кода. Обязательная среда выполнения позволяет доверять большему количеству сторонних библиотек и систем реального времени.
Горутины против «Процессов»
Параллелизм выполняется в Go через горутины. Просто добавьте к методу go, подойдет любой метод. следующим образом:
hello("Bob")
// To...
go hello("Bob")
В этом отношении Elixir очень похож на Go. Go использует go для создания горутин, а Elixir порождает «процессы» (здесь процессы порождения не являются процессами операционной системы). Также обратите внимание, что функция должна находиться внутри модуля Elixir.
# From...
HelloModule.hello("Bob")
# To...
spawn(HelloModule, :hello, ["Bob"])
# Or by passing a function
spawn fn -> HelloModule.hello("Bob") end
Большая разница здесь в том,go
ничего не возвращает,spawn
вернет идентификатор процесса.
Обе системы реализуют связь между подпрограммами через очереди сообщений. Go называет это каналами, а Elixir называет это процессными почтовыми ящиками.
В Go вы можете начать с определения канала, и если у вас есть ссылка на этот канал, что угодно может передавать через него сообщения. В Elixir сообщения могут передавать сообщения процессам по идентификатору или имени процесса. Каналы в Go должны определяться типом сообщения, а Elixir использует сопоставление с образцом для обработки почтовых ящиков сообщений.
Процесс, который отправляет сообщение в Эликсир, эквивалентен отправке сообщения в канал, контролируемый горутиной. 🌰: Перейти на канал
messages := make(chan string) // Define a channel that accepts strings
go func() { messages <- "ping" }() // Send to messages
msg := <-messages // Listen for new messages
fmt.Println(msg)
Почтовый ящик процесса Эликсир
send self(), {:hello, "world"}
receive do
{:hello, msg} -> msg # This reciever will match the pattern
{:world, msg} -> "won't match"
end
Оба могут установить тайм-аут при прослушивании сообщений. Поскольку в Go есть разделяемая память, горутины могут быть преобразованы непосредственно в ссылки на память, но во избежание конфликтов требуется мьютекс. В идеале горутина, прослушивающая канал для обновления памяти, не требует мьютекса.
В дополнение к этой функции, вещи будут расширяться.
Erlang определяет набор шаблонов передового опыта, объединенных в «OTP» при работе с параллельной и распределенной логикой. В большинстве случаев в коде Elixir вы никогда не коснетесь нативногоspawn
а такжеsend/receive
функция, которая откладывает абстракцию функциональности.
Task
Пакет реализует простуюasync/await
вызов стиля.Agent
Поддерживайте и обновляйте общее состояние для параллельных процессов.GenServer
Может использоваться для реализации более сложной пользовательской логики.
Чтобы ограничить максимальный параллелизм, который может поддерживать конкретная очередь, конвейеры Go реализуют буферы для приема определенного количества сообщений (ограничение отправителя по количеству). По умолчанию, если он не готов принять сообщение, канал всегда блокируется, если не установлен буфер.
Почтовые ящики процессов Elixir по умолчанию не имеют ограничений на обработку сообщений, но вы можете использоватьTask.async_stream
Определяет максимальное количество параллелизма для операции. Это то же самое, что и конечный буфер, установленный на канале для блокировки отправителей.
Стоимость подпрограмм на обоих языках очень мала, каждая горутина имеет размер всего 2К, а у Эликсира на процесс всего 0,5К. Эликсирные процессы имеют свое собственное независимое пространство кучи, которое будет освобождено отдельно, когда процесс завершится. Горутина использует разделяемую память и освобождает ресурсы с помощью сборщика мусора на уровне приложения.
обработка ошибок
Обработка ошибок — это то, чем два языка отличаются больше всего. От вызовов функций до паники (краш-исключения Go) Go имеет явную обработку ошибок на каждом уровне. Обработка ошибок в Эликсире считается «запахом кода». Перечитаю позже.
Как это работает? Помните, мы говорили об Эликсире ранее?spawn
Будет ли вызов возвращать идентификатор процесса? Его можно использовать не только для отправки сообщений, его также можно использовать для мониторинга процесса и проверки его активности.
Из-за низкой стоимости процессов стандартная практика Elixir состоит в том, чтобы создавать два процесса одновременно, один для запуска и один для мониторинга.
Этот подход известен как модель менеджера. Приложения Elixir, как правило, работают в рамках своего рода дерева наблюдения. Процесс-супервизор создает процесс с помощью функции spawn_link.Если созданный процесс выйдет из строя, процесс-создатель также выйдет из строя. Регулирующие органы справятся с этими сбоями и быстро перезапустят процесс.
Пример разделения с контролируемым процессом. Деление на 0 приведет к сбою процесса, но супервизор немедленно перезапустит его, что позволит продолжить дальнейшие операции. Дело не в том, что обработки ошибок не существует, а в том, что она обрабатывается супервизорным процессом.
Наоборот, в Go нет возможности отслеживать выполнение каждой отдельной подпрограммы. Таким образом, обработка ошибок на каждом уровне должна быть очень явной, что приводит к следующему коду:
b, err := base64.URLEncoding.DecodeString(cookie)
if err != nil {
// Handle error
}
Целью этого является добавление обработки исключений везде, где могут возникать исключения, будь то в горутине или нет.
Таким же образом горутины могут передавать ошибки в каналы. Однако, если возникает паника, каждая горутина должна что-то с этим делать, иначе вся программа рухнет. В отличие от исключений в других языках, паники предназначены для системных событий «что-либо остановить».
Это исключение завернуто в ошибку нехватки памяти. Если горутина вызывает ошибку нехватки памяти, даже с соответствующей обработкой ошибок, программа выйдет из строя из-за состояния среды выполнения с общей памятью.
Для Эликсира, поскольку каждый процесс имеет свое собственное независимое пространство кучи, максимальное значение кучи может быть установлено индивидуально.Как только максимальное значение будет превышено, процесс рухнет, будет собран мусор и перезапущен отдельно, не затрагивая другие вещи.
Это не значит, что Эликсир непобедим. Виртуальным машинам по-прежнему может не хватать памяти по другим причинам, но проблема решается внутри процесса.
Чтобы не притеснять Go, каждый язык, о котором вы когда-либо слышали и использующий разделяемую память, должен иметь дело с этой проблемой. Это просто особенность дизайна Erlang/Elixir.
Способ Go заставляет разработчиков идти прямо туда, где могут возникнуть ошибки, что требует явного проектного мышления, но может привести к хорошо спроектированным приложениям.
Основная концепция Elixir, по выражению Джо Армстронга, заключается в том, что приложение должно работать все время. Точно так же, как вызов планировщика с помощью Go, вы также можете вызывать планировщик с помощью Go.sutureБиблиотека реализует супервизор.
Примечание. Обработчики, которые вы реализуете в Go на большинстве серверов, уже обрабатывают панические ошибки. Поэтому сбоя веб-запроса недостаточно, чтобы убить процесс. Даже в этом случае вам все равно придется решать это на горутине. Это объяснение не подразумевает хрупкости Go, в конце концов, это не так :)
изменчивый и неизменный
При сравнении Go и Elixir важно понимать компромиссы, возникающие при использовании изменяемых и неизменяемых данных.
Go использует тот же стиль управления памятью, что и большинство программистов для изменения или перераспределения памяти через разделяемую память, указатели и структуры данных. Этот метод более эффективен при работе с большими структурами данных.
Неизменяемые данные Elixir используют копирование при записи. В том же пространстве кучи он фактически передает указатель на данные, но всякий раз, когда над ними выполняется операция, создается новая копия.
Например, списку значений будет передан список указателей на не-данные в памяти, в то время как сортировка вернет список указателей в другом порядке, потому что можно было бы доверять значению в памяти как неизменяемому. Изменение значения в списке возвращает новый список с указателем на новое значение. Если этот список будет передан другому процессу, он будет скопирован в новое пространство кучи.
кластер
Еще одним компромиссом между изменяемым и неизменяемым является кластеризация. С Go удаленные вызовы могут выполняться без проблем. Но из-за указателей и общей памяти, если вы вызываете метод на удаленном терминале, который ссылается на локальный параметр, это не будет работать должным образом.
В Elixir все передается через сообщения, поэтому весь стек приложений можно кластеризовать на любом количестве машин. Данные передаются в функцию, которая возвращает ответ. Никакого преобразования в памяти не происходит при любом вызове функции, что позволяет Elixir находиться в другом стеке, на другой машине или в совершенно другом центре обработки данных, точно так же, как любой другой вызов функции в собственном стеке кучи.
Многие приложения не требуют кластеризации, но есть много приложений, которые выигрывают от этого, например программа чата, в которой пользователи подключаются с разных компьютеров, или система связи, используемая горизонтально распределенной базой данных. Оба могут использовать конвейер среды Phoenix и решение для базы данных Mnesia от Erlang соответственно. Кластеризация необходима для горизонтального масштабирования любого приложения, не полагаясь на промежуточные узлы с узкими местами в производительности.
библиотека
Go имеет широкийстандартная библиотека, что позволяет большинству разработчиков делать гораздо больше, не нуждаясь в третьей библиотеке.
Elixirстандартная библиотекаБолее краткий, но также содержит библиотеку Erlang, которая является более полной и включает в себя три встроенные базы данных ETS/DETS/Mnesia. Другие пакеты должны быть извлечены из других сторонних библиотек.
И у Elixir, и у Go есть множество доступных сторонних библиотек. Используйтеgo get
Импорт удаленных пакетов. Elixir использует Mix, инструмент сборки, который вызывает пакеты Hex так же, как пользователи знакомы с управлением пакетами на других языках.
Go все еще работает над реализацией полной схемы управления пакетами. Большая часть сообщества Go предпочитает использовать стандартную библиотеку, когда это возможно, а не стороннюю библиотеку, а не стандартную библиотеку. Несколько инструментов управления пакетами уже доступны.
развертывать
Развертывание в Go простое. Приложение Go запрограммировано как двоичный файл со всеми зависимостями и может запускаться локально или на платформе. Компилятор Go может компилироваться с фреймворками любой архитектуры, независимо от компьютера, на котором он работает. Это самая большая сила Го.
Elixir на самом деле имеет много вариантов конфигурации, но предпочтительный метод — через превосходный пакет ликеро-водочного завода. Он инкапсулирует приложение в двоичный файл, содержащий зависимости (которые можно развернуть вместе в целевой системе).
Разница между ними в том, что архитектура Elixir должна быть такой же, как и архитектура целевой системы. Документы (должна быть ссылка здесь) содержат несколько обходных путей для этого сценария, но самый простой способ — собрать выпуск в контейнере Docker с целевой архитектурой.
В обоих решениях вы должны остановить работающий код, заменить двоичные файлы и перезапустить приложение в большинстве развертываний в сине-зеленом стиле.
Горячее обновление
У Elixir есть еще один метод развертывания — виртуальная машина BEAM. Это немного сложнее, но есть определенные типы приложений, которые подходят для этого сценария. Это называется горячей перезагрузкой или горячим обновлением.
Использование ликероводочного завода--upgrade
аргумент для упрощения команды, которую вы создаете, но это не значит, что вам придется использовать ее все время.
Прежде чем говорить о том, когда его использовать, нам нужно понять, что это такое. Erlang был разработан для телефонных систем и в настоящее время управляет половиной телефонных систем на планете. Он спроектирован так, чтобы никогда не выходить из строя, но когда кто-то звонит в систему, развертывание становится сложной проблемой.
Как я могу развернуть, не отключая всех в системе? Хотите запретить входящий трафик и вежливо подождать, пока все закончат вызов?
Ответ — нет, именно здесь официально требуется горячая перезагрузка.
Из-за изоляции стека между процессами обновление версии не прерывает запущенные процессы. Неактивированные процессы заменяются напрямую. Вновь развернутый процесс существует одновременно с запущенным процессом и принимает новый трафик. Работающий процесс может продолжать работать до тех пор, пока задача не будет завершена.
Это позволяет развертывать многомиллионные обновления системы вызовов без прерывания существующих вызовов. Представьте, что вы заменяете еще одну кучу пузырей в небе новой кучей паровых барабанов, вот как работает горячая перезагрузка. Старые волдыри дрейфовали, пока не лопнули.
Зная это, вы можете увидеть несколько сценариев, в которых Queue может быть полезна:
- Пользователь подключается к системе веб-чата на указанном компьютере.
- Сервер заданий, который обновляет систему, не прерывая выполнение заданий.
- CDN с высоким трафиком подключается к очень медленному веб-запросу
В частности, веб-сокеты, развернутые на машине с миллионами активных подключений, не страдают от мгновенных всплесков попыток повторного подключения из-за прерываний или даже потери информации в процессе передачи. Кстати, именно поэтому Whatsapp использует Erlang. Горячая перезагрузка используется для обновления бортового компьютера в полете.
Недостатком горячего обновления является то, что при необходимости его будет сложнее откатить. Вам, вероятно, не нужно использовать его, если у вас нет сцены, где он вам действительно нужен. Всегда хорошо иметь выбор.
То же самое касается кластеризации, она не всегда нужна, но необходима, когда вы это делаете. Кластеризация, горячие обновления и распределенные системы идут рука об руку.
В заключение
Этот пост длинный, но, надеюсь, он дает подробное описание различий между Elixir и Go. Самый эффективный способ думать о двух языках — думать об Elixir как о системе, а о Go как о специализированной программе.
Быстрые и целенаправленные решения Go отлично подходят для разработки. Elixir создает среду, в которой различные программы могут сосуществовать, работать и взаимодействовать, не мешая друг другу, даже когда они развернуты. Go можно использовать для создания одного микросервиса, тогда как ЭликсирumbrellaВ среде можно создать несколько микросервисов.
Go более сфокусирован и прост в использовании. Это также просто, если понять идею Эликсира. Мир OTP и необъятность Erlang могут пугать, если ваша цель — освоить его, прежде чем использовать.
Оба очень хорошие языки, и я всегда использую их для всего, что касается программирования.
Для очень целенаправленного кода, переносимых инструментов системного уровня, ресурсоемких задач и API-интерфейсов Go трудно превзойти. Для полнофункциональных веб-приложений, распределенных систем, систем реального времени или встроенных приложений я бы выбрал Elixir.