Всем привет! Сегодня хочу поделиться опытом изучения языка и быстрой реализации высоконагруженного сетевого проекта, использующего так популярные и востребованные сейчас неблокирующие асинхронные сетевые соединения, на новом, красивом, элегантном и очень эффективном языке Rust.
Особый упор в посте сделаю на быстрое и ясное объяснение возможностей языка и платформы специалистам, имеющим большой опыт в веб-разработке, ибо сам таким и являюсь. Существует заблуждение, что кривая вхождения в Rust — очень-очень крутая. Но я покажу, что это далеко не так. Наливаем кофе и погнали!
Краткая история ценностей в программировании
Чтобы материал хорошо залег в голову и сердце, неплохо кратко вспомнить, что люди хотели сделать в программировании за последние 50 лет и что в итоге у них получилось. Без обид, только личное субъективное мнение и холивар, подкрепленный 20-летним опытом разработки.
Низкоуровневые языки: C, C++
Понятно, что можно писать программу сразу в виде цифр на машинных кодах и многие этим занимались на ZX Spectrum, БК0010-01 и на PC — код получается очень быстрым :-) Но мы люди, в голову много информации не помещается, мы отвлекаемся и поэтому даже изобретение ассемблера не особо помогло — код на таком низком уровне пишется очень редко и очень метко и, скорее всего, если вы не занимаетесь разработкой драйверов, микроконтроллеров или хитрых встраиваемых систем, это в жизни не пригодится.
В начале 70-х в Bell Labs придумали язык C, который прижился благодаря лаконичному синтаксису и очень «дешевым» абстракциям, практически став «переносимым ассемблером». Понятно, что если принять постриг, 10 лет писать на C ночами, не есть мясо, молиться и не отвлекаться на соцсети и формы прекрасного пола, то можно писать очень даже полезные и быстрые программы, о чем красноречиво свидетельствуют GNU, прекрасные производительные игры, не всеми любимая, но безальтернативная по качеству Windows и еще можно привести много примеров.
Но обратная сторона медали постоянно дает о себе знать — регулярно открываемые дырки в безопасности (создана целая индустрия по «дыркам в софте»), обусловленные дырками в концепции самого языка С — компилятор подобен безответственному быку, с неимоверной мощью, интенсивным оргазмом и короткой памятью. Любая неосторожность — и можно не просто уронить программу (разыменование нулевого указателя, двойное освобождение указателя, выход за пределы массива), а безвозвратно испортить данные и долго этого не замечать, пока не начнут звонить клиенты и когда уже поздно («неопределенное поведение», отличающееся от компилятора к компилятору).
Бьёрн Страуструп только еще больше запутал ситуацию в начале восьмидесятых, добавив возможности ООП к C. Несмотря на большую популярность, С++, в целом, видится как серия экспериментов над программированием, как таковым, с разной успешностью исходов, в т.ч. летальных. Иногда даже кажется, что смысла в C++ не было с самого начала, либо он был постепенно утерян, родив в результате нагромождение объективно переусложненных и противоречащих друг другу концепций, которых становится все больше и больше с каждым новым стандартом. Несмотря на отличную цель «zero-cost абстракции», позволяющие получать быстрый код, для создания надежного решения, как и на С, требуется выполнение следующих условий:
- опытная, от слова «очень», команда (годы практики, «монахи» программирования)
- хороший статический анализатор кода
- департамент тестирования (в коде могут быть дырки, оставленные безответственным компилятором, которые долго могут давать о себе знать)
- согласованные всеми членами команды требования, которые тщательно контролируются (не используются «сырые» указатели, строгая инкапсуляция, жесточайшая дисциплина именования переменных, инициализация объектов и т.п.)
Понятно, что соблюдение указанных требований, особенно в контексте возрастающей потребности бизнеса на «работающий без внезапных сюрпризов код», очень дорого. Код в подобных проектах пишется долго, тестировать его нужно продолжительное время и тщательно, но, иногда, без C/C++, до изобретения Rust, действительно сложно БЫЛО обойтись.
Еще раз резюме по С/C++ — мы имеем мощный, но «безответственный» компилятор с «текущими абстракциями», очень мало помогающий разработчику. В результате все проблемы перекладываются на плечи программиста. Если хоть один программист в команде не опытный, не очень осторожный и не знает всех тонкостей работы компилятора (на самом деле никто не знает всех тонкостей и их находят пользователи потом) — жди беды. Но зато программа работает быстро и, вероятно, правильно :-) Это, разумеется, породило целый рынок «костылей» — статических анализаторов, платить за которые должен, как оказалось, Заказчик. Возникает вопрос — а разве нельзя было что ли написать более строгий и безопасный компилятор, помогающий разработчику и рождающий программы без сюрпризов и низкоуровневых дырок в безопасности?
Java, C#, Kotlin
Ситуация с откровенно слабым контролем «неопределенного поведения» и очень высокими требованиями к разработчикам в C/C++ породило желание сделать безопасную среду разработки, в т.ч. для интернета, доступную большинству желающих. Так в конце 90-х появилась Java.
В принципе, теперь любой желающий с разным уровнем подготовки мог писать все, что угодно и как угодно и ОНО РАБОТАЛО и что в программу не засовывай — низкоуровневых «дырок» в безопасности и спонтанных крешей уже не было (почти, но они уже были вызваны багами в виртуальной машине и исправлялись централизованно). Оставались только логические «дырки» или скрытый медленный код (рожденный незнанием алгоритмов, понятия алгоритмической стоимости и тормозящий при увеличении объема данных), который был уже не так страшен и позволяет делать программы быстро, а при необходимости — переписывать маленькие куски квалифицированной командой на C/C++.
Интересные и ценные вещи, которые принесла в мир Java, следующие:
- код пишется один раз под виртуальную машину, которая работает на любых архитектурах (windows, linux, mac)
- разработчик больше не управляет памятью напрямую, как в C/C++ — это перекладывается на плечи «сборщика мусора»; так снимается риск неопределенного поведения, незаметной порчи данных и потенциальных низкоуровневых дырок в безопасности
- код компилируется «на лету», во время работы программы (Just_In_Time-компиляция), т.к. известно, что в программе активно и часто выполняется только малая часть, поэтому все компилировать смысла нет
- проще стало писать эффективный многопоточный код, т.к. была строго специфицирована модель памяти (гораздо раньше, чем в C++), но, о чем далее, не все проблемы с «логической надежностью работы нескольких потоков» были решены (остались возможны deadlocks, например или data-races)
Разумеется, появление такой дружелюбной платформы позволило писать много полезных и бесполезных программ, чем сразу воспользовался бизнес. А поддержка обратной совместимости в Java в 15 и более лет обусловила такую популярность технологии в мире энтерпрайза.
Да, у меня тоже возникают аналогии с безалкогольным пивом и резиновыми женщинами.
Однако, далеко не сразу, в Java вылезли следующие проблемы:
- из-за логической «дырки» в системе типизации, которая перешла по наследству из C/C++, в Java попал тип «Null», который и по сей день порождает много проблем и постоянно приводит к падению программ «не очень опытных» программистов, которые не знают про тип Optional и не интересуются монадами из функционального программирования
- проекты на Java, работая иногда незначительно медленнее, требуют на порядки больше оперативной памяти, чем на C/C++, нужной сборщику мусора (у нас несколько проектов «кушают» десятки-сотни гигабайт ОЗУ); это часто не является прямо проблемой, т.к. цены на оперативную память снижаются, но «осадочек остался»
- несмотря на jit-компиляцию и большие инвестиции в ускорение сборщика мусора (которых много и новые появляются регулярно и постоянно говорят: «попробуйте новый GC, он значительно лучше»), уже 20 лет некоторые высоконагруженные проекты на Java работают с регулярными паузами по несколько секунд, которые нельзя убрать никак (происходит вынужденная сборка мусора и как винтики в GC не крути, сборщик мусора просто не успевает все убирать, даже работая параллельно и даже пожирая больше ресурсов, чем сама программа)
- известно, что GUI-приложения на Java иногда зависают и тормозят, вызывая гнев пользователей, из-за той же сборки мусора и надежды нет и не предвидится
- синтаксис языка очень многословен и избыточен (нужно писать и затем читать слова «public static functon» вместо, скажем, «pub fn» или «def»), вынуждая иногда разбивать пальцы об ногти и вызывает объективную резь и кровотечение из глаз
- резкое снижение требований к уровню квалификации разработчиков и достаточная низкая кривая вхождения породило много кода «странной степени свежести», постоянно дающего о себе знать на всю жизнь запоминающимся зловонием
После прочитанного должно быть понятно, что все на Java писать нельзя, это будет писаться довольно медленно, потом будет под нагрузкой регулярно тормозить и зависать на секунды, «жрать» иногда много оперативной памяти (обычно съедается как минимум половина ОЗУ на сервере), производительные игры тоже не попишешь, но какие-то куски бизнес-логики, не особо требовательные к производительности и ОЗУ, особенно многопоточные — вполне можно, полезно и поэтому мы видим такую популярность технологии в мире энтерпрайза. И мы сами регулярно и активно используем Java для сервисов компании.
C#, Scala, Kotlin
«А когда же будет про Rust?» — погодите, нужно еще чуть-чуть подготовиться скушать эту сладкую хурму. Если не рассказать об особенностях других технологий, вы не поймете, почему появился Rust и почему от такой популярный и востребованный.
Итак, пытаясь сделать Java лучше и прогрессивнее, Microsoft, в лице автора TurboPascal/Delphi, в начале нулевых придумал C# и концепцию .NET. Объективно, по мнению многих авторитетных экспертов, да и в курилках разработчиков, «C#» сексуальнее Java, хотя, конечно, пока еще, несмотря на Mono, сильно привязан к Microsoft со всеми втекающими и вытекающими :-)
Scala, несомненно, большой шаг вперед, т.к. язык, конечно, получился научно-заумный и навороченный, взявший немало полезных и бесполезных вещей из мира функционального программирования. Но вот с популярностью что-то как-то не очень понятно. Однако Apache Spark действительно хорош и популярен, нечего сказать.
Kotlin же популярен, т.к. делает Java более эффективной и простой для начинающих разработчиков, особенно на мобильных платформах, у которых нет времени серьезно изучать программирование :-)
Но основная проблема и в C# (на платформе .NET), а также в Scala, Kotlin и в других JVM-языках — осталась. Сборщик мусора, Карл, создающий заметную нагрузку на сервер и остановки выполнения кода на секунды при нагрузках и прожорливые требования к оперативной памяти! И сколько еще языков со сборщиком мусора и jit-компиляцией не придумают, указанные проблемы останутся и надежды пока нет, даже в теории.
Скриптинг
«А как же PHP?». Да, сейчас поговорим про скриптинг. Казалось бы, зачем он? Примерно в конце 80-х стало очевидно, что, если нужно быстро решить задачу с помощью кода, не обязательно супербыстрого, причем безопасно, без неопределенного поведения и низкоуровневых дырок, можно написать скрипт. Скрипты писали и раньше, на bash, perl, awk, но, оказалось, что на python можно писать большие-пребольшие скрипты, особенно научные, причем годами!
Lua нашел свою нишу в геймдеве и машинном обучении (Torch), JavaScript — в веб-разработке как на стороне браузера, так и на стороне сервера (все же знают, что «npm» в Node.js написан на «rust»?). Python — в машинном обучении и анализе данных, а также в системном скриптинге. А PHP — прекрасно справляется с серверными задачами по веб-разработке.
Плюсы скриптинга очевидны:
- Безопасный, эффективный код без неопределенного поведения и низкоуровневых дырок в безопасности, использующий популярные алгоритмы и структуры данных (списки, словари, очереди), создается буквально за минуты
- Очень быстрый старт и пологая кривая вхождения. Достаточно нескольких дней, чтобы разобраться и написать полезный скрипт на python, поменять логику работы nginx через скриптинг на Lua или сделать интеграцию интернет-магазина на PHP
- Если писать скрипты дисциплинированно, соблюдая CodeStyle и максимально строго, то можно очень быстро решать сложные задачи и создавать большие программные продукты: родилось огромное количество научных библиотек на python, системы управления бизнесом и сайтами на PHP, нейросетевая платформа Torch на LuaJit, игровой скриптинг в очень известных и популярных играх и т.п.
- Некоторые виды многопоточного программирования, где узким местом является ввод-вывод, довольно эффективно решаются на python. А используя последние возможности python с futures типа «async/await» можно еще проще решать задачи обработки большого числа сетевых сокетов
- Очень элегантно задача асинхронной обработки большого числа сокетов даже без многопоточности решена в Node.js из коробки
- Создавать веб-страницы, использовать многочисленные unix-библиотеки, ходить в базу данных по-прежнему легко и удобно с помощью PHP и продуктов на его основе
- На python, PHP, JavaScript очень удобно проверять гипотезы и делать прототипы за часы, а не месяцы
- Для аналитики и обработки данных традиционно очень удобно использовать python/R в соединении с большим количеством качественных библиотек (pandas, scikit-learn, seaborn, matplotlib ...)
Однако нельзя не знать потенциальных минусов скриптинга:
- Низкий уровень вхождения и очень быстрая скорость написания скриптов иногда, при отсутствии должного контроля над разработчиками, рождает тонны неэффективного и трудно-поддерживаемого кода на любой платформе
- Отсутствие компилятора, системы типов и статической типизации требует динамической проверки передаваемых в функции и методы параметров на типы и качественного покрытия кода автотестами. Иначе любое изменение в коде может сломать программу и об этом первым узнает клиент, а не разработчик (да, я знаю про TypeScript, но это же костыли и мертвому припарки)
- Объективно и очевидно, что в языках без статической типизации и компиляции, где многие вещи происходят в runtime, нельзя использовать многочисленные оптимизации и поэтому в ряде задач скрипты работают на порядки медленнее и потребляют в разы больше ресурсов. Несмотря на попытки внедрить jit-компиляцию в python (pypy), PHP (hhvm), JavaScript (трансляция в машинный код, вау, Node.js v8), LuaJIT — давайте не будем себя обманывать: все это выглядит как слабо эффективные костыли. Причина в том, и это нужно понять один раз и навсегда, что из-за сознательной слабой типизации языков, для скриптов вряд ли когда-либо получится написать эффективный runtime, приближающийся по скорости к гораздо более строго-типизированным Java/C#
- А в python, похоже, никогда не будет эффективной многопоточности, как в Java/C#, из-за GIL
Однако, т.к. в большинстве бизнес-приложений, в т.ч. системах управления сайтами, CRM и т.п. большая часть времени выполнения кода тратится на запросы в базу данных, то преимущество jit Java/C# глубоко нивелируется скоростью написания решений на PHP/python/JavaScript и лично я, для создания веб-приложений, выберу 10-20 строк на PHP, чем 10 000 строк и кучу потрохов на Java/Spring. А PHP7 как-то разогнали так, что он работает быстрее python3 ;-)
Какой вывод можно тут сделать? Не нужно все задачи решать, как сейчас популярно, только скриптингом — в некоторых случаях разумно взять другой, более подходящий инструмент, если на это есть весомые причины:
- очень высокие сетевые нагрузки и «экстремальная» многопоточность, тысячи — десятки тысяч сетевых сокетов и т.п.
- ограничение на использование оперативной памяти и «железа»
- интенсивная обработка данных и вычисления, матрицы с миллионами сущностей, GPU (хотя тут может помочь python/numpy/pandas)
Часто в компании мы практикуем такой подход:
- быстро делаем решение на PHP/python/JavaScript/Node.js, запускаем в бой и начинаем решать задачи клиентов
- обкатываем фичи, возможности, улучшаем сервисы
- в редких случаях, по опыту, обычно не ранее, чем через несколько лет, часть таких, уже стабильных по функционалу, сервисов, переписывается на C/C++/Java/Golang/Rust
Поэтому лучше начинать, как правило, со скриптинга, запускаться в бой, а дальше, если прямо-прямо нужно, подбирать другие инструменты и это проходит, обычно, очень гладко и без рисков.
Функциональное программирование, Lisp, Haskell, F#
Многие, за целую карьеру разработчика, так и не приходят сюда, а вот зря. Крайне полезно понять, почему появилось ФП (функциональное программирование) и почему в некоторых областях оно так популярно.
Объясню просто. Есть в математике такая нерешаемая проблема, как «halting problem». Если изложить очень простыми словами и совсем нестрого, зато понятно, то нельзя придумать алгоритм, доказывающий, что программа будет работать без багов. Почему? Потому-что изначально люди начали программировать агрессивно и императивно, используя:
- мутабельные переменные
- циклы с условиями
- функции, создающие побочные эффекты
И стали ошибаться. Мы видим это сейчас, наблюдая огромное количество багов как в вебе, так и в десктопных и мобильных приложениях. И как ни покрывай автотестами код, баги все равно продолжают просачиваться, разбегаться по полу и хихикать.
Чтобы остановить этот кошмар наступления глючного и беспощадного софта, еще в конце 50-х появилось направление функционального программирования и язык Лисп. Сейчас же это семейство языков представляет, более-менее адекватно — Haskell.
Несмотря на то, что огромное количество возможных ошибок в коде действительно устраняется за счет:
- алгебраических типов данных и pattern-matching по ним
- отсутствия циклов (только через рекурсию) и мутабельных переменных в пределах доступности вытянутой руки (костыли как это обойти, конечно, можно найти)
- очень строгой статической типизации и, одновременно, очень удобного автовывода типов Хиндли-Милнера
- очень мощных функций, с разделением на «чистые» и «с побочными эффектами», с возможностью частичного применения
- поддержки безопасного параллельного программирования
- поддержки комбинаторов через монады на все случаи жизни (подробнее дальше, в разделе про Rust)
Haskell откровенно не взлетел по причине агрессивных «понтов» — чтобы хорошо понять его терминологию, нужно быть, как минимум, кандидатом наук со специализацией в области одной из лидирующих теорий в современной математике: теории категорий. А еще в Haskell явно намудрили с «ленивостью», то ли граф выполнения память переполнил, то ли выполнение началось, и память кончилась потом, что нередко больно бьет по его боевому применению. Именно поэтому солдаты в бою на территории противника бросают Haskell и берут автомат Калашникова.
И, конечно, совсем забыли про сборщик мусора — в Haskell он, к сожалению, тоже есть, что ставит его по эффективности и производительности на одну ступень с конкурентами типа Java/C#.
Но это не означает, что язык не нужно учить. Haskell развивает программисту, в первую очередь,
Новые «C/C++» — Golang, D
Системное программирование, еще в начале нулевых, реализовывало себя в основном через C/C++ (возможно, нельзя не упомянуть в этом контексте и знаменитый Forth). А мнение Линуса Торвальдса про C++ как тогда, так и сейчас не утратило своей актуальности, несмотря на попытки великого и ужасного Андрея Александреску изменить ситуацию в D (со сборщиком мусора — ну как же так, опять наступили на эти грабли).
Однако, видимо людям надоело постоянно заниматься системным садо-мазохизмом и писать на С/C++ в рыцарских доспехах c парадигмой RAII и кучей ограничений и запретов, изначально доступных в языке и библиотеках. И вот в конце 2009 в недрах Google появляется язык Golang.
Если быть честным и откровенным, то Golang особо ничего нового не принес в мир системного программирования, а, скорее, был сделан шаг назад и кувырок в бок. Golang представляется как урезанная по всем направлениям до минимума Java/C# (ООП порезано и упрощено до неузнаваемости) и да, со сборщиком мусора… Единственные утешения, идущее от Golang:
- встроенные в язык «green threads» (аля корутины), но они есть уже даже в python
- компиляция в нативный код, только непонятно, зачем (возможно, чтобы Docker работал) :-)
И совсем мутная ситуация с пакетным менеджером. Даже в Node.js и python он есть.
Возможно, Golang был создан в рамках конкурентной борьбы крупных корпораций, Google vs Sun/Oracle, за сердца разработчиков, но мы этого, скорее всего, никогда не узнаем :-) Очевидно, что создание «сильно упрощенной Java/C#» привлечет и уже привлекает толпы поклонников решения системных задач, но выиграет ли от этого индустрия — нам еще предстоит увидеть. Хотя Docker на Golang вот уже появился и перевернул мир в верном направлении. А еще объективная польза от Golang — это язык с низким уровнем вхождения и если нет времени изучать Java/C#, а нужно решить системную задачу, то самое то.
Интересно на этом фоне смотрится, конечно, Swift, c «более продвинутым» сборщиком мусора и свежими идеями. Но не всегда же разрабатывать под macOS.
Выход есть — Rust!
Мы же взрослые люди, умеем читать между строк и понимаем, что последние 40-50 лет постоянно предпринимались попытки создать быстрый, безопасный, строгий и системный или близкий к системному язык программирования, желательно с «zero-cost» абстракциями, но, что-то не очень-то получается :-) То язык получается относительно строгий, но, откровенно иногда тормозящий (Java/C#), то очень быстрый, но безнадежно дырявый (C/C++), то еще какой-то, но с цепочкой и гирей на левой ноге — сборщиком мусора. Ну нельзя что ли хорошенько подумать и написать компилятор, который МОЖЕТ ВСЁ вышеперечисленное ОДНОВРЕМЕННО?
Оказывается — можно. Чудо и, наверно, лучшее, что произошло за последние 50 лет в области создания инструментов программирования, случилось в середине 2010 года в Mozilla Research. Коллеги создали компилятор, который обладает следующими возможностями:
- имеет «zero-cost» абстракции (изюминка C/C++ и мечта Бьярна Страуструпа), т.е. ничего не выполняется внезапно тяжело и долго, без разрешения разработчика, даже цепочки итераторов-фильтров-мапперов-замыканий-монад-и-девушек преобразуются в «быстрый машинный код» (серьезно, это не шутка, есть многочисленные примеры ассемблерного кода в результате)
- имеет удобный менеджер пакетов cargo, в котором легко управлять зависимостями и версиями
- компилятор дает гарантию безопасной работы с памятью(* — пояснение звездочки в конце поста): проверяет код и контролирует, что работа с памятью — корректна и не возникнет неопределенного поведения и «низкоуровневых дырок в безопасности», и если код скомпилировался, то, значит, сюрпризов не будет
- компилятор дает гарантию, что память правильно, предсказуемо и однозначно освобождается после использования и поэтому в языке НЕТ СБОРЩИКА МУСОРА от слова СОВСЕМ (а это значит, что память не будет расходоваться впустую, не нужно выделять половину ОЗУ на сервере для GC и, из-за отсутствия runtime, нагрузка на сервер будет минимальной)
- строгая статическая типизация с автовыводом типов по схеме Хиндли-Милнера, без типа Null (!), что ускоряет и упрощает процесс написания кода и значительно сокращает число ошибок еще во время компиляции
- алгебраические типы данных из Haskell и pattern-matching по ним, что позволяет строго и точно описывать данные и обрабатывать их с автоматической проверкой всех вариантов Enumeration (Golang, кстати, так не умеет и это не планируется)
- безопасное многопоточное программирование с проверкой необходимых условий на этапе компиляции (об этом мечтает Java, но надежды нет)
- приложение компилируется в нативный код без рантайма (в Golang есть рантайм — сборщик мусора; в Java/C#/Erlang и т.п. конечно есть рантайм тоже, иногда серьезно замедляющий работу)
- Великолепный и строгий движок макросов, преобразующихся в код перед компиляцией (о чем мечтал C/C++)
Уверен, вы ощутили шок, как и я когда-то. Как такое может быть? Это невозможно! Как соединили несоединимое и впихнули невпикуемое, что не удавалось сделать за последние 50 лет. Оказалось, возможно, «просто» обобщили ошибки предыдущих языков, написали действительно грамотный и пессимистичный компилятор и ввели в обиход несколько не встречавшихся раньше в программировании, уникальных концепций.
Rust можно представить как «легкий» и, разумеется, очень строгий и мощный по своей сути Haskell для системного программирования.
Еще Rust оказался более безопасным языком, чем, внимание, Java :-) — нет Nulls, алгебраические типы данных и pattern-matching по ним, строгие и мощные generics и traits и действительно безопасное многопоточное программирование. Вот это точно никто не ожидал. И код быстрее получается, и ресурсов меньше ест, и ошибок в коде меньше по причине очень строгого, «похожего на Haskell», компилятора и НЕТ СБОРЩИКА МУСОРА.
Теперь кратко обо всем по порядку.
Как же удалось отказаться от сборщика мусора?
А вот так! Память в компиляторе начали представлять как ресурс, «владеть» («ownership») которым может одна и только одна переменная. Если переменная выходит из области видимости, компилятор вставляет вызов деструктора (деструктора может не быть, тогда ничего не вызывается).
Если переменная передается в функцию/метод, то владение передается туда и в конце функции деструктор будет вызван компилятором. Если переменная возвращается из функции/метода, то владение тоже возвращается на уровень выше и деструктор вызывается на уровне выше.
Таким простым способом раз и навсегда избавились от страшной мути в C++, связанной с конструкторами-копировщиками и move-семантики. Если описание показалось сложным, попробуйте написать код с деструкторами в rust и вы увидите, что все очень строго и логично.
Как добились zero-cost абстракций?
В языке нет навороченного ООП и наследования, но есть мощные и гибкие traits, а также средства инкапсуляции. В этом язык, конечно, похож на Golang. Все крутится вокруг структур и методов на них, но, из-за алгебраических типов данных и pattern-matching, код получается точным и строгим (что забыли добавить в Golang, к сожалению). На структуру/данные можно создать ссылку на чтение и ссылку на запись. Но одновременно может быть создан только один тип: одна ссылка на запись или много ссылок на чтение. В терминологии языка это называется заимствование («borrowing»). Это позволяет снизить необходимость копировать объекты, а также избежать гонки данных (за счет «read-write locks», реализуемых заимствованиями). Все это, внимание, строго проверяет компилятор и если код не компилируется, значит нужно его поправить и доказать пессимистичному компилятору, что ошибок больше нет.
Для работы со строками и другими операциями используются низкоуровневые типы slice, что служит, конечно, скорости. Однако есть проблема — строки устроены «немного сложно» и, похоже, они сами появились в языке вне воли разработчиков в результате мутации компилятора; но они работают, работают быстро и предсказуемо и на этом со строками пока все.
Безопасное программирование и функциональщина
Язык очень сбалансировано напичкан возможностями функционального программирования:
- по умолчанию, переменные доступны только для чтения
- глобальные переменные нельзя менять (на самом деле можно, но это отдельная продвинутая и не рекомендуемая техника)
- вместо манипуляций с циклами и массивами, используется принцип итераторов-мапперов-фильтров-замыканий, которые компилируются в быстрый машинный код
- внезапно вы узнаете, что используете монадические вычисления в вышеупомянутых цепочках, которые разумно обрабатывают ошибки, однако тут нет умничания с терминами из теории категорий
В целом понятно, что все сложные и противоречивые концепции системного и многопоточного программирования (освобождение памяти, гонки данных) переложили на систему уникальных аффинных типов со строгими гарантиями компилятора и оно… работает. Строгая система типов и суровая алгебра логики в сочетании с умным компилятором дают вам гарантии получения безопасного/многопоточного кода, работающего со скоростью C/C++ и потребляющего так же мало ресурсов. Причем изначально никто в это не верил.
Многопоточное программирование
За счет описанных выше уникальных для языка и встроенных в компилятор возможностей владения и заимствования, не возникает проблем с порядком установки-снятия блокировок, а многопоточное программирование делается как-то легко и без сюрпризов. Если что-то не так, код просто не компилируется. Я не буду описывать детали трейтов Sync/Send, это нужно увидеть вживую, но ничего сложного и мистического не происходит: реализуете логику, если компилятор ругается, исправляете, вот и всё.
Если хочется реализовать многопоточный сетевой сервис с неблокирующими соединениями, обрабатывающий десятки тысяч сокетов одновременно почти незаметно для процессора, можно взять готовую библиотеку и за 1-2 часа реализовать самые смелые мысли по работе с futures в функционально строгом стиле. Получаем тоже самое, что внутри Node.js и python с async/await, но с гарантией строгих алгебраических типов, работающее гораздо быстрее и с на порядки меньшим потреблением ресурсов. А если результат одинаковый, зачем же платить больше?
Встроенные unit и интеграционные тесты
Удивительно, но об этом тоже подумали и возможность написания unit и интеграционных тестов встроена в среду разработки и инструменты по умолчанию. Сразу пишешь к коду тесты и они выполняются.
Удобный пакетный менеджер — cargo
Приводя аналогию из Java, «Maven под именем cargo» уже встроен в среду разработки, описываются зависимости и дальше магия: качаются исходники, компилируются и все работает. Удобно, кстати, полазать по исходникам, иногда очень помогает.
Heap и стек
По умолчанию, память для структур и объектов выделяется в стеке, однако очень легко ее выделить и потом автоматически освободить в heap. Память, как уже описано выше, автоматически освобождается при выходе ее владельца (ссылающейся переменной) из области видимости. А если очень нужно, умные указатели разных видов тоже есть (здравствуй Swift). Если написанное выше показалось сложным, то это не так — напишите код, выделяющий память в heap и выведите в деструкторе сообщение об освобождении памяти и все станет ясно и понятно. Подвохов никаких нет — память всегда будет освобождена и проверяет это не программист, а компилятор и еще гарантию дает.
Что может быть очень непонятно
На самом деле видно, что действительно получилось создать умный, сбалансированный, очень строгий и быстрый язык для безопасного системного программирования. Да, он еще молод (2 года как стабилизировался) и в библиотеках еще не все, говорят, есть, хотя я нашел все что нужно, от скоростной неблокирующей обработки сетевых сокетов, парсинга аргументов командной строки и подключения к AmazonWebServices до углубленной криптографии c tls/ssl, которую пришлось немного под задачу доработать и это не вызвало никаких проблем.
Однако, некоторые вещи, которые, к счастью, встречаются в реальный задачах очень редко, все-таки потребуют углубленного изучения. К таким особенностям я бы отнес понятие областей видимости («lifetimes») и их использования.
Еще, пока видимо не очень прямо удобным местом является малое количество сред разработки. Неплохо ведет себя IntelliJ с модулем для rust, но иногда не справляется с подсказкой сложных типов. Однако можно писать и в Notepad++ — умный компилятор с гарантиями предупредит вас о ошибках в коде.
Общие принципы разработки на Rust
- Поймите, что вам действительно нужно написать очень быстрый системный код, работающий под высокими нагрузками и потребляющий минимум ресурсов и без всяких накладных расходов типа сборщика мусора. Если нет, напишите скрипт на python/PHP/Node.js и вернитесь к задачке через 2-3 года (в 97% случаев возвращаться не приходится).
- Не бойтесь Rust. Основная изюминка языка — очень умный и математически строгий компилятор, который НЕ ПРОПУСТИТ опасный и некорректный код. Методом проб и ошибок, обходя острые углы, вы научитесь с помощью компилятора программировать строго и безопасно :-)
- Начните изучение языка с "Rust book". Прекрасная, сексуальная книжка, читается легко и увлекательно. Но ее лучше прочитать 2 раза, «прорешивая» все примеры.
- Очень помогает "Rust cookbook". Ее тоже желательно «прорешать» и посмотреть, как работает код, пощупать его.
- Есть литература про Rust для продвинутых пользователей, например Rustonimicon, но я не рекомендую забивать себе голову действительно сложными техническими вещами без нужды.
Поймите еще раз — чтобы овладеть технологией, нужно на ней начать писать полезный для компании код. Компилятор Rust настолько умен, что дает гарантии (на самом деле это очень важно, так не умеет делать компилятор C++) и не скомпилирует опасный/повреждающий память код, поэтому экспериментируйте сколько хотите — и код получите быстрый и безопасный и еще лучше станете программировать :-)
Детали реализации проекта
Раскрою немного детали проекта. В очередь Amazon SQS льются сотни пакетов данных в секунду. Очередь читается, разбирается воркерами локально, каждое сообщение обрабатывается брокером и перенаправляется на другой, внешний сервер. Внешних серверов — несколько. Изначально решение было реализовано через скриптинг: скрипт, занимая процесс в операционной системе, читал сообщения, обрабатывал и пересылал по сети. На нескольких мощных железных серверах (по 8 ядер, 16 ГБ ОЗУ) были запущены сотни скриптов, параллельно читающих из SQS, обрабатывающих и пересылающих данные. Просто, надежно, но потребление железа начало надоедать.
На Rust использовали преимущественно стандартные библиотеки и модули из cargo:
- rusoto_sqs — для работы с Amazon Web Services, никаких вопросов, работает как нужно из коробки
- rustls — для tls, в т.ч. взаимодействия с TLS-сервисами Google, Apple и использованием клиентских сертификатов
- signal-hook — для перехвата kill и доведения до конца транзакции по отправке данных
- futures, tokio-rustls — «ленивая», многопоточная, асинхронная работа с сетевыми TLS-сокетами, аля Node.js
- clap — парсинг аргументов командной строки
- serde_json — формирование json, тут ничего интересного
Без использования «unsafe» блоков и «std::mem::transmute», к сожалению, не обошлось — в стандартной библиотеке не удалось найти инструментов для парсинга бинарных данных в деревья.
Основной, если можно так его назвать, «затык» случился в компиляции — библиотеки не собирались на CentOS6 по причине «устаревшего ассемблера» в binutils, но на CentOS7 никаких проблем не возникло.
Общее впечатление — разработка на Rust напоминает, скорее «строгий скриптинг», чем системное программирование, не особо дольше скриптинга или веб-разработки как по ресурсам, так и по тестированию. При этом — строгая статическая компиляция, отсутствие сборщика мусора и алгебраически типы данных.
Общее ощущение — очень положительные. Еще бы, вместо нескольких железных серверов (8 ядер, 16 ГБ ОЗУ), задача стала решаться одним процессом (с десятками потоков), кушающим не более 5 ГБ ОЗУ и создающим не очень заметную нагрузку на ядра, при трафике в районе 0.5-1 гигабит.
Заключение
Ну вот и закончился длинный, но, очень надеюсь, вдохновляющий и полезный пост про эффективную технологию. Теперь вы знаете еще один инструмент и более смело сможете им воспользоваться при необходимости. Мы обозрели историю развития языков программирования, их возможности и особенности и, возможно, сделали или сделаем правильные выводы. Удачи вам в проектах и хорошего, нет, отличного настроения!
P.S.:
* — да, чуть не забыл. Нужно, конечно, рассказать о блоке «unsafe». В этом блоке можно:
- программировать вещи, недоступные в обычном rust — разыменовывать «сырые» указатели и делать еще несколько потенциально опасных вещей, иногда требующихся в системном программировании. Но все не так страшно:
- блоки unsafe видны и их либо совсем нет, либо в них сосредоточен ответственный код
- стандартная библиотека rust тщательно протестирована и значит мы верим, что блоки unsafe в ней работают
- популярные библиотеки в cargo тоже тщательно тестируются (в rust же встроено все для unit и интеграционного тестирования) и поэтому их можно смело брать и использовать
Т.е. в блоке «unsafe» нельзя заниматься произвольным развратом, доступным в C — а только cтрого определенными видами опасных активностей. Поэтому можно и нужно спать спокойно :-)
Автор: AlexSerbul