dev notes
1.39K subscribers
33 photos
6 videos
189 links
Пишу про Go, Vim, и про то, как я медленно ползу в сторону FAANG.

С предложениями: @junsenpub
Download Telegram
​​Книга "Высоконагруженные приложения: программирование, масштабирование, поддержка" Мартина Клеппмана оказалась скорее справочником, чем линейным пособием для изучения.
Много, нет, ОЧЕНЬ МНОГО сложных технических моментов, порой с переходом в математические абстракции, да так, что у меня флешбеки с универа начинали пролетать.
Некоторые главы главы забросил на середине, некоторые наоборот - читаются с огромным интересом за один подход.
Учитывая бэкграунд автора в Кэмбридже, литература у него получается вполне себе университетская :) Но, дочитаю обязательно. И, вероятно, некоторые главы буду
перечитывать второй раз уже с ручкой, чтобы лучше усвоить информацию.

А в качестве бонуса вот тебе список из 4-х пунктов, старательно собранных в первой половине книги, и объясняющих, почему стоит забыть MySQL в пользу других реляционных баз данных:
1. Большинство реляционных БД выполняют оператор ALTER TABLE за доли секунд, работая с указателями.
MySQL - полностью копирует таблицу, изменяет её, и записывает назад, что порой занимает несколько часов.

2. Репликация - мастхэв для больших проектов, и вероятно у тебя на проекте есть что-то подобное.
Так вот MySQL не умеет делать снимок состояния ведущего узла, а многие другие СУБД, например Postgres, - умеют.

3. Изоляция снимков состояния - тема, которая, вероятно, много раз спасала твою задницу от ошибок, а ты об этом даже не знал, потому что эта технология поддерживается автоматически
некоторыми СУБД. Но если из MySQL убрать подсистему хранения InnoDB - MySQL и тут ничего не сможет.

4. Есть такой приятный механизм - автоматическое обнаружение потери обновлений. Это когда 2 процесса в один момент прочитали из базы, а затем, например, делают инкримент прочитанного значения.
Один сделал быстрее, второй - медленнее. И второй перетёр то, что записал первый, таким образом мы увеличили значение не на 2, а на 1.
Postgres и другие умные пацаны умеют решать такие проблемы автоматом: они повторяют цикл чтения - записи и не теряют обновления. А MySQL что? Правильно, а MySQL не умеет.

Чем этот список полезен? Ну, во-первых, у меня как-то проект прогорел из-за того, что MySQL не справлялся с данными и ALERT TABLE вешал таблицу на пару часов. Во-вторых, многие (например S7)
спрашивают на собесе, чем плох MySQL. Полезно подготовиться и набросать так, чтобы после этого вопроса тебя сразу взяли :D

Вообще, автор не очень жалует MySQL, так что к концу книги, думаю, найдётся ещё пара-тройка пунктов, почему MySQL - не тру, которыми я обязательно поделюсь.
По моему опыту - многие не понимают, зачем нужны хранимые процедуры. Вот есть они, можно туда какой-то код унести, а зачем это всё?
Кто-то переписывает на них весь backend, ловя потом сотни ошибок (https://habr.com/ru/company/lingualeo/blog/515530/), кто-то использует их не по назначению, а кто-то не использует совсем.
И я тоже не понимал, где их можно применять, а сегодня как понял.
Клеппман в "Высоконагруженные приложения" приводит отличный пример их применения, и я попробую его кратко резюмировать:

Есть огромное количество ошибок, связанных с состоянием гонки в различных СУБД: грязное чтения и грязная запись, потерянное обновление, фантомные записи.
Всё это происходит из-за того, что 2 транзакции в конкурентном режиме, очень условно говоря - параллельно, выполняют запросы над одним объектом.
Как этого избежать?

Лет 30 назад умные ребята предложили избавиться от конкурентности и пускать всё в одном потоке, а все транзакции хранить в оперативной памяти и, очень быстро их оттуда доставая, применять.
Лет 15 назад оперативная память стала стоить настолько дешёво, что это удалось реализовать.

Но тут встал другой вопрос: часто сервера базы данных и сервера приложения физически располагаются в разных местах.
Приложение формирует запрос, скажем, включив туда 1.000.000 id пользователей, и отправляет эту огромную строку на сервер СУБД, где СУБД в однопоточном режиме пытается её считать, а потом применить.
Приём и передача данных по сети становится серьёзной проблемой, сильно аффектит скорость и выполнение в одном потоке становится невозможным.
Тут и приходят на помощь хранимки - формирование запроса можно унести в СУБД, а по сети передать только вызов хранимой процедуры из пары строк.

Таким образом: мы избавились от проблем конкурентного доступа и более-менее наладили скорость.

Сегодня несколько СУБД позволяют работать в таком режиме, одна из них, о которой ты скорее всего слышал - Redis. Но, надо помнить, что хранимки таят много проблем, которые нужно учитывать прежде чем применять такой радикальных подход.

На сегодня всё, мир 🤙
Как при анализе файла восхититься сортировкой, философией Unix и понять, где украл идеи пресловутый Agile

10-ая глава "Высоконагруженных приложений", помимо всего прочего, начинается с небольшого примера анализа файлов в Linux. Анализируем - стандартный nginx-лог файл:

cat /var/log/nginx/access.log | awk '{print $7}' | sort | uniq -c | sort -r -n | head -n 5

"На выходе получаем 5 самых запрашиваемых сайтов нашего веб-сервера" - говорит автор. Ради эксперимента подключился к рабочему ssh, выдал этой цепочке команд лог на 4гб - молниеносно получил ответ: 5 строк, на каждой сайт и цифра слева от него - число запросов, отсортированных в порядке убывания. Далее Клеппман описывает, почему ответ при анализе нескольких Гб получается так быстро, и резюмируя, вот почему:

Каждая из этих утилит в Linux выполняет ровно одну функцию, и выполняет её удивительно хорошо. Например, sort под капотом автоматически обрабатывает данные большого размера, умеет записывать их на диск, оптимизировать различными структурами и параллелить сортировку там, где это нужно по нескольким ядрам процессора. Всё это - следствие философии Unix.

Что такое философия Unix? Это набор правил, описанных сообществом разработки и пользователей Unix в 1978 году:
1. Каждая программа - делает что-то одно, но делает это хорошо. Для решения новой задачи напишите новую программу, а не усложняйте старую.
2. Выходные данные каждой программы могут быть входными следующей, которая заранее неизвестна. Не засоряйте выходные данные, возвращайте атомарные и чистые результаты.
3. Проектируйте и разрабатывайте программное обеспечиние, даже операционные системы, так, чтобы протестировать их можно было как можно раньше, в идеале - за пару недель. Если какая-либо часть ПО написана плохо - без колебаний выбрасывайте её и переписывайте.
4. Чтобы упростить программирование - используйте инструменты, вместо неквалифицированной помощи, даже если для их создания приходится отвлечься от разработки основной задачи, а впоследствии - отказаться от некоторых из них.

Ничего не напоминает? Автоматизация, быстрое прототипирование, инкрементная итерация, поощерение экспериментов, разбивка крупных проектов на управляемые блоки - привет, современный Agile. Теперь понятно, кем вдохновлялись ребята, подписывая Agile-манифест в 90-ых годах. Удивительно мало изменений за 40 лет.
Много раз слышал восхищение разработчиков касательно Apache Kafka: "Этот брокер позволяет даже в случае отключения сервера восстановить все сообщения в очередях!"
Многие говорят о том что Kafka - единственный брокер, гарантирующий доставку в случае любых сбоев. И так оно и есть, практика это показывает.
Но только сегодня, с подачи Клеппмана в его "Высоконагруженных приложениях" я узнал, почему это так работает.
Apache Kafka - брокер на основе журнала. Т.е. вместо прямой доставки сообщений через очередь, читай через оперативную память (как это работает, например, в RabbitMQ), Kafka всё записывает в файл, указывая каждому сообщению соответствующее смещение. Подписчики, вычитывая смещения, точно знают, откуда читать свежую запись. В случае, если произошёл сетевой сбой - журнал остался нетронутым, и Kafka считывает оттуда сообщения обратно в очередь.
Теперь на вопрос на собеседовании: можно ли сломать Kafka - можешь смело отвечать, что достаточно дискового сбоя.

Возникает вопрос: как достигается такая пропускная способность? По скорости и объёму передаваемых данных Apache Kafka не уступает своим конкурентам с прямой доставкой сообщений. А достигается это за счёт крутой системы секционирования сообщений, и системы репликаций по журналам, что даёт журналируемому подходу ещё больше надёжности.

Так же заблуждением является то, что Kafka единственный брокер с таким подходом. На основе журналов так же работают: Amazon Kinesis Streams и Twitter DistributedLog. Есть ещё Google Cloud Pub/Sub, но этот парень чтение и запись в журнал абстрагирует в виде API.

Всем новых знаний, пацаны 🤙
По какому принципу обычно выбирают брокера сообщений? Из моего опыта, как правило, звучит что-то вроде "да давайте kafka возьмём, я о ней на конференции слышал, попробуем". И берут. Даже, если kafka не подходит под задачи, которые приходится решать.

Как выбрать правильно? Клеппман даёт простой алгоритм: если задача, обрабатываемая брокером, занимает много времени - лучше подходит стиль обработки сообщений формата JMS/AMQP (из самого известного - RabbitMQ). Если задача выполняется быстро и без задержек - можно взять брокера на основе журнала, например, Kafka.
Почему?

Потому что обработка журнала, даже с учётом секционирования, последовательная. И тут встаёт та же базовая проблема, что и в реляционных СУБД при последовательной обработке транзакций: если одна транзакция (в нашем случае - сообщение из очереди) выполняется очень долго, все остальные встают в ожидание. Теряется скорость, переполняется очередь, и последствия могут быть печальными.
Конечно, отчасти эта проблема решается правильным секционированием журнала (разделением журнала на группы, где каждую группу читает один или несколько получателей): в случае, если журнал сильно секционирован, и только одна секция зависает из-за длительной обработки, то эту ситуацию можно обработать. Но всё же, если задача выполняется долго или может начать выполняться долго - предпочтительней будет RabbitMQ или его не журналируемые аналоги.
Интересная мысль, описываемая Клеппманом в его "Высоконагруженных приложениях" - это внедрение паттерна репликации master-slave в рамках разных систем. Если просто, то: есть реляционная СУБД, пусть будет Postgres. Она выступает как master-реплика. И есть, например, система с поисковыми индексами, пусть будет ElasticSearch - она выступает как slave-реплика.

У большинства СУБД есть такой механизм, как журнал репликации. СУБД записывает туда все операции, производимые с этой базой данных и даёт возможность репликам вычитывать оттуда данные и применять их.

Основываясь на этом журнале, были разработаны инструменты, которые декорируют работу с журналом в виде API. Например, для Postgres - https://github.com/confluentinc/bottledwater-pg (или https://debezium.io/documentation/reference/connectors/postgresql.html), для MongoDB - https://github.com/stripe/mongoriver. Мы можем вычитывать данные из этого журнала и перекладывать их в журнал брокера, например, Apache Kafka. Потребители, в свою очередь, смогут последовательно и в том же порядке применять эти данные на другую систему, в нашем примере - ElasticSearch.

Таким образом, мы можем записывая данные в один источник реплицировать их на разные системы хранения данных.
Выходя из праздничного анабиоза решено было вернуться к чтению всякого полезного. Начал читать "Чистую архитектуру" дяди Боба. И вот вам оттуда интересная история.

Был один учёный, оказавший огромное влияние информатику - Эдсгер Дейкстра. Из под его пера в середине двадцатого века вышла статья "Go To Statement Considered Harmful" - статья, доказавшая с точки зрения математики то, что программы, где слишком часто используется оператор goto - опасные.
А пришёл Дейкстра к этому вот как: в далёких пятидесятых он решил найти универсальный способ писать качественные программы. Проделав огромную работу длиною в пару лет, он пришёл к выводу, что если программу раздробить на маленькие части, выделив под каждое атомарное действие один метод, эти атомарные части можно интерпретировать как математический алгоритм, и доказать с помощью типичных математических доказательств, таких как, например, доказательство по индукции.
И тут обнаружился интересный момент: те программы, где использовался оператор goto - часто не доказывались математически. "Чистые" же программы, которые не использовали оператор передачи управления - доказывались и их можно было назвать корректными.

После публикации статьи в журналах - Дейкстра, по-факту, устроил первый холивар в it мире. Споры продолжались много лет, но факты остаются фактами: через 10 лет с момента публикации во многих языках оператор goto был ограничен, а какие-то и вовсе реализовывлись без него. Вот как правильно устраивать холивары :)
Интересная мысль из "Чистой архитектуры": последние полвека мы, как разработчики, учимся тому, как делать не надо.

