2018-05-20

О DynamoDB

А продолжим о DynamoDB.
Краткое содержание предыдущей серии. DynamoDB — одна из старейших облачных NoSQL БД. Живёт в облаке Амазона (aka AWS).
Модель данных у DynamoDB очень напоминает таковую у кассандрового CQL. Есть таблицы. В таблицах хранятся itemы. В таблице определён первичный ключ, по которому ищутся itemы. Первичный ключ состоит из обязательного partition key (он же hash key) и необязательного sort key (он же range key).
Partition key определяет партицию, куда будут помещены данные. Поиск возможен только по полному совпадению значения этого ключа. Получается, что любая операция в БД работает только с единственной партицией.
Sort key отсортирован. Возможен поиск по диапазону значений этого ключа.
Кроме ключей в itemах можно хранить атрибуты. Каждый атрибут имеет имя и значение. Значения бывают разных типов: строки, числа, множества (set), списки (list) и карты (map). Элементами множеств, списков и карт могут быть другие типы. Таким образом можно хранить любые структурированные данные, а ля JSON.
DynamoDB Schema
Посмотрим подробнее, как можно строить запросы. В Spring, как всегда, есть свои обёртки над всякоразными API разных БД. А в стандартном амазоновом SDK вся пляска идёт вокруг объекта Table (если мы не используем мапинг в Java объекты). Для запросов этот объект реализует интерфейс QueryApi. Самый мощный его метод выглядит так:
ItemCollection<QueryOutcome> query(QuerySpec spec)
Соответственно, всё, что может QuerySpec, может и DynamoDB. А ItemCollection результата можно просто засунуть в for-each цикл и вытащить все itemы.
В каждом запросе должны или могут присутствовать:
  • Точное значение partition key.
  • Условие выборки по sort key. Больше, меньше, между, начинается с подстроки.
  • Направление сортировки по sort key. Можно в обе стороны.
  • Список атрибутов, которые нужно извлечь. Проекция.
  • Фильтры для дальнейшего уточнения выборки. По любым атрибутам можно проверить кучу условий. Больше, меньше, между, существует ли, содержит ли подстроку. Важно, что лишь первичный ключ ограничивает набор просматриваемых itemов в хранилище. Фильтры лишь отсеивают itemы, которые нужно вернуть. Поэтому нужно хорошо думать о первичных ключах, чтобы они ограничивали выборку, а фильтры использовать лишь как вспомогательный инструмент.
  • Условие объединения фильтров, если их больше одного. «И» или «ИЛИ».
  • Флаг строгой целостности. Да, DynamoDB пытается предоставить какие-то гарантии, чтобы чтение после записи могло прочитать только что записанные данные.
  • Ограничения на количество просматриваемых записей, размер и количество страниц. Под капотом DynamoDB работает через HTTP, и размер страницы ограничивает размер одного HTTP ответа. Каждая страница возвращается отдельным HTTP ответом. Все эти тонкости хорошо запрятаны в SDK и на уровне итерации по результатам запроса почти незаметны.
DynamoDB Query
У нас есть два взаимно исключающих способа работы с DynamoDB. Это касается условия выборки по ключу, проекций и фильтров.
Первый способ — более старый. Раньше появился в API. Здесь вы указываете точные значения ключей, имена атрибутов, конкретные операции и значения для сравнения.
Такой запрос на Kotlin выглядит примерно так:
val filters = sensors.map { QueryFilter(it).exists() }

val querySpec = QuerySpec()
    .withHashKey("u", location)
    .withRangeKeyCondition(RangeKeyCondition("t").between(
        Instant.parse("2018-04-20T00:00:00Z").epochSecond,
        Instant.parse("2018-04-21T00:00:00Z").epochSecond))
    .withScanIndexForward(false)
    .withAttributesToGet(*sensors.toTypedArray())
    .withConditionalOperator(ConditionalOperator.OR)
    .withQueryFilters(*filters.toTypedArray())
    .withMaxResultSize(1000)

val items = table.query(querySpec)
for (item in items) {
    // ...
}
Второй способ — более новый. Здесь появляется понятие выражений, expressions. Для ключей, проекций и фильтров. Текстовые читабельные выражения. Примерно такие же, что стоят после WHERE в SQL. И в эти выражения можно подставлять параметры.
Почему-то выделяют два вида параметров. Параметры для имён используются для подстановки имён атрибутов, и выглядят они в выражениях как «#name». Параметры для значений используются для подстановки значений, с которыми будут сравниваться ключи или атрибуты, и выглядят они как «:value». Задаются имена и значения обычными мапами.
Запрос с выражениями на Kotlin выглядит примерно так:
val querySpec = QuerySpec()
    .withKeyConditionExpression(
        "u = :location AND t BETWEEN :start AND :end")
    .withScanIndexForward(false)
    .withProjectionExpression(sensors.mapIndexed { index, _ -> "#attr$index" }
        .joinToString(", "))
    .withFilterExpression(sensors.mapIndexed { index, _ -> "attribute_exists(#attr$index)" }
        .joinToString(" OR "))
    .withMaxResultSize(1000)
    .withNameMap(sensors.mapIndexed { index, name -> "#attr$index" to name }.toMap())
    .withValueMap(mapOf(
        ":location" to location,
        ":start" to Instant.parse("2018-04-20T00:00:00Z").epochSecond,
        ":end" to Instant.parse("2018-04-21T00:00:00Z").epochSecond
    ))

val items = table.query(querySpec)
for (item in items) {
    // ...
}
Я тут замутил, запрашиваю произвольный набор атрибутов и выискиваю itemы, где эти атрибуты присутствуют.
Казалось бы, зачем эта морока с составлением строковых выражений (где ещё и некоторые слова зарезервированы). Объектами ведь проще. Для запросов оно, наверное, и так. Но для апдейтов выражения дают некоторые уникальные возможности.
Conditional Update
Апдейты делаются через интерфейс UpdateItemApi, который, конечно же, реализуется нашим Table.
Дело в том, что я считаю метрики. И интенсивно использую возможность атомарного инкремента числовых значений. Ну и атомарного добавления элементов во множества тоже.
Оно работает так. Я говорю, что хочу добавить в item с данным первичным ключом, к атрибуту с конкретным именем, такое-то число. Если атрибута или даже всего itemа не существует, он создаётся автоматически. Начальным значением считается нуль, и он атомарно инкрементируется на указанное число. Если атрибут уже существует (и содержит число), его значение просто инкрементируется. Можно даже одним запросом обновить так несколько атрибутов одного itemа. Очень удобно.
Выглядит это примерно так:
val key = PrimaryKey("u_period", hashKey, "t", rangeKey)

val updateOps = listOf(
    AttributeUpdate("${sensor}_count").addNumeric(aggregate.count),
    AttributeUpdate("${sensor}_sum").addNumeric(aggregate.sum)
)

table.updateItem(key, *updateOps.toTypedArray())
AttributeUpdate задаёт операции над конкретным атрибутом. А затем список операций выполняется над конкретным itemом.
И теперь добавляем две магии. Их присутствие сильно подняло крутизну DynamoDB в моих глазах.
Магия первая. Перед апдейтом можно проверить выполнение некоторого условия на любых атрибутах itemа. Если условие выполняется, апдейт происходит. Если условие не выполняется, но наш код получит ConditionalCheckFailedException и может что-то с этим сделать. Условие можно добавить, передав в updateItem помимо AttributeUpdate ещё и коллекцию объектов Expected.
Магия вторая. Оказывается, выражения позволяют обращаться к вложенным атрибутам с указанием пути (path). Если у вас есть атрибут с именем «map», содержащий мапу, а в этой мапе есть поле с именем «field», то можно использовать путь «map.field», чтобы обратиться с значению в мапе. Если где-то там есть список, то можно использовать числовой индекс элемента в списке в квадратных скобках: «list[0]». Как в JSONPath. Появление специальных символов типа точки вносит некоторую неоднозначность, поэтому настоятельно рекомендуется такие символы не использовать в именах атрибутов. Эти вложенные пути работают только в выражениях, и это хороший повод перейти на выражения.
На самом деле, любой апдейт должен или может содержать:
  • Точное значение partition key.
  • Точное значение sort key, если он есть. Как видите, нельзя проапдейтить несколько itemов одной операцией.
  • Набор операций обновления данного itemа или update expression.
  • Набор условий для проверки возможности обновления данного itemа или condition expression.
