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.
Вот больше красивых картинок: