Путешествие из Node в Crystal

в 10:47, , рубрики: crystal, javascript, Node, node.js, Блог компании RUVDS.com, разработка, Разработка веб-сайтов, серверные приложения

В компании Duo много лет, в качестве основной платформы, использовали Node. Однако, в последнее время они экспериментировали с очень новым, ещё не вполне оформившимся языком Crystal. По их словам, чем больше они им занимались — тем сильнее к нему привязывались.

Сегодня мы хотим поделиться с вами переводом их рассказа о сильных и слабых сторонах платформ Node и Crystal, и о том, почему в Duo всё больше серверных проектов переводится на Crystal.

Путешествие из Node в Crystal - 1

Node

▍Ожидания

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

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

▍Реальность

У серверного и клиентского кода совершенно разные цели, эти виды кода требуют знания очень разных приёмов работы. Обычно клиентский код — это взаимодействие с пользователем, обновление интерфейса, выполнение запросов данных с сервера. Наши разработчики обычно работали с webpack или browserify для упаковки кода, разрабатывали интерфейсы на React и использовали CSS-фреймворки для упрощения разметки страниц.

На сервере программист имеет дело с SQL-запросами к базам данных, с ORM, с чтением и записью файлов, организует взаимодействие со сторонними API. Потоки данных на сервере подчиняются модели «запрос — ответ». Между запросами все задачи должны обслуживать ответы, и всё это нужно делать специфическим образом. Если некий шаг полагается на результаты, полученные на предыдущем шаге, соответствующие процессы должны выполняться по порядку, если нет — их можно выполнять параллельно.

▍Стандартная асинхронность

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

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

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

Роаль Даль, создатель Node, весьма удачно описал эту ситуацию, сравнивая Node и Go в интервью:

Но интерфейс, который эта система предоставляет программисту, блокирующий, и я думаю, на самом деле, что это — более удачная модель программирования. Использование блокирующего подхода позволяет, во многих ситуациях, гораздо лучше видеть суть выполняемых действий. Скажем, если есть куча последовательных действий, весьма полезна возможность сообщить системе примерно следующее: «Реши задачу А, подожди ответа, возможно — выдай ошибку. Реши задачу B, подожди ответа или выдай ошибку». И в Node, из-за необходимости постоянно «прыгать» между вызовами функций, достичь такого гораздо сложнее.

▍Динамические типы

Любой, кто регулярно программирует на JavaScript, рано или поздно познакомится с ошибкой «undefined is not an object». Эта ошибка возникает, когда вы пытаетесь обратиться к методу или свойству переменной, которую вы считаете объектом, но в которую записано значение null. Недостаточно контролировать то, какие данные передаются между асинхронными участками кода, необходимо ещё и быть в курсе того, что творится с типами в любом месте кода приложения. Каждый раз, когда приложение получает данные от одного процесса и передаёт их другому, может произойти сбой. Если вы не предусматриваете возможность обработки всех возможных значений, сервер выдаст ошибку, или, что гораздо хуже, сделает что-нибудь неожиданное.

Crystal

Во время работы с Node я исследовал множество других языков и платформ, в том числе — Python, PHP, Ruby и Go. Они, как правило, либо были медленнее, чем Node, либо не так удобны для целей разработки. Скорость и синтаксис — это две вещи в языке, которые можно оптимизировать лишь до определённого предела.

Затем, в прошлом году, я прочитал статью о языке Crystal. Он — из нового поколения языков, которые компилируются в машинный код через LLVM. Его синтаксис похож на Ruby (мне это нравится), но работает он так же быстро, как Go (а этому зверю скорости не занимать!).

Crystal всё ещё очень молод, но я решил переделать на нём некоторые серверные части нашей системы управления контентом. Получилось просто замечательно. Вот, что я выяснил в ходе работы:

  • Crystal отличается высокой производительностью. Для моих задач он оказался в 2 раза быстрее Node.
  • Он использует очень мало памяти. Crystal обычно надо менее чем 5 Мб на процесс, а Node — более 200 Мб.
  • У него имеется отличная стандартная библиотека, в результате для решения типичной задачи нам понадобилось лишь 12 зависимостей, в сравнении с сотней зависимостей Node.
  • Код, по умолчанию, выглядит синхронным, он использует, как и Node, цикл событий, но для организации параллелизма применяются легковесные потоки (fibers), взаимодействие организовано через каналы, как у Go. Это упрощает понимание кода.
  • Crystal статически типизирован, поэтому об ошибках можно узнать при компиляции.
  • Crystal выводит типы, в результате, его системой типов легко пользоваться, так как не приходится слишком часто использовать аннотации типов.

Мне так понравилось программировать на Crystal, что мы переписали весь бэкенд нашей CMS на этом языке. Его API совместимо с нашей CMS, основанной на Node, в результате вебсайты можно перевести на новую систему, или вернуть обратно на старую, затратив сравнительно мало усилий. Это важно, так как Crystal — всё ещё молодой проект и нам нужна страховка.

После того, как наша DuoCMS была полностью переписана на Crystal, мне понадобилось протестировать её в продакшне. Собственно говоря, оригинал этого материала размещён на сайте, который работает на Crystal.

▍Сравнение кода на Node и Crystal

Ниже, для сравнения, приведена слегка упрощённая версия кода контроллера, написанного на Crystal и Node.

Вот контроллер на Node (используется фреймворк Express).

const express = require('express')
const app = express()
const bodyParser = require('body-parser')
const UserService = require('user-service')
app.use(bodyParser.json())

app.get('/', function (req, res) {
  res.send('Hello World!')
})

app.post('/api/users', function (req, res) {
  if(request.body){
    UserService.save(request.body)
    .then(function(){
      res.send('user saved')
    })
    .catch(function(err){
      res.send(err)
    })
  }else{
    res.send("no user provided")
  }	
})

app.listen(3000, function () {
  console.log('Example app listening on port 3000!')
})

Вот — контроллер на Crystal (используется фреймворк Kemal).

require "kemal"
require "user"
require "user-service"

get "/" do
  "Hello World!"
end

post "/api/users" do |ctx|
  if (json = ctx.request.body)
    user = User.from_json(json)
    UserService.new.save(user)
    "user saved"
  else
    "no user provided"
  end
end

Kemal.run

Несложно заметить, что структура этих двух примеров очень похожа. Однако, когда отпала необходимость в промисах, общий объем кода уменьшился. При написании более крупных приложений это заметно ещё сильнее. Серверный код DuoCMS 5 состоит из примерно 15609 строк на JavaScript. Объём кода DuoCMS 6 близок к 10186 строкам. На данный момент DuoCMS 6 имеет больше возможностей, для реализации которых потребовалось на 30% меньше кода. При этом, благодаря отсутствию промисов, этот код гораздо легче читать и поддерживать.

Чего не хватает в Crystal?

Разработчики называют текущий релиз Crystal альфа-версией. Тут надо сказать, что мне приходилось использовать гораздо менее проработанные фреймворки, предназначенные для продакшна. В худшем случае я сказал бы, что Crystal сейчас в состоянии бета-версии. Однако, я могу понять осторожность разработчиков. Они говорят об альфа-версии, так как это даёт им пространство для манёвра, для внесения изменений, даже для того, чтобы поломать какое-нибудь API, и так далее.

Я использую Crystal уже примерно год и столкнулся лишь с немногими изменениями, которые объясняются развитием проекта и тем, что это — всё ещё альф-версия. У меня было больше проблем с обновлением React на фронтенде. Кроме того, стоит сказать, что Crystal написан на Crystal, то есть, если что-то окажется нерабочим, вы можете вносить исправления в язык и в стандартную библиотеку (я так и поступал).

На данный момент основными недостающими возможностями Crystal можно назвать следующие:

  • Всё ещё нет поддержки Windows (меня это не беспокоит, работаю я на Mac, код разворачиваю на Linux).
  • До сих пор нет настоящего параллелизма (в Node его тоже нет).
  • Нет инкрементной компиляции (это было бы очень удобно, так как сейчас, для компиляции нашей системы после внесения изменений в код, требуется около 8 секунд).
  • Существует не так много хорошо поддерживаемых опенсорсных библиотек для Crystal, но здесь всё придёт в порядок, когда начнётся использование Crystal в серьёзных проектах.

Для нас ни один из перечисленных недостатков не стал причиной отказаться от Crystal. Мне понадобилось внести несколько дополнений, реализующих отсутствующие возможности, в используемые нами библиотеки, но при работе с Node такое тоже случалось. В итоге могу сказать, что я весьма доволен Crystal.

Итоги

Стоит ли вам попробовать Crystal? Да, стоит! Штука это действительно замечательная. На Crystal приятно программировать, код просто читать и править. И, кстати, чем больше людей будет пользоваться Crystal и вносить вклад в разработку этого языка — тем лучше он будет становиться. Хотите увидеть всё своими глазами? Вот инструкции по установке. Вот — сайт проекта. А это — чат Crystal, если что — пишите мне на @crisward.

Если вы спросите — следует ли вам использовать Crystal в продакшне, ну — это как хотите. Лично я думаю, что единственный способ сделать что-либо пригодным к практической эксплуатации — попробовать это на тех задачах, на которых допустимы сбои, а потом постепенно переходить на это в более масштабных проектах. Мы не используем Crystal везде, например — на проектах с очень высоким трафиком, или на критически важных участках. Мы мониторим все наши сайты и регулярно их бэкапим, кроме того, Node всегда на подхвате — на тот случай, если с Crystal вдруг что-нибудь приключится.

Уважаемые читатели! Планируете ли вы попробовать Crystal в своих проектах?

Автор: ru_vds

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js