Итак. А что, если у нас много счётчиков. И они образуют развесистую иерархию. Например, мы хотим подсчитать, сколько раз какой-то сенсор принимал определённые значения. При этом мы знаем название сенсора, и это может быть именем атрибута. Но мы не хотим вдаваться в детали, какие именно значения принимал каждый сенсор. Мы просто засовываем в атрибут мапу, где имена полей будут значениями, а хранить мы будем счётчики, сколько раз это значение встречалось.
И теперь мы хотим атомарно инкрементировать числа, вложенные в мапы. Как мы делали это с числами, которые непосредственно хранились в атрибутах. С вложенными путями в выражениях это возможно. Нужно задать update expression вида «SET map.nested = map.nested + 1». Это будет работать.
Но беда в другом. Сама мапа, если её не существует, нет такого атрибута, не будет создана автоматически. И то же касается всех вложенных полей мапы. Нельзя прибавить число к тому, чего нет.
Проблему можно решить таким алгоритмом. Добавляем condition expression, который проверяет наличие вложенного поля для инкремента. Если условие не срабатывает, ловим исключение и делаем уже другой апдейт: создаём нужное поле мапы, сразу с начальным значением. И ставим здесь другое условие: на существование самой мапы. Если условие не срабатывает, ловим исключение и уже создаём мапу.
Как-то так:
try {
    table.updateItem(key,
        "SET #sensor.#state.#name = #sensor.#state.#name + :inc",
        "attribute_exists(#sensor.#state.#name)",
        mapOf("#sensor" to sensor, "#state" to state, "#name" to name),
        mapOf(":inc" to 1))
} catch (e: ConditionalCheckFailedException) {
    try {
        table.updateItem(key,
            "SET #sensor.#state.#name = :newName",
            "attribute_exists(#sensor.#state)",
            mapOf("#sensor" to sensor, "#state" to state, "#name" to name),
            mapOf(":newName" to 1))
    } catch (e: ConditionalCheckFailedException) {
        // ...
    }
}
Тут получается рекурсивный отлов исключений, в зависимости от глубины вложенности нашей мапы. В худшем случае, когда это совершенно свежий item, придётся сделать соответствующее число неудачных попыток, прежде чем создать сразу вложенную мапу нужной глубины. Зато последующий инкремент того же поля сразу сделает нужный атомарный апдейт.
Не вполне ясно, как тут будет с гонками, сериализуются ли апдейты в одной партиции. Пока вроде работает, но одиночные ошибки, когда разные клиенты будут одновременно пытаться создать мапу, поди ещё отлови.
Обратите внимание, что вложенные пути, которые с точками, должны присутствовать именно в выражении. Если передать строку с точками в качестве параметра, это не будет работать как вложенный путь.
fun UpdateItemApi.tryToIncrement(key: PrimaryKey, path: List<String>, increment: Int) {
    val pathExpression = path.toPathExpression()
    val attributesMap = path.toNameMap()

    try {
        updateItem(
            key,
            "SET $pathExpression = $pathExpression + :inc",
            "attribute_exists($pathExpression)",
            attributesMap,
            mapOf(":inc" to increment)
        )
    } catch (e: ConditionalCheckFailedException) {
        tryToCreateMap(key, path, increment)
    }
}

private fun UpdateItemApi.tryToCreateMap(key: PrimaryKey, path: List<String>, value: Any) {
    val pathExpression = path.toPathExpression()
    val attributesMap = path.toNameMap()

    val upperPath = path.dropLast(1)
    val upperPathExpression = upperPath.toPathExpression()

    if (upperPath.isNotEmpty()) {
        try {
            updateItem(
                key,
                "SET $pathExpression = :new",
                "attribute_exists($upperPathExpression)",
                attributesMap,
                mapOf(":new" to value)
            )
        } catch (e: ConditionalCheckFailedException) {
            val lastName = path.last()
            tryToCreateMap(key, upperPath, mapOf(lastName to value))
        }
    } else {
        updateItem(
            key,
            "SET $pathExpression = :new",
            attributesMap,
            mapOf(":new" to value)
        )
    }
}
С такими мощными условными атомарными обновлениями вложенных полей DynamoDB изрядно приближается по удобству к той же MongoDB. При этом все соглашения по поводу стоимости и производительности остаются в силе. DynamoDB вполне можно использовать для счётчиков realtime подсчёта агрегатов (да, я сам не понял, что сказал :).

2018-05-02

О tinc

Непонятно почему, заинтересовался я VPNами. Оказывается, VPNом называют всё что ни попадя. Совершенно разные технологии, созданные для разных целей.
Путаницы вносит ещё наверное то, что в Android VPNом называют довесок к сетевым настройкам. Приложение, которое может гонять через себя трафик любого другого приложения. Как оно гоняет трафик, через настоящий VPN, через прокси, через /dev/astral — не важно. В Android всё это будет называться VPN.
Typical VPN Vision
Начнём с простого. HTTP proxy. Старый добрый Squid. Или даже маленький его карманный «аналог» Polipo. Как правило это кэширующие прокси. Это значит, что если много пользователей будут запрашивать одну и ту же страницу, эта страница может быть сохранена в кэше и выдана из кэша. Можно сэкономить на трафике. Существенно, кстати. Поэтому раньше провайдеры частенько принудительно заруливали клиентов на прокси. Это называлось transparent proxy.
Но с HTTPS так не выйдет. Потому что тут есть сертификаты, и клиент проверяет идентичность сервера. Поэтому HTTPS трафик пропускается через прокси с помощью метода CONNECT. Получается, что браузер разговаривает с прокси по протоколу HTTP (или HTTPS), и говорит ему: «А соедини-ка меня с тем вот сервером на таком-то порту». И дальше шлёт через прокси произвольный трафик на этот порт. Как правило, прокси сконфигурированы пускать клиентов только на 443 порт.
Ребята пошли дальше и подумали, что вовсе не нужен HTTP, чтобы общаться с прокси. И придумали специальный протокол под названием SOCKS. Это именно протокол общения с прокси. И в текущей его версии под номером пять можно даже попросить прокси перенаправлять UDP датаграммы на нужный сервер.
SOCKS изначально придумывался как способ договориться с межсетевым экраном. Мол, пусти меня туда, потому что. Можно рассматривать это как некий аналог UPnP, только не ограниченный локальной сетью.
SOCKS оказался достаточно удобным, чтобы направить трафик конкретного приложения (которое умеет быть SOCKS клиентом) через некоторое другое приложение-прокси, чтобы это прокси сделало с этим трафиком что-нибудь хитрое. Именно таким образом организуется вход в туннель Shadowsocks или Тёмный Интернет Tor. Обратите внимание, что соответствующие прокси вы запускаете локально, максимум, в той же локальной сети. А вот вход в них, перенаправление, например, трафика браузера, делается через SOCKS.
Можете побаловаться вот такой командой, если у вас есть ssh доступ к какому-нибудь серверу:
$ ssh -D 1080 -f -C -q -N [email protected]
У вас получится маленький прокси «для бедных» на localhost:1080, который будет гонять трафик по ssh через сервер, к которому вы подключились. Можете направить на него трафик того же браузера и посмотреть, будет ли тормозить. В принципе, с помощью программок, называемых общим словом proxifier, можно заслать в SOCKS прокси трафик почти любого приложения, даже которое не знает, что такое SOCKS.
SOCKS прокси — это лишь точка входа в какой-то туннель. И запускать прокси где-то там — не очень хорошая идея. Сам по себе SOCKS не занимается шифрованием данных. И даже пароли аутентификации в SOCKS5 идут открытым текстом.
SOCKS Proxy Usage
И вот тут наконец-то появляется слово VPN. Virtual Private Network. Задача состояла в том, чтобы обеспечить подключение удалённых сотрудников к локальной (частной) сети компании. Или связь нескольких удалённых филиалов. Это — бизнес. Серьёзные ребята вообще прокладывают свой личный кабель между офисами. А те, кто не может, вынуждены пользоваться публичным Интернетом. А чтобы нехорошие конкуренты не подслушали, нужно шифрование.
VPNов такого рода весьма много. Большинство — проприетарные. vpnc, OpenConnect — придумали Cisco, и подключаться там нужно к их железкам. pptp придумали Microsoft, и он лучше всего поддерживался в Windows. Единственный свободный — OpenVPN.
Все эти VPNы работают схожим образом. По принципу старых добрых аналоговых модемов. Клиент явно подключается к серверу, создаётся соединение point-to-point. А дальше весь (или не весь, как настроен клиент) трафик идёт через это соединение. Соединение разорвалось? Надо переподключаться. И всё такое.
А как же связь двух филиалов? Нет, не так. Как же связь двух датацентров? Есть у нас такая задача на одном проекте. Есть несколько географически распределённых датацентров. В каждом сидит несколько серверов. И серверам в разных датацентрах нужно интенсивно общаться. БД там реплицировать. За эталонными данными, чтобы тут закэшировать, сходить. Нужно.
Обычно для такого рода вещей используют туннели. Давным давно я использовал GRE. Просто на машрутизаторе появляется ещё один интерфейс. И нужные пакеты мы пропихиваем через него. И они магическим образом вылезают через подобный интерфейс на другом маршрутизаторе. Хоть пройдя полинтернета между ними. Туннель. И никакого специального соединения не требуется, ибо это всего лишь ещё одна обёртка пакетов перед отправкой.
На самом деле можно даже без специальных протоколов. IP можно заворачивать в IP. IPv4 в IPv6 и наоборот. Это то, что называется ipip, ipipv6, ipip6 и тому подобное. Все эти прелести давно поддерживаются ядром Linux.
Но эти туннели — без шифрования. Просто обёртка пакетов. Можно добавить шифрование IPsec. Если разберётесь, как настроить. И почему-то с этого момента эти самые туннели снова начинают называть VPN.
Но самая крутая штука из этой серии шифрованных туннелей — WireGuard. Написано с нуля. Модные шифры. Работает на уровне модулей ядра, и использует шифрование ядра. Вроде как поддерживается в Android и OpenWrt. Отлично, но дополнительные модули ядра не загрузишь в контейнер OpenVZ. И все эти туннели, как правило, требуют, чтобы оба конца туннеля имели статический адрес, что не всегда возможно.
В общем, встречайте, Tinc. Чудесная штука, ведущая родословную аж с 1998 года. (OpenVPN — с 2001). Шифрованный (aka VPN) многоконечный туннель (aka mesh сеть).
Network
Допустим, у нас есть два датацентра. В одном имеется сеть 10.10.1.0/24 и некий шлюз с публичным интернет адресом 1.2.3.4. В другом имеется сеть 10.10.2.0/24 и некий шлюз с публичным интернет адресом 3.4.5.6. Мы хотим, чтобы сервера в одной сети успешно могли подключаться к серверам в другой сети, и наоборот. Как всегда в таких случаях, сети в двух датацентрах не должны пересекаться.
Демон, который делает tinc, конечно же называется tincd. И у него очень интересная конфигурация. Один демон может обслуживать несколько различных mesh сетей. А для каждой сети нужна отдельная папочка конфигурации. Вот и создадим /etc/tinc/vpntun/hosts. «vpntun» — это название нашей сети. Теоретически, оно может быть любым, но по умолчанию tincd создаст tun интерфейс с этим именем, и лучше, чтобы это было одно слово, без дефисов и подчёркиваний.
В подкаталоге «hosts» нужно сложить файлы хостов. Каждый демон tincd должен знать всех других демонов tincd в той же mesh сети «в лицо». Иначе он откажется иметь с ними дело. Поэтому на наших обоих гейтвеях должно лежать по два файла. Это составляет некоторую сложность в конфигурации tinc. Файлы хостов нужно распространить по всем хостам.
Пусть один файл называется netA:
Address = 1.2.3.4

Subnet = 10.10.1.0/24

-----BEGIN RSA PUBLIC KEY-----
...
-----END RSA PUBLIC KEY-----
А другой называется netB:
Address = 3.4.5.6

Subnet = 10.10.2.0/24

ConnectTo = netA

-----BEGIN RSA PUBLIC KEY-----
...
-----END RSA PUBLIC KEY-----
В этих файлах описана вся конфигурация нашей сети.
Address — это публичный статический адрес, по которому можно подключиться к данному tincd. Если, конечно, он есть. Если нет, ну что ж, значит, этот tincd будет работать в «клиентском» режиме, сам принимать подключения не будет, но может подключаться в другим серверам. Для подключения нужно открыть порт 655 (по умолчанию), и tcp, и udp.
Subnet — сети (можно указать несколько), обслуживаемые данным tincd. Это очень важный параметр. Дело в том, что после того, как пакет будет направлен ОС в tun интерфейс, tincd должен решить, на какой узел mesh сети он должен его направить. Для этого и используются эти сведения. Маршрутизация. Если вы хотите «выйти» из mesh сети в интернеты, вам нужно объявить, что один из узлов «обслуживает» сеть 0.0.0.0/0.
ConnectTo — к какому узлу подключаться. Mesh сеть надо с чего-то начать. И в данном случае сеть B подключается к сети A. Если сетей больше двух, вероятно, имеет смысл настроить так, чтобы все сети подключались к некоторому «центральному» узлу. Дальнейший трафик, согласно идеологии tinc, может идти и напрямую между ближайшими tincd демонами. Но для начала они должны как-то все узнать друг о друге.
А дальше идёт публичный ключ. Тут как в ssh, аутентификация узлов происходит по RSA ключу. Публичные известны всем. А приватные каждый узел хранит у себя. Сгенерить ключи просто. Команда tincd -n vpntun -K4096 создаст файлы /etc/tinc/vpntun/rsa_key.priv и /etc/tinc/vpntun/rsa_key.pub. А ещё допишет публичный ключ в «свой» файл в каталоге hosts.
Какой хост «свой» задаётся в следующем файле конфигурации /etc/tinc/vpntun/tinc.conf. Тут достаточно написать кто мы есть и какую версию IP будем использовать.
Name = netA
AddressFamily = ipv4
Соответственно, на другом хосте вместо «netA» должно быть «netB».
И этого ещё недостаточно. Нужно настроить tun интерфейс, и запульнуть в него нужные маршруты. Делается это двумя скриптами, которые tincd запускает, когда создаёт или гасит интерфейс. Эти файлы должны быть исполняемыми (chmod +x tinc-*).
Файл /etc/tinc/vpntun/tinc-up:
#!/bin/sh

ip link set $INTERFACE up
ip addr add 10.10.1.1/16 dev $INTERFACE

ip route add 10.10.0.0/16 dev $INTERFACE
Что мы тут делаем? Мы подымаем интерфейс. Это понятно. И назначаем этому интерфейсу такой же адрес, что на уже существующем eth1. Это возможно, пока маски на eth1 и vpntun различаются, а они различаются. На самом деле без разницы, какой IP вы навесите на интерфейс туннеля. И лучше навесить адрес из локальной сети, чтобы не запутаться. Однако иметь на интерфейсе туннеля некий уникальный адрес имеет смысл, когда вы подключаете к VPN один единственный хост. Тогда tinc нет нужды знать, в какой локальной сети этот хост сейчас находится, он просто будет слать пакеты на этот единственный уникальный туннельный адрес.
И добавляем маршрут. Если мы знаем, что все наши последующие датацентры будут в сетях 10.10.2.0/24, 10.10.3.0/24 и так далее, мы можем просто зарулить всю сеть 10.10.0.0/16 в tinc. Тогда локалка будет локально. А остальные датацентры будут где-то там, за tinc. Очень удобно.
На другом tincd tinc-up будет таким же, только изменится адрес интерфейса. Он будет 10.10.2.1/16.
Файл /etc/tinc/vpntun/tinc-down:
#!/bin/sh

ip addr del 10.10.1.1/16 dev $INTERFACE
ip link set $INTERFACE down

ip route del 10.10.0.0/16
Если tinс погашен, все следы нужно замести. Хотя не обязательно.
Ну вроде и всё. Много файлов? Ну зато они все на месте и выполняют свою функцию. Как сисадмин говорю, что конфигурация tinc очень удобна. Единственная сложность: синхронизировать содержимое hosts на все узлы. Вероятно, имеет смысл эти файлы вообще держать где-то в системе контроля версий, и рассылать через Ansible. Таки там зашита топология сети.
После создания всех файлов можно запустить tincd в дебаге:
$ sudo tincd -n vpntun -D -d3
И посмотеть, что выйдет. Завершить демона можно по Ctrl + \.
Если всё хорошо, то заставляем эту нашу сеть vpntun подыматься самостоятельно при старте. В файл /etc/tinc/nets.boot нужно добавить строчку vpntun. Ну и убедиться, что сервис tinc стартует при запуске системы.
Tinc Logo
Итак, у нас есть разные задачи. Направить трафик одного приложения (или даже отдельных запросов) куда-нибудь туда, чтобы что-нибудь обойти. Подключить один хост к закрытой сети где-то там. Соединить несколько сетей безопасным образом. И для этого у нас есть прокси, VPN, туннели. А ещё есть tinc, который не просто зашифрованный туннель из одной точки в другую, а волшебный телепорт для передачи пакетов в несколько связанных точек.

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
Для такой инкрементальной разработки отдельные ветки не нужны.
И для максимально частых релизов отдельные ветки не нужны. Но это отдельный больной вопрос. Не все разработчики могут придумать, как делать фичу маленькими видимыми кусочками. Чтобы хвастаться каждым маленьким изменением. Не все даже умеют делать маленькие коммиты. Тут надо тренироваться. Наполовину работающий прототип таки лучше, чем совсем ничего.
Но ветки, конечно же, иногда нужны. Иногда случаются эксперименты, которые с высокой вероятностью вообще никогда не сольются с основным кодом. Их разумно делать в отдельной ветке. Иногда совсем не получается запилить фичу, не сломав всё остальное. Такие длинные масштабные изменения тоже разумно делать в отдельной ветке. Но все эти эксперименты и рефакторинги на практике плохо совместимы с режимом горящей задницы :) Это ж надо время, чтобы неспешно всё перетряхнуть.
Фигачьте в мастер! Очень прошу. Это работает. Это не страшно. Это, за счёт уменьшения бюрократических процедур слияния и досмотра, заметно ускоряет доставку фич заказчику.