Источник фото: Arroyo
Источник фото: Arroyo

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

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

Так почему же мы решили использовать Rust и как это сработало?

(Краткая) история языков для инфраструктуры данных

C++

Зарождение современной инфраструктуры данных можно отнести к публикации трех монументальных статей Google: Google File System (2003 г.) и Map reduce (2004 г.), за которыми последовала Bigtable (2006 г.). В этих документах описывалось, как Google удалось хранить, обрабатывать и обслуживать петабайты данных, которые составляли индекс ранней сети.

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

Эти системы были написаны на языке C++, который в то время был стандартом системного программного обеспечения. C++ предлагал точный контроль над памятью и позволял Google максимально эффективно использовать свое оборудование, что было особенно важно, когда ваши серверы были процессорами Pentium III с тактовой частотой 1,4 ГГц и 2 ГБ оперативной памяти!

Java

Вдохновленный статьями Google, инженер по имени Дуг Каттинг решил создать собственную реализацию новой поисковой системы. Позже компания Yahoo наняла его на работу, и эта работа привела к созданию проектов с открытым исходным кодом таких как Hadoop File System (HDFS) и Hadoop Map Reduce.

Ранее Каттинг создал на Java поисковый индекс Lucene (прим. пер.: библиотека для высокопроизводительного поиска) и остановился на языке для Hadoop. Это сильно повлияло. Следующая волна систем больших данных возникла в последующие годы, когда всё больше компаний обнаружили, что им нужна распределенная обработка данных, включая HBase, Cassandra, Storm, Spark, Hive, Presto. Почти все они были написаны на Java или других JVM языках.

У Java было много преимуществ перед C++: сборщик мусора (GC) освобождал программистов от утомительного и подверженного ошибкам ручного управления памятью, а виртуальная машина позволяла запускать Java на множестве различных архитектур и операционных систем с минимальными изменениями. Кроме того, этот язык был намного проще, чем C++, что позволяло новым инженерам быстро освоиться.

Go

Тем временем Google продолжал писать свою инфраструктуру на C++, а программное обеспечение прикладного уровня — на Python и Java. В 2009 году они выпустили новый язык, предназначенный для этой ниши уровня приложений: Go. Разработанный частично Робом Пайком и Кеном Томпсоном — легендами в этой области — и оснащенный примитивами параллелизма высокого уровня и веб-библиотеками, он быстро нашел применение в мире распределенных систем.

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

Изначально он был написан на Java, но в какой-то момент разработки перешёл на Go. Его успех привел к огромной волне систем данных Go, включая CockroachDB, InfluxDB и Pachyderm.

C++ возвращается, и на сцену выходит Rust

Мир технологий бесконечно цикличен, и за последние несколько лет произошел еще один поворот колеса. Такие проекты, как ScyllaDB и Redpanda, добились успеха, переписав системы с Java (Cassandra и Kafka соответственно) на C++ для повышения производительности и более предсказуемых операций. Новые базы данных и механизмы запросов, такие как DuckDB и Clickhouse, пишутся с нуля на C++.

Rust 1.0 был выпущен в 2015 году как современный системный язык, стремящийся занять ту же нишу, что и C++. В Rust нет сборщика мусора, он фокусируется на zero-cost abstractions и обеспечивает низкоуровневый контроль над процессом выполнения. В отличие от C++, его компилятор может проверять нарушения безопасности (например, использование неинициализированной памяти или двойное освобождение) и предотвращать состояния гонки в многопоточном коде. Его стали часто выбирать как для переписывания компонентов существующих систем с Go и Java (TiKV , InfluxDB IoX), так и для реализации новых систем (MaterializeReadyset).

Почему Rust

После этой истории, почему мы решили использовать Rust? Одна из причин весьма эгоистична: я являюсь поклонником Rust с тех пор, как познакомился с этим языком в 2014 году, и уже много лет использую его в личных проектах. Но у меня не было возможности внедрить его (за исключением небольших инструментов) в компаниях, в которых я работал, и при открытии компании впервые никто не мог сказать мне «нет». Короче говоря, Rust чрезвычайно хорошо подходит для написания инфраструктуры данных, сочетая в себе производительность и контроль C++ с безопасностью и простотой разработки Java.

Отличная производительность по умолчанию

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

Например, самый быстрый Java-код совсем не похож на идиоматический Java. Он в значительной степени полагается на примитивы и массивы, а не на объекты и контейнеры стандартной библиотеки. Он написан для минимизации создания и уничтожения объектов и снижения нагрузки на сборщик мусора. А в системах, ориентированных на данные, фактическое хранилище данных часто обрабатывается библиотекой C++, такой как RocksDB, что требует медленной связи через Java интерфейс (JNI).

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

Например, итераторы и лямбды — это мощные высокоуровневые функции, но такой код (взят из системы водяных знаков Арройо, определяющей самый старый водяной знак с учетом простоя и потенциально отсутствующих водяных знаков):

self.cur_watermark = self
    .watermarks
    .iter()
    .fold(Some(Idle), |current, next| {
        match (current?, (_next)?) {
            (EventTime(cur), EventTime(next)) => Some(EventTime(cur.min(next)))
            (Idle, EventTime(t)) | (EventTime(t), Idle) => Some(EventTime(t)),
            (Idle, Idle) => Some(Idle),
        }
    });

очистит и оптимизирует до чего-то, по крайней мере, такого же быстрого, как написанный вручную цикл for. А поскольку такой код итератора более понятен оптимизатору, его часто можно более эффективно векторизовать, чем цикл, написанный вручную.

Нет сборщика мусора/Да, безопасность

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

В частности, системы потоковой обработки обычно представляют собой трудные случаи для GC. Обработка событий очень быстро создает множество объектов, сохраняя при этом некоторые из них в течение непредсказуемого времени. Эксплуатация потоковых процессоров JVM, таких как Apache Flink, в большом масштабе означает стать экспертом в конфигурации и настройке GC, но при этом ожидать случайных сбоев. Многие пользователи используют значительно больше памяти, чем необходимо, чтобы избежать этого. После четырех лет работы в продакшене с Flink в Lyft и Splunk я практически мог видеть логи GC во сне.

Исторически здесь был компромисс. Вы можете либо справиться с проблемами масштабирования сборщика мусора, либо с ошибками, сбоями и проблемами безопасности на языке с ручным управлением, таком как C++. Но Rust нашел способ решить эту проблему: его программа проверки заимствований и система времени жизни позволяют управлять памятью во время компиляции.

Это позволяет избежать необходимости использования GC (динамического управления памятью) и позволяет языку гарантировать во время компиляции отсутствие ошибок безопасности памяти в коде (например, использование после освобождения или двойное освобождение). Эти гарантии действительны только для безопасного кода Rust, который используется по умолчанию. unsafeпозволяет пользователям манипулировать необработанной памятью способами, которые потенциально могут их нарушить, что необходимо для некоторых оптимизаций и для взаимодействия с библиотеками C и ОС. Однако на практике unsafeв кодовых базах Rust встречается редко; В Арройо всего несколько небезопасных блоков в десятках тысяч строк кода.

Некоторые утверждают, что можно написать на C/C++ безопасно для памяти, но долгая история эксплойтов, связанных с памятью, опровергает это. Пока я пишу это, Google и Apple срочно исправляют проблему переполнения буфера в libwebp, которая активно используется в Интернете 1.


Безопасность также порождает производительность. Например, гарантии безопасности Rust значительно упрощают правильное использование указателей во избежание копирования. Серверная (state) часть Арройо (используется для хранения данных, необходимых для вычислений, таких как окна) включает в себя кэш, который хранит десериализованные структуры в различных структурах данных для конкретных целей. Когда операторы запрашивают своё состояние, они могут получать элементы из кэша, если они доступны.

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

pub async fn get_time_range(
        &mut self,
        key: &mut K,
        range: Range<SystemTime>,
    ) -> Vec<&V>

Тип возвращаемого значения — Vec<&V>вектор (расширяемый массив) неизменяемых ссылок. Эти ссылки связаны соответствующими сроками жизни, что предотвращает случайное использование после освобождения. А поскольку они неизменяемы, вызывающая функция не может их изменить или освободить. Возврат ссылок позволяет нам избежать потенциально дорогостоящих копий данных, а Rust не позволяет нам нарушать свойства правильности и безопасности нашей структуры данных. В C++ подобный код был бы очень опасен и требовал бы от пользователей повышенного внимания.