Судите сами: первый подход, выделенный как подход к разработке и доживший до наших дней - это структурное программирование. Используя 3 основные конструкции: последовательность, итерацию и выбор - мы можем написать любую программу. Когда Дейкстра проводил свои исследования и доказал, что прямая передача управления посредством оператора goto - опасна, он выделил структурное программирование как одну из парадигм разработки, которая дожила до наших дней.
Структурное программирование запрещает нам передавать управление напрямую.

Вторая парадигма - объектно-ориентированное программирование. Мы привыкли думать, что ООП дало нам наследование, инкапсуляцию и полиморфизм. Это не совсем так. Инкапсуляция была до появления ООП: отличный пример - .h файлы в С. Клиенты этих файлов могут работать с методами, зная только их сигнатуру, описанную в заголовочном файле.
Наследование, хоть и в менее удобном виде, тоже было задолго до появления ООП: имея 2 структуры с одинаковой сигнатурой первых функций, мы можем дописать в одну из структур ещё несколько методов, и создавать её через родительскую, приводя тип. Вторая структура занимает то же состояние стека, что и первая, дополняя его своими методами. По-сути, так и работает наследование в ООП, только выполнено оно в более красивой обёртке.
Что-то похожее на полиморфизм тоже проделывали до появления ООП: оперируя указателями на функции, можно было подменять дочерние функции на другие, с такой же сигнатурой. ООП только лишь усилило полиморфизм и позволило нам работать с ним гораздо удобней. Таким образом, вторая парадигма лишь отнимает у нас возможность косвенной передачи контроля - она делает полиморфизм более строгим.

Третья парадигма - функциональное программирование. Функциональная программа делает удивительную вещь: она ограничивает нас в присваивании. Каноничный функциональный код не имеет изменяемых переменных, только инициализируемые.

Индустрия обрастает новыми технологиями, под капотом которых - старые и базовые знания, которые с годами становятся всё строже.
Спрашивают тут, куда пропал: никуда не пропадал, просто было много работы и параллельно пилил небольшой пет-проект, который продолжаю пилить по мере времени.
Тем временем, спустя несколько месяцев после первого прочтения "Высоконагруженных приложений" решил перечитать и забить в памяти интересные моменты. Пока успешно повторил и законспектировал 2 главы, и ниже представляю краткий конспект главы "Модели данных и языки запросов". На днях будет конспект 3-ей главы: "Подсистемы хранения и извлечения данных".

Какие причины широкого внедрения NoSQL-решений?

1. Потребность в бОльшей масштабируемости, чем у реляционных СУБД: возможность обрабатывать очень большие объёмы данных и возможность большой пропуской способности
2. Открытый исходный код
3. Некоторые запросные операции, плохо поддерживаемые реляционной моделью
4. Стремление к более динамичным моделям, выходящим за рамки реляционных схем

Какой вопрос стоит себе задать чтобы понять, можно ли для набора информации использовать документоориентированную СУБД?
- Вполне очевидный: является ли самостоятельным документом набор информации?

Извлекая документ (например, резюме), представленное в реляционном виде, придётся сделать множество запросов (к каждой таблице по внешним ключам). Если документ лежит в документоориентированной базе, то его можно извлечь одним запросом.

Идея исключения полного дублирования (напр, хранение внешних ключей вместо строк) лежит в основе нормализации баз данных
ЭМПИРИЧЕСКОЕ ПРАВИЛО: если значения, которые могут храниться в одном месте - дублируются, то база НЕ НОРМАЛИЗОВАНА

В древовидной структуре, которую пропагандирует документоориентированная модель, соединения (по внешним ключам) - не нужны, и их поддержка очень слаба (сложно сделать свзяь многие-к-одному (напр, многие люди к одному городу))
Соединения поддерживаются в RethinkDB, не поддерживаются в MongoDB и поддерживаются в заранее описанных представлениях в CouchDB.

В реляционных СУБД оптимизатор запросов автоматически принимает решение о том, в каком порядке выполнять запрос, и делает это так, чтобы это было максимально эффективно. В случае, если был объявлен новый индекс - оптимизатор запросов перестраивает логику выполнения запроса.

Основные доводы в пользу документноориентированных СУБД - гибкость схемы, лучшая производительность вследствие локальности и большая близость к применяемым структурам данных (не всегда). Реляционная модель отвечает на это лучшей поддержкой соединений, а также связей "многие-к-одному" и "многие-ко-многим".

Если структура приложения представляет собой дерево связей "один-ко-многим", причём всё дерево загружается сразу - документоориентированная модель - ОК.
Если в приложении используется множество связей "многие-ко-многим", то документоориентированная модель не так привлекательна. Количество соединений можно снизить за счёт денормализации, но придётся написать больше кода для поддержки согласованности. Соединения эмулируются в коде, а не в СУБД, что обычно медленнее, чем через СУБД.

Документоориентированные базы не требуют проверку схемы и часто их называют бессхемными. По-факту, они проверяют схему при чтении (schema-on-read), в отличии от проверки схемы при записи в реляционных СУБД (schema-on-write). Схема при чтении аналогично динамической проверке типов в ЯП, в то время как схема при записи аналогична статической (во время компиляции) проверке типов.

MySQL - известный пиздец, когда требуется выполнить alter table. Есть утилиты, которые позволяют сделать это более комфортно:
* https://www.percona.com/software/database-tools/percona-toolkit
* https://github.com/soundcloud/lhm
* https://github.com/github/gh-ost

Оператор UPDATE будет выполняться медленно в любой базе данных на большой таблице, как так требуется перезаписывать все строки.

"schema-on-read" предпочтительна, если:
* существуют множество различных типов объектов, и нет смысла помещать их в разные таблицы
* структура данных определяется внешними системами, которые могут изменять данные, и это нам не подконтрольно
Почему ещё НЕ следует использовать документоориентированные СУБД: если документ большой, то при update требуется перезаписать весь документ (невозможно внести локальные изменения)
Некоторые драйверы для MongoDB разрешают, автоматически, ссылки на БД как в реляционных СУБД.

Декларативные языки (SQL, напр.) могут автоматически задействовать параллельную реализацию языка запросов, что гораздо сложнее сделать в случае с императивным описанием.

MapReduce - модель программирования для обработки большого количества данных от Google. В ограниченном виде поддерживается MongoDB и CouchDB в качестве механизма, выполняющего только чтение запросов по многим документам.
Например, аналогичные запросы в PostgreSQL и MongoDB с использованием mapreduce:
SQL:
SELECT date_trunc('month', observation_timestamp) as observation_month,
sum(num_animals) as total_animals
FROM observations
WHERE family = 'Sharks'
GROUP BY observation_month;

