Облака - не сахарная вата, какой их рекламируют, а неудобный набор велосипедов.
Некоторое время назад отовсюду можно было услышать радостные крики о том, насколько облачные технологии нам всем помогут и упростят разработку. Сегодня я поведаю вам, почему это нихрена не так, как я потратил неделю на написание CRUD'а в облаке и как этого избежать.
Как всё начиналось
Одним днём мне захотелось размяться и сделать кое-какой проект. В чем суть проекта — не так важно. На мысли о том что хочется сделать что-нибудь наложилось ещё и то, что я не так давно праздного интереса ради смотрел бесплатные тарифы Яндекс.Облака (Yandex.Cloud, далее YC), и в целом они меня порадовали: бесплатный тир достаточно щедрый и в целом цены не то чтобы очень дорогие (хотя и всё равно дороже AWS даже с текущим курсом). Это особенно важно, учитывая, что мы живём в России 2023 года, где никакие зарубежные сервисы не работают, а среди Российских облачных провайдеров бесплатный тир есть... только у Яндекса ¯\_(ツ)_/¯.
На самом деле это не самый первый мой опыт работы с Serverless системами — я делал сокращалку ссылок на Cloudflare Workers (wido.dev/evil, например). Но опыт там был на Rust'е с использованием всего у чего только была приписка Experimental... в общем опыт интересный, но я знал, во что ввязывался (кстати бесплатные тарифы Cloudflare просто фантастически щедрые).
Суть в том, что я, наслушавшись хвалебных статей о величии и удобстве облаков, ожидал увидеть полноценный mature сервис с SDK и обёртками под все языки и фреймворки. Ох как же я ошибался — их даже для AWS то мало, что уж про Яндекс... Хотя нет, по Яндексу конкретно я ещё проедусь — дальше.
Выясняем разные облака на вкус
Итак, вооружаемся словарём хайповых слов которые вы слышали и гуглим таблицу аналогов яндекса:
- AWS Lambda - Yandex Cloud Functions: "Function as a Service" — просто удалённое выполнение функции (на самом деле кода в любом объёме и с любым количеством функций) на серверах. По существу при получении запроса рантайм просто загружает и исполняет вашу функцию. Формулировка так себе, но она важна: существует конкретный рантайм который загружает вашу функцию. Важно это потому, что накладывает ограничения на код: он должен запускаться в определённом рантайме (например только Java 11, не выше) и быть структурирован таким образом, чтобы рантайм смог ваш код загрузить (он не просто выполняет
python3 myCode.py)
- EC2 - Yandex Compute Cloud: по существу — виртуальные машины (сервера) с возможностью автоматического масштабирования под нагрузку.
Я мог бы долго перечислять аналоги, но эти два мало того что основные, так ещё и прекрасно показывают суть serverless сервисов для тех, кто с ними не знаком: есть обычный облачный (чаще - managed) режим, а есть serverless.
Оплачиваются такие сервисы по системе Pay-As-You-Go — по фактическому использованию. Однако в случае с managed вариантом... фактическим использованием будет время использования виртуалки! А значит вы по существу просто арендуете ограниченный VPS. Ладно. Будем использовать Serverless - он дешевле.
Yandex Cloud Functions
Сразу сделаю ремарку, что я по стеку — Java разработчик, и всё это решил делать для тренировки работы со Spring Boot, и оценивать буду соответственно с позиции джависта. Однако, думаю, мои мысли применимы ко всем, кроме разработчиков на С++ и Python. И может Go, судя по тому, что я видел в SDK.
Итак, открываем YCF и видим следующую картину в плане рантаймов:
- NodeJS (12, 14, 16)
- PHP (7.4, 8.0)
- Python (3.7 - 3.11)
- Golang (1.16 - 1.19)
- Java (11)
- .NET 3.1
- R (4.0, 4.2)
- Bash
Список мягко говоря... не впечатляет, особенно меня, как джависта: пока весь мир уже сидит на 17 джаве, в YC до сих пор 11. Да и остальные рантаймы не то чтобы сильно актуальны.
Ладно, не 8 и на том спасибо. Как там с этим работать...
Вариант 1: Сделать реализацию стандартного интерфейса Function (или яндексовского YcFunction) и указать её в качестве точки входа. Очевидно что подойдёт это только для совсем простых функций, да и ограничения там тоже уже есть.
Вариант 2: О, есть раздел про Spring Boot! Как раз мой стек! Ну-ка... мда, раздел совсем маленький, и большая его часть — описание ограничений.
- Не поддерживается Spring Boot Loader (как его убрать, Appendix E.6 или моя заметка на Gist).
- Функция на Spring Boot не имеет информации о том, какой её эндпоинт был вызван. Для этого надо использовать API Gateway (и соответственно писать OpenAPI спеку, если вы ещё этого не сделали).
- Не поддерживаются некоторые метода HttpServlet* классов.
Лааааадно, пофигу. Пишем простой Hello World на спринге, загружаем, настраиваем API Gateway, делаем наконец-то curl запрос и!.. 500 миллисекунд на ответ??? Как-то... ну вообще не впечатляет. Из плюсов — загружать проект можно исходниками, джарником или maven проектом. Удобно.
В начале было слово, и слово это было — докер. Yandex Serverless Containers
Слишком уж не впечатляет производительность и "удобство" Cloud Functions, давайте попробуем контейнеры! Тут нам никто не указ по части рантайма, да и Spring Boot 3.0 официально поддерживает GraalVM Native Image.
Для того чтобы использовать контейнеры, нам надо создать реестр в YC (тоже отдельный сервис кстати) и настроить CLI утилиту. Качаем тулзу (есть в AUR), настраиваем, компилируем Native Image (не забывайте -Pnative чтобы собрать минимально возможный и оптимизированный образ, ценой 100% загрузки всех 12 ядер), тегаем как того просит документация, заливаем, делаем curl... 300 мс (и 1.5 секунды на первый холодный запуск).
Очевидно что контейнеры лучше по всем параметрам, но вот производительность всё равно оставляет желать лучшего. Если что, время запуска на локальной машине у меня — 50 миллисекунд.
Ладно, черт с ним со временем ответа, не критично, пора подключать базу данных.
YDB — самый нижний круг ада
На этом этапе я ещё даже не подозревал, что меня ждёт...
Итак, какие варианты serverless баз данных есть у YC? Да никаких, только YDB. Все остальные базы данных, представленные в облаке — managed, т.е. по сути виртуалка с помесячной оплатой, причём стоят они все конских денег — самый нищенский вариант среди всех баз данных начинаются от трёх тысяч рублей в месяц (хотя Clickhouse и Redis минимально можно взять за 2500, а MongoDB минимум 5000).
Есть YDB, у которой существует serverless вариант с оплатой в подобающем pay as you go формате, а значит мы его и выбираем. Идём в консоль, создаём базу и сразу же сталкиваемся с тем, что таблица можем быть либо в YDB формате (реляционном), либо в документном (AWS DynamoDB-совместимая).
Я совершил фатальную ошибку — я выбрал YDB режим.
Хорошо, идём в документацию и видим следующие факты:
- Язык запросов — YQL (очередной велосипед, которые Яндекс очень уж любит переизобретать).
- Существует SDK под джаву.
- Если найти таблицу сравнения SDK, то лучше всех поддерживается Python и C++.
Ладно, открываем ссылку на SDK для джавы, не теряя времени в ридми находим ссылку на пример использования, открываем и видим две вещи: феерическое качество кода "тестов" и безрадостную картину голого использования JDBC, причем с кастами всего что можно в свои велосипеды (Connection -> YdbConnection, PreparedStatement -> YdbPreparedStatement, ...), что убивает использование даже JdbcTemplate.
Ну не может же у базы данных с JDBC драйвером (пусть даже и таким) не быть библиотеки для работы со Spring Data (JPA)??
Шерстим issues и ищем по коду в надежде найти что-то, но ничего. Абсолютно. В стадии "отрицания" отправляемся в гугл и... внезапно находим другой SDK — легаси, на верхушке ридми которого написано про deprecated и дана ссылка на новый SDK, где мы только что были. Но несмотря на то что это "легаси" — этот сдк не брошен, там всё ещё есть коммиты недельной давности.
И, что самое главное, тут есть разделы spring-data-jdbc и spring-data-jpa! Радостно открываем их и... видим, что последний коммит в них был 2 года назад и там даже нету джарников и pom (а значит и возможности собрать самостоятельно).
Ну такого ведь не может быть, да?
На этом моменте я потратил целый день и перерыл гугл, исходники обоих SDK, все issue, поиск по всему гитхабу (встроенными средствами и через sourcegraph) и могу вам заявить — так и есть. Возможности нормально использовать YDB со спрингом нет. И даже Datasource для спринга создать не получится — драйвера диалекта для Hibernate-то нет, у нас же велосипед, YQL.
Теоретически может быть возможно указать случайный диалект в Hibernate и использовать Native Query, однако даже так у меня не получилось установить соединение с БД, да и оно намертво отказывалось компилироваться в Native Image, так что я спустя некоторое время просто плюнул на это.
На этом этапе я стал железобетонно уверен, что разработчики Яндекса ненавидят других программистов и людей в целом, и изобретают велосипеды только чтобы мучить всех остальных своими несовместимыми со всеми реализациями.
Помните документный режим, совместимый с DynamoDB? Давайте попробуем его!
В этот момент я потерял веру в человечество (и в первую очередь в Яндекс) и решил попробовать документный режим. С ними я никогда раньше не работал (даже с MongoDB), но это меня не остановило. Я удалил старую таблицу и начал создавать новую, документную и...
Тут всего три типа данных (String, Number и Binary) и есть ключи партицирования и ключи сортировки. Ладно, возможно погуглить всё же придётся.
Итак, суть для таких же как я:
- Вам не нужно больше типов данных, потому что вам нужна 1-2 колонки в админке, собственно под ключи (документные БД не имеют схемы, вы просто загрузите json со всеми нужными колонками).
- В таблице может быть только ключ партицирования, а может ключ партицирования и сортировки. Ключ партицирования — это и есть обычный Primary Key (на самом деле нет).
Если есть ещё и ключ сортировки, то PK выступает ключом группировки (т.е. могут быть несколько одинаковых значений ключа партицирования.
Грубо говоря — есть таблица песен, где есть исполнители и названия песен. Ключ партицирования — это имя исполнителя, а ключ сортировки — название песни. - Не пытайтесь создавать таблицу в которой будет только PK типа Number — это плохая идея, идущая против best practices, да и вас в этом не поддержит SDK: он генерирует случайные UUID (Строковые), а не инкрементальные айдишники. И по-возможности всё таки используйте ключ сортировки — хорошим вариантом всегда является дата (создания, добавления в базу, чего угодно) (официальная статья mongodb с ориентирами на то, как проектировать БД)
- Там нет напрямую языка запросов, все действия делаются через API. Однако дальше SDK и библиотеки для спринга этот момент нам абстрагируют.
Ставим форк форка бибилотеки для Spring Data (не JPA. Не ставьте стартер JPA, он будет просить драйверы для Hibernate!), делаем всё как в ридми кроме сущности. Если вы сделали простой ключ (только ключ партицирования), то делайте как в ридми; если композитный (партицирование + сортировка), то тут чуть запарнее: видите ли, под капотом там всё равно используются самые обычные аннотации вроде @Entity и @Id, и именно последний ставит палки в колёса: он-то может быть только один, а ключей у нас два...
Поэтому нам надо создать абстракцию, которая будет нашим айдишником. Выглядит это примерно так (с использованием Lombok):
// ShowId.java @Getter @Setter @NoArgsConstructor @AllArgsConstructor public class ShowId implements Serializable { @Serial private static final long serialVersionUID = 1L; @DynamoDBHashKey @DynamoDBAutoGeneratedKey private String showId; @DynamoDBRangeKey @DynamoDBTypeConverted(converter = ZonedDateTimeToStringConverter.class) private ZonedDateTime addedAt; } // Show.java @Getter @Setter @DynamoDBTable(tableName = "shows") public class Show { @Id // Если тут будут геттеры то SDK // подавится и упадёт, даже с @Transient. @Getter(AccessLevel.NONE) @Setter(AccessLevel.NONE) @Transient public transient ShowId _showId = new ShowId(); @DynamoDBHashKey private String showId; public String getShowId() { return _showId.getShowId(); } public void setShowId(String showId) { _showId.setShowId(showId); } @DynamoDBRangeKey @DynamoDBTypeConverted(converter = ZonedDateTimeToStringConverter.class) private ZonedDateTime addedAt; public ZonedDateTime getAddedAt() { return _showId.getAddedAt(); } public void setAddedAt(ZonedDateTime addedAt) { _showId.setAddedAt(addedAt); } @DynamoDBAttribute private String name; @DynamoDBAttribute private String description; @DynamoDBAttribute private Integer episodes; } // ZonedDateTimeToStringConverter.java public class ZonedDateTimeToStringConverter implements DynamoDBTypeConverter<String, ZonedDateTime> { @Override public String convert(ZonedDateTime zonedDateTime) { return zonedDateTime.toString(); } @Override public ZonedDateTime unconvert(String s) { return ZonedDateTime.parse(s); } }
Да, как можно заметить, SDK амазона до сих пор не умеет конвертировать новые классы даты в строки (а вот старые — умеет).
Ах да, в конфигурации DynamoDB бин создания БД должен выглядеть как-то так (endpoint так же, как указано в админке яндекса, регион тоже тот, который выбирали (i.e. ru-central1
))
@Bean public AmazonDynamoDB amazonDynamoDB() { return AmazonDynamoDBClientBuilder.standard() .withCredentials(new AWSStaticCredentialsProvider( new BasicAWSCredentials(dynamoDbAccessKey, dynamoDbSecretKey) )) .withEndpointConfiguration( new AwsClientBuilder.EndpointConfiguration(dynamoDbEndpoint, dynamoDbRegion)) .build(); }
Кстати, видите Access Key и Secret Key? В админке YC это называется "Статический ключ доступа". А говорю я это потому, что в YC ШЕСТЬ СПОСОБОВ АВТОРИЗАЦИИ! Я сразу оставлю ссылку на документацию, она вам понадобится. Помните про велосипеды? Вот то-то и оно...
На самом деле, использования DynamoDB был значительно менее болезненным, однако я всё равно в шоке, что SDK амазона тоже ничего не умеет. Казалось бы, AWS — это с огромным отрывом лидер в облачных технологиях, но нет, если бы не коммьюнити — пришлось бы писать на встроенном маппере... чуть лучше JDBC, но приятного мало.
Вместо вывода
На то чтобы написать простой CRUD ушло просто непозволительно много времени, пусть и частично из-за моего упёрства и перфекционизма, но мы что, варвары, писать на JDBC без причины в 2023 году?
Я не хочу сказать, что Яндекс Облако — плохой сервис, как раз наоброт: это удобный сервис, и единственный, который работает в т.ч. по формату B2C, потому что все остальные serverless решения в России работают почти исключительно в формате B2B.
Тем не менее, желание Яндекса переизобретать велосипеды, которые до него изобретали уже десятки корпораций, которые уже устоялись и стандартизировались, накорню рубит удобство использования их продуктов, и это очень печально. Яндекс запустил YDB в облаке уже давно (а год назад его ещё и открыл в опенсорс), однако не позаботился о написании нормального SDK, как это сделали амазон — их SDK тоже не фонтан, но это всё равно не идёт ни в какое сравнение с тем, что на общее обозрение выставил яндекс.
Не изобретайте велосипеды, пожалуйста. Придерживайтесь стандартов индустрии.
Стандарты позволяют чуть-чуть упростить разработку, потому что сейчас: