2018-04-22

О DynamoDB

Мы — в облаках. Где в облаке взять базу данных?
Можно запустить самую обычную виртуалку, и водрузить на неё какой-нибудь PostgreSQL. Вполне рабочий вариант. Особенно, если это не банальный PostgreSQL, а какая-нибудь редкая БД, которая ещё нигде толком не поддерживается. Свой собственный кластер чего-то экзотичного придётся подымать именно так.
Можно взять тот же PostgreSQL в виде managed ресурса. То, что в амазоновом облаке называется RDS (Relational Database Service). Технически, это будет такая же виртуалка, но чуть дороже. Только установкой СУБД, обновлениями, бэкапами, мониторингом будете заниматься не вы, а облако. Специально обученные киберминьоны, управляемые из облачной консольки. Это очень соблазнительное предложение. Это действительно удобно. Таким образом можно запустить многие популярные реляционные СУБД: MySQL, PosgtreSQL, Oracle, MS SQL Server. И даже парочку популярных кэшей: Memcached и Redis.
Cloud Database
А ещё у нас есть сугубо облачные БД. Они живут где-то на мощностях облака. Они — multitenancy, общие мощности облака делятся между множеством клиентов. Поэтому нельзя сказать, что СУБД запущена на какой-то виртуальной машине. Она запущена там, в облаке.
Так как такие СУБД — масштабные распределённые системы, они, как правило, — нереляционные. То есть, NoSQL. Но встречаются и исключения, СУБД, которые выглядят как SQL, но, на самом деле, являются не совсем SQL.
У Амазона есть Redshift. Штука для аналитики. То, что называется Data Warehouse. Вы туда загружаете данные, а потом делаете SQL запросы по данным. Долго. Дорого.
А недавно у Амазона появилась Aurora. Штуковина с сетевым протоколом MySQL или PostgreSQL на выбор. Но полностью облачная. Я не пробовал, но сам Амазон настойчиво склоняет на её использование вместо обычных инстансов RDS.
Вернёмся к NoSQL. В Azure есть удивительнейшая Cosmos DB. Внизу там некий супер-дупер крутой распределённый транзакционный слой. А поверх можно натянуть несколько разных моделей данных, и разных сетевых протоколов. Можно просто key-value, с интерфейсом, совместимым с другой лазурной облачной NoSQL под названием Table. Можно нечто под названием SQL API, а на самом деле хранилище для JSON документов, просто там запросы на чём-то SQL-подобном делаются. Можно сэмулировать протокол MongoDB, тоже документное хранилище получается. Можно сэмулировать протокол Cassandra, получается колоночная БД. А можно хранить графы, причём со стопочкой стандартных графовых API.
Вся эта многослойная мультимодельная концепция Cosmos DB очень сильно напоминает FoundationDB. Там была такая же крутотень, но её можно было запустить у себя. Три года назад FoundationDB с потрохами купила Apple. А буквально на днях её снова выбросили в опенсорц. Надо потыкать, что после Apple там осталось.
DynamoDB Logo
Пока же я погрузился в пучины облачной DynamoDB. Кажется, это один из самых древнейших представителей облачных NoSQL. Кажется, DynamoDB была в AWS всегда :) Сначала это было просто key-value хранилище, но постепенно оно обрасло фичами.
Модель данных в DynamoDB подозрительно напоминает Cassandra. Ту Кассандру, что доступна через CQL.
Есть таблицы — уникальные именованные сущности в пределах AWS аккаунта. Таблица — это единица доступа и ограничения скорости.
Запись в таблице называется Item. Каждый Item идентифицируется первичным ключом. Первичный ключ может состоять из двух частей.
DynamoDB Hash Keys
Первая часть, обязательная, — это hash key. Это — некоторое примитивное значение (строка, число, blob), от которого будет считаться хэш. Это — partition key. Хэш определяет партицию, куда будут помещены данные. Партиция — вполне физическая сущность с конкретно ограниченными физическими возможностями. Поэтому partition key нужно подбирать так, чтобы имелось достаточное количество различных значений. Ибо это определяет максимально достижимую физическую производительность операций над таблицей.
Hash key, его конкретное значение, никаких диапазонов или больше-меньше, нужно указывать и в операциях записи, и в операциях чтения. Это накладывает определённые ограничения. Значения ключей или алгоритм их формирования нужно где-то знать. Нет эффективного способа узнать набор уже существующих ключей. В худшем случае нужно выдумать какие-то псевдоключи только чтобы сформировать нужное число партиций.
DynamoDB Range Keys
Вторая часть первичного ключа, необязательная, — это sort key. Тоже примитивное значение. Тут возможны уже хитрые запросы по больше, меньше и between. По этому полю, и только по этому полю, можно сортировать. Хорошо, что сортировать можно в обе стороны.
С ключами всё. Выбирать Itemы можно только по этим ключам. Причём partition key — обязателен. Впрочем, есть ещё вторичные индексы, где можно указать другие partition и sort keys по тем же данным. Но по сути, это просто автоматически заполняемая копия данных, проиндексированная по другим полям.
Помимо ключей в Itemах хранятся атрибуты. У атрибутов есть имя — строка. (У ключей, у каждой части, тоже должно быть своё имя). И у атрибутов есть значение. Вот тут интересно.
Можно хранить примитивные типы. Строки, до 400 килобайт. Числа, как в JavaScript подразумевается, что все числа — вещественные. Над числами есть атомарная операция инкремента (на значение, определённое в апдейте), легко делать счётчики. Binary, просто бинарные данные, до 400 килобайт.
Можно хранить множества, set, примитивных значений. Можно добавлять и удалять элементы из множества. Можно хранить списки и мапы. Таким образом можно хранить полноценные JSON документы, только уровень вложенности ограничен 32.
Общий размер Item ограничен теми самыми 400 килобайтами. Всё-таки это не Кассандра. И даже не Монга. Но в рамках этих 400 килобайт можно засунуть сколько угодно атрибутов.
Каждый Item в таблице может иметь свой собственный набор атрибутов, никак не связанный с другими Itemами.
DynamoDB Items
При записи нужно указать первичный ключ и значения атрибутов. Будет сделан upsert, если записей нет, они добавятся, если атрибуты были, они обновятся. Есть операции для модификации чисел, множеств, списков и мапов. Так что можно делать частичные апдейты.
Для чтения есть две операции: Scan и Query.
Scan — это просто последовательный просмотр всей таблицы, с применением фильтров. Scan совершенно никак не оптимизирован и не использует индексы. Поэтому он настоятельно не рекомендуется к использованию. Но он есть.
Query — это уже полноценный запрос. Точное значение partition key. Диапазон sort key. Это ограничит набор просматриваемых данных. Направление сортировки. Проекция, можно выбрать лишь часть атрибутов Itemов. Фильтры, можно ещё ограничить содержимое выборки уже по значениям атрибутов. Равно, не равно, больше, меньше, существует, не существует, содержит подстроку, начинается с подстроки. Много проверок можно сделать. Но фильтры не улучшают производительность запроса, ибо поиск делается только по ключам. Но могут сэкономить немного трафика.
Работать с DynamoDB проще всего через сооветствующее SDK. Под Java там вполне приличное ООП, позволяющее конструировать запросы из объектов. Явовые коллекции совершенно прозрачно кладутся во множества, списки и мапы. Но на низком уровне там API через HTTP. Так что в простеньких случаях можно взять обычный HTTP клиент.
С этой моделью данных можно натворить почти всё, что угодно. Нужно только быть аккуратнее с partition key. Там не должно быть одного-двух значений, тогда будет слишком мало партиций. Туда не стоит помещать время или что-то монотонно возрастающее. Во-первых, при запросе придётся перебирать каждую секунду (или какая у вас там гранулярность). Во-вторых, будет эффект «горячей» партиции, когда вся запись идёт в «сегодня».
DynamoDB Query
Самое интересное: производительность и деньги. В облачных БД одно с другим неразрывно связано.
Платить надо. За «единицы ресурса записи», сколько операций записи в секунду будет разрешено. За «единицы ресурса чтения», сколько операций чтения в секунду будет разрешено. При этом под «операцией» понимается запись чего-то не более 1 килобайта, или чтение чего-то не более 4 килобайт. Чтение значительно (раз в десять, или даже в сорок, если объёмы сравнивать) дешевле записи. За объем хранимых данных, погигабайтно. За передаваемые данные, тоже погигабайтно в месяц. Входящие (то есть запись) — бесплатно. Исходящие (то есть чтение) — нет.
Дабы не мучиться с подсчётом нужных «единиц ресурса» можно настроить автомасштабирование этих параметров. Но, похоже, это может привести к попаданию на бабки при внезапном всплеске активности.
Кстати, помните, что скорость ограничивается для каждой таблицы отдельно, и равномерно пилится между партициями. Опять partition key важен. И хоть производительность — штука платная, она тоже имеет ограничение. Впрочем, текущих десятков тысяч попугаев вроде должно хватить всем.
Cost Comparison
Также отдельно надо платить за репликацию (в том числе и между датацентрами), за бэкапы, за дополнительные инстансы кэша (ухты, есть и такое), за DynamoDB Streams и за триггеры.
Streams — это возможность проиграть все операции записи в БД куда-то ещё. Например, скормить их в Lambda и сделать там что-нибудь полезное.
Триггеры в DynamoDB — это такие же Lambda, которые могут влиять на результат записи. При их использовании придётся платить за Lambda.
Сама DynamoDB неплохо интегрируется с инфраструктурой AWS. Данные из DynamoDB можно скормить в тот же Redshift. Или натравить на таблицы DynamoDB целый Hadoop и делать агрегирующие запросы на HiveQL. (В самой DynamoDB никаких group by нет).
Получается нормальный такой облачный NoSQL. Если уж вы оказались в AWS, и вам нужно довольно быстро или очень много данных, попробуйте начать с DynamoDB. Возможно, её вполне хватит. А если не хватит, данные можно перегрузить во что-нибудь более мощное.

2018-04-08

О CodeFest 2018

Я понял, почему уже который код не хочу ехать на CodeFest, но всё равно еду. И еду именно на CodeFest, а не на какие-нибудь JPoint или HighLoad++.
Не хочу, потому что ну сколько можно уже. Докладчиком не берут, потому что местом проживания и работы не вышел. Сибирские да омские докладчики авторитетом не пользуются. Приглашают только своих. За свои деньги ехать, это надо ещё жабу поуговаривать. Ничего нового обычно не бывает.
А еду, потому что Новосибирск сильно ближе, чем Москва. И, в отличие от кучи московских узкоспециализированных конференций, CodeFest — обо всём и сразу. Всё, чем живёт немосковское российское ИТ, сразу становится видно. А если не видно ничего нового, это же хорошо. Это значит, что я — в тренде, и ничего не упускаю. В этом тоже стоит убедиться.
CodeFest 2018
Итак, CodeFest 2018. Доставлял. Было улётно.
В этот раз, в первый день конференции, был целый поток в большом зале под названием Keynote. И это было шикарно. Лучший поток среди всех потоков всех конференций, где я побывал. Так бы и сидел там весь день, но нужно было разведать некоторые более специфичные области.
Началось всё с выступления голландца с простым именем, но непроизносимой фамилией Sander Hoogendoorn. Рассказывал он, по сути, про бирюзовые организации в ИТ. Делайте не проекты, а продукты. К чёрту оценки и планы. Пусть люди сами самоорганизуются в маленькие динамичные эффективные команды. И всем будет счастье.
Вполне очевидные вещи. Для меня. Я сам мог бы что-нибудь такое задвинуть, если нужно. Но я не живу в Амстердаме, и не курю травку. И я не написал пять книжек про IT на нидерландском. Может, пора?
Потом выступал Григорий Бакунов, тот самый Бобук из Яндекса. Говорил, как изобретать изобретения. И как он сам изобретает. Зачем? Ну, наверное, иначе не может. Похвастался парочкой своих изобретений. Например, как обманывать нейросеточки, распознающие наши лица. Парочка тщательно рассчитаных другими нейросеточками полосок на лице, и вас не узнают. Или даже узнают не вас. Очень просил изобрести телепорт. Показал собранную им статистику, где от момента первого упоминанния какого-нибудь изобретения, где-нибудь в фантастической повести, до момента появления чего-нибудь подобного в реальности проходит в среднем тридцать лет. Впечатлённые дети вырастают и воплощают мечту детства. С телепортом как-то не сложилось :(
Пока на Keynote был перерыв, я успел послушать Михаила Ярийчука про Garbage Collector в .NET. Что-то у меня сложилось впечатление, что в JVM-то этот самый сборщик мусора покруче будет. По крайней мере, я давно не слышал, чтобы куча мелких короткоживущих объектов создавала проблемы. Зато в .NET некоторые объекты можно создавать в стеке. Ну а так, это был доклад не столько про сборщик мусора, сколько про вполне разумные техники экономии памяти. Меньше объектов. Больше локальных примитивов. Никакой LINQ магии. Циклы for — наше всё.
Снова Keynote. Александр Орлов, половинка дружного коллектива Орлова и Панкратова по имени «Стратоплан». Рассказал, как он выгорал на работе. На работе с Панкратовым. Когда всё надоедает, ничего не хочешь делать, начинаешь делать глупости. И отдых не помогает. Указанный выход оказался неожиданным. Нужно пойти к психотерапевту. Казалось бы, если выгораешь, значит, работа виновата? Выходит, не всегда так.
Павел Мочалкин, известный своими глубоко философскими докладами, продолжил Keynote. Про силу ограничений. Но я туда не попал. Ради Котлина.
Дмитрий Грязин расскал про Kotlin/Native. Кто не в курсе, это такой Котлин, который компилируется в нативный код, в первую очередь для того, чтобы можно было на нём писать для iOS. И, оказывается, это уже сейчас действительно возможно.
Во-первых, Kotlin/Native просто очень даже неплохо взаимодействует с C. Нужно взять код на C, прогнать одну утилитку, и получить интерфейсы(?) на Котлине, которые можно использовать из Котлина. Сишные функции вызываются как котлиновые функции. Указатели и прочие прелести представлены соответствующими классами. Гораздо проще JNI. Почти так же легко и удобно, как в Python. Я даже подумал, что, с такой хорошей интероперабильностью с C, Котлин может потеснить Питон в этих наших бигдатах да машинных обучениях. И недавнее интервью с Андреем Бреславом это даже немного подтверждает.
Во-вторых, можно писать проекты на Kotlin, которые будут с одной стороны компилироваться в байткод для JVM, а с другой стороны — в нативную разделяемую библиотеку. Тогда получится этот код использовать и из Android приложения, и из iOS приложения. Это уже работает. Проблема, как я понимаю, пока в том, что пока ещё нет достаточного количества библиотек на Kotlin или pure-Kotlin кода. Например, вам банально нужно сделать GET запрос на некий URL и забрать оттуда какой-нибудь JSON. В JVM вы можете использовать java.net.URL. А в нативном коде что? Можно прибиндиться к libcurl. Но общий котлиновый интерфейс для того и для другого вам пока ещё придётся рисовать самим. Как я понимаю, в этом направлении, всеобщекотлиновом стандартном API, вовсю и работают.
Я не удержался и задал вопрос: «Можно ли, используя какой-нибудь инструмент мобильной кросплатформенной разработки, будь то Kotlin/Native, или React Native, или Xamarin, воплотить в жизнь мечту заказчика об экономии затрат (за счёт кросплатформенной разработки) в два раза?» И целая толпа мобильных разработчиков мне дружно ответила: «Нет!»
Снова Keynote. Иван Ямщиков из ABBYY поведал историю развития искусственного интеллекта. Пытаюсь вспомнить, о чём был доклад, и не могу. Кажется, ничего такого, чего нельзя было бы прочитать в научно-популярных книжках, там не было.
Потом выступал Dylan Beattie. Прикольный дядька, все два дня конференции ходил в ковбойской шляпе и сапогах. А оказался чистокровным британцем. Который к тому же учит русский язык. Обещал через три года выступить на CodeFest на русском. Сделал несколько забавных айтишных каверов на известные песни, всех их можно послушать на его YouTube канале. Видимо, этим и знаменит среди организаторов конференции.
Здесь он рассказал интересные, но банальные вещи. Подробно расписал, как котик, сфотографированный на один телефон, попадает на другой телефон. Включая GSM/3G, TCP/IP, HTTP и прочих. Отлично проиллюстрировал JPEG сжатие с потерями на примере рецепта щи. Если из пятнадцати пунктов рецепта выкинуть треть наименее важных, это всё равно останутся щи.
Хоть всё это и банально, лично мне кажется, что 90% программистов действительно не представляют всех этих многослойных абстракций, которые лежат под обыденными вещами нашей цифровой повседневности. Иногда надо погружать в контекст.
Завершил день киноутов Костантин Осипов. Олег Бартунов не смог приехать. И пришлось Косте отдуваться за будущее баз данных. Копнул он хорошо и глубоко.
Существующие БД никуда не денутся. NoSQL тоже никуда не денется. NewSQL будет процветать, ибо SQL пока что лучший язык запросов к БД. Все будут тырить друг у друга идеи. Опенсорсом денег не заработаешь. Есть угроза со стороны облачных БД, которые вроде те же MySQL, но полностью managed в облаке. Гребут деньги лопатой, но тому же MySQL с этого ничего не перепадает. Но облачные БД не могут захватить весь рынок, потому что есть Edge. Это те же автономные автомобили, которым локально нужно собирать и хранить кучу данных, и они не могут всё перенести в облако. Как-то так.
Первый день конференции закончился. Начался афтепати. Спасибо той неизвестной мне рок-группе, что играла отличную живую музыку в баре «Rock City». На этом про афтепати всё.
CodeFest
На второй день целого потока Keynote не было. Зато было два потока Backend. Один из них открывал снова Костантин Осипов. К сожалению, мы успешно опоздали на первые доклады. И я застал лишь самый конец. Судя по всему, это был отличный обзор различных, применяемых на практике, схем шардирования. Со всеми их преимуществами и недостатками. И о том, что шардирование не заменяет мозги, и всё равно нужно думать, куда и зачем складывать данные. Обязательно надо посмотреть, как только появятся слайды и видео.
Иван Панченко отлично рассказал про PostgreSQL 10. Про новую фичу логической репликации. До этого в Постгресе была отличная потоковая репликация, когда WAL файлы мастера передаются на слейвы и там буквально воспроизводятся. Получается точная (с точностью до задержки репликации) копия мастера. Всей БД целиком. Кроме того, что тут нельзя отреплицировать лишь кусочек БД, есть ещё и проблема с длинными транзакциями на слейве. Пока мы тут что-то долгое на слейве выбираем, с мастера может прийти удаление части данных.
И вот теперь тот же самый поток WAL может более интеллектуально разбираться на слейве. Слейв становится полноценной БД, куда можно писать. Но ещё туда прилетают инсерты и апдейты с мастера. И применяются как обычные инсерты и апдейты. DDL пока не передаётся, так что придётся руками синхронизировать схемы. Зато можно проделывать хитрые фокусы. Например, иметь мастер без индексов, для быстрой вставки. И несколько слейвов, с разным набором тяжёлых индексов для разных отчётов. Или можно сделать частичный мультимастер, чтобы разные таблицы писались на разных узлах, но взаимнореплицировались. Кстати, у Postgres Professional (импортозамещение, помните) есть свой мультимастер.
Иван Круглов рассказал про service mesh в Booking.com. Идея хорошая, и действительно годная, когда у вас много микросервисов. Идея в том, чтобы отделить обязанность поиска других сервисов и связи с ними от самих микросервисов. Не делать это в виде умной библиотеки (на определённом языке), а делегировать прокси, запущенном на localhost.
Каждый сервис, когда ему нужен другой сервис или даже БД, просто обращается к localhost, на том же хосте или даже в том же контейнере, как оно обычно и происходит в девелопменте. А на локалхосте живёт хитрый прокси, который знает, куда запрос переправить, кэширует открытые соединения, делает повторы и балансировку, если нужно. Знания о маршрутах и местоположении других сервисов он берёт из единого центра управления этим хозяйством. В качестве прокси в Booking.com используют Envoy.
Идея вполне здравая, особенно если у вас сервисы написаны совсем на разных языках. Но я также убедился, что discovery и взаимодействие сервисов, которое я напилил в нашем текущем проекте, тоже имеют право на жизнь. Пусть там пока и нет выделенного прокси :)
Ещё один архитектурный доклад был от Andrea Giunta. Он всё правильно рассказал про длинный путь от монолита, через клиент-сервер и трёхслойные архитектуры, до микросервисов и функций в облаке. Но было скучно. Никто даже не родил вопросов по ходу лекции. Немотря на многочисленные «Questions?» докладчика. Может, функции и были бы для меня откровением, но я с ними разобрался двумя неделями ранее :)
А потом я продолжил собственное погружение в мир кросплатформенной мобильной разработки, начатый ранее с Kotlin/Native. Послушал Андрея Оздьона про React Native, Дмитрия Моисеева про Xamarin, Сергея Лагнера про Qt. Ещё сам немного вспомнил про PhoneGap/Cordova.
Глобальных подхода к кросплатформенности у нас получается три.
Номер раз. Берём от платформы что-то переносимое, что позволит там рисовать и реагировать на события. Либо WebView, а в него засовываем почти обычное веб-приложение, это будет Cordova. Либо OpenGL surface, а на нём уже рисуем свои виджеты, это будет Qt.
Номер два. Берём хороший и популярный язык, фреймворк и/или среду выполнения. Тщательно обёртываем нативные возможности каждой платформы в этот наш язык. Как бонус, можно налепить сверху общезнаменательное API, чтобы можно было ваять, не оглядываясь на конкретную платформу. Если языком будет JavaScript, а фреймворком React, получится React Native. Если языком будет C#, а средой выполнения .NET, получится Xamarin.
Номер три. Пишем максимально общий независимый код, бизнес-логику и всё такое, на каком-нибудь языке, который сможет скомпилироваться в библиотеку на любой платформе. Например, на C или, теперь уже, Kotlin. И пишем нативный UI для каждой платформы, который использует эту общую библиотеку.
Особняком ещё стоят игровые движки. Которые вполне можно рассматривать как средство кросплатформенной разработки. Похоже, они изрядно в этом направлении продвинуты.
Лично мне кажется наиболее перспективным и полезным для пользователей подход номер три. Поэтому топим за Котлин :)
Но почему живы и процветают другие подходы? Потому что всё решают ресурсы. Человеческие ресурсы. Где-то завалялись тонны кода на C++, которые никто не будет переписывать. А воткнуть C++ в Android проще через Qt. Где-то есть куча .NET разработчиков, и их можно и нужно утилизировать на мобильную разработку на Xamarin. А где-то развелось фронтендеров, которые, на Reactе-то, научились серьёзным методологиям и подходам, и теперь тоже смогут писать под мобилки с помощью React Native.
Но в любом случае нужны настоящие Android и iOS разработчики. Хоть полчеловека. Чтобы затыкать дыры и дописывать слои совместимости. Вот так.
7bits at CodeFest
Конференцию закрывал Александр Лысковский. Жизнеутверждающим докладом про мощь айтишников. Про то, как ИТ компании приходят в не-ИТ бизнес, и добиваются успеха. Потому что умеют всякий Agile и вообще люди с головой на плечах. Любой айтишник разберётся с тем, как доить кур. Но никакая доярка никогда не сообразит, как настраивать Bitrix. Упоминал Dodo pizza (Фёдор Овчинников закрывал CodeFest в прошлом году), Tinkoff bank (был спонсором CodeFest в этом году), Amazon, Uber и прочих прочих. Жить будем, господа коллеги. Даже если кодеров и верстальщиков заменит нейросеточка, постановщики задач никуда не денутся.
Итак. Кросплатформенная мобильная разработка. Базы данных (PostgreSQL). Микросервисы. Kotlin. Блокчейн. Бизнес. Будущее. Светлое будущее для всех, с информационными технологиями.
И да, англоговорящих докладчиков всё больше, и понимающих их без перевода тоже всё больше.
P.S. Я даю здесь ссылки на доклады на сайте CodeFest. Скоро там появятся презентации. А чуть позднее — видео.

2018-03-25

О Lambda

Итак, микросервисы. В амазоновом AWS. Отложим в сторону тот факт, что микросервисы на JVM будут не очень микро. Им нужны сотни мегабайт памяти для нормальной работы. Нас интересует вопрос: как запускать сервисы?
Очевидно, можно запустить виртуалочки EC2, с любимой ОС. Поставить туда любимую среду выполнения. И запускать любимые микросервисы, или макросервисы, или даже громадные монолиты. Не проблема.
Можно взять Докер и разворачивать сервисы в виде контейнеров в Elastic Container Service (ECS). Можно приплести и Kubernetes в виде EKS. Контейнеры по-прежнему подразумевают, что портами, запросами, библиотеками, репозиториями, развёртыванием занимаетесь вы сами, пусть и манипулируя сущностями AWS.
Идём дальше. А дальше у нас — феномен под названием Serverless computing.
Идея такая. Вы ничего не знаете о серверах, ни виртуальных, ни реальных. Вы ничего не знаете о выделенных ресурсах. Порты, инстансы, балансеры — всё у вас отобрали. Сама инфраструктура становится фреймворком, который вызывает ваши обработчики, когда нужно. Вот такие обработчики в AWS называются Lambda. В Azure это называется Functions.
Serverless
Написать Lambda на Java или Kotlin довольно просто. Вам нужен код обработчика. Это может быть либо типизированный обработчик, принимающий и возвращающий некие объекты, которые, на самом деле, сериализуются и десериализуются в/из JSON.
class Handler: RequestHandler<InputData, ResultData> {

    override fun handleRequest(input: InputData, context: Context): ResultData {
        context.logger.log("Input: $input")
        //...
        return ResultData("OK")
    }

}
Или же это может быть обработчик, просто принимающий и возвращающий потоки бинарных данных. И уж он-то сам может извратиться с нужной ему сериализацией.
class Handler: RequestStreamHandler {

    override fun handleRequest(input: InputStream, output: OutputStream, context: Context) {
        val inputReader = input.reader()
        val messages = parser.parse(inputReader)
        //...
        output.writer().apply {
            println("OK")
            flush()
        }
    }

}
Можно и без реализации каких-либо интерфейсов, главное, чтобы сигнатура метода была понятна Lambda.
Context — это специальный объект, дающий некоторое представление о том, где выполняется наша лямбда. Помимо примитивного логгера (println() ничуть не хуже), там есть метод getRemainingTimeInMillis(), который, очевидно, говорит, сколько времени у вас осталось. А логи, будь то stdout, Log4j или даже SLF4J, всё равно попадут в CloudWatch.
Настраивать лямбды, передавать им параметры, можно только переменными окружения. При создании лямбды можно выставить что-то своё. Но целая туча переменных передаётся автоматически. Включая ключи и секреты доступа к самому Амазону. В результате клиенты из AWS SDK, которые можно использовать в лямбдах, получают доступ к сервисам Амазона автоматически.
Конечно, доступ к сервисам лямбде даётся не просто так. Lambda работает от явно определённой роли AIM. Это такая служба раздачи прав в Амазоне. Какие права в роли пропишете, такие права лямбда и получит. Ни больше, и ни меньше.
Lambda Logo
Лямбды можно писать на Node.js, Java 8, Python, втором или третьем, .NET Core или Go. Код лямбд загружается zip архивом. В случае Java можно загрузить jar архив. Только это должен быть абсолютно классический super jar. Безо всяких извращений, которые могут туда привнести Shade или Spring Boot. Но все зависимости нужно тащить с собой, там будет голая Java.
Из Ansible лямбда загружается примерно так:
- name: create Lambda
  lambda:
    region: '{{ aws_region }}'
    aws_access_key: '{{ aws_access_key }}'
    aws_secret_key: '{{ aws_secret_key }}'
    name: '{{ lambda_name }}'
    state: present
    zip_file: '{{ lambda_file }}'
    runtime: 'java8'
    role: '{{ lambda_role }}'
    handler: '{{ lambda_handler }}'
    timeout: '{{ lambda_timeout }}'
    memory_size: '{{ lambda_memory_size }}'
