🗣 Java и C# устарели в эпоху Docker

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

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

Под устаревшим в данном случае не имеется ввиду отсутствие использования или неважность. Справедливо было бы назвать COBOL устаревшим языком, поскольку он больше не нужен для решения конкретных задач. Однако это не означает, что необходимо утилизировать все работающие на COBOL системы или прекратить писать на нем. COBOL все еще имеет значение, поскольку он по-прежнему используется в крупных системах, управляющих финансами.
Здесь имеется в виду, что если завтра все эти системы рухнут, и вам придется их восстанавливать, вы бы использовали COBOL? Будет ли COBOL так же выгоден, как современные языки программирования? Конечно нет. У нас есть подходящие технологии для создания софта с нуля и нет нужды использовать «динозавров».

Java и C# находятся в аналогичном положении. Чтобы строить корпоративные системы в облаке, сегодня не нужно создавать ориентированный на виртуальную машину системный язык. До появления контейнеров и облаков имело смысл развернуть ПО на любом сервере в виде jar-файла. С помощью Docker и других контейнерных технологий мы продвинулись еще на один шаг вперед. Вместо Java Virtual Machine у нас есть Linux Virtual Machine, которую можно создать с помощью контейнера Docker и работать где угодно.

Вместо запуска Java Virtual Machine или .NET в операционной системе, вы запускаете контейнер Docker, создающий эквивалент виртуальной машины Linux в любой ОС.

В чем преимущество? У вас есть целая среда в ОС с файловыми системами, службами и т. д.

Подход Java и C# означал создание общеязыковой среды выполнения, где вы могли бы применять специальные языковые варианты, вроде Jyton (JVM) или Iron Python (.NET), создающие код для этой конкретной виртуальной машины. И Java, и C# используют JIT-компиляторы. То же самое относится и к промежуточному языку .NET (IL). Аппаратное обеспечение, которое непосредственно понимает байт-код Java или .NET IL, не существует в реальном мире. Это воображаемое оборудование, воплощенное в жизнь с помощью программного обеспечения.

Зачем создавать альтернативные версии каждого языка для запуска на одной виртуальной машине, когда вы можете просто запустить их все в одном контейнере Docker?

☕ Подтянуть свои знания по Java вы можете на нашем телеграм-канале «Библиотека Java для собеса»

Разница в подходе Java и C#

Что говорят в Microsoft на эту тему:

Приложения Windows 8.1, настольный софт Windows и нацеленные на .NET фреймворки пишутся на определенном языке программирования и компилируются в промежуточный язык (IL). В рантайме JIT-компилятор отвечает за компиляцию IL в машинный код для локальной машины непосредственно перед первым выполнением.

Другими словами, программы на Java и C# распространяются одинаково. Оба поставляются в виде байт-кода или промежуточного языка (IL). Java может использовать сторонний код в jar-файлах, C# может использовать сторонний код в сборках.

The Native Image Generator (NGEN)

Ключевое отличие Java заключается в том, что C# обычно использует так называемый Native Image Generator (NGEN). Он все еще подразумевает стратегию компиляции just-in-time, а еще использует тот факт, что JIT создает кучу нативного кода в памяти. Это представление в памяти всего скомпилированного кода обычно называется образом.

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

Поскольку автор не является экспертом в C#, будем использовать Julia в качестве примера. Она предназначена для JIT-компиляции. Чтобы создать образ стандартной библиотеки, всегда плодится много кода для осуществления связи всех важных областей. Это приводит к тому, что большинство функций компилируются в машинный код. По сути тут создается кэш. У вас нет никакой гарантии, что удастся управлять всем с JIT, поэтому вам все еще нужна среда выполнения и JIT-компилятор для запуска кода. Вы просто избавляете себя от повторной компиляции часто используемых функций.

То же самое относится и к .NET NGEN – он не компилирует все в машинный код и вам также нужна среда выполнения и JIT-компилятор для взаимодействия со сборками .NET (эквивалент библиотек DLL).

.NET Native

С .NET Native вы можете скомпилировать свои программы на C# в один автономный бинарник. Он, например, развертывается непосредственно на сервере или в контейнере Docker без обычной волокиты с .NET.