Если он компилируется, это, вероятно, правильно

Компилятор Rust педантичен. Это самый навязчивый рецензент кода, с которым вам приходилось работать. Я говорю это как человек, который раньше проверял свои PR на C++ одним из авторов руководства по стилю кода C++ от Google… Если вы передадите 32-битное целое число функции, которая ожидает 64-битное целое число, она вам этого не позволит. Если вы попытаетесь разделить непотокобезопасную структуру данных между потоками, ваша компиляция завершится неудачно. Игнорировать тот факт, что пути файловой системы могут быть произвольными байтами, и попытаться использовать их как строки UTF-8? Прямо в тюрьму компилятора.

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

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

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


Идиоматический Rust также поощряет шаблоны, способствующие корректности, например интенсивное использование алгебраических типов данных (ADT). ADT существуют в двух формах: типы произведения ( struct) и типы суммы ( enum). Объединив их, вы сможете кратко определить данные вашего приложения, предотвращая при этом ошибки. ADT широко распространены в Rust, например, при обработке ошибок ( Optionи Result).

Но они также могут сделать многие оптимизации проще и безопаснее. Например, Cow (клонирование при записи) в стандартной библиотеке представляет собой перечисление, определенное следующим образом:

pub enum Cow<'a, B>
where
    B: 'a + ToOwned + ?Sized,
{
    Borrowed(&'a B),
    Owned(<B as ToOwned>::Owned),
}

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

Почему это полезно? Для поставщика части данных могут быть случаи, когда я могу избежать копирования и вернуть ссылку на некоторые другие данные, которыми я владею. Самый распространенный пример —  строкаCow<str>, клонируемая при записи. При десериализации строки из буфера в некоторых случаях можно напрямую использовать байты в буфере для поддержки строки (если она действительна в формате utf-8), а в других случаях вам может потребоваться скопировать и изменить их. Возвращая Cow<str>вместо String(принадлежащий типу String) или &str(ссылку на строку), мы можем поддерживать оба случая без дополнительных копий. В потоке данных Арройо узел может иметь один нисходящий узел или может разветвляться на несколько нисходящих узлов. Это отличный случай для Cow, потому что это означает, что в первом случае мы можем дать узлам собственный объект (поскольку это единственный потребитель), а во втором мы можем дать ему ссылку (поскольку в противном случае нам пришлось бы копировать его для каждого потребителя). Возвращая Cow, я позволяю своим потребителям прозрачно обрабатывать оба случая в зависимости от того, нужна ли им собственная копия базовых данных или они могут просто работать со ссылкой. Этот шаблон может сохранить множество ненужных копий по всей вашей кодовой базе.

Или, если взять пример из Арройо, вот enum из нашего движка:

pub enum QueueItem {
    Data(Arc<dyn Any + Send>),
    Bytes(Vec<u8>),
}

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

Этот enum позволяет нам предоставлять данные, которые являются либо Vec<u8>(сериализованными), либо Arc<dyn Any>(объектами в памяти), избегая ненужной сериализации/десериализации, в то же время предотвращая путаницу типов у потребителей.

Это также прекрасно сочетается с системой трейтов Rust. Например, мы предоставляем следующую реализацию признака TryFrom, которая позволяет потребителям напрямую преобразовывать QueueItem в Message(основной тип потока данных Арройо):

impl<K: Key, T: Data> TryFrom<QueueItem> for Message<K, T> {
    fn try_from(value: QueueItem) -> Result<Self> {
        match value {
            QueueItem::Data(datum) => _datum.downcast()?,
            QueueItem::Bytes(bs) => {
                bincode::decode_from_slice(&bs, config::standard())?.0
            }
        }
    }
}
 
// used like this
let message: Message<K, T> = item.try_from()?;

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

Cargo и экосистема crates

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

Cargo — это стандартный инструмент сборки и менеджер пакетов Rust, а crates.io — репозиторий сторонних библиотек. В отличие от фрагментированного мира C/C++ с CMake, Bazel, Buck и многими другими, в Rust есть единый способ создания пакетов, совместного использования кода и интеграции сторонних библиотек.

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

Для Арройо, пожалуй, самым ценным из них является DataFusion, часть проекта Apache Arrow. Как продукт, это очень быстрый механизм запросов SQL. Но для нас (и многих других в пространстве данных Rust) это также библиотека, которая может работать на других механизмах SQL.

Арройо использует DataFusion для анализа (преобразования текстового SQL в дерево абстрактного синтаксиса), планирования (преобразования этого AST в граф вычислений) и оптимизации (переписывания этого графа в соответствии с правилами оптимизации) запросов. Каждая из них представляет собой сложную, трудную задачу, которая в некоторой степени ортогональна потребностям конкретной системы запросов. Не будет преувеличением сказать, что это сэкономило нам тысячи часов разработки.

Сложные части

Хотя мы очень довольны своим выбором Rust, не все из них показали успешную компиляцию и превосходящую производительность. Написав 2000 слов о том, почему мы любим этот язык, я думаю, стоит рассказать и о некоторых проблемах, с которыми мы столкнулись.

Требуется время, чтобы нахвататься

У Rust жёсткий язык. Это явно заслужено; Гораздо проще перейти от опыта работы с Java или Python и получить что-то работающее на Go, чем на Rust. Хотя люди, знакомые с C++, обычно гораздо легче освоят Rust. Обучение работе с системой проверки заимствований и системами времени жизни требует времени, а трейты и макросы могут быть более сложными, чем их эквиваленты на других языках.

Мы видели, что новичкам в Rust требуется несколько месяцев, чтобы действительно освоиться. Для стартапа на ранней стадии эти месяцы могут особенно сильно ослабить импульс. Если бы у нас не было одного члена команды (меня), который уже был довольно опытным программистом на Rust, было бы намного сложнее.

Асинхронность все еще довольно груба

Об асинхронности в Rust написано много. Async/await — это большой набор функций, впервые выпущенный в 2019 году, которые упрощают написание неблокирующего кода. Хотя многие языки используют async/await, реализация в Rust сильно отличается. Rust не требует рантайма и предназначен для настройки — от микроконтроллеров до суперкомпьютеров. Это ограничивает пространство для проектирования функций высокого уровня, таких как async/await. Решение, предложенное Rust, является необычным для программистов, знакомых с Go или Javascript, и приводит к большой путанице. Если это вас описывает, то это отличный обзор того, как работает асинхронность в Rust: ссылка.

А если вы пишете сетевые сервисы на Rust, асинхронность на данном этапе практически неизбежна. Почти все сетевые и сервис-ориентированные платформы полагаются на асинхронность. Для Арройо к ним относятся tonic (gRPC), Hyper (HTTP), object_store (чтение/запись в S3), kube-rs и многие другие.

Не имеет значения, считаете ли вы, что вашу проблему можно было бы легко решить без асинхронности. Фактически, оригинальный прототип Арройо использовал потоки. Поскольку каждая подзадача оператора обрабатывает сообщения последовательно, это действительно работает довольно хорошо. Но мы быстро перешли на асинхронность по вышеуказанной причине: экосистеме библиотек. Эта миграция заняла некоторое время (и много вопросов к очень полезному Tokio discord), но в итоге оказалась отличной с точки зрения производительности (с самого начала мы получили прирост пропускной способности примерно на 30%).

После нескольких месяцев написания асинхронного кода на Rust я почувствовал себя довольно комфортно и продуктивно. Но все еще существует ряд пробелов и недостающих функций. Самая большая из них — асинхронные traits, которые вскоре будут частично стабилизированы. Есть ряд других острых моментов, из-за которых асинхронность по-прежнему кажется незавершенной четыре года спустя, например, трудности с созданием и именованием асинхронных замыканий или передачей ссылок через точки ожидания.

Нет стабильного ABI

В Rust отсутствует устаревший двоичный интерфейс приложений (ABI). Что это значит? Если я скомпилирую двоичный файл с помощью Rust версии X и попытаюсь связать его с общей библиотекой, скомпилированной с помощью Rust версии Y, нет никакой гарантии, что они будут совместимы.

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

Существует несколько обходных путей, таких как использование C ABI или WASM, но они идут на компромисс с точки зрения производительности и выразительности.

Подведение итогов

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

Комментарии (29)


  1. lazy_val
    30.09.2023 10:54
    +11

    На Java или Go возможно писать быстрый код, но в Rust он по умолчанию быстрый

    Не верю )) Минусуйте ))


    1. bel1k0v Автор
      30.09.2023 10:54

      1. EvilFox
        30.09.2023 10:54
        -1

        https://salsa.debian.org/benchmarksgame-team/benchmarksgame/-/issues/499

        Там автор ангажирован уже давно.


        1. bel1k0v Автор
          30.09.2023 10:54

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


  1. SpiderEkb
    30.09.2023 10:54
    +1

    В Rust отсутствует устаревший двоичный интерфейс приложений (ABI). Что это значит? Если я скомпилирую двоичный файл с помощью Rust версии X и попытаюсь связать его с общей библиотекой, скомпилированной с помощью Rust версии Y, нет никакой гарантии, что они будут совместимы.

    Или я чего-то не понимаю, или это ломает все.

    Если у нас есть кодовая база в миллионы строк на Rust версии X то нам все придется пересобрать с обновлением Rust с версии X на версию Y? Вы серьезно?

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


    1. vabka
      30.09.2023 10:54
      +12

      1. Далеко не так много монолитных систем, у которых счёт идёт на миллионы строк кода

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

      3. Отсутствие стабильного ABI позволяет достаточно много нововведений вносить не беспокоясь о том, как новые фичи натянуть на старый ABI, и как эту самую обратную совместимость не сломать.

      4. Если тебе явно нужен ABI, то можно использовать C-ABI, использовать wasm, или какой-то другой способ взаимодействия. Ну или дождаться введения стабильного ABI.


      1. SpiderEkb
        30.09.2023 10:54
        +2

        1. Далеко не так много монолитных систем, у которых счёт идёт на миллионы строк кода

        А почему вы решили что речь идет о монолитах?

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

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

        И нам постоянно приходится или добавлять новые модули или модифицировать старые (где-то требуется оптимизация, где-то изменение внутренней логики).

        Но никогда и никому в голову не придет "пересобрать все". Это просто физически нереально в тех объемах кода, с которыми мы работаем.

        Что страшного в том чтобы пересобрать при наличии всех исходников и отсутствии ломающих изменений?

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

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

        Интерфейс модуля - это заранее оговоренный и зафиксированный контракт. Который вы не имеете права менять в одностороннем порядке.

        Как вы себе это представляете? Вот есть у вам модуль. Он рассчитывает получить на вход получить некий набор данных и преобразовать их в соответствии с заложенной в нем логикой. А вы ему на вход подает что-то иное. И что он с этим будет делать?

        Обратное также верно. Модуль А передает набор данных модулю Б, который ... (см выше) Вы меняете логику и ожидаемый набор данных для модуля Б. Но Модуль А тоже должен об этом знать т.к. он готовит вполне определенные денные для модуля Б и ожидает от него определенного результата.

        Так что это так не работает. Если вы хотите внедрить новую логику - выв внедряете новый модуль со своим контрактом. Если вы хотите изменить логику (например, оптимизировать ее) - можете сделать это внутри имеющегося модуля, но с сохранением контракта потому что этот модуль может вызываться из 100500 мест (например, это какой-то ретирвер, формирующий по заданному набор у параметров некий набор данных из БД с какой-то нетривиальной логикой отбора и преобразований этих данных).


        1. Gorthauer87
          30.09.2023 10:54
          +7

          Так я понимаю, что такие системы пишут на джаве же?

          Но с другой стороны, в Расте можно просто c abi для модулей взять или есть нечто такое, чтобы не писать это все вручную. https://crates.io/crates/abi_stable


          1. SpiderEkb
            30.09.2023 10:54

            Нет. Для того, что работает на центральных серверах, джава слишком медленная и слишком прожорливая.

            На нашей платформе есть специальный язык. Специально для коммерческих расчетов. С поддержкой типов данных с фиксированной точкой, дат и времени, возможностью использовать SQL запросу непосредственно в коде (фактически все типы данных SQL нативно поддерживаются языком).


            1. Gorthauer87
              30.09.2023 10:54
              +3

              Ну вот такую систему вполне можно и на Расте написать, в конце концов, ведь у тех же плюсов, к примеру, тоже нет стандарта на abi, они его ещё и ломали однажды, есть просто обещание не ломать его в будущем


            1. easyman
              30.09.2023 10:54

              (deleted)


        1. MiraclePtr
          30.09.2023 10:54
          +1

          Мы не можем полагаться на то, что пересобранный новым компилятором mission critical модуль будет работать корректно

          Вы всмысле все изменения деплоите сразу на прод вообще без тестирования?


        1. vabka
          30.09.2023 10:54
          +3

          Интерфейс модуля - это заранее оговоренный и зафиксированный контракт. Который вы не имеете права менять в одностороннем порядке.

          Как вы себе это представляете? Вот есть у вам модуль. Он рассчитывает получить на вход получить некий набор данных и преобразовать их в соответствии с заложенной в нем логикой. А вы ему на вход подает что-то иное. И что он с этим будет делать?

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

          Ну и stabi, который в соседнем комменте упомянули.

          Все интерфейсы бинарные (извините, но запаковывать-распаковывать джейсоны при тех плотностях вызовов с которыми нам приходится иметь дело - слишком большая роскошь в плане накладных расходов).

          Никто не обязывает передавать именно json-ы. Есть куча бинарных форматов для IPC.

          Мы не можем полагаться на то, что пересобранный новым компилятором mission critical модуль будет работать корректно

          Я правильно понимаю, что вы не проводите интеграционное тестирование? Не страшно?


        1. vtb_k
          30.09.2023 10:54
          +1

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

          Вы считаете отсутствие интеграционных тестов поводом для гордости? Да ещё продвигаете такой подход в массы?


    1. Mingun
      30.09.2023 10:54
      +7

      Ну у С++ стабильный ABI, но что-то не видно проектов на миллионы строк кода, которые с легкостью обновляют компилятор. Так что серьезно, это ни о чем.


      1. SpiderEkb
        30.09.2023 10:54
        -1

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

        У нас (не С/С++, хотя и их тоже используем для некоторых задач) куча старых модулей, которые написаны вообще на старом диалекте языка. Который по синтаксису сильно отличается от того, на чем сейчас пишем. И это совершенно не мешает писать новые модели на новом диалекте и интегрировать их в систему.

        А компиляторы у нас вообще часть ОС - в новых версиях ОС новые версии компиляторов.

        И ABI на нашей платформе универсальный - что для С/С++, что для основного языка. Така мультиязыковая среда разработки - в основном используется специальный язык, но что-то низкоуровневое удобнее на С/С++ писать. И все это легко стыкуется между собой.


        1. Mingun
          30.09.2023 10:54
          +4

          Ну так для стыковки с кодом на других языках и загружаемых модулях есть C ABI, чем он не устраивает, что нужно свой городить? По-моему, так хорошо, что не разводят зоопарк, ведь иначе бы другим языкам тоже пришлось бы поддерживать новый ABI, вздумай они воспользоваться динамическими модулями, написанными на Rust-е.


          Который по синтаксису сильно отличается от того, на чем сейчас пишем.

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


    1. aegoroff
      30.09.2023 10:54
      +1

      Если я скомпилирую двоичный файл с помощью Rust версии X и попытаюсь связать его с общей библиотекой, скомпилированной с помощью Rust версии Y


      На самом деле тут все очень странно - в Rust обычно используются зависимости (общие библиотеки) в виде крейтов (crate), которые прописываются в файле Cargo.toml. Он суть связи на уровне исходного кода, т.е. для сборки они загружаются cargo с crates.io или вашего локального зеркала в виде исходников и собираются у вас на машине, вашим компилятором. Зависимости в виде бинарных Rust компонентов по моему это нонсенс (использование FFI и сишных либ не в счет). Поэтому этот аргумент очень спорный.


      1. NN1
        30.09.2023 10:54
        +1

        Почему бинарная зависимость это нонсенс?

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

        Поэтому решением является сборка проекта 1 для получения бинарного файла, а проект 2 просто его использует.

        Разумеется решаем общение через C API.


        1. aegoroff
          30.09.2023 10:54
          +1

          я вроде написал - "использование FFI и сишных либ не в счет" а вы как раз про них. Да, такие зависимости - нормальное явление.


          1. NN1
            30.09.2023 10:54

            Я не про библиотеки на C.

            А про ситуацию с двумя проектами C++ или Rust.


            1. aegoroff
              30.09.2023 10:54

              Хорошо, я неверно выразился - библиотеки Си не в чистом виде конечно (собраные из исходников на Си сишным компилятором), а библиотеки собранные Rust компилятором, но которые делают библиотеку в C ABI - конечно такое должно нормально работать ибо C ABI не зависит от версии компилятора Rust. Но вот не знаю насколько это распространено (делать бинарные либы на Rust в C ABI) - вообще о таком впервые слышу.


              1. SpiderEkb
                30.09.2023 10:54

                Но вот не знаю насколько это распространено (делать бинарные либы на Rust в C ABI) - вообще о таком впервые слышу.

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

                Уже писал - на нашей платформе реализована "интегрированная языковая среда" (ILE) которая позволяет использовать разные языки для реализации разных частей в рамках одной задачи.

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

                Но если речь заходит о том, что в рамках той же задачи требуется работа с низкоуровневыми системными объектами, то делать это на этом языке можно, но получается коряво и неудобно. А на С/С++ это все проще и естественней (в отличии от реализации бизнес-логики).

                И тут на помощь приходит ILE - все компиляторы генерируют универсальный "объектный код" (точнее, код в низкоуровневых машинных инструкциях - MI). Что позволяет несколько "модулей" (аналог объектного файла) на разных языках собирать (bind в местной терминологии) в один программный объект.

                Нечто подобное на других платформах реализовано в рамках LLVM.

                Изначально непривычно, но когда в это втянешься, то понимаешь, что это действительно удобно и эффективно. Но в том случае, когда речь идет о разных по возможностям и "специализации" языках". Как у нас - один для простой работы с БД, нативной работы с фиксированной точкой, датой, временем, строками (без создания дополнительных объектов). И другой для всяких низкоуровневых вещей на уровне системных API.


                1. aegoroff
                  30.09.2023 10:54

                  так раст как раз на LLVM и основан, - он его использует для генерации бинарей, оптимизации и т.д. Вот код крейта который всем этим занимается https://github.com/rust-lang/rust/tree/master/compiler/rustc_llvm


      1. SpiderEkb
        30.09.2023 10:54

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

        А оптимизация "под процессор" н нашей платформе решена несколько иными способами и более эффективна. И работает для любой программы, на чем бы она ни была написана.


        1. aegoroff
          30.09.2023 10:54
          +2

          Зависимости на уровне исходников на мой взгляд не слишком устойчивы

          В общем случае это верно, но если в Cargo.toml гвоздями прибить версию - например serde_json = "=1.0.107" и хранить в VCS тот же Cargo.lock файл - вполне себе устойчиво


          1. SpiderEkb
            30.09.2023 10:54

            По-моему, это излишние сложности.

            Если у нас я должен думать только о сохранении контракта, а версия не играет роли (более того, версия может измениться в связи с необходимым изменением внутренней логики), то тут еще и версию отслеживать надо.

            Причем, это не какое-то фундаментальное свойство языка, несущее существенные профиты к других местах, а просто следствие пока еще не стабилизированного ABI (которое в конечном итоге будет стабилизировано наверняка).

            Я не берусь оценивать Rust как язык т.к. не пробовал писать на нем ничего серьезного (и не факт что придется). И ни в коей мере не хочу его критиковать (я уже за долгие годы в разработке доказал самому себе что могу освоить любой язык, со всеми его плюсами, минусами, особенностями и ограничениями, если того потребует поставленная задача).

            Просто отметил что вот именно отсутствие стабилизированного ABI и лучше чтобы его не было.


            1. aegoroff
              30.09.2023 10:54

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


            1. DarkEld3r
              30.09.2023 10:54
              +1

              а просто следствие пока еще не стабилизированного ABI (которое в конечном итоге будет стабилизировано наверняка).

              Ой не факт. К слову, в С++ периодически предлагают сломать ABI чтобы больше производительности выжать (что для данного языка имеет смысл) и ломали в прошлом.