#    vpc_subnet_ids:
#      - subnet-123abcde
#      - subnet-edcba321
#    vpc_security_group_ids:
#      - sg-123abcde
#      - sg-edcba321
    environment_variables: '{{ lambda_environment }}'
Регион и ключи. Как обычно, для любого обращения к API AWS они нужны.
Имя лямбды. Имя уникально для AWS аккаунта. Это тот самый адрес, под которым лямбда будет доступна из других сервисов.
Zip файл. Тот самый zip или jar, содержащий код лямбды.
runtime указывает, в какой среде будет запускаться лямбда. Для JVM есть только «java8».
role — это AIM роль вида «arn:aws:iam::123456789012:role/YourLambdaRole». Роль определяет права лямбды в AWS.
handler — это имя метода-обработчика. Для Java это будет что-то вроде «your.java.package.Handler::handleRequest».
timeout — допустимое время выполнения обработчика, в секундах. Это время жёстко ограничено. Если время выйдет, обработчик принудительно завершится. Можно поставить любое малое значение, но не более 300 секунд. Лямбды должны выполняться быстро.
memory_size — лимит по памяти, в мегабайтах. Жёсткий лимит. Больше памяти просто не будет. Мало памяти — Java будет тормозить и падать с OutOfMemoryError. Минимум — 128 мегабайт, для hello world на Java хватает. Но для работы DynamoDB SDK нужно хотя бы 256.
По умолчанию лямбда работает где-то в своих облаках. У неё есть доступ в интернеты и к большинству managed сервисов AWS. Но некоторые сервисы недоступны из интернетов, а живут в вашем приватном VPC, например, так работает ElastiCache. Поэтому лямбду иногда тоже надо запускать в VPC, дав ей соответствующие права.
environment_variables — это ваши переменные окружения, через которые вы хоть как-то можете сконфигурировать лямбду.
Serverless Concept
Лямбду задеплоили, теперь её можно вызывать. Как, зачем и откуда? Это самое интересное.
Множество сервисов AWS могут порождать события, которые могут вызвать (trigger) вызов лямбды. При этом само событие придёт на вход обработчику лямбды, и лямбда сможет с этим что-то сделать.
Файлопомойка S3 может вызывать лямбды на события создания, модификации и удаления объектов-файлов.
Нереляционная база данных DynamoDB может вызывать лямбды на операции записи. И это будут самые настоящие (и единственные доступные) триггеры в этой БД.
Сервис уведомлений SNS. Это такой облачный publish-subscribe с поддержкой подписчиков в виде телефонов для СМС, емейлов для почты и даже мобильных пушей. Он может вызвать лямбды. Лямбда фактически подписывается на топик и обрабатывает сообщение.
CloudWatch, сервис мониторинга всея Амазона, может вызывать лямбды при наступлении определённых событий. Например, когда в очереди SQS накопились сообщения. Сама SQS не умеет лямбды, но через CloudWatch можно таки вызвать лямбду, а она уже разгребёт очередь. Ну или просто по расписанию, есть в CloudWatch и такие события.
Amazon Alexa — персональный ассистент от Амазона. Говорящий динамик. Самый верный способ разрабатывать для него — писать свои лямбды-обработчики.
Amazon API Gateway. Serverless веб сервер. Вы определяете правила маршрутизации запросов, а обработку делают ваши лямбды. Микросервиснее и облачнее уже некуда, кажется.
AWS IoT Button. Волшебная кнопка "Сделать всё хорошо". Настоящий физический брелок с кнопкой в реальном мире. Нажатие на кнопку вызывает вашу лямбду в облаке.
Просто AWS IoT. Загадочный сервис интернета вещей. Как минимум, он умеет принимать сообщения по, внезапно, популярному протоколу MQTT, который, как оказалось, широко используется в этом IoT, и скармливать эти сообщения вашей лямбде.
И конечно же, лямбду можно запустить из лямбды. Запуск лямбды — это нормальная часть AWS SDK, которую вполне можно использовать.
Ну вы поняли, да? Лямбды — это автоматизация всего в облаке. Лямбды — это всё. Ну или почти всё.
API Gateway
Но как же и где же это выполняется? Тем более, что Java (да и JavaScript) — далеко не самый лучший язык для написания скриптов. Java долго запрягает, требует много памяти, начинает быстро работать только после основательного «прогрева», т.е. неоднократного выполнения этих наших запросов.
Я не нашёл внятного описания среды выполнения. Известно, что это Amazon Linux (подвид RedHat Linux). Упоминаются контейнеры. Практика показывает, что первый прогон (после деплоя) лямбды на Kotlin, с записью в DynamoDB (используя стандартный Java SDK) выполняется секунд за 30. Долго. Но довольно быстро время снижается до 60 миллисекунд.
Так что хорошая новость в том, что лямбды запускаются один раз, а потом обслуживают множество запросов. Соответственно, прогрев наших любимых JIT вполне себе имеет место. А учитывая, что ограничения по памяти выполняются очень жёстко, это всё действительно запускается в контейнере.
Получается, что код лямбды разворачивается где-то в контейнере. И живёт, и выполняет запросы непрерывно. Более того, одновременно может быть запущено несколько лямбд, максимум до тысячи штук. И, похоже, это масштабирование происходит автоматически. Полагаю, верно и обратное, что в отсутствие запросов последний контейнер будет остановлен, и затем заново случится холодный старт.
Кстати, холодный старт лямбды в VPC будет дольше где-то на минуту. Потому что для лямбды будет выделяться Elastic Network Interface.
Получается, реально всю заботу об инфраструктуре здорово запрятали. Всё бы ничего, но отлаживать это дело тяжеловато. Поди пойми, что там у тебя случилось, нехватка памяти или сетевой таймаут, если лямбда была убита раньше по собственному лимиту времени выполнения.
Lambda Resource Limits
Почему лимиты на время и память? Потому что платить за лямбды нужно, исходя из времени работы обработчика, и указанного лимита памяти. Ну и немного исходя из количества запросов, если их миллионы. Время округляется до 100 миллисекунд.
В кои-то веки снова приходится писать так, чтобы было быстро и ело мало памяти. Прощай, Spring Boot, я буду по тебе скучать :)
Кстати, начинают появляться фреймворки для писания и деплоя именно под Lambda. Serverless для Node.js. Zappa для Python.
И, тссс... Эти самые Dynos от Heroku — это ж и есть эти самые лямбды.

2018-03-17

О ветках

Допустим, у нас есть Git. Или другая распределённая система контроля версий. Например, мой любимый Mercurial. Такая система, где ветки являются отдельным измерением.
И у нас есть проект, команда и сроки. Проект горит. Сроки горят. Задницы команды тоже горят.
Раз есть ветки, появляется соблазн их использовать. Раз хотим использовать, появляется вопрос «Как?». И куча рекомендаций по бранчеванию от различных источников разной степени доверенности.
Весьма, к сожалению, популярен Git Flow.
Git Flow
Смотрите, сколько тут красивых веток. master, где лежат аккуратно помеченные тегами семантического версионирования, окончательные и бесповоротные, чудовищно стабильные релизы. develop, где, якобы, происходит постоянная ежедневная разработка. Хотя на самом деле разработка происходит в feature branches, которые ответвляются от develop для каждой фичи, и вливаются обратно, когда фича готова. А ещё есть ветки подготовки релиза. А ещё есть ветка хотфиксов. Раздолье для любителей наводить порядок.
Спасибо что тут merge, а не rebase. А то ведь бывают ещё чудовищные варианты не только с постоянным rebase, но и со squash веток в master. Министерство правды завидует этим героям. Так затирать историю даже они не умели.
Социальный кодинг («Fork you!») добавляет масштабов. Ветки могут являть собой форки репозитория. А изменения оттуда приносятся через Pull/Merge Request и соответствующую процедуру кодоревью. Это уже GitHub Flow получается.
GitHub Flow
Я не люблю пулреквесты. За то, что это сторонний инструмент, относительно самой системы контроля версий. Самое ценное: собственно ревью и соответствующее обсуждение — хранятся где-то совсем отдельно от историй изменения кода. Это — неправильно. С другой стороны, принимать изменения со стороны только через процедуру ревью — выглядит хорошо.
Но сейчас меня интересует работа одной команды. Нет изменений со стороны. Не нужны пулреквесты. Зато есть очень-очень большая необходимость выкатывать любые изменения как можно быстрее. Чтобы любой чих, любая новая кнопочка, любые новые экранчики были бы сразу видны заказчику. Чтобы можно было честно хвастаться достижениями каждого дня.
Нужно быстро. Поэтому работа одного разработчика должна быть как можно быстрее доступна другим разработчикам. Фронтендер должен как можно быстрее заполучить хоть как-то частично работающий вызов бэкенда. Пользователи библиотеки или сервиса должны как можно быстрее заполучить хотя бы заглушки нужных функций.
И что нам тут дадут ветки? Разработчик может запереться в своей ветке и неделями что-то пилить, что никто не увидит. Если он ещё забывает вытягивать изменения из того же develop, то он получит ещё и увлекательные два дня мержа потом. Ну и смысл такого изоляционизма?
К тому же возникает вопрос готовности фичи. Когда мержить в develop? Когда написан код? Когда проведено тестирование? Кем и где протестировано? Но у нас же всё горит, и гораздо выгоднее выкидывать на заказчика не полностью готовую фичу, а малейшие видимые инкрементальные изменения.
Показать заказчику быстро — довольно просто. Нужен Continuous Integration/Delivery. Чтобы, как только изменения появляются, сразу собирать и деплоить их. Но нужно знать, откуда брать изменения. Как правило, это одна ветка. Хотите — develop. Хотите — master.
Одна ветка. И изменения в ней должны появляться как можно чаще.
На Lean Pocker было ещё веселее. Там деплоились запушенные коммиты в любую ветку. Суровый «фигак-фигак и в продакшен».
Production
К чему это я? Не делайте веток. Фигачьте сразу в develop. Можно даже фигачить в master. Оставьте ветки для изоляции окружений. А если у вас одно единственное окружение, и оно сразу продакшен, ну и фигачьте сразу в master.
Не думайте, что если будете все работать в одной ветке, то избавитесь от мержа. Мерж всё равно происходит. С ветками вы их явно мержите. Желательно часто, чтобы ничего не пропустить. А если работать в одной ветке, мерж сам происходит при pull. Постоянно. Всегда. А в этом деле чем чаще — тем лучше.
Боитесь что-нибудь сломать кривым коммитом? Во-первых, кривым должен быть пуш, а не коммит. Если вы что-то сломаете, а следующим коммитом почините, а потом запушите всё вместе, ошибки никто и не заметит. Ибо ваши коммиты остаются только вашими, пока они не запушены. А во-вторых, нефиг деплоить ошибки. Ваш Continuous Integration должен гонять все доступные тесты, и не деплоить тот код, который тесты не прошёл. И должен громко кричать «Ай-ай-ай», и вывешивать ваше имя на доску позора, если вы накосячили.
Говорят, что мастер должен быть стабильным? Не мастер, а последний тег мастера. Расставляйте теги в те моменты, когда считаете, что релиз готов. Деплоить вполне можно и теги. И продолжайте фигачить в мастер. Если у вас вообще есть понятие «релиз». А то в режиме горящих задниц каждый пуш должен быть отрелизен.
Каждая фича должна разрабатываться изолированно? Зачем вам это? Всё равно при мерже всё смешается (в Git). А ФИО разработчика (для blame) никуда не денется. Поставьте номер фичи в коммите, и все тасктрекеры радостно притянут эти коммиты к задаче.
Но ведь нельзя делать фичу так, чтобы не сломать (хотя бы временно) то, что уже есть? Можно! Ещё как можно.
Это вопрос организации кода. И культуры разработки. Тот самый SOLID. Не должно быть общих файлов, куда вынуждены лезть для правок все разработчики (Привет солюшенам VS и прочим IDEшным файлам, не должно им быть в репозитории). Все изменения должны добавлять код/файлы, но (по возможности, конечно) не менять существующие. Стоит избегать дублирования, когда одни и те же изменения приходится вносить в несколько файлов.
Грамотно структурированный код представляет собой дерево. И это дерево должно расти от листьев. Новые функции — это новые листья, классы, файлы. Их можно и нужно писать независимо от всего остального. И тестировать независимо. И всё дерево будет работать как прежде, пока новые листья болтаются без дела, но компилируются, и их тесты проходят.
Чуть позже, когда листик будет достаточно готов, к нему достаточно протянуть веточку. Вписать новую директиву в какой-нибудь конфигурационный файл, добавить определение Spring Bean. И вот оно, пожалуйста, новый код начал работать. Уже протестированный и достаточно стабильный код.
Mercurial metadata
Для такой инкрементальной разработки отдельные ветки не нужны.
И для максимально частых релизов отдельные ветки не нужны. Но это отдельный больной вопрос. Не все разработчики могут придумать, как делать фичу маленькими видимыми кусочками. Чтобы хвастаться каждым маленьким изменением. Не все даже умеют делать маленькие коммиты. Тут надо тренироваться. Наполовину работающий прототип таки лучше, чем совсем ничего.
Но ветки, конечно же, иногда нужны. Иногда случаются эксперименты, которые с высокой вероятностью вообще никогда не сольются с основным кодом. Их разумно делать в отдельной ветке. Иногда совсем не получается запилить фичу, не сломав всё остальное. Такие длинные масштабные изменения тоже разумно делать в отдельной ветке. Но все эти эксперименты и рефакторинги на практике плохо совместимы с режимом горящей задницы :) Это ж надо время, чтобы неспешно всё перетряхнуть.
Фигачьте в мастер! Очень прошу. Это работает. Это не страшно. Это, за счёт уменьшения бюрократических процедур слияния и досмотра, заметно ускоряет доставку фич заказчику.

2018-03-03

О Lossy

Все мы любим хорошую музыку. Ещё совсем недавно хорошей цифровой музыкой был Audio CD. Это 44100 Гц, стерео, 16 бит (линейных) на канал, никак не пожатое, а значит, если верить Википедии, 1411.2 кбит/с.
Но в конце XX века, в эпоху зарождения мультимедиа, когда музыку стали играть не только на проигрывателях, но и на компьютерах, оказалось, что Audio CD (то есть голый PCM) всё же лучше сжимать. Был, например, Microsoft ADPCM, который это дело немного сжимал, без потери качества, в файлы WAV. Но, как правило, исходные 44 кГц стерео таким образом всё равно требовали много места. Поэтому качество понижали до 22 кГц моно. Один из первых мультимедиа альбомов того времени: «Погружение» группы «Наутилус Помпилиус» — до сих пор где-то у меня валяется, так и делал.
Тогда победил MP3. Для хранения и распространения сжатой музыки. На 128 кбит/с «обеспечивающий CD качество».
MP3 возник странно. Технически это MPEG-1 Audio Layer 3. Слой для сжатия аудиоданных в крутом и прогрессивном тогда модном стандарте для запихивания видеоданных на Video CD. Только упакованный в свой собственный формат файла .mp3. Video CD теперь никому не интересен. Следующий стандарт MPEG-2 используется на DVD и в цифровом (не HD) ТВ вещании. А ещё следующий стандарт MPEG-4 используется сейчас для HD видео и продолжает развиваться.
MP3 был революционен. Это был (почти) первый формат сжатия с потерями. Когда мы не пытаемся сохранить всё, что было в исходном сигнале, а, на основе некоей психоакустической модели, выпиливаем то, что человек всё равно не услышит, а оставшееся сжимаем. Как JPEG.
Тогда я пробовал оцифровывать накопленную аудиоколлекцию. Компакт-кассеты (просто «кассеты», но правильнее «компакт-кассеты») оказались полным дерьмом. Частотный диапазон там такой, что дискретизировать с более чем 22 кГц смысла не было. Катушечных магнитофонов в доме не водилось. А вот виниловые пластинки потрясли качеством звука. На хорошем оборудовании можно вытянуть качество получше, чем CD. Только от щелчков нужно избавляться.
И тогда же я понял, что MP3 — тоже дерьмо. На этих самых 128 кбит/с качество звука сильно страдает. И что самое жуткое — появляются мерзкие металлические призвуки там, где их быть не должно. Моим ушам нужно хотя бы 192 кбит/с, а лучше больше.
Возьмём одну композицию известной в прошлом панк-рок группы. В виде FLAC. Это такой современный стандарт сжатия без потерь, который благополучно заменил WAV. Потому что свободный.
Оригинал в CD качестве, поэтому, как и положено, присутствуют частоты до 22 кГц.
Original Flac
Будем жать с помощью FFmpeg, а точнее с помощью LAME.
На 320 кбит/с и на 256 кбит/с спектрограмма выглядит почти как у оригинала.
На 192 кбит/с видны признаки подрезания частот на 16 кГц. Спектрограмма «темнеет», видимо, пcихоакустическая модель что-то повырезала. На слух самые высокочастотные «всплески» действительно пропали.
MP3 192 kbps
На пресловутых 128 кбит/с всё уже конкретно подрезано на 16 кГц. Фоновые звуки «замазаны» и начинают «подбулькивать». Уже ничего общего с оригиналом в плане наслаждения музыкальными деталями.
MP3 128 kbps
Но в MP3 можно сделать и 64 кбит/с. Пропало стерео. Всё жутко булькает и раздражает совершенно посторонними призвуками.
MP3 64 kbps
И даже 32 кбит/с. Ой, ну даже Рабинович лучше напоёт. Не слушайте этот ужас.
MP3 32 kbps
Ну вы поняли, да? От исходного звука остались только дырки. Не делайте так. И вообще не используйте MP3.
Потому что есть Ogg Vorbis. Или просто Vorbis. Он появился в 2002. Именно как специальный независимый свободный формат для сжатия аудиоданных с потерями. И в нём всё гораздо лучше.
Вот вам Vorbis на 128 кбит/с. Никаких обрезаний частот нет. Всё звучит прекрасно. Разве что самые «верхи» чуток смазаны.
Vorbis 128 kbps
Можно и 64 кбит/с. Здесь частоты подрезаны на уровне 16 кГц. Звучит глуше, но вполне терпимо. Лишних призвуков не появляется.
Vorbis 64 kbps
Теоретически, можно урезать битрейт ещё меньше, но ffmpeg так не умеет.
Для ужимания музыки, когда хочется сэкономить место, для FLACа не хватает, я с тех самых пор выбираю Vorbis. Для моих ушей он сильно лучше.
Но те же самые ребята, из Xiph.Org, в 2012 придумали Opus. Это ещё более крутой кодек, подходящий не только для музыки в качестве Audio CD, но и для сжатия речи, например, в телефонии. Он, соответственно, умеет и совершенно низкие битрейты.
Opus на 128 кбит/с начинает немного подрезать частоты, где-то на 20 кГц. Это едва заметно.
Opus 128 kbps
На 64 кбит/с видно, что работает переменный битрейт (VBR). Opus пытается сохранить частоты, но в половине случаев обрезает ниже 16 кГц. Звучит лучше, чем Vorbis на том же битрейте.
Opus 64 kbps
На 32 кбит/с Opus пытается держаться, но видно, что высокие частоты редеют. Звучит сильно хуже, глухо и «пережато». Но исходная композиция вполне узнаваема, не тошнит, как от MP3 на 32 кбит/с.
Opus 32 kbps
Opus на 16 кбит/с. Через две подушки :) Но высокие частоты иногда проскакивают.
Opus 16 kbps
Opus на 8 кбит/с. Всё, всё потерялось. Остались только слова, хрипы и ритм.
Opus 8 kbps
Opus на 4 кбит/с. Похоже, Opus сфокусировался на голосе. На фоне общего безобразия человеческий голос остаётся различим.
Opus 4 kbps
Тут Opus, конечно, сдался, и переключился в «телефонный» режим. Но слова слышно. Пусть и плохо :)
Opus — хорош. Очень хорош. Правда, он не рекомендует частоту дискретизации 44100 Гц. И через ffmpeg получилась передискретизация в 48000 Гц. С другой стороны, вроде как оборудование сейчас работает как раз на 48 кГц. И ещё непонятно, кто лучше сделает ресэмплинг: кодек или Pulse Audio.
Вот больше красивых картинок:

2018-02-18

О JSON-RPC

Уже на втором настоящем проекте я внедряю JSON-RPC. Пока никто страшно не жалуется :)
A Logo
JSON — это, как всем известно, JavaScript Object Notation. Я не люблю JSON. Я помню ещё те времена, когда вместо JSON балом правил XML. В XML было всё: и схемы, и трансформации, и многочисленные форматы, протоколы и стандарты, на нём основанные. Хотя бы Jabber/XMPP вспомните.
JSON сейчас полностью заменил или собирается заменить XML. Хотя лучше он лишь по одному параметру: объему сериализованных данных. Ну ладно, ещё нет проблемы «тег или атрибут».
И JSON, и XML годны лишь для автоматической сериализации структурированных данных. Изначальные человекочитаемость и человекописабельность, заложенные в обоих, себя не оправдали. В XML хоть комментарии есть. А вот делать конфиги из JSON, прошу вас, не надо.
И JSON, и XML уязвимы к атакам на парсеры, ибо дозволяют неограниченный уровень вложенности. Иронично, что сам JavaScript парсит JSON документы абсолютно так же, как это делают другие языки. Ибо через eval() — ну совсем уж небезопасно.
В XML всё было текстом. И были существенные проблемы представления бинарных данных. В JSON, вслед за JavaScript, пытаются ввести какие-то типы данных. Но они такие же убогие, как в самом JavaScript. Нет целых чисел. Нет правильных дат. Тут BSON и другие бинарные форматы — вне конкуренции.
XML vs JSON
Ну ладно, есть хипстерский JSON. Значит, должен быть JSON-RPC. RPC — Remote Procedure Call. Как подсказывает Википедия, к RPC относятся такие монстры как CORBA, DCOM. И в Java есть RMI. И NFS внутрях работает через RPC. И на XML у нас имеются широко используемый SOAP и никому неизвестный XML-RPC.
Изначально это наше объектно ориентированное программирование подразумевало пересылку сообщений между объектами. Как-то быстро сложилось, что эти сообщения — это какая-то просьба сделать что-то вот с этими вот параметрами и вернуть результат. То есть вызов метода у объекта. Самые популярные реализации ООП решили, что вызов метода объекта есть то же самое, что вызов функции локального процесса. Все вот эти стеки, переходы, возвраты. Это действительно эффективно. Но также сообразили, что имеет смысл иметь один объект где-то там, и слать вызовы удалённо, по сети, сериализуя и десериализуя параметры и результаты. Это и есть RPC. Ну или микросервисы, если хотите.
Сейчас в этих наших микросервисах, по крайней мере, в вебе, модно использовать REST. Странно. REST гвоздями прибит к HTTP. А HTTP — далеко не самый эффективный транспортный протокол. REST — он про данные. Ну и что, что девяносто процентов всех обращений к серверу — это получение или запись данных. А что делать для остальных десяти процентов? Вводить фиктивные сущности и очереди-коллекции этих сущностей только для того, чтобы POST в эту коллекцию запускал какую-то процедуру?
Есть ещё системы обмена сообщениями. Но в этих самых сообщениях всё равно нужно указать получателя, и что мы от него хотим. RPC — это ведь и есть обмен сообщениями. Где «получатель» — это объект или сервис, а «что мы хотим» — это имя метода. Разница только в том, что в RPC обычно подразумевается синхронный обмен, и в ответ на вызов метода ожидается результат здесь и сейчас. А при «обычном» обмене сообщениями подразумевается асинхронный обмен, ответ либо вообще не требуется, либо придёт когда-нибудь потом, может быть, даже по другому каналу связи. Но синхронность или асинхронность — это скорее особенности организации клиента и сервера, а также работы транспортного протокола, чем принцип собственно RPC.
RPC,Messaging,REST
Вот и JSON-RPC. Это лишь «спецификация» на одной странице, как представлять запросы и ответы в виде JSON. Этой спецификации абсолютно всё равно, каким образом эти запросы и ответы пересылаются. Можно через HTTP, тогда этот JSON просто POSTится на сервер, и тогда имеет смысл замапить варианты ответов на HTTP коды. Можно через голый TCP. Можно через ZeroMQ, мы и так делали.
Но ведь через HTTP, TCP и ZeroMQ можно слать любые JSON сообщения. Зачем использовать JSON-RPC? Потому что вам в любом случае придётся выработать какое-то соглашение. Что кодировать в JSON? Что угодно? А как кодировать ответ? Всё равно ведь придёте к чему-нибудь типа { "success": true }. Вот JSON-RPC и есть такое простенькое соглашение, которое уже есть. И ничуть не хуже любых других соглашений, которые вы можете выдумать.
JSON-RPC не определяет адресацию. Адресация, то есть поиск того самого объекта, на котором будем делать вызовы, это забота транспортного протокола. В HTTP это будет URL эндпоинта, куда POSTтить. В TCP это будет просто хост и порт, куда слать. В ZeroMQ это будет identity получателя или вообще та схема адресации, которую вы выдумаете.
JSON-RPC определяет синтаксис запроса.
{
  "jsonrpc": "2.0",
  "method": "subtract",
  "params": { "subtrahend": 23, "minuend": 42 },
  "id": 3
}
Есть обозначение версии протокола ;) Есть имя метода. Есть параметры. Тут может быть либо JSON объект в случае именованных параметров, либо JSON массив в случае позиционных параметров. Но не может быть одиночного значения. А ещё params может быть совсем опущен, если это метод без аргументов.
А ещё у нас есть id. Строка или число. По id клиент сможет связать ответ с запросом. Именно наличие id позволяет JSON-RPC без проблем работать асинхронно. Это уже забота транспорта и клиента, когда и как получить ответ. А по id уже можно понять, ответ на что это был. (Совершенно немыслимая забота в мире HTTP и других синхронных протоколов).
Можно послать сообщение и без id. В JSON-RPC это называется «Notification». В этом случае клиента не беспокоит судьба его запроса и ответ ему не интересен. А сервер не должен на нотификации отвечать. Чем не «обычная» отправка сообщения?
Результат выполнения метода кодируется как result.
{
  "jsonrpc": "2.0",
  "result": 19,
  "id": 3
}
Результатом может быть любой валидный JSON, включая строки, числа, объекты, массивы, и null. (Кстати, при отображении в Java-объекты фиг ты различишь, где там null, а где отсутствие данного поля).
Если что-то пошло не так, результата не будет, будет error.
{
  "jsonrpc": "2.0",
  "error": { "code": -32601, "message": "Method not found" },
  "id": "1"
}
В error имеется код. Есть несколько предопределённых кодов для типичных ситуаций (вроде отсутствия метода), но для специфичных ошибок вам придётся придумать свои коды. Имеется сообщение, типа человеку. И можно добавить data с чем угодно, jsonrpc4j туда пихает класс и сообщение из исключения.
Ну вот и вся спецификация. Есть ещё батчи, но там всё очевидно. Просто? Понятно? Имхо, да.
JSON-RPC Format
Мы в спринговых микросервисах используем jsonrpc4j. Кажется, это единственная серьёзная реализация для Java. Там есть свои клиенты и серверы, и какая-то интеграция со Spring. В результате получается весьма прозрачно, как оно и положено в RPC. Есть интерфейс, с объявленными методами, с заданными именами параметров. На серверной стороне есть реализация этого интерфейса, которая и представляет собой этот наш объект, который будем удалённо вызывать. На клиентской стороне создаётся Proxy, который подключается к серверу, и реализует этот самый интерфейс для локальных вызовов. В качестве параметров и результатов можно использовать всё что угодно, пока оно сериализуется Jacksonом. Получается красиво, но синхронно.
Очень хочется этот jsonrpc4j слегка допилить. Убрать задавание path для HTTP сервера в интерфейсе, всё же это личное дело сервера, по какому адресу располагать эндпоинт, а значит, path должен быть задан у реализации, а не у интерфейса. Убрать странные и дурацкие ограничения на этот path. Например, что мешает на один path навешать кучу объектов, пока они не пересекаются по методам? Добавить поддержку автоматического распознавания имён параметров, ибо это должно работать в Java 8 и Kotlin. Но пока некогда... И так неплохо работает.

2018-01-27

Об AWS

Они говорили: «Большие облачные провайдеры — это хорошо». Они говорили: «Там есть отличная поддержка Docker контейнеров». Они говорили: «Контейнеры идеально подходят для микросервисов». Они говорили: «Микросервисы — это лучшая современная архитектура».
AWS Logo
На прошлой неделе меня развлекала Azure. На этой неделе мы переехали на Amazon Web Services aka AWS. В надежде, что в AWS будет меньше нежданчиков. Ну и просто, AWS был привычнее, и заказчику, и мне.
Нежданчики были. Но они имели объяснение, и часто даже описание в документации.
Документация в AWS мне нравится больше. Вроде объем сравним с азуровской. Но технических подробностей больше. Объясняется, что это, зачем это и с чем это взаимодействует. Меньше пошаговых туториалов со скриншотами. Почти нет маркетинговой чепухи.
Консоль управления (не Portal, а Console) — другая. В Азуре всё напихали панельками в одно большущее веб-приложение. В Амазоне слепили из лоскутков чудовище Франкенштейна. Но это хорошо. Каждый лоскуток — сам по себе, прост и понятен. Разные лоскутки — консольки разных сервисов, оформлены немного по-разному, в разных стилях. Где-то подновлено, попросторнее, настройки чётко сгруппированы. Где-то более старенькое, с большущими боковыми меню.
Единственный мозговзрывающий пункт — список всех сервисов. Его сделали, ура, в несколько колонок на весь экран. И только текстом. И с поиском. Не заблудишься. И, ура, отдельно вынесены последние сервисы, которые открывал.
Самой большой проблемой с консолью были права доступа. Мне не дали суперадмина на весь аккаунт. Пришлось постоянно натыкаться на отсутствие прав и запрашивать их отдельно. И в Азуре, и в Амазоне сущностей, на которые можно раздать права, так много, что просто невозможно заранее выдать нужные ограниченные права.
CLI тоже есть. Известный aws. Тоже написан на Python, хошь на втором, хошь на третьем. Под капотом — библиотечка boto.
В отличие от Азуры, где клиент срёт кучей разных файлов в ~/.azure, с aws и boto всё чётко. Есть лишь два файла: ~/.aws/config, где прописывается нужный регион (десятки их у Амазона), и ~/.aws/credentials, где нужно указать ключ и секрет. И эти файлы нужно создать руками. Можно и несколько профилей указать, если одновременно нужно в разных аккаунтах рулить. Ключа и секрета достаточно, чтобы от имени владельца ключа рулить Амазоном. И из Ansible тоже.
Ансибль может AWS. По сравнению с Азурой, модулей больше, они разнообразнее, они старше минимум на пару лет. Но некоторые свежие фичи, которые в AWS завезли в 2017, Ансиблем ещё не поддерживаются.
Console screenshot
Single Page Applications aka SPA, типа на React, можно заливать непосредственно в файлопомойку Амазона под названием S3. Что, кстати, расшифровывается как Simple Storage Service, любят они слово «Simple». И прикрыть это дело CDN под называнием CloudFront. В обоих сервисах магия сводится к настройке, чтобы в случае любой ошибки, в том числе 404 Not Found, возвращать /index.html.
SPA Lifecycle
Докеры. Реестр для образов имеется: Elastic Container Registry aka ECR. Слово «Elastic» в Амазоне любят ещё больше.
В отличие от Азуры, где пароль для docker login выдаётся раз и навсегда, Амазон выдаёт длииииинющий пароль, который действует лишь 12 часов. Мол, раз вы пароль передаёте в docker login, то он остаётся в истории команд, а это нехорошо. Ну и кто из них больше думает о безопасности?
Новый пароль приходится получать командой aws ecr get-login --no-include-email. Или даже так:
$ $(aws ecr get-login --no-include-email)
Потому что выхлопом команды является уже готовый к исполнению docker login с аргументами. Так что бы не выполнить его сразу?
Можно прикрутить ECR Credentials Helper, штуку такую, расширение для Докера, чтобы Докер сам ходил в Амазон за авторизацией. Но что-то у меня оно сразу не заработало.
В отличие от Азуры, Docker Hub и прочих Docker Registry, ECR не создаёт репозиторий для образа автоматически. Нельзя сделать docker push для образа, про который ECR ещё ничего не знает. Нужно явно создавать репозитории, через консоль, через CLI или через Ansible.
ECR, ECS relations
Образы есть. Они вполне доступны из реестра образов. Теперь нужно деплоить контейнеры. Для этого есть Elastic Container Service aka ECS. Эта штука не совместима ни с чем. И работать с ней нужно через консольку, CLI или Ansible.
Для начала нам нужен ECS кластер. В котором будут выполняться задачи (Task), представляющие собой какие-то сервисы (Service), описанные в определении задач (Task Definition).
В Ansible определение задачи выглядит примерно так:
- name: create ECS task definition
  ecs_taskdefinition:
    state: present
    aws_access_key: '{{ aws_access_key }}'
    aws_secret_key: '{{ aws_secret_key }}'
    family: '{{ service_name }}'
    network_mode: host
    containers:
    - name: '{{ service_name }}'
      image: '{{ service_image_name }}'
      memoryReservation: 64
      portMappings:
      - containerPort: '{{ service_port }}'
      logConfiguration:
        logDriver: 'awslogs'
        options:
          awslogs-group: '{{ ecs_cluster_name }}'
          awslogs-region: '{{ aws_region }}'
          awslogs-stream-prefix: '{{ ecs_cluster_name }}'
      environment:
      - name: SERVER_PORT
        value: '{{ service_port }}'
      - name: DATABASE_URL
        value: 'jdbc:postgresql://{{ service_database_host }}/{{ service_database_name }}'
      - name: DATABASE_USER
        value: '{{ service_database_user }}'
      - name: DATABASE_PASSWORD
        value: '{{ service_database_password }}'
  register: task_output
Для консольки или CLI то же самое нужно переписать в JSON. Поэтому, кстати, внутри определения контейнера появился camelCase, не свойственный Ансиблю.
Как видите, тут нужны ключ и секрет для доступа к AWS.
family — это «семейное» имя для определения задачи. Определения версионируются, поэтому актуальное имя определения будет вида «family:revision».
Про network_mode поговорим попозже.
В одной задаче можно запустить сразу до десяти контейнеров. Это будет одна единица масштабирования. Эти контейнеры будут гарантированно запущены на одном Docker хосте. Раз на одном хосте, у них будет прямая сетевая видимость друг друга.
Контейнер, как положено, запускается из определённого образа, image.
Контейнеру обязательно нужно указать требования по памяти. memoryReservation — это мягкий лимит, в мегабайтах.
Как обычно в Docker, можно прокинуть порты, portMappings. Порт внутри, порт снаружи. Впрочем, для network_mode: host порт нужен только один.
logConfiguration — это такая специальная штука, где с помощью драйвера awslogs можно засовывать логи контейнера в CloudWatch — фиговину для мониторинга.
В environment контейнеру передаются переменные окружения, как обычно.
Результат создания описания задачи мы записываем в task_output, потому что он будет нужен для создания сервиса.
Сервис из Ansible создаётся так:
- name: create ECS service
  ecs_service:
    state: present
    aws_access_key: '{{ aws_access_key }}'
    aws_secret_key: '{{ aws_secret_key }}'
    name: '{{ service_name }}'
    cluster: '{{ ecs_cluster_name }}'
    task_definition: '{{ task_output.taskdefinition["family"] }}:{{ task_output.taskdefinition["revision"] }}'
    desired_count: 1
    deployment_configuration:   # allows to shut down and restart the service
      minimum_healthy_percent: 0
      maximum_percent: 100