Такой подход явно ведет C# в направлении, отличном от задуманного разработчиками. Например, вы не сможете:

  • выполнять вызовы из собственного двоичного .NET-файла в другие сборки .NET;
  • использовать рефлексию для вызова произвольного кода в сторонних сборках.

Вы вынуждены выбирать – либо запускаете свои C#-приложения через JIT и получаете доступ к любому количеству сторонних библиотек (в виде сборок), либо предварительно компилируете, и оно может использовать только код из бинарника.

Из-за чего возникает ограничение?

Для языков .NET не определен бинарный интерфейс нативного кода. Нет никакого способа вызвать скомпилированную в нем сборку, т. к. заранее неизвестен тип соглашения о вызове. В отличие от Swift 5, который был разработан для нативной компиляции с нуля и имеет хорошо определенный стабильный бинарный интерфейс приложения (ABI).

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

🧩☕ Интересные задачи по Java для практики можно найти на нашем телеграм-канале «Библиотека задач по Java»

Облако решает проблему развертывания

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

Больше нет смысла изучать Java или C#?

Эти языки все еще имеют значение, из-за огромных экосистем и миллионов нуждающихся в поддержке строк кода. Новые системы будут по-прежнему строиться с использованием Java и C#, потому что они пользуются преимуществом за счет наличия большого количества библиотек.

Go – это Java для Docker-эпохи

Go – предвестник этой новой эры. Возможно нет ничего удивительного в том, что Docker и Kubernetes сами написаны на Go.

Когда приложение работает в контейнере Docker, можно свободно компилировать его в машинный код, что и делает Go. Это дает несколько преимуществ:

  • Контейнеры Docker могут быть меньше, поскольку не нужно устанавливать большую инфраструктуру виртуальных машин, JIT-компилятор, среду выполнения и т. д.;
  • Простое развертывание – для использования без контейнера гораздо проще распространять созданный Go бинарник. Можно загрузить его на ПК и запустить без необходимости установки JVM или Common Language Runtime;
  • Кросс-компиляция позволяет легко создавать бинарные версии для различных платформ.
Для большинства современных программ двоичный код – это небольшая часть объема ПО. Вы можете увидеть, например, как Apple сегодня распространяет OS X в двоичных файлах, содержащих инструкции x86 и ARM (раньше в них были инструкции x86 и PowerPC). Это не увеличивало размер софта. Файлы данных, изображения и другие ресурсы обычно являются основной в смысле размера частью приложения.

Java, C# и другие управляемые среды, которые были популярны в 90-е годы, теперь просто представляют собой ненужный дополнительный слой абстракции. Контейнеры создают прослойку между вашей средой и реальным оборудованием. Зачем создавать еще одну? Она поглощает больше ресурсов и замедляет работу системы.

Являются ли JIT-компиляторы устаревшими?

И Java, и C# используют JIT-компиляцию, в отличие от Go и Rust. Можно подумать, что такая компиляция – путь в никуда. Все не так просто: Lua, JavaScript, Julia, Pharo (производные от Smalltalk), R, Matlab, LISP и Python – это языки, которые либо уже используют JIT, либо начали на него переходить. Они не являются системными языками программирования и они динамически типизированные.

Для динамических языков JIT является предпочтительным. В отличие от статически типизированных, они часто используются в интерактивном режиме в командной строке, а компиляция ahead-of-time (AOT) в этом случае была бы невозможна или очень громоздка.

Из различных бенчмарков можно увидеть, что несмотря на многолетнее лидерство, Java обычно уступает Go почти по всем показателям. Go будет выполнять больше вычислений, обрабатывать больше запросов, потреблять меньше памяти и давать меньшую задержку. Однако для динамических языков JIT дает огромное преимущество, а выполнять ahead-of-time компиляцию для динамического языка непрактично и неудобно.

Важно ли это в реальном мире?

Java и C# устарели в теории, но бесчисленные рабочие места требуют этих технологий, так имеет ли это наблюдение какое-либо практическое значение для реального мира? В краткосрочной перспективе – нет. Мы увидим постепенное снижение популярности, люди будут мигрировать на другие альтернативные языки.

Эти альтернативы обычно обеспечивают лучшую производительность, более простое развертывание и меньшее использование памяти. Для любого, кто начинает новый проект, это имеет значение. Иногда Java и C# будут единственным естественным выбором, потому что конкурентам не хватает библиотек и инструментов в конкретной области. По мере того, как конкуренты расширят экосистемы и инструментарий, привлекательность Java и C# будет снижаться.

Настольные (GUI) и мобильные приложения

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

Настольные приложения обычно адаптируются к среде исполнения. Развертывание на любой ОС или оборудовании – меньшая проблема. Например, что бы вы предпочли использовать для разработки под OS X: Java или Swift? Основанная на виртуальной машине Java не дает никаких преимуществ! Вы получаете больший объем расходуемой памяти и длительное время запуска. Помните, что в отличие от серверного ПО, настольное не работает месяцами – оно запускается на несколько часов. В этом случае компиляция Ahead-of-Time дает явные преимущества.

Можно сравнить iOS и Android. Отклик софта на iPhone всегда быстрее даже на слабом железе, поскольку Swift не нуждается в JIT-компиляции или сборке мусора. Это также дает преимущества в сфере безопасности. В iOS вы можете использовать криптоподпись страницы в памяти, чтобы убедиться в отсутствии ее модификации. Вы не можете сделать это с помощью JIT-кода, потому что заранее не знаете, как он будет выглядеть.

Что насчет Windows? C# популярен для создания GUI. В свое время он был долгожданным событием после Win32 C API и Microsoft Foundation Classes (MFC), но это было связано больше с дизайном библиотеки чем с языком. Кросс-платформенный GUI Qt tookit обеспечивал во многом те же преимущества с точки зрения простоты программирования. Qt написан на C++, а не на C# или Java. Если вы пишете сложные GUI-приложения сегодня, скорее всего вы предпочтете Qt любому решению C#.

Как Java и C# попытаются нанести ответный удар

Хорошо зарекомендовавшие себя технологии редко сдаются без боя. Скорее всего мы увидим возросшие усилия по превращению Java и C# в среды для компиляции в машинный код. .NET Native и GraalVM native Image – примеры этого. Примерно так Python пытается противостоять Julia, создавая собственные JIT-решения. Это работает, но огромное наследие было построено на совершенно других принципах. Функции вроде рефлексии могут перестать работать.

Вопрос заключается в том, станут ли варианты выполнения нативной компиляции C# и Java такими же упрощенными, как у Go, Rust и Swift, и получат ли они такие же богатые экосистемы и наборы инструментов.
Обычно проводится параллель с автомобильными технологиями. Tesla представляет новое направление развития, но у них есть огромные проблемы с качеством, производственными процессами, масштабированием и т. д. Они страдают в тех областях, где преуспевают традиционные автопроизводители. Вопрос в том, будет ли Tesla развиваться в своем производственном мастерстве быстрее или медленнее, чем GM, Ford и Toyota научатся делать хорошие электромобили.

В чем самая большая проблема? Быть незрелым или иметь тяжкое наследие? Общество любит делать ставки на новичков, но устоявшаяся индустрия часто удивляет. Oracle и Microsoft вполне могут в конечном итоге одуматься, переосмыслив свои языки и инструменты. Время покажет.

Заключение

Docker– это просто икона нового времени. Мир не вращается вокруг него. Вы можете использовать LXC, Mesos, OpenVZ или другие варианты. Существуют специализированные контейнеры, вроде AppImage, Snap и Flatpak.

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

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

Это касается не только Linux. Приложения из AppStore также основаны на контейнерах. Здесь мы видим полную альтернативу подходу Java и C#. Облачная служба определяет оборудование, а пользователи загружает специально разработанные под его машину приложения. Если такой механизм распределения слишком сложен, можно создать «жирный» универсальный бинарник.

Любое решение устраняет необходимость установки JIT-компилятора, сред выполнения и фреймворков для Java или C#. Поставляемый в контейнерах софт, облегчает распространение и развертывание.

Дополнительные материалы:

Источники

ЛУЧШИЕ СТАТЬИ ПО ТЕМЕ

matyushkin
18 марта 2020

ТОП-10 книг по C#: от новичка до профессионала

Отобрали актуальные книги по C#, .NET, Unity c лучшими оценками. Расположил...
Библиотека программиста
12 июля 2017

Что такое Docker, и как его использовать? Подробно рассказываем

Разберем по косточкам, ведь Docker – это мощный инструмент, и огромное коли...
admin
05 апреля 2017

6 книг по Java для программистов любого уровня

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