MongoDB с mapreduce:
db.obsercations.mapReduce(
// вызывается однократно для каждого документа
function map() {
var year = this.observationTimestamp.getFullYear()
var month = this.observationTimestamp.getMonth() + 1;
emit(year + '-' + month, this.numAnimals) // на выходе порождает строку вида (дата, кол-во животных)
},
function reduce(key, values) { // все пары из map автоматически группируются по ключу (т.е. с одной и той же датой)
return Array.sum(values); // для них однократно вызывается reduce, суммирующий количество животных
},
{
query: { family: "Sharks" }, // отсеиваем только акул декларативно, это расширение MongoDB модели MapReduce
out: "monthlySharkReport" // итоговые результаты записываются в коллекцию minthlySharkReport
}
)

map и reduce должны быть чистыми, т.е. могут только выполнять однозначную работу и передавать данные, но не могут делать доп. запросы

В Mongo с версии 2.2. добавлена поддержка декларативного описания запросов - конвейер агрегирования, где тот же запрос будет выглядеть так:
db.observations.aggregate([
{ $match: { family: "sharks" } },
{ $group: {
_id: {
year: { $year: "$observationsTimestamp" },
month: { $month: "$observationsTimestamp" }
},
totalAnimals: { $sum: "$numAnimals" }
} }
])
Как я уже писал выше - перечитываю "Высоконагруженные приложения" и кидаюсь в вас конспектами с самым соком. К слову, зачем читать книгу второй раз? Лично для себя открыл, что второе чтение оставляет в памяти максимальное количество полезной информации (а параллельное конспектирование основных тем - цементирует прочитанное). Прочитав же 1 раз - в течении пары месяцев 90% забывается.

Итак, сегодня у нас 3-я глава: подсистемы хранения данных. Клеппман описывает, как и на основе чего устроены индексы в современных СУБД и раскрывает 2 основные структуры данных, на основе которых индексы строятся. Зачем это знать? Во-первых, на больших нагрузках нужно понимать, как оптимизировать или скорость, или чтение индексов, в зависимости от задачи. Во-вторых, понимание того, как устроены индексы полезно для выбора СУБД на очередной проект. В-третьих, это просто очень интересно :)
Глава большая, поэтому ниже - конспект первой половины.

Любые индексы в реляционных СУБД, как правило, замедляют запись. Индексы надо подбирать так, чтобы чтение было быстрым, но и запись осталась быстрой.

При любой записи на диск - индекс тоже обновляется, всегда.

Хеш-индексы - индексы, на основе hash-таблицы - один из самых распространённых типов индексов. Например, так работает подсистема хранения Bitcask в NoSQL СУБД Riak.

Уплотнение (compaction) сегментов журнала - операция, отбрасывающая дублирующиеся ключи и сохраняющая только последний ключ, тем самым уменьшая дисковое место, занимаемое файлами журнала. Уплотнённые сегменты, если их размер значительно уменьшился - можно слить между собой.

SS-таблица
Отсортированная строковая таблица (sorted string table, SS-таблица) - отсортированная по ключу таблица, хранимая на диске, где каждый ключ встречается 1 раз (благодаря автоматическому процессу уплотнения и слияния сегментов). Преимущества перед журнальными сегментами с хеш-индексами:
1. Объединение сегментов выполняется просто и эффективно, благодаря тому, что сегменты отсортированы. При слиянии мы читаем сразу все сегменты, и оставляем последний записанный ключ в результирующем сегменте.
2. Чтобы найти в файле конкретный ключ, не нужно хранить индекс всех ключей в оперативной памяти. Если мы знаем индекс соседних ключей от искомого - точно можно сказать, что он будет между ними, и нам достаточно посмотреть срез данных между соседями.
3. Блоки, на которые есть индексы в ОП можно сжимать пачками по несколько килобайт, так как расжать и просмотреть несколько Кб данных можно очень быстро.

Красно-чёрные деревья или AVL-деревья позволяют записывать ключи в любом порядке, а читать только в нужном.
Процесс записи в SS-таблицу выглядит так:
1. При поступлении записи помещаем её в оперативной памяти в сбалансированную структуру данных, например в красно-чёрное дерево.
2. Когда размер записи превышает определённое пороговое значение, например несколько мегабайт, записываем его на диск в фиде файла SS-таблицы. Это операция выполняется достаточно быстро, так как записи на выходе отсортированы. Файл становится последним сегментов базы данных.
3. При поиске данных сначала ищем их в memory-сегменте, если там нет, то в последнем, затем в предпоследнем и так далее.
4. В фоне время от времени запускается процесс уплотнения и слияния данных.
5. Так же на диске нужно держать отдельный журнал - копию со всеми записанными в memory-таблицу данными, чтобы в случае фатального сбоя восстановить memory-таблицу.

А это вообще где-то применяется, спросишь ты? Да, например в levelDB и rocksDB - библиотеках, которые можно юзать сами по себе, а можно в рамках других СУБД. Так, levelDB используется в СУБД Riak. Аналогичные системы используются так же в Cassandra и HBase.

Подсистемы хранения, основанные на принципах слияния и уплотнения, часто называют LSM (Log-Strucrured Merge-Tree) подсистемами.

Часто с LSM-системами используют фильтры Блума - структуру данных, позволяющую узнать, есть ли в множестве заданный элемент. Это позволяет не просматривать все сегменты в поисках элемента, которого в них нет.
Индексы на основе журнала - не самый популярный тип индексов. Чаще используются индексы на основе B-дерева.
В отличии от журналированных индексов, которые индексируют базу данных сегментами по несколько мегабайт и всегда записывают эти сегменты на диск последовательно, B-деревья разбивают базу на сегменты по несколько килобайт (часто - по 4Kb) и читают/пишут по 1 странице за раз. Такое разбитие и такой размер сегментов отлично накладываются на нижележащие аппаратные слои.
Все страницы имеют свой адрес, и на основе таких ссылок строится дерево. Одна из страниц - корень, с него начинается любой поиск ключа. Все ключи во всех сегментах - отсортированы, и любой узел дерева содержит некоторый диапазон сегментов и несколько ссылок на поддиапазоны.
Если на странице заканчивается место, то создаётся ещё одна ссылка, и страница делится на две полу-пустых страницы.
Такой алгоритм гарантирует, что дерево будет сбалансированным, т.е. любой поиск будет занимать O(log n), что весьма быстро.
Чтобы обезопасить данные от потери во время сбоя, на диске так же хранится журнал упреждающей записи, в который все добавляемые данные записываются перед тем, как попасть в B-дерево.

А зачем тогда нужны LSM-деревья, если есть B-деревья? LSM-деревья реализуют как одну из подсистем во многих СУБД из-за того, что они быстрее при записи. Нам не нужно идти по дереву и искать место, куда добавить ключ. Нам достаточно записать данные в in-memory-структуру и сдублировать их в журнал, и всё. И для таких задач LSM-деревья до сих пор используются.
В свою очередь, B-деревья быстрее при чтении.
Сегодня, продолжая 3-ю главу, опять поговорим о том, какие бывают индексы и как их правильно использовать.

Индексы могут быть кластеризованными (по ключу хранятся все данные) или некластеризованными (по ключу хранится ссылка на данные). Например, InnoDB в MySQL - использует кластеризованные индексы, т.е. когда мы делаем запрос с индексом, сначала идёт поиск в memory-структуре, в зависимости от типа индекса, и затем, в случае успеха, мы сразу можем получить запрашиваемые данные. У некластеризованных индексов в случае успешно найденного значения, представляющего ссылку, нам потребуется выполнить переход по ней, считать значение и только потом его вернуть.
Бывает так же и компромисс между кластеризованным и некластеризованным - охватывающий индекс. Этот тип индекса хранит в себе не всю строку, а часть столбцов. Как правило, эта та часть, которая чаще всего запрашивается запросами.

Если мы запрашиваем сразу несколько столбцов строки, то нам могут потребоваться составные индексы. Самый частый тип составных индексов - сцепленные индексы: объединение нескольких полей в один ключ, например: имя-фамилия.
Второй по популярности тип составных индексов - многомерные индексы, особенно часто они используются при работе с пространственными данными, например, при работе с PostGIS от Postgres. Для многомерных пространственных индексов используются R-деревья, позволяющие производить поиск по многомерным данным.
Многомерные индексы применяются не только для работы с адресами. Например, можно использовать двумерный индекс (год, температура), чтобы одним запросом найти данные за 2013 год, когда температура была -20 градусов. Иначе, без многомерных индексов, нам придётся сначала найти все данные за 2013 год, а затем фильтровать их по температуре.

Интересная, в плане использования индексов, тема - полнотекстовый поиск. Например, известные индексы Lucene, лежащие в основе эластики, хранят словарь термов в SS-структуре (которую я описывал в прошлый раз), а рядом с этой структурой они хранят небольшой индекс, который описывает, как разбивается (на буквы, или на триграммы, или как-то иначе) каждый терм. На всё это накладываются различные алгоритмы, например конечный автомат в связке с расстоянием Левенштейна, что позволяет очень быстро искать слова по заданному соответствию.

Все описанные выше структуры так или иначе подразумевают запись на диск. Хотя основная работа происходит в оперативной памяти, в случае с LSM-таблицами на диске у нас хранятся уплотнённые сегменты, а в случае с B-деревьями на диске у нас хранится информация, куда ссылается B-дерево. И, само собой, на диске хранятся данные самих таблиц.
По мере удешевления RAM у дискового хранения остался 1 аргумент над хранением в оперативной памяти: надёжность. Хотя в последние годы, благодаря различным инструментам и подходам (репликации на разные сетевые копии; память, питаемая от отдельного аккумулятора; ротация журнала состояния на диск) появилась возможность хранить данные только в оперативной памяти. Уже существуют несколько РСУБД, которые работают в RAM: VoltDB, MemSQL, Oracle TimesTen.
Начал читать "Микросервисы" Ричардсона, а поэтому начинаю вести краткие конспекты.

В микросервисной архитектуре есть несколько типов шаблонов, из которых строится сама архитектура, инфраструктура и процесс разработки.

1 тип: Шаблоны для разбиения приложения на микросервисы
* Разбиение по бизнес-возможностям
* Разбиение по проблемным областям

2 тип: Шаблоны взаимодействия
* Транзакционный обмен сообщениями
* Стиль взаимодействия
* Надежность
* Обнаружение
* Внешний API

3 тип: Шаблоны согласованности данных для реализации управления транзакциями
Для обеспечения слабой связанности каждый сервис должен иметь собственную базу данных. Такой подход чреват проблемами, для решения которых следует использовать шаблон "Повествование".

4 тип: Шаблоны запрашивания данных в микросервисной архитектуре
Использование отдельной базы для каждого сервиса имеет ещё один недостаток: некоторые запросы должны объединять информацию с нескольких сервисов, а значит, мы хотим иметь возможность использовать транзакционный подход к этим запросам. Тут помогут такие шаблоны запросов, как:
* Объединение API (обращение к API одного или нескольких сервисов и агрегирование результатов)
* CQRS (командные запросы с разделением ответственности, которые хранят одну или несколько копий данных и позволяют легко к ним обращаться).

5 тип: Шаблоны развертывания сервисов
* Традиционно - развертывание сервисов в формате упаковки определённого языка (нельзя масштабировать для поддержки микросервисной архитектуры)
* Аналог - развертывание в виде виртуальных машин или контейнеров
* Бессерверные технологии

6 тип: Шаблоны наблюдаемости, позволяющие понять, как ведёт себя приложение
* API проверки работоспособности - роут, возвращающий набор метрик, показывающих, как работает приложение
* Аггрегация журналов - логи, желательно с поддержкой поиска
* Распределённая трассировка - назначение уникальных идентификаторов для каждого запроса для отслеживания его перемещений между сервисами
* Отслеживание исключений - автоматическая реакций на ошибки, часто это отдельный сервис, умеющий определять по логу какую команду оповещать
* Показатели приложения
* Введения журнала аудита - журнал действий пользователя

7 тип: Шаблоны автоматического тестирования сервисов
* Тестирование с растчетом на потребителя - проверка того, что сервис отвечает ожиданиям клиентов
* Тестирование на стороне потребителя - проверка того, что клиент может взаимодействовать с сервисом
* Тестирование компонентов сервиса в изоляции

8 тип: Шаблоны для решения сквозных проблем
* Шаблон шасси микросервисов - построение архитектуры с выбором ниболее подходящего инструмента под конкретную задачу

9 тип: Шаблоны безопасности:
Шаблоны безопасности выбираются в зависимости от метода коммуникации сервисов и клиентов. Например для API наиболее часто применяется такой шаблон как JWT-токен.

Мне показался крайне интересным закон Конвея, который приводит Ричардсон:
Организация, проектирующая системы, обречена воспроизводить архитектуру, имитирующую структуру собственных коммуникаций.

Это говорит о том, что множество небольших продуктовых команд гораздо более эффективны, чем одна большая команда на несколько десятков человек.
Продолжаю обзор "Микросервисов" Ричардсона. Сегодня вторая глава, дающая уже более интересный материал, чем просто перечисление паттернов.

Чтобы разобраться с тем, как проектировать и строить микросервисную архитектуру, нужно сначала дать определение архитектуре программного обеспечения как таковой. Ричардсон приводит определение Лена Басса:
"Программная архитектура вычислительной системы - это набор структур, необходимых для её обсуждения и состоящих из программных элементов, связей между ними и свойств, присущих этим элементам и связям".

Модель представлений архитектуры вида 4+1
Архитектура 4+1 - это универсальный способ описания архитектуры любого приложения, предложенный Филиппом Кратченом, который состоит из четырёх разных представлений архитектуры ПО, где каждое описывает определённый аспект и состоит из определённого набора элементов и связей между ними:
* Логическое представление - модули, создаваемые разработчиками. Например, в ООП языках это классы и пакеты. Связи между ними - отношения между классами, включая наследование и зависимости.
* Представление реализации - результат работы системы сборки. Для компилируемых языков, например, это исполняемый файл, с соответствующими зависимостями, представленными в компилируемом виде.
* Представление процесса - компоненты на этапе выполнения. Каждый элемент является процессом, а отношения между ними - межпроцессорное взаимодействие.
* Развертывание - то, как процессы распределяются по устройствам. Элементами тут выступают сервера и процессы. Связи между ними - сеть.
+1 - это сценарии, определяющие связи и то, как представление обрабатывает запрос.

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

Есть разные стили архитектуры.

Классический - многоуровневый стиль, в котором можно выделить наиболее частый - трёхуровневый:
* уровень представления - код, реализующий пользовательский интерфейс или внешние API
* уровень бизнес-логики
* уровень хранения данных - реализация взаимодействия с базой данных
Недостатки многоуровневой архитектуры:
* Подразумевается единый уровень представления и не учитывается, что клиентов может быть несколько
* Единый уровень хранения данных не подразумевает, что будет работа с более чем одной базой
* Уровень бизнес-логики зависит от уровня хранения данных - в теории эта зависимость не позволяет тестировать бизнес-логику отдельно от БД

Шестигранный стиль - аналог многоуровневой архитектуры, ставящий бизнес-логику в центр. Вместо уровня представления у приложения есть адаптеры, которые обрабатывают внешние запросы, вызывая бизнес-логику. Вместо уровня хранения данных используются адаптеры, вызываемые бизнес-логикой и обращающиеся к внешним приложениям, например через брокер сообщений. В чём профит? Бизнес-логика не зависит от адаптеров, наоборот - адаптеры зависят от бизнес-логики. Инверсия зависимостей в чистом виде.
Микросервисная архитектура - тоже архитектурный стиль. Реализация, как правило, представима в виде набора компонентов. Компоненты предствлены сервисами, а в качестве коннекторов - коммуникационные протоколы (например, AMQP вместе с rabbitmq).
Каждый сервис имеет собственную архитектуру, как правило - шестигранную (читай выше).
Каждый сервис можно выделить из какой-либо бизнес-возможности. Например для приложения-доставки, работающего с ресторанами, курьерами и пользователями, можно выделить следующие сервисы (на самом деле потенциально сервисов может быть гораздо больше):
* Сервис ресторанов - светит наружу API, с помощью которого клиент может посмотреть меню и сделать заказ;
* Сервис доставок - светит наружу API, с помощью которого курьер видит, какой заказ и куда ему доставлять.
* Сервис заказов - сервис, обрабатывающий заказы, получая их от ресторана и передающий их в сервис доставки.
* Сервис уведомлений - сервис, с помощью которого другие сервисы могут отправлять клиентам и курьерам информацию о готовности заказов.

Связь между сервисами реализуется с помощью механизма межпроцессорного взаимодействия, например REST API или асинхронного обмена сообщениями.
API состоит из команд (например, обновить заказ), запросов (получить данные по заказу) и событий (заказ создан), которые могут потреблять внешние клиенты.
API сервиса инкапсулирует его внутреннюю реализацию. В отличие от монолита, этот подход не позволяет разработчику писать код, минующий API. Благодаря этому обеспечивается модульность приложения.
Ключевое здесь - слабая связанность сервисов, т.е. каждый сервис ограничен в том, как он может общаться с другими сервисами.
Каждый микросервис обладает собственной архитектурой и иногда разным стеком технологий. Но, как правило, все сервисы имеют шестигранную архитектуру: бизнес-логика вызывается адаптером операций, а события, которые она генерирует, публикуется адаптером событий наружу.

Слабо-связанные сервисы обязательно должны взаимодействовать только через API, и исключать коммуникацию через базу данных.


Теперь коротко об интересном и холиварном: как выделить микросервисы?
Зачем нам вообще приложение и какой его основной функционал? Обрабатывать запросы. Поэтому первый шаг - формируем ключевые запросы, описывая, что мы хотим от приложения и что оно должно сделать в ответ. Например, мы хотим, чтобы клиент смог разместить заказ. А ещё хотим, чтобы ресторан мог этот заказ принять. Наши хотелки - это требования, выраженные в виде пользовательских историй. Когда мы сформировали все требования, из них мы можем выделить внешние запросы, которые будут отправляться приложению. В нашем случае это createOrder, для создания заказа от пользователя и acceptOrder, для приёма заказа рестораном.
Когда все запросы описаны, переходим ко второму шагу - разбиваем на сервисы исходя из запросов. Каждый запрос можно отнести к тому или иному домену: например, запрос createOrder можно отнести к абстрактному домену "Order", запрос acceptOrder можно отнести к доменам "Order" и "Restaurant".
Разбив все запросы на домены, мы сможем выделить, например, сервисы Order, Restaurant, Kitchen, Delivery и так далее.
Третим шагом мы назначаем всем сервисам те операции, которые были выделены на первом шаге. Тут уже будет приблизительно понятно, как сервисы будут взаимодействовать между собой и какой примерный API у них будет.
Следующие главы более подробно описывают то, как правильно выделить микросервисы. Информации много, и чтобы корректно её понять в полном контексте, крайне желательно прочитать и посмотреть на все таблицы своими глазами. Я лишь приведу очень сокращенный конспект.
Как я уже писал выше, автор предлагает делать это в 3 этапа:
* Определить системные операции: определяем пользовательские истории и связанные с ними сценарии использования.
Сначала определяется доменная модель: перечисление основных сущностей приложения, завязанных на требования к приложению. Например, для приложения-доставки это могут быть: Order, Restaurant, Delivery, Courier и другие.
Затем определяются операции, связанные с этими сервисами. Например, клиент может создать заказ - операция createOrder. Ресторан может взять заказ на обработку - операция acceptOrder. Курьер может доставить заказ - операция deliveryDelivered. Аналогично с операциями опередляются запросы, которые приложение будет обрабатывать: запрос на доступные рестораны - findAvaliableRestaurants, и так далее.
На этом этапе, вероятно, не получиться учесть все операции и запросы, но чем больше операций и запросов получиться выделить и спроектировать - тем проще будет их сгруппировать и тем больше будет вероятность того, что сервисы будут выделены верно.
* Второй этап - разбиение на сервисы, исходя из запросов, описанных ранее. Вероятно, доменная модель сможет показать сервисы без каких-либо дополнительных действий. Но, автор приводит и несколько стратегий, которые можно применить, если явно выделить сервисы не получается.
Первая - разбиение на сервисы по бизнес-возможностям. Бизнес-возможности определяют то, чем занимается организация и что она предлагает своим клиентам. Например, что предлагает клиентам приложение-доставка?
1. Управление поставщиками - курьерами и информацией о ресторанах.
2. Управление клиентами
3. Прием и выполнение заказов: создание заказов, управление заказами, логистика, управление доступностью курьеров, управление доставкой
4. Бухучёт: отчётность по клиентам, по курьерам, по доставкам и так далее.
5. ...

Иногда сервисы создаются для бизнес-возможностей верхнего уровня, а иногда отдельный сервис может быть создан для подвозможности. Например:
Управление курьерами - сервис Courier (сервис для подвозможности)
Управление информацией о ресторанах - сервис Restaurant (подвозможность)
Управление клиентами - сервис Consumer
Управление заказами для клиента, создающего заказ - сервис Order (подвозможность)
Управление заказами в ресторане - сервис Kitchen (подвозможность)
Управление доступностью курьеров и доставка - сервис Delivery (подвозможности)
Бухучёт - сервис Accounting
И так далее.

Решение, для каких возможностей создавать отдельный сервис - субъективно.

Вторая стратегия - разбиение по проблемным областям. Тут уже включается DDD, с его необычайно большим набором терминов, и этот момент я пропущу, так как автор затрагивает его очень поверхностно, а для полного понимания нужно читать дополнительную литературу. Но, в общем и целом, разбиение по проблемным областям часто дают схожий результат с разбиением по бизнес-возможностям.

Смотря на примеры сервисов, приведённые автором, явно вспоминается принцип единственной ответственности, и он тут на самом деле фигурирует. Мы выделяем команды и запросы, а потом группируем их и формируем на их основе отдельный сервис, который будет иметь, очень абстрактно, одну причину для изменений.
Ещё один принцип, применяемый при декомпозиции - принцип согласованного изменения: изменение пакета должно затрагивать все его классы (Роберт Мартин). Или, другими словами: если два класса изменяются вместе по одной и той же причине - они должны входить в один пакет.

Какие трудности могут возникнуть при разбиении приложения на сервисы? Помимо пары сотен других, автор приводит следующие:
1. Латентность сети - определённый вид декомпозиции вынуждает сервисы часто обмениваться данными. Есть много способов минимизировать эту проблему, начиная от перепроектирования, и заканчивая реализацией API для извлечения нескольких объектов за один вызов.
2. Синхронное межпроцессорное взаимодействие: если один сервис оказался заблокированным, мы не сможем синхронно к нему обратиться, что ухудшит доступность.
3. Обеспечивание согласованности: если один вызов подразумевает обновление информации в нескольких сервисах - нужно что-то вроде логической транзакции, чтобы данные остались согласованными.
4. Получение согласованного представления данных: разные БД могут выдавать не согласованные данные в контексте одного процесса.
5. Божественные классы: раздутые классы, используемые в разных частях приложения. Проблема тут очевидна - слишком сильная связанность. Одно из решений - упаковка такого класса в библиотеку и создание центральной базы. Проблема тут в том, что такой подход нарушает принципы микросервисной архитектуры и, опять же, приводит к связыванию. Ещё одно решение - сделать для разных сервисов свою модель класса, привязав его только к тем возможностям, которые требует данный сервис. Например, в сервисе Delivery класс Order будет работать с адресом получения, временем получения, адресом и временем доставки.
Тот же класс Delivery в сервисе ресторанов будет работать с только с тем, что связывает заказ и рестораны.
Как обычно мы биндим аргументы для сервиса в Symfony?
Правильно, через services.yaml. Например, мы создали интерфейс, у коготорого есть 3 реализации. Массив реализаций нам нужно передать куда-то в конструктор. У нас есть autowiring, и мы хотим его использовать. Переходим в services.yaml:

App\Service\SightingScorer:
arguments:
$scoringFactors:
- '@App\Scoring\TitleFactor'
- '@App\Scoring\DescriptionFactor'
- '@App\Scoring\CoordinatesFactor'

Где каждый из перечисленных классов реализует интерфейс ScoringFactorInterface.

Со временем реализаций становится всё больше, и всё больше вероятность, что при очередной деплое мы забудем обновить конфигурацию и прибиндить очередной класс. Так вот, ребята с symfonycast описали очень крутой способ, как биндить такие реализации автоматом. Идём в Kernel.php, там переопределяем метод build, и в нём добавляем одну строчку:
protected function build(ContainerBuilder $container)
{
parent::build($container);

$container->registerForAutoconfiguration(ScoringFactorInterface::class)
->addTag('scoring.factor');
}

Всё, после этого все реализации будут помечены тегом "scoring.factor", и в services.yaml нам достаточно один раз прописать:
App\Service\SightingScorer:
arguments:
$scoringFactors: !tagged_iterator scoring.factor

Такой подход называется тегированный итератор, и о нём можно почитать более подробно: https://symfony.com/doc/current/service_container/tags.html

Главное не забыть о том, что для того, чтобы автовайринг сработал нам нужно передавать аргументом итератор, а не массив:

private iterable $scoringFactors;