Снова нужны ключи доступа в Амазон.
name — это имя сервиса. Уникальное в пределах кластера.
cluster — это имя кластера. Он уже должен быть. Так как его обновлять не нужно, проще создать его один раз из консоли. Хотя, конечно, есть и другие варианты.
task_definition — это определение задачи для этого сервиса. Тут нужно указать конкретное определение, включая его фамилию и ревизию. Именно для этого мы записывали результат создания определения в task_output.
desired_count — сколько экземпляров сервиса мы хотим видеть живыми. Получается, что сервис — единица масштабирования. И все контейнеры, объявленные в определении задачи, будут созданы в данном количестве экземпляров.
Но не обязательно. deployment_configuration задаёт требуемые параметры «живости» сервиса. По умолчанию, если мы сменили определение и перезапустили сервис и его контейнеры, ECS сначала создаст вторую копию сервиса, а лишь потом удалит старую копию. Он стремится к zero downtime. Но это создаёт проблемы, если контейнер занимает определённый порт хоста, а хост с Докером у нас только один. Второй контейнер на том же порту не запустишь, поэтому всё ломается. В этом примере разрешается нулевая «живость», и сначала убивается старый контейнер, а потом создаётся новый.
Tasks in ECS
В ECS нужно создать кластер, который будет рулить сервисами, создавать задачи по определениям, в нужном количестве. Но что такое кластер? Тут есть два варианта.
Недавно в AWS появилась штука под названием Fargate. Это магическая фиговина, которая запускает контейнеры незнамо где. Имхо, довольно удобно. И ECS, конечно же, умеет работать через неё. Но Fargate пока доступен только в одном датацентре Амазона, в Северной Вирджинии.
Остальным нужно запускать кластер на старых добрых инстансах EC2, то есть на виртуалках. Всё честно, вам сразу говорят, нужны виртуалки, чтобы запускать контейнеры. На этих инстансах нужно поставить Docker и запустить ECS агента. Но логичнее и проще взять готовый ECS-optimized образ виртуалки, где всё уже стоит. Можно запускать EC2 инстансы не руками, а через Auto Scaling группу. Тогда ECS сам будет заботиться, сколько инстансов нужно, в зависимости от ресурсов, которые будут есть контейнеры.
Вся эта магия делается за минуту через визард создания кластера в консоли. Тем не менее, все кишки и задействованные системы торчат наружу, и их можно потом поднастроить.
Важен один нюанс. EC2 инстансы должны иметь выход в интернеты. Ибо агентам на них нужно общаться собственно с ECS, а он для них находится где-то там.
Кстати, Auto Scaling можно включить и для сервисов в ECS. Тогда количество задач, то есть количество запущенных контейнеров для этого сервиса, тоже будет динамически подстраиваться в зависимости от нагрузки.
ECS Instances
Контейнеры деплоятся. Хорошо. Но у нас микросервисы. К каким-то сервисам нужен доступ снаружи. А какие-то сервисы нужны для внутренних нужд. Нужно, чтобы сервисы знали друг о друге и имели прямой сетевой доступ друг к другу.
Собственно, эти вопросы в рамках одного хоста умеет решать Docker Compose. Контейнеры видят друг друга по имени сервиса, указанного в docker-compose.yml. И вполне имеют сетевой доступ друг к другу, если находятся в одной сети. В рамках нескольких Docker хостов эти вопросы решает Swarm, с помощью своей overlay сети. Kubernetes, полагаю, имеет схожие механизмы.
Что нам предлагает ECS для организации сети между контейнерами? Он нам предлагает то же, что голый Docker Engine двухгодичной давности. Плюс одна амазоноспецифичная плюшка.
network_mode: host. В этом случае никаких дополнительных сетевых прослоек не имеется. Процесс в контейнере слушает порт на хост машине. Соответственно, на одном порту можно запустить лишь один контейнер на хост. Нужно больше? Тогда нужно больше EC2 инстансов cнизу. Просто и эффективно. Но в общем случае контейнер не знает, на каких EC2 инстансах (с какими IP адресами) запущены другие контейнеры.
network_mode: bridge. Обычный сетевой мост Докера. Внутри контейнера процессы могут вешаться на любой порт. Этот порт может быть выставлен наружу, то есть на EC2 хост, под любым другим номером. Контейнеры на одном EC2 инстансе могут ходить по сети друг к другу. Но, во-первых, нет никакой гарантии, что они окажутся на одном инстансе, только если не были определены в одном ECS таске. Во-вторых, они всё равно не знают ни доменных имён, ни IP адресов (внутренних) друг друга. Поддерживаются старые добрые линки. Но это опять-таки работает только в рамках одного EC2 хоста. К тому же линки требуют, чтобы контейнер, на который ссылаются, был уже запущен. А такой жёсткий порядок запуска, мягко говоря, не удобен.
network_mode: awsvpc. Новая штука. Поддерживается в Fargate. Требует дополнительной настройки агента на EC2 инстансах. В этом случае каждый контейнер становится полноценным участником сетевого обмена в инфраструктуре AWS. Каждый контейнер получает свой собственный elastic network interface в рамках VPC. Проблема в том, что количество этих интерфейсов на один EC2 инстанс ограничено, от двух до пятнадцати, в зависимости от размера инстанса. А это значит, что на одной EC2 машинке можно будет запустить лишь от двух до пятнадцати контейнеров ECS.
Bridge network
В любом случае всё остаётся на уровне голого Docker Engine. Механизма контейнерам обнаружить друг друга и ходить друг другу в гости ECS не предоставляет.
В моих микросервисах уже сложился свой механизм service discovery. Я лишь решил слегка допилить его. Хранить реестр сервисов во внешнем Redis, который в AWS представлен в виде ElastiCache. А код для регистрации себя и обнаружения других сервисов встроить в сами сервисы.
Кстати, узнать IP адрес текущего EC2 инстанса (и контейнеров в нём) легко посредством запроса к Instance Metadata. Буквально вот так:
$ curl http://169.254.169.254/latest/meta-data/local-ipv4
Существуют и более амазоноспецифичные способы самообнаружения контейнеров. Можно запрашивать Load Balancer, ибо он может знать обо всех контейнерах, которые он балансит, где они находятся и на каких портах. Можно внедрить в каждый контейнер агента, который будет регистрировать адрес и порты контейнера в амазоновом DNS (который называется Route 53).
Можно рассматривать это как прикладное применение теоремы Гёделя о неполноте. Контейнеры сами по себе не могут решить задачу самообнаружения. Нужна внешняя сущность в виде базы данных (хотя бы DNS сервер), к которой имеют прямой доступ все контейнеры.
Discovery with DNS
Контейнеры есть. Друг к другу ходить могут. Как обеспечить к ним доступ извне? Логичнее всего использовать Elastic Load Balancing aka ELB. Его вариант, разбирающийся в HTTP, под названием Application Load Balancer.
Балансировщик нагрузки принимает входящий HTTP или HTTPS трафик. Можно навешать свой домен. Можно навешать свой сертификат. И по правилам направляет запросы на Target Groups. В качестве правил можно использовать либо пути, либо заголовки запроса.
В целевых группах указываются либо EC2 инстансы, либо IP адреса, в рамках VPC. Но ведь наши EC2 инстансы для наших контейнеров могут создаваться и удаляться, autoscaling ведь. Чтобы решить эту проблему, ECS может управлять балансировщиком, прописывая туда, где сейчас и на каком порту висит соответствующий сервис. Нужно только в настройках сервиса указать, каким балансировщиком и какой Target Group управлять.
Application Load Balancer в AWS — такая же тупая штука, что и в Azure. Url rewriting не умеет. Проверяет работоспособность бэкендов HTTP GET запросами. Так что все прибабахи для сервисов, типа специального контроллера на /health, остаются в силе. А ещё он не умеет CORS.
CORS aka Cross-Origin Resource Sharing — это такая фигня, которую современные браузеры делают, когда API, куда стучится JavaScript, находится не в том же домене, что страница, на которой выполняется JavaScript. Вполне полезная штука в плане безопасности. Браузер делает OPTIONS запрос, спрашивая: «А вот с этого домена можно?» Сервер отвечает в заголовке Access-Control-Allow-Origin: «С этого можно».
Поддержку CORS нужно делать в контейнере. Можно водрузить Nginx. Можно добавить чуток конфигурации в Spring.
Есть в AWS и более мощная штука под названием API Gateway, которая это всё вроде может. Но API Gateway — это скорее про конструирование API-заглушек, например, для мобильных приложений. Чтобы потом заполнять их вызовами кода из Lambda. Это скорее не про микросервисы, а про конструирование API в облаке на лету, API as a service. Боязно и странно.
Load Balancing
Ладно. Есть докеры двухгодичной свежести. А есть что поудобнее? Есть. Elastic Container Service for Kubernetes aka EKS. Похоже, это заразно, играть буквой «C» в аббревиатурах. Container → Kontainer → Kubernetes.
Вот только EKS в Амазоне «is in Preview». А превью в Амазоне — значительно серьёзнее, чем в Азуре. Это значит написать им, объяснить, зачем нам это надо, и ждать две недели, может быть включат.
EKS
С этими облаками и контейнерами всё получается так. Сначала создаются все эти сущности: балансировщики, кластеры, репозитории. Потом собираются образы и выкладываются в реестры. Потом запускаются или перезапускаются контейнеры по этим образам.
Ансибль в этом мире Immutable Infrastructure выглядит чужеродно. Ибо всё равно все его задачи выполняются на localhost с типом подключения local. Всё равно ведь нужно к AWS API подключаться.
Чувствую, что, когда в следующий раз пойду за облаками, я возьму Terraform. Ты просто описываешь, что, где, с какими свойствами, с чем связанное, в каком количестве тебе развернуть. И оно разворачивает. У AWS есть своя такая штука, CloudFormation называется. Но Terraform более переносим, он умеет не только AWS.
Terraform vs Ansible