public function __construct(iterable $scoringFactors)
{
$this->scoringFactors = $scoringFactors;
}

Очень крутая штука, как по мне. И, в теории, что-то подобное можно реализовать и для других языков и фреймворков.
Продолжаю конспекты Ричардсона и его микросервисов. В прошлый раз я остановился на второй главе, где рассказывалось, как определить сервисы и что вообще такое микросервисная архитектура. Сегодня открываю третью главу - "Межпроцессорное взаимодействие в микросервисной архитектуре".

Сервисы могут использовать множество технологий для связи между друг другом: rest или grpc поверх http, или же механизмы коммуникации сообщений, такие как amqp или stopm. Форматы сообщений тоже различаются: от json до двоичных avro или protocol buffers.

Существуют множество стилей взаимодействия между клиентом и сервисом. Их можно разделить на два уровня. Первый опередляет отношения "один к одному" и "один ко многим".
"Один к одному" - каждый запрос обрабатывается ровно одним сервисом.
"Один ко многим" - каждый запрос обрабатывается несколькими сервисами.
Второй уровень определяет выбор между синхронным и асинхронным взаимодействием.
Синхронное - клиент ждёт ответ сразу после запроса и может даже заблокироваться на время ожидания.
Асинхронное - клиент не блокируется, а ответ, если придёт, может быть отправлен не сразу.

Общение "один к одному" может быть нескольких видов:
1. Запрос - синхронный ответ;
2. Запрос - асинхронный ответ;
3. Однонаправленные уведомления - клиент не ждёт ответа от сервера.

Общение "один ко многим" так же может быть нескольких видов:
1. Издатель/подписчик - клинет публикует сообщение с уведомлением, которое потребляется любым количеством заинтересованных сервисов;
2. Издатель/асинхронные ответы - клиент публикует сообщение с запросом и ждёт определённое время ответа от заинтересованных сервисов.

API приложения важно проектировать таким образом, чтобы его можно было развивать и поддерживать без урона для клиентов. Если минорные правки или новые API-методы, не ломающие логику, мы можем ввести в API без проблем, то мажорные изменения, такие как изменение формата данных уже существующих методов, нужно делать с помощью версионирования. Хороший подход - семантическая нумерация версий (semver.org). Это набор правил, регламентирующий, как использовать и увеличивать номера версий. Каждая версия API состоит из 3-х частей: major.minor.patch, где:
major - изменяется при внесении в API несовместимых изменений;
minor - изменяется при внесении в API изменений с обратной совместимостью;
patch - изменяется при исправлении ошибок с сохранением обратной совместимости.

Например, в случае REST API мажорную версию можно указать в качестве первого элемента URL-адреса (https://.../v1/...). Если сервис задействует обмен сообщениями, мажорную версию можно включать в публикуемое сообщение.
Ричардсон подробно описывает REST, его дальнейшую модель развития и то, почему часто в микросервисной архитектуре не получится сделать RESTful, даже если очень захочется. Если коротко:
выделили мы сервис Order, и решили подвести его под RESTful. Что используется для обновления данных? Правильно, PUT. За что отвечает сервис Order? Что мы можем обновить? Например, статус заказа, и, например, мы можем отредактировать заказ. Можно сделать одну ручку, которая будет обновлять заказ, в рамках который мы сможем обновить любые поля, например, только статус. Но помимо самих заказов у сервиса Order может быть ещё несколько сущностей, связанных с заказами, которые тоже нужно будет обновлять. Таким образом PUT потеряет идемпотентность, и придётся делать несколько ручек на обновление, что уже не будет накладываться на RESTful.

Резюмируя тему REST API, приводятся основные преимущества и недостатки REST:
* Он простой и привычный
* API на основе HTTP достаточно просто тестировать
* Он имеет встроенную поддержку взаимодействия вида запрос-ответ
* Протокол HTTP дружественнен к брандмауэрам
* Он не нуждается в промежуточном брокере, что упрощает систему
И недостатки:
* Он поддерживает только 1 стиль - запрос-ответ
* Степень доступности снижена. Поскольку клиент и сервис взаимодействует между собой напрямую, без промежуточного звена для буферизации сообщений, они оба должны работать на протяжении всего обмена данными
* Клиенты должны знать местонахождение (URL) сервиса
* Извлечение нескольких ресурсов за один запрос связано с определёнными трудностями
* Иногда непросто привязать несколько операций обновления к HTTP-командам

Несмотря на недостатки, REST считается де-факто стандартом для построения API. Но, есть и множество альтернатив, например - gRPC, которую мы дальше и рассмотрм.


gRPC - двоичный протокол на основе сообщений, для написания многоязычных клиентов и серверов. Проектирование сервиса должно начинаться с его API. В gRPC API описывается с помощью языка IDL на основе Protocol Buffers - многоязычного механизма сериализации структурированных данных от Google. Компилятор Protocol Buffer генерирует клиентские заглушки и серверные каркасы, и поддерживает разные языки. Клиенты и серверы обмениваются сообщениями в формате Protocol Buffers используя HTTP/2.

gRPC API состоит из определений сервисов и сообщений вида "запрос/ответ". Определение сервиса - что-то вроде интерфейса в ООП языках: набор строго типизированных методов. Помимо стандартного флоу вида запрос-ответ, gRPC поддерживает поточный вызов процедур: сервер может вернуть клиенту поток сообщений, и клиент может отправить на сервер поток сообщений.
Ниже приведу пример gRPC API для сервиса Order, описывающего несколько методов, включая createOrder():

service OrderService {
rpc createOrder(CreateOrderRequest) returns (CreateOrderReply) {}
rpc cancelOrder(CancelOrderRequest) returns (CancelOrderReply) {}
rpc reviseOrder(ReviseOrderRequest) returns (ReviseOrderReply) {}
}

message CreateOrderRequest {
int64 restaurantId = 1;
int64 consumerId = 2;
repeated LineItem lineItem = 3;
}

message LineItem {
string menuItemId = 1;
int32 quantity = 2;
}

message CreateOrderReply {
int64 orderId = 1;
}

Преимущества протокола gRPC:
* Он позволяет легко спроектировать API с богатым набором операций
* Он имеет эффективный компактный механизм IPC, что особенно явно проявляется при обмене крупными сообщениями
* Поддержка двунаправленных потоков
Недостатки:
* Для JS, например, процесс описания API на gRPC более трудоёмок, нежели для REST из-за особенностей типизации
* Старые брандмауэры не поддерживают HTTP/2