📱

Get Our Mobile App

Take your business learning on the go!

Download on the App StoreGet it on Google Play

Ультимативный гайд по System Design (все IT направления) / Пройти интервью на Middle+

Антон Назаров I Осознанная Меркантильность1:25:05

Transcription

Наверняка ты знаком со Slack. Это такой мессенджер, который крупные компании типа OpenAI и Spotify используют для коммуникации внутри. Но слышал ли ты о том, как плохой системный дизайн привёл к падению акций Slack на 14%, сколько? И квартальной выручки на 5%.

29 июля девятнадцатого года Slack пережил крупный технический сбой. Более 2 часов клиенты и компании по всему миру не могли отправлять и принимать сообщения. В итоге Slack потратили около 8,2 млн долларов на компенсации своим клиентам. Причины сбоя крылись в неудачных архитектурных решениях. Во-первых, из-за централизованной архитектуры была затронута большая часть пользователей. Во-вторых, нагрузка на базы данных была распределена неравномерно. В-третьих, были недооценены риски перегрузки отдельных компонентов, которые потянули вслед за собой вниз остальные сервисы. Наконец, система просто не умела работать в ограниченном режиме.

Даже в успешном сервисе небольшой перекос в архитектуре может стоить миллионов долларов за несколько часов. И в этом видео ты узнаешь, как этого избежать. FA уже давным-давно добавил секцию System Design и интервью для всех сотрудников Middle Plus наравне с поведенческим интервью и кодингом. Потихоньку этот тренд подхватывают и СНГшные компании, добавляя этап System Design для большинства направлений наряду с другими техническими этапами. По сути, тебе дают задачу типа: "Спроектируй систему уведомлений для Tinder", и ты рисуешь архитектуру этого решения на доске, параллельно объясняя, что и зачем в ней нужно. Также тебя могут попросить спроектировать совершенно новый сервис или аналог уже существующего продукта. Менее частый случай — проработка гипотетических ситуаций, в которых тебе нужно спроектировать систему, которая будет бесконечно масштабироваться.

Главное здесь не нарисовать красивую картинку из квадратиков и стрелочек, а показать, что ты понимаешь, как собирать и уточнять требования, на какие архитектурные компромиссы идти, например, сделать систему максимально быстрой для одного запроса, либо позволить ей обрабатывать огромное количество запросов параллельно. И, наконец, какие подходы и технологии применимы конкретно к этой задаче. Поэтому научите меня системному дизайну. Это запрос, который почти никогда не возникает отдельно, но при этом почти всегда всплывает в ходе работы с нашими менторами. Проблемы с процессами на разных стадиях разработки, кривая архитектура сервиса, невозможность масштабироваться. В конце концов, system design и интервью, которое тебя ждёт на собеседованиях на уровень Middle Plus — это те твои запросы, которые мы закроем в сегодняшнем видео.

Тебе точно будет полезно это видео, и его рекомендуется досмотреть, не откладывая в долгий ящик, если ты относишься к следующим людям. Все ээ-разработчики, претендующие на уровень Middle Plus, у вас это точно спросят, даже если это не понадобится в работе. Архитекторы, тимлиды и менеджеры. Это поможет при проектировании систем и принятии архитектурных решений. Девопсам и SRE поможем в понимании отказоустойчивости и масштабирования. Аналитики, вам мы поможем лучше формализовывать требования и взаимодействовать с архитектурой проекта. И наконец, а вы думали, я вас пропущу? Frontend и mobile разработчики, вы тоже работаете с API и распределёнными системами. Вам нужно масштабировать ваши продукты и приложения. И поэтому сейчас вам может попасться system design интервью, даже учитывая то, что вы как бы за фронт отвечаете. И увы, но ответ: "Да я как бы мобильщик, я в бэкенде не секу, не прокатит", вы просто получите отказ.

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

Немного об авторах курса перед тем, как мы начнём. Это Senior Go-разработчик, который строил архитектуру в Авито. Senior Backend разработчик, старший системный аналитик из Альфа-Банка, эээнд-разработчик из одного большого классифайда, тех из Premiere One и, конечно, ваш покорный слуга — волк AK Антон Назаров, а iOS разработчик из Apple. Привет, это курс по системному дизайну от сообщества Осознанной меркантильности и IT-менторов. Очень быстро я тебе расскажу, как он будет выходить. Хорошо, смотри, это первая часть. Посмотри её, оцени качество подготовки материала, как мы постарались наполнить его актуальными практическими кейсами и нужной теорией для собесов. Вторая часть уже доступна на бусте для участников сообщества ОМ. Там же на бусте будут выходить следующие части по главам, примерно раз в неделю, раз в две. И потом когда-нибудь не скоро, когда мы соберём от вас все фидбеки и учтём все правки, он выйдет на YouTube, может быть, через полгода, может через год. Если тебе прямо сейчас хочется посмотреть дальше, то переходи на бусти. Ну а если денег нет и хочется бесплатного обучения, то придётся подождать. Ну и добро пожаловать в первую часть. Вперёд к обучению.

Теперь к самому систем-дизайну. Правильно будет начать с определения. Системный дизайн — это процесс проектирования программного обеспечения и всей инфраструктуры с учётом компонентов и взаимодействия этих компонентов, а также требований по функциональности, масштабируемости, надёжности, безопасности и стоимости. Системой может являться, например, приложение или отдельный сервис. Системный дизайн включает в себя снятие требований, выбор технологий и методов реализации архитектуры и непосредственно проектирование архитектуры системы.

Прикладное значение системного дизайна в разработке достаточно очевидно. Он помогает создавать более эффективные и устойчивые решения, которые легко масштабируются и удовлетворяют запросам бизнеса и пользователей. Например, когда Amazon начали показывать стабильный рост пользователей и заказов, они столкнулись с ограничением. Их старый монолитный сайт перестал справляться с нагрузкой. Возникло новое бизнес-требование: устойчивость и масштабируемость системы при нагрузках в миллионы пользователей. Amazon сделал следующее: разделил систему на несколько независимых сервисов. В дальнейшем это начали называть микросервисной архитектурой, простроил масштабируемые базы и очереди и уделил отдельное внимание обеспечению отказоустойчивости и надёжности. Amazon не просто выдержали рост, они превратили свои наработки в отдельный бизнес — AWS, который сейчас приносит десятки миллиардов долларов.

А теперь подробнее разберём каждый компонент системного дизайна в разработке.

Сбор требований. На этом этапе мы определяем, что именно нужно системе. Сами требования делятся на функциональные и нефункциональные. Функциональные требования — это то, что система должна делать, какие функции предоставлять своему пользователю. Для абстрактного онлайн-сервиса выгула котов это функции бронирования, уведомления о начале и завершении прогулки. Нефункциональные требования — это качество системы, то есть как она выполняет свои функции. Эти требования обычно отвечают на вопросы, насколько удобно, быстро, надёжно что-то происходит. Для такого онлайн-сервиса по выгуливанию котов требования могли бы быть такими: система должна обрабатывать до 10 000 одновременных запросов от пользователей, должна поддерживать рост пользователей до миллиона без перезапуска сервисов и должна быть возможность интегрировать внешние API карт.

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

Выбор технологий. На этом этапе мы выбираем конкретные инструменты под нашу задачу, чтобы детализировать нашу схему. Подробнее и конкретнее о том, как это выглядит на собеседовании и что конкретно от тебя ожидают, ты можешь увидеть в последней главе. Коротко, здесь от тебя ожидают конкретики, если твоя специальность это позволяет, но углубляться внутрь технологии не стоит. Например, мы можем решить, что наш бэкенд будет на Go, фронтенд на Vue.js и базу мы выберем PostgreSQL.

Масштабируемость. Теперь мы переходим к качествам системы. Масштабируемость нужна для того, чтобы наша система выдержала рост нагрузки. Например, сейчас сервис обрабатывает 1 000 заявок в час, но архитектура должна позволять горизонтальное масштабирование до 100 000 заказов без переработки всей системы.

Надёжность и отказоустойчивость. Это означает, что система должна работать даже при сбоях. Например, если один дата-центр вышел из строя, сгорел, то запросы должны перенаправляться в другой.

Безопасность. Здесь мы учитываем защиту данных и доступов. Например, здесь мы можем решить, что все наши запросы будут через HTTPS, данные пользователя будут шифроваться, доступ к API будет ограничен ролями, и у нас будет двухфакторная аутентификация.

Производительность. Тут всё просто. Система должна отвечать быстро и эффективно расходовать ресурсы. Например, часто запрашиваемые данные, в нашем случае история заказов, могут храниться в кэше. Это поможет сократить издержки.

Управление данными. Здесь мы решаем, как храним, обрабатываем и защищаем наши данные. Все пользовательские данные хранятся в PostgreSQL. Есть регулярное резервное копирование и репликация для отказоустойчивости.

Интеграция. В этом блоке мы соединяем разные части системы и внешние сервисы. Например, наш сервис может интегрироваться с внешней платёжной системой через API.

Наблюдаемость. Здесь мы говорим о том, как контролируем работу системы. Пространство для действий здесь довольно большое, вплоть до того, что мы можем настроить Telegram-бота, который будет присылать нам уведомления о том, что у нас ошибка там или что-то упало.

Тестирование. Тут проверяем, что система работает правильно при различных сценариях. Классический пример: нагрузочное тестирование на 10x от реальной нагрузки, юнит-тесты на бизнес-логику, интеграционные тесты на API.

Документирование — так нелюбимое многими. Здесь мы описываем нашу архитектуру и ключевые решения. Тут великое множество. Это могут быть UML-диаграммы или RFC или простой онбординг для новых разработчиков.

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

Если всё ещё сложно и непонятно, зачем вся эта муть нужна, есть простой продакшн-пример. Ещё до покупкой WhatsApp всякими террористическими, экстремистскими организациями ходила легенда, и СМИ активно рассказывали про то, что WhatsApp — сервис с сотнями миллионов пользователей по всему миру — поддерживается небольшой командой, всего из пятидесяти инженеров. То же самое примерно недавно выдавал на интервью Павел Дуров, рассказывая, что Telegram пилят там 50 человек. Насколько это правда или скорее маркетинговая стратегия? Решать вам. Ну в целом Павел Дуров никогда своих подписчиков не обманывал, и мы ему верим. Это такая вот легенда про то, как хороший системный дизайн и грамотный подход к разработке позволяет поддерживать такие огромные продукты на плаву малыми силами сплочённой команды. Но важно понимать исторический контекст. Сегодня WhatsApp — это уже совсем другой мессенджер с кучей функций, голосовых сообщений, видеозвонков. Это часть огромной инфраструктуры одной известной организации. И там уже, конечно, не 50 человек, а сотни или тысячи. Реальных цифр нам, к сожалению, никто не назовёт и не раскроет. С инженерной точки зрения WhatsApp того периода интересен нам тем, что хранит сообщения в распределённой системе, доставляет их с минимальной задержкой даже при плохом интернете, обеспечивает надёжность доставки. Офлайн-сообщения доходят, когда пользователь снова появляется в сети, применяет end-to-end шифрование, масштабируется до миллиарда пользователей. Именно поэтому WhatsApp так часто фигурирует в учебных примерах. Он сочетает в себе простую идею обмена сообщениями и сложную реализацию на уровне глобальной распределённой системы.

В следующей главе ты узнаешь о том, как тебе самому спроектировать первую систему. Но сейчас, в самом начале, представь, что ты уже на собеседовании. Тебе уже задали задачу типа: "Спроектируйте онлайн-сервис для выгула котов", и уже ждут от тебя каких-то действий, фраз, рисунков. Первое, что тебе нужно сделать — это собрать формализованные требования. Я предлагаю тебе записать вопросы, которые нужно задать интервьюеру, чтобы понять, что от тебя требуется, и повторить это упражнение после просмотра видео до конца. Подробный разбор того, как собирать требования с интервьюера так, чтобы они действительно были тебе полезны по ходу интервью, можно посмотреть по ссылке в описании на моём бусте. А перед тем, как мы перейдём к основам проектирования систем, маленький интерактив. Напиши, пожалуйста, в комментарии, приходилось ли тебе уже сталкиваться с System Design, что это была за задача и в какой компании её спросили. Суперкратко, просто чтобы мы поняли, что осветили все вопросы в этом видео и рассмотрели все возможные задачи. Ну а если ты с таким ещё не сталкивался, и если тебе просто нравится это видео, поставь, пожалуйста, лайк и подпишись на канал. Впереди ещё очень много образовательных проектов, полные гайды, курсы и всё-всё-всё. Очень не хочется, чтобы ты это пропустил.

Я системный аналитик и ментор. И сегодня мы поговорим об основах проектирования систем. Создание любого цифрового продукта, будь то сайт, приложение или сервис, означает проектирование системы. Проектирование системы начинается от взаимодействия пользователя и до ответа сервера и до его способности выдерживать рост. Для наглядности разберём с вами продукт сервиса аренды самокатов, например, Whoosh, Urent или Яндекс Go. Пользователь открывает приложение, видит текущие самокаты, выбирает самокат, далее арендует его, катается и возвращает. Просто, не правда ли? А теперь посмотрим, что за этим у нас стоит.

Когда вы приступаете к проектированию, важно выбрать не технологии, а именно характер будущего продукта. Речь идёт не о том, насколько хорошо вы напишите, ээ, ваш бэкенд, а о том, насколько ваш продукт будет безопасным, устойчивым и быстрым. Представьте, снова пользователь открывает приложение Whoosh, Urent или Яндекс Go, и, соответственно, он выбирает нужный вам самокат, нажимает на кнопку "Начать поездку". Всё должно происходить довольно-таки быстро, то есть пользователь ожидает, что будет мгновенная реакция со стороны приложения. И как этого добиться нам? Чтобы это происходило чётко, без сбоев, нам необходимо придерживаться определённых свойств. В инженерии существуют чёткие измеримые свойства, которые становятся нашими целями при проектировании нашего сервиса. Давайте более подробно разберём эти свойства.

Первое свойство — это масштабируемость. Система должна расти вместе с бизнесом. А это значит то, что когда у нас сначала 1 000 пользователей, потом система вырастает до 100 000 пользователей, система должна автоматически распределять нагрузку так, чтобы от наплыва пользователей она не упала.

Следующее свойство — это производительность. Всё должно быть быстро, то есть отклик сервера должен быть мгновенным.

Надёжность. Данные о поездках и оплате должны храниться в базе данных. В случае, если пользователь оплатил свою поездку, то она никак не должна у нас исчезнуть из базы данных.

Следующее свойство — это отказоустойчивость. Даже в случае, если произошёл какой-то сбой, сервис должен оставаться доступным и перенаправлять запросы на другой сервер.

Консистентность. Информация должна оставаться согласованной и обязательно быть одной и той же, как в базе данных, так и в приложении. Соответственно, если в приложении был арендован самокат, то в базе данных он никак не может считаться быть свободным.

Итак, последнее наше свойство — это безопасность. Система обязана защищать наши данные, защищать данные пользователя. Соответственно, это происходит при помощи токенов доступа и шифрования.

Самое главное, что нельзя ничего сделать идеально сразу. Между скоростью, стоимостью и безопасностью нужно искать определённый баланс. И это суть системного дизайна.

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

К функциональным требованиям у нас относится то, что делает система. Например, пользователь зарегистрировался в системе, пользователь произвёл оплату, пользователь забронировал самокат.

К нефункциональным требованиям перейдём. Это то, как система должна выполнять свою работу. То есть это информация о качестве системы, о качестве её работы. Соответственно, в качестве примера можно привести здесь то, что максимальное время разблокировки самоката не должно превышать 3 секунд. Или, например, количество попыток при входе в приложение должно блокировать пользователям.

На собеседовании по системному дизайну вас могут попросить спроектировать систему по аренде самокатов. И, соответственно, первым делом, что вы должны сделать — это задать вопросы. Причём вопросы такого характера, которые помогут вам принять архитектурные решения. К таким вопросам относятся, например: какой охват вы планируете? Это будет один город-миллионник или федеральная сеть? Ответ максимально влияет в данном случае на результаты архитектурного решения. Соответственно, в случае ответа, что будет один всего лишь город, то нам здесь достаточно использование обычного монолита. Но в случае, если мы будем работать с десятью городами, то в этом случае нам необходимо использовать уже микросервисы и, скорее всего, шардирование.

Итак, следующий вопрос — это требование к реальному времени. Насколько нужно часто обновлять информацию о геоданных? К примеру, если, да, нужно обновлять достаточно часто и обновлять их каждые 5 секунд, то, соответственно, в этом случае мы будем использовать веб-сокеты и так далее. Если же достаточно обновлять данные каждые 30 секунд, то мы остановимся на обычных REST API.

Теперь мы с вами перейдём к архитектуре системы. После того, как мы определили с вами цели, свойства и требования, мы переходим непосредственно к каркасу нашей системы. Архитектура описывает, как наша система связана и с чем она взаимодействует. Она определяет путь данных, взаимодействие, также распределение нагрузки и способность справляться с ростом нагрузки. Мы с вами начнём с самого простого и будем имитировать реальный проект и постепенно его наращивать.

Сейчас мы с вами поговорим про монолит. Когда сервис только запускается, никто не строит какие-то сложные системы. И, соответственно, для MVP более чем достаточно использовать монолитную систему. Монолит — это единое приложение, где всё находится в единой кодовой базе. На примере нашего сервиса аренды самокатов Whoosh, Urent или Яндекс Go, мы понимаем, что пользователь выполняет запрос, арендует самокат. Этот запрос уходит на бэкенд, бэкенд отправляет данные в базу данных, и таким образом пользователь получает арендованный самокат. Плюсы монолита — это простота загрузки и скорость. К минусам монолита можно отнести то, что это тяжело масштабировать. Также в случае какого-то-либо сбоя это необходимо перезапускать целое приложение. Монолит отлично подходит на старте проекта, но когда проект начинает расти, то монолит начинает просто захлёбываться от роста нагрузки.

Следующим этапом — это будет добавление балансировщика нагрузки. Балансировщик нагрузки принимает запросы. Далее балансировщик нагрузки распределяет запросы между копиями бэкенд-серверов. Из плюсов можно добавить то, что система позволяет горизонтально масштабироваться, то есть добавлять дополнительные копии бэкенд-приложения. Из минусов всё также и остаётся сам монолит. В случае сбоя одного какого-то сервиса, точнее модуля в монолите, это может привести к сбою всей системы в целом.

После добавления балансировщика нагрузки мы смело можем сказать, что пора переходить к следующему этапу — это разделение монолита на микросервисы. Когда система вырастает, монолит становится неуправляемым, и, соответственно, нам необходимо разделить его на микросервисы. Микросервисы представляют собой отдельные модули, которые выполняют свою бизнес-задачу. В таких приложениях, как Whoosh, Urent или Yandex Go, это может выглядеть таким образом. К примеру, сервис Auth или AuthService — это тот сервис, который отвечает за регистрацию пользователя в системе. UserService — это тот сервис, который отвечает за профили пользователей. Vehicles. Vehicle будет отвечать у нас за информацию о самокатах, соответственно, где они находятся, какой у них уровень заряда. Service Rentals будет отвечать за то, когда завершилась аренда, когда она началась. Service Payments будет отвечать за все платежи. Соответственно, у нас остаётся сервис Notification, который будет отвечать за уведомления.

Итак, благодаря микросервисам мы добиваемся того, что каждая команда не будет мешать друг другу. То есть, например, команда Auth-сервера выполняет какие-то задачи, и она никоим образом не будет мешать Payments-сервису. И также, если будут выполняться задачи на Payment-сервисе, то это никак не будет влиять на Notification. К примеру, в выходные количество аренд сильно возросло, и нам необходимо доработать, точнее, добавить копии к Rentals-серверу. Соответственно, эта история никоим образом не повлияет на всю систему в целом. То есть мы не будем трогать остальные части нашей микросервисной архитектуры. И в этом и заключается суть микросервисного подхода.

Теперь давайте поговорим про хранение данных. Для обеспечения высокой скорости обработки запросов нам необходимо использовать гибридную архитектуру хранения. Для этого мы используем PostgreSQL для хранения данных в качестве основной базы данных. Также мы используем реплики, не забываем про кэш-данные, которые храним непосредственно в Redis. Также мы используем S3 для хранения медиафайлов. Такая комбинация делает систему быстрой и надёжной. То есть в случае чего мы можем брать информацию из кэша. Но если произошёл какой-то сбой, то мы можем всегда обратиться к главной базе данных и получить информацию.

Итак, пришла пора поговорить на тему нагрузки и масштабирования. На первых порах система работает отлично: 100, 1 000, 10 000 пользователей. Система работает превосходно, обрабатывает всё это количество пользователей. Но наступает лето, и количество пользователей возрастает, возникает необходимость обрабатывать более 100 000 пользователей. И система начинает просто задыхаться. Она не понимает, как обрабатывать все запросы. А точнее, не просто не понимает, она не справляется с тем, чтобы обработать такое количество запросов. В таком случае нам необходимо подумать о масштабировании.

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

Вертикальное масштабирование — это означает, что мы растём вширь. Вместо того, чтобы усиливать один сервер, мы берём и добавляем несколько серверов и распределяем нагрузку между ними. Как это работает? Пользователь открывает приложение, летит запрос на сервер, соответственно, его принимает балансировщик нагрузки, например. Далее балансировщик нагрузки выбирает тот сервер, который наименее загружен, и направляет туда запрос. Если же пользователь становится ещё больше, то мы просто добавляем ещё какое-то количество серверов в пул.

Итак, о'кей. Мы количество бэкенд-серверов расширили. Но что у нас происходит с базой данных? База данных у нас так и остаётся узким местом, потому что все запросы идут в одну точку. Решением будет являться добавление репликаций, то есть мы будем разделять базу данных на чтение и на запись. То есть у нас будет главная база данных, которая будет отвечать за запись данных. К ней будут обращаться в случае, когда пользователь арендует самокат, завершает аренду самокатом или, например, оплачивает аренду самоката. Копии будут обслуживать чтение, например, посмотреть историю поездок. Таким образом, мы разгружаем базу данных. И в случае, если пользователю нужно посмотреть историю поездок, то обращение будет идти к реплике. А в случае, если пользователю необходимо оплатить аренду самоката, то, соответственно, будет идти запись в главную базу данных.

Но что, если репликация уже не спасает и количество пользователей растёт и растёт? В данном случае нам необходимо прибегнуть к шардингу, то есть мы будем распределять наши данные на шарды. В сервисе аренды самокатов это удобно делать по регионам. И, к примеру, Москва будет у нас находиться в кластере 1, Санкт-Петербург будет находиться в кластере 2, а Казань будет находиться в кластере 3. Каждая база данных обслуживает только своих пользователей. И, соответственно, в случае, если в Москве находится пиковая нагрузка, то это никак не влияет на Санкт-Петербург и Казань. Самая большая сложность в данном случае — это маршрутизация, но её легко решить при помощи Sharding ID, например, User ID, который будет определять местоположение нашего пользователя и направлять в нужный шар.

Теперь поговорим про проектирование API или просто API. API по сути — это язык. Язык, по которому взаимодействует сервер и клиент. И каждое действие пользователя, начиная от входа в приложение до начала аренды самокатов, это всё происходит при помощи взаимодействия сервера и клиента. И всё это происходит при помощи API. В данном случае фронтенд у нас не знает ничего о том, что происходит внутри бэкенда. То есть он не знает о нашей внутренней архитектуре, о наличии микросервисов, о базах данных и кэше. Пользователь выполняет какое-то действие. Фронтенд делает запрос к бэкенду и просит: "Дай мне список самокатов на основе моих координат". И бэкенд обрабатывает этот запрос и в итоге говорит: "На, держи". Всё, что делает пользователь и взаимодействует с приложением, происходит через API.

Самый распространённый способ общения с сервером — это REST API. Он основан на HTTP протоколе и интуитивно понятен. REST довольно легко читается, тестируется. И именно поэтому он практически по стандарту, по де-факто он используется для взаимодействия с мобильным приложением. А должен быть интуитивно понятным. И, соответственно, для этого мы используем максимально простые действия, например, как HTTP глаголы. Такие HTTP глаголы, как

get для получения ресурса, пост для создания ресурса, put или патч для обновления ресурса и delete для удаления ресурса.

Также в REST API важна структура, то есть все запросы будут строиться, например, в множественном числе. К примеру, get/users, get/rentals.

Также в Rest API мы используем версионирование. Оно обязательно в случае, если у нас старое приложение и новые какие-то доработки ломают его, то в данном случае нам необходимо использовать версионирование. Плюс его заключается в том, что старое приложение будет работать на одной версии API, а новое приложение будет работать на новой версии API. К таким примерам можно отнести v1/users, V2/users. Соответственно, старое приложение работает с версией V1, а новое приложение работает с версией V2.

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

Также REST API должен быть безопасным. То есть у нас API является основной точкой входа в систему. И также каждый REST API должен сопровождаться токеном авторизации. На стороне API Gateway должна производиться проверка токена авторизации. И Gateway должен подтверждать, что это действительно доверенный запрос, и он может продолжать выполнять свою работу дальше. Также App Gateway должен выполнять проверку на то, что все запросы идут по https. И такие чувствительные данные, как платёжные токены, они должны шифроваться. Такой подход защищает не только пользователей, но и защищает сам бизнес.

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

Итак, напомню, что меня зовут Инара. Подписывайтесь на мой канал. Надеюсь то, что информация, которую я вам сегодня рассказала, была максимально полезной. Всем спасибо за внимание.

Привет, это снова я и третья глава гайда по систем-дизайну. В прошлой части мы уже разобрались в различиях монолита и микросервисов, рассмотрели базовые подходы к шардингу и масштабированию, но в реальном продакшене, да ещё и под нагрузкой, может всплыть целый ворох проблем. Например, среднее время ответа 20 миллисекунд, но иногда секунды, и пользователи жалуются, что ничего не работает и всё глючит. Рекламная акция дала всего + 5% посетителей, а продакшн лёг. Попытались ускорить систему, а в итоге замедлили её, да ещё и поймали плавающие баги. Про эти эффекты нужно думать уже на стадии проектирования, в том числе когда вы проектируете систему на собеседовании по систем-дизайну.

В этой главе мы последовательно разберём следующие проблемы: latency, задержку и хвост задержек. Почему среднее время ответа в системе маленькое, а пользователи всё равно страдают? Пропускная способность, throughput и очереди. Как посчитать, какую нагрузку ваша система реально выдержит? Шардирование стратегии, динамическое распределение шардов, кроссшард и миграции без простоя. Распределённые транзакции между сервисами: тупик, TCC, саги и как не утонуть в примерно согласованных деньгах. Многослойное кэширование и когерентность. Что делать, когда у вас CDN, Redis, локальный кэш и feature flags поверх релизов? Трейдофы и оптимизации, где реально стоит платить за сложность, а где можно просто добавить побольше железа и жить спокойно.

Задача этой главы - дать тебе язык и инструменты, чтобы обсуждать архитектуру не в стиле "Нормально делай, нормально будет", а в терминах задержек, стоимости и консистентности. Начнём с базовой механики производительности: Latency, хвост задержек, throughput и очередей. Поверх неё нарастим шардирование, распределённые транзакции и продвинутое кэширование. Те инструменты, которыми ты будешь спасать сервис, когда он будет тонуть под нагрузкой. Ну а пока мы не начали, поставь, пожалуйста, лайк этому видео и подпишись на канал, хотя бы за те усилия, которых мне, фронтенд-макаке, стоило всё это выучить и тебе понятно и сжато рассказать.

Представь себе, что ты открыл приложение, нажал на какую-то кнопку, и оно думает. Первый раз ты думаешь: "Ладно, тратил я время и на более бесполезные вещи. Подожду." Второй раз думаешь, раздражает. На третий раз просто закрываешь приложение и уходишь к конкуренту. Разработчики при этом читают твои жалобы, идут смотреть метрики и видят, что среднее время отклика в системе равно 200 миллисекунд. Всё отлично летает. Но пользователям безразлична средняя температура по больнице. Их волнует одно: уложился ли их конкретный запрос в нужные миллисекунды или прошло несколько секунд. Таким образом, медленно работающие сервисы и приложения приводят нас к потере клиентов, а значит, денег. Это важная проблема, её ни в коем случае нельзя игнорировать и в работе, и на собеседовании, где ты должен показать, что вы не просто какие-то абстрактные задержки, секунды, шарды обсуждаете, но ты понимаешь, зачем это делается, чтобы приносить бизнесу деньги и предостерегать, уберечь его от их потерь.

Чтобы понять, что с этим делать, для начала определимся с базовой терминологией. У нас есть три главных понятия: это latency, задержка, пропускная способность throughput и уровень параллелизма concurrency. Latency - это сколько времени наш запрос живёт внутри системы, от момента, когда мы в неё зашли до момента, когда мы из неё вышли, например, 200 миллисекунд или 2 секунды. Пропускная способность throughput: сколько запросов в секунду мы можем стабильно завершать? Например, 1.000 RPS. Уровень параллелизма concurrency: сколько запросов наша система может обрабатывать параллельно? Какие-то стоят в очереди, какие-то уже обрабатываются. Всё это висящие запросы.

Представим какую-нибудь "Пятёрочку" рядом с домом и применим эти термины к ней. Latency в данном случае - это время, сколько клиент проводит в магазине от входа до выхода. Throughput - сколько людей в минуту проходит через наши кассы. Concurrency - сколько людей сейчас в очереди? Очевидно, что все эти понятия взаимосвязаны. То есть, если перед вами уже есть очередь, то время ожидания будет больше. Ровно как и если бы throughput был выше, то очередь накапливалась бы меньше. Эта закономерность описывается законом. Вот формула.

Давай разберём на примере. Система стабильно держит 1.000 RPS и в каждый момент внутри 3.000 запросов. Тогда latency будет равняться 3 секундам. Задержка равна уровень параллелизма разделить на пропускную способность. То есть задержка равно 3.000 / 1.000. Получаем 3 секунды.

Теперь добавим поток входящих запросов. Входящий поток 1.500 RPS. Система успевает стабильно обрабатывать только 1.000 RPS. Разница 500 RPS превращается в рост очереди. Каждую секунду внутри становится на 500 висящих запросов больше. Concurrency растёт и через формулу задержка равно уровень параллелизма разделить на пропускная способность, автоматически растёт и задержка. Если система в таком режиме будет работать довольно долго, то получаем печальную ситуацию. Очередь растёт без верхней границы. Latency растёт и стремится к бесконечности. Пользователь видит ситуацию: "Всё умерло". Ничего не работает. Хотя железо по факту работает, система по факту жива, но к обработке запроса пользователя она приступит очень нескоро. Таким образом, если поток реальных запросов хотя бы на несколько секунд превышает реальный throughput, то очередь будет расти и latency будет расти. Вдобавок система может начать деградировать под такой нагрузкой, и всё станет ещё хуже.

Сейчас ты можешь спросить меня здесь, что конкретно требуется? Ну, в смысле, зачем мне нужна эта информация? На практике это понимание помогает принимать решения. То есть ты видишь, что в системе сейчас 10.000 запросов, у тебя 2.000 RPS. Таким образом, уже 5 секунд. И неважно, что каждый конкретный обработчик сам по себе быстрый. Из этих соображений следует, что очередь будет увеличиваться до тех пор, пока не вырастет throughput через горизонтальное масштабирование или через оптимизацию тяжёлых участков или пока ты не ограничишь входящий поток. Это rate limiting, back pressure или очереди. На собеседовании так и нужно рассуждать. Не давайте просто увеличим количество серверов и всё будет хорошо. А при таком количестве реальных запросов и неизменном throughput в очередь неизбежно будет расти и будет расти latency. Чтобы этого избежать, нам нужно изменить баланс concurrency и throughput.

Значит ли это, что мы можем просто измерить latency и пойти домой? Да, к сожалению, здесь ещё сценария на 40 страниц, поэтому, видимо, нет, не можем. Мы только в самом начале разберём такой пример. У нас есть 100 запросов. 90 обрабатываются за 100 миллисекунд, 10 за 5 секунд. В среднем у нас 590 миллисекунд. На бумаге это выглядит терпимо. По факту 90 человек получают быстрый ответ и думают: "Ну, нормально работает". А 10 человек видят загрузку на 5 секунд и идут писать негативные отзывы. Бизнесу важны не абстрактные 590 миллисекунд в среднем, а какая доля пользователей попадает в негативный опыт и насколько плох этот опыт по времени.

Для этого вводят перцентили. Перцентиль в данном случае - это время, быстрее которого обслуживается определённый процент запросов. Например, перцентиль 50. Половина запросов быстрее этого времени. По сути, медиана. При перцентиле 95% запросов быстрее этого времени. Ну и 99 запросов быстрее. Представим, что для нашего продакшена метрики следующие. Как нам это читать и что это значит? Половина запросов улетает за 150 миллисекунд. Великолепно. Ещё 45% укладываются в 400 миллисекунд. Терпимо. Последний 1% сидит по 2 секунды. Это те самые недовольные пользователи из нашего примера выше. Тут возникает новый термин Tail latency. Это тот самый небольшой процент очень медленных запросов. Да, кстати, для правильного проговаривания всех англицизмов я специально ездил учиться в Гарвард на 2 месяца. Так что напишите в комментариях, если вам понравился мой English accent.

В реальной жизни и на собеседованиях разговор про производительность всегда должен идти в терминах девяносто пятый и девяносто девятый перцентиль, а не, ну, в среднем у нас всё норм. При этом в распределённых системах хвосту свойственно удлиняться. Почему так происходит? В монолите запрос на создание заказа может состоять из четырёх звеньев: веб-сервер, поход в базу, поход в кэш и немного логики внутри самого монолита. В микросервисной же архитектуре в цепочке становится гораздо больше звеньев. Веб-сервер, сервис А, сервис B, сервис C. При этом каждый из сервисов входит в свою базу и кэш. Теперь представим, что у каждого звена пятидесятый перцентиль - 50 миллисекунд, а девяносто девятый - 200 миллисекунд. Большинство запросов будут быстрыми, но вероятность попадания запросов в хвост задержек растёт по теории вероятности с увеличением количества звеньев. В итоге по отдельности все сервисы нормальные. Девяносто девятый перцентиль 200 миллисекунд. Нормально. Но девяносто девятый перцентиль по нашему запросу может достигать 2 секунд. Если мы поймаем хвост задержек в каждом из звеньев.

Ты или твой коллега, который ещё не смотрел этот потрясающий гайд, могут сказать: "Давайте просто добавим серверов, оно само как-то и починится". Далее мы как раз будем обсуждать, почему добавлением железа проблему не решить и как нам уменьшить хвост задержки и не дать ему убить систему. Но откуда берутся эти хвосты? Ведь они, по определению, возникают лишь иногда, и причина плавающая, и вряд ли в коде, не так ли? На то есть три группы причин: это аппаратные, сетевые и программные. Аппаратные - это когда, например, throttling на диске из-за лимита чтения записи или Thermal Throttling на процессоре. Живой пример одного из авторов курса. Его продукт был расположен на московских ЦОДах. Как-то раз выдался особо жаркий день. И хотя не с их стороны, ни с чьей-либо никаких релизов и изменений не было. Система в тот день сильно деградировала, мало что вообще грузилось, а запросы падали с 502, 504 ошибкой. Сетевые - это, например, congestion из-за соседнего сервера или долгие DNS-запросы. Программных причин вообще бывает миллион. Это очереди, конкуренция за ресурсы, оптимистичные, пессимистичные блокировки. Эффект noisy neighbor, когда другой сервис на той же ноде начинает потреблять выделенные ресурсы и вам их не хватает, бывает на виртуалках или в облаках. Сборка мусора, ротация или retention логов, перезапуск процессов, sync базы. Для хвостов не нужно, чтобы что-то было сломано. Достаточно лишь маленькой вероятности плохого сценария, которое при большой нагрузке и большом трафике обязательно случится.

Одиночный хвост, конечно, сам по себе неприятен, но систему не убивает. Гораздо хуже, когда система пытается умно на него реагировать. Как это может выглядеть? Наш запрос поймал хвост. Клиент, мобильное приложение или фронтенд сделал повторный запрос. API Gateway тоже умеет retries, ну, для надёжности. И в нашем сервисе тоже прописано retries с тайм-аутом. В результате вместо одного застрявшего запроса мы можем получить 27, если на каждом из наших слоёв будет по три попытки на три уровня. Это и называется шторм повторных запросов. И вот его характеристики: пятидесятый перцентиль почти не меняется. Девяносто пятый и девяносто девятый перцентиль улетают в космос. На отдельных нодах скачет нагрузка по CPU и сети, хотя бизнес-трафик не сильно вырос. Это не значит, что мы убираем retries. Они сами по себе нужны, но за ними нужно следить, ограничивать количество попыток и на каждом слое, и в цепочке в целом. То есть не while true. Бесконечно отправляем запросы. Привязывать ко времени. Экспоненциальный бэкоф. Стерм по-простому, когда время между ретраями увеличивается: через секунду, через 3, через 10. Выделить ответственную часть системы за retries. Либо клиент, либо сервис. Короче, без UML диаграммы здесь не обойтись. Иначе надёжность через retries легко превращается в механизм добивания и без того больной системы.

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

Начнём с уменьшения хвостов. И первый способ - hatched requests. Идея hatched запросов в том, чтобы не бесконечно ждать, а вдруг повезёт и отработает, а при возникновении повисшего запроса дать ему шанс на другой ноде. Схема такая: сначала измеряем latency и строим распределение. Выбираем порог, например, перцентиль девяносто пятый. Когда приходит запрос, шлём его на обычную реплику. Если он не успел завершиться за девяносто пятый перцентиль, отправляем вторую копию на другую реплику. Берём первый успешный ответ, второй считаем лишним и отменяем, если это возможно. Число параллельных копий ограничиваем. Обычно две, иногда три, но не по всем инстансам. Важно заметить, что такую технику можно применять только на идемпотентных операциях. Если, например, мы два раза спишем денежку, то пользователь будет не очень доволен, как и бизнес. Здесь мы платим дополнительными запросами за стабильный девяносто пятый, девяносто девятый перцентиль. Если всё настроено аккуратно, прирост нагрузки получается небольшой, а хвост заметно подрезается. Из минусов: нагрузка на систему, необходимость калибровки порогов и аккуратной интеграции с ретраями.

Есть ещё один способ уменьшить хвост: поставить перед тяжёлым ресурсом очередь. Вместо того, чтобы попытаться обслужить все запросы сразу, принимаем задачи и складываем их в очередь. Воркеры забирают их с фиксированной скоростью, не убивая базу, диск или внешний API. Так мы сглаживаем пики нагрузки и контролируем concurrency. Однако рискуем получить дополнительную latency и рискуем получить head of line blocking, если будем неаккуратны в выборе размера очередей и политики обработки. Также есть Q-based leveling, который выравнивает нагрузку через очередь. Подход, связанные запросы используют несколько очередей, чтобы быстрее найти свободного исполнителя, но без лишних дублей работы. По шагам выглядит так: клиент шлёт запрос на сервер А. Через маленькую паузу порядка одного-двух round trip time, то есть RTT 5-10 миллисекунд, копию на сервер B. Оба запроса несут общий request ID. Когда один сервер берёт запрос в работу, он помечает request ID как "в работе", через быстрый store внутренний протокол и шлёт cancel для копии. Если на второй ноде запрос всё ещё стоит в очереди, его просто выкидывают или понижают приоритет. Как и в этом подходе, когда мы отталкиваемся от загруженности очереди сервиса, есть подход, который распределяет загрузку в зависимости от скорости ответа ноды. Latency Aware Load Balancing. Обычный round robin считает, что все ноды одинаковы, но на практике часть нод может быть загружена или, например, попасть на более медленный диск. Latency Aware Load Balancing же смотрит на историческую задержку по нодам и шлёт задачи тем нодам, у которых latency выше. Таким образом даёт отдохнуть уставшим или деградировавшим нодам. Из минусов это, конечно, усложняет работу балансировщика.

Есть ещё одна техника - request coalescing. В ней первый запрос реально идёт вниз. Остальные, которые пришли с тем же ключом, мы временно подвешиваем. Когда первый вернулся, раздаём его результат всем ожидающим. Мы уменьшаем concurrency на нижнем уровне, становится меньше нагрузка на БД, внешний сервис и ниже шанс, что девяносто девятый перцентиль превратится в секунды. Также можно размазать нагрузку по времени. Это называется background jittering. Добавляем случайную компоненту в расписание. Вместо 1.000 задач в конкретное время получаем 1.000 задач, размазанных по минуте. Если же какие-то процессы запускаются не только по ночам, а в течение всего времени, да ещё и не на одной машине, то может помочь обратный подход - synchronized disruption. Мы можем запускать эти процессы везде в один момент. Тогда только запросы, пришедшие в этот момент, подвиснут, а в остальное время хвостов не будет. То есть система не всё время подтормаживает, а потратила пару секунд, чтобы потормозить, и дальше снова работает хорошо. Back pressure - это ограничение входного потока под пропускную способность ресурса. Идея в том, что система должна честно сказать: "Я больше не вывожу". И не принимать больше ничего на вход, вместо того, чтобы раздувать очереди и увеличивать latency. Как это можно организовать? Устанавливаем лимиты на одновременные запросы к BD, Redis, внешним или очередям. Ограничиваем размер очередей перед воркерами. При превышении лимита есть варианты. Мы либо быстро отдаём 429 или 503, или отправляем сообщение в Dead Letter Queue, или просим клиента retrying позже с backoff + jitter. Все эти умные слова и англицизмы - это просто паттерны для уменьшения хвоста. Это нормально, что ты не можешь сразу их запомнить и осознать, в конце концов, кто из нас не читал "Банду четырёх" и не офигевал от того, сколько там всего есть и как это вообще применять, а потом использовал буквально два-три паттерна в повседневной жизни. На всякий случай мы выведем всё, что мы перечислили здесь на экран. Пока можно это не зубрить и не запоминать, вернуться к этому позже. Главное осознать принципы и сами идеи борьбы с этими хвостами.

Теперь пройдёмся по подходам маскировки хвоста. Паттерн Circuit Breaker похож на Back Pressure, но работает не с мощностью, а со здоровьем удалённого ресурса. Механизм такой: мы следим за долей ошибок и тайм-аутов при обращениях к BD и внешнему API. Если процент ошибок превышает лимит, то мы размыкаем цепь сразу, возвращаем запросы с ошибкой или быстрым fallback. Мы вообще перестаём ходить в зависимость, которая явно нездорова. Пока breaker открыт, мы периодически делаем пробные запросы. Если они начинают успешно проходить, мы закрываем breaker и постепенно возвращаем трафик. То есть вместо того, чтобы каждый запрос висел до тайм-аута, мы быстро возвращаем контролируемую ошибку или быстрый ответ. Таким образом спасаем остальную систему и наш UX.

Есть ещё один подход - Deadline Propagation. Мы считаем какой-то приемлемый порог выполнения запроса и пропихиваем его в заголовке, например, на уровне балансера. Так хвостовые запросы не зависают бесконечно, а система тратит меньше ресурсов на заведомо проигрышные попытки. Любимый многими подход - good enough. Как можно догадаться из названия, лучше сделать быстро как-нибудь, чем идеально, но долго. Например, не удалось нам получить свежие рекомендации, отдаём дефолтный, предподготовленный список там бестселлеров или продуктов товаров. Не можем быстро посчитать цену с учётом всех скидок, купонов, акций. Говорим, что вот приблизительная цена, и честно пишем, что там точную цену сможем посчитать после нажатия на кнопку вашего. Думаю, что если кто-нибудь покупал билеты где-нибудь на Avia, ну, короче, вы примерно знакомы с этим паттерном. Ну или поисковый движок отдаёт результаты чуть хуже, не такие релевантные, но зато быстрее, вместо того, чтобы ждать, пока там умный поиск жёстко найдёт всё правильно, но очень долго.

Как ты мог заметить, почти все подходы опираются на сбор определённых метрик: перцентили latency, длину очередей и прочее. То есть эти метрики нам нужны не просто для отчёта перед тим-лидом. В распределённых системах необходимо делать distributed tracing, то есть связывать логи разных компонентов по идентификатору, ну, например, по идентификатору запроса. Это помогает увидеть, какое звено в цепочке даёт хвост, и отличить хвост базы данных от хвоста сети или хвоста очереди. Про алерты, конечно, тоже не стоит забывать, если мы хотим узнать о деградации системы не из гневных комментариев от пользователей, а как-то сами по внутренним дашбордам и мониторингам. Это могут быть алерты по девяносто девятому персентилю, всплескам ретраев или увеличению длины очередей.

Резюмируя эту часть, пожалуй, две главных мысли, которые хочется донести. Хвосты - это не баги, это естественное поведение сложных систем. Задача команды разработки не сделать хвосты нулевыми, а мониторить и контролировать их, предотвращая поломку пользовательского опыта.

Итак, мы разобрали, что по закону Литла, если мы увеличим пропускную способность, то задержка уменьшится. Если мы хотим удержать задержку на приемлемом уровне при повышении нагрузки, нам нужно либо увеличивать пропускную способность, либо ограничивать конкурентность, о чём мы также немного поговорили ранее. Как увеличить пропускную способность? Первое, что приходит на ум: давайте просто купим ещё железо, и всё будет зашибись. Но увеличение количества серверов может как ускорить нас, так и, наоборот, замедлить. Поэтому, прежде чем скоропалительно делать такое решение, давайте обратимся к законам масштабирования пропускной способности. Всего их три: закон Амдаля, закон Густавсона и Universal Scalability Law или же Закон универсальной масштабируемости. Кажется логичным. Нужна больше пропускная способность. Давайте добавим больше серверов, воркеров или реплик.

Законом Амдаля говорит нам о том, что ускорение системы ограничено той частью её, которую нельзя распараллелить. Представим какой-то запрос. 70% времени тратится на параллелящуюся часть. Можно растащить по серверам или воркерам, а 30% времени - строго последовательная часть. Один мастер, один лог, а один общий ресурс. Формула Амдаля на ваших экранах. В ней P - доля параллелящейся части, в нашем примере равна 0,7. То есть, согласно этой формуле, хоть миллион серверов ты добавь, но если доля непараллелящейся работы 30%, то максимально ты ускоришься в три и три раза примерно.

Теперь Густавсон. Дело в том, что запрос бизнеса не всегда звучит как: "Нам нужно сделать то же самое, только быстрее". Часто запрос звучит по-другому: "У нас есть фиксированное время, например, 200 миллисекунд или 1 час ночной джабы. Сколько мы можем успеть сделать работы за это время, если добавим ресурсов?" Тут уже нам пригождается закон Густавсона. Как и из закона Амдаля, мы не получаем линейного ускорения от добавления новых серверов, но формула уже другая. Формула Густавсона для ускорения при N, в нашем случае восьми узлах, у вас на экранах. Альфа здесь - это доля непараллелящейся части, в данном случае 10%. Идеал, как мы понимаем, X8, но в реальности мы можем ускориться только в 7,3 раза. 10% непараллелящейся работы остаются узким горлышком. В терминах Густавсона мы не уменьшаем задержку отчёта, мы увеличиваем объём работы, который можем сделать за то же самое время.

Амдаль и Густавсон говорят нам, какая теоретическая польза от добавления ресурсов. В реальности же, при добавлении ресурсов мы начинаем платить дополнительные издержки, например, за конкуренцию, за общие ресурсы или за согласование состояний между репликами или слоями системы. Это описывает закон универсальной масштабируемости. Формулу вы можете увидеть на своих экранах. К сожалению, я слишком тупой, чтобы это прочитать вслух. Здесь S(n) - это SpeedUp. Насколько же вырастет пропускная способность или объём работы в единицу времени при N узлах по сравнению с одним узлом? N - число узлов. Сигма - накладные ресурсы на конкуренцию за общие ресурсы. Каппа - накладные расходы на согласованность между репликами.

Какой вывод можно сделать из этого закона и примера кривой? Здесь можно увидеть, откуда берётся реальный эффект. Добавили ещё серверов. Стало только хуже. Если мы возьмём для этой формулы слишком много узлов, то лишь сильнее увеличим внутреннюю координацию и борьбу за общие ресурсы. Давайте приведу пример, которым вы можете потом выпендриваться на собеседованиях и делать вид, что вы сами когда-то с этой проблемой сталкивались и знаете, как её решать. Возьмём базу данных биллинга в каком-то маркетплейсе. Вначале у нас одна база данных платежей, один сервер, все операции с деньгами проходят через него. При умеренной нагрузке всё летает. Затем мы добавляем реплики для чтения. Чтобы снять нагрузку с мастера и кормить отчёты или аналитику. Кажется, пропускная способность растёт. Фронтенд и API масштабируется. Чтение уезжает на реплики. Но возникает ряд проблем. Все операции по деньгам: списание, возврат, удержание - всё равно упираются в мастер. Транзакции row lock. Проверка инвариантов вокруг балансов не дают распараллелить этот кусок. По мере роста числа приложений, которые бьются в ту же самую базу данных, растёт contention. По горячим строкам и индексам мы наращиваем количество приложений и реплик, но Сигма и Каппа в формуле

USL растут быстрее, чем пропускная способность по деньгам. Рост нагрузок приводит к росту lock contention и стоимости репликации. В какой-то момент добавление новых сервисов или реплик перестаёт помогать. Пропускная способность по критичным операциям платежей почти не растёт, а хвост задержки чекаута начинает ухудшаться.

Что мы можем с этим поделать? Тут есть две основные практики. Первое - это разделение нагрузок и данных, изоляция ход ресурсов. Об этом мы чуть позже поговорим. А о второй я уже упомянул. Предлагаю вам в комментариях поспорить, какой из законов нам может здесь помочь и как.

Представим такую ситуацию. У нас есть большая таблица Orders. В ней уже десятки и сотни миллионов строк. Индексы раздулись. Вакуум занимает много времени и никогда не заканчивается. Бэкапы занимают всю ночь. Секскан по этой таблице начинает занимать секунды. Сама таблица весит уже терабайты. Тем временем мы хотим, чтобы данные за последние дни, недели, то есть горячие данные, обрабатывались быстро, ретенtion старых данных проходил быстро, отчёты за год не перегружали базу, ну и бэкап был быстрым.

Что мы делаем? Профилируем и переписываем запросы. Работаем над индексами, нормализуем и денормализуем данные там, где это уместно. Выжимаем вертикальное масштабирование. И только когда всё это уже не спасает, вводим партиционирование на одном кластере.

Что такое партенирование? Это когда одна логическая таблица разбита на несколько физических кусков, но при этом всё это живёт в одном инстансе базы данных. и при этом по возможности прозрачно для приложения. При этом мы можем резать таблицу вертикально по столбцам и горизонтально по строкам.

Шардирование в принципе то же самое, но живёт всё в разных инстанцах. На всякий случай хочу уточнить, что понятие партиционирования, да и шардирования, о которых мы сейчас будем говорить, довольно размыты. Например, тот самый Мартин Клепман использует партиционирование как общий термин для разрезания данных на часть, а шар или партия просто разные названия этих кусков данных в разных системах. Зачем я это уточняю? Просто если какой-то душный лид, синьор начинает уточнять у тебя термин или гонять тебя в глубину насобесие, не нужно жёстко стоять на своём и говорить: "Нет, вот только так и по-другому никак". В IT, безусловно, есть термины, которые можно озвучивать только там в каноничном академическом определении, а есть термины вот такие, которые разные люди понимают по-разному. Поэтому лучше в каких-то моментах понять, что там твой техлит фанат именно вот этой статьи и сказать: "Ладно, хорошо, типа, пускай не будем здесь зарубаться".

Но зачем вообще нужны шарды, когда есть партиции? Шардирование пригодится, когда необходимо увеличить пропускную способность записи. Например, один HDD даёт 200 Мб в секунду, а SSD больше. Распределить CPU нагрузку. Геораспределение. Например, пользователя из Европы отправляем на кластер, находящийся в Европе, когда таблицы или индексы физически не помещаются на один сервер, там в один диск или скоро перестанут помещаться. При этом почти всегда шардирование идёт в паре с репликацией. Каждый шарт имеет свои реплики, иначе падение одной машины повлечёт смерть куска данных или сервиса.

Шардирование, как и партионирование, сильно усложняет систему. Поэтому важно понимать, что это не то, что нужно закладывать в систему с самого начала, и лучше обходиться без этого, пока это возможно. Например, GitHub долгое время жил на монолитной базе и выдерживал 950.000 транзакций в секунду.

Прежде чем двигаться дальше, определимся с базовой терминологией. Что такое shart key, hot key, перекос распределения и маршрутизация?

Шар key, то есть ключ шардирования - это ответ на вопрос, по какому признаку мы разбиваем данные и на какой шарт отправится наш запрос. Например, user ID, created или region. Выбор правильного ключа шардирования крайне важен, потому что от него будет зависеть баланс нагрузки, цена миграции и сложность запросов. Что делает ключ шардирования хорошим? Он более-менее равномерно размазывает нагрузку и объём данных. И типичные бизнес-запросы выполняются в пределах одного шарда, ну или на малом количестве шардов.

Хотки или горячий ключ - это ключ, который генерирует несоразмерно большой трафик по сравнению с остальными. Например, топовый продавец, по которому постоянно смотрят отзывы и проверяют его товары или какой-то популярный товар, лабубу, что там популярно у молодёжи, я я не знаю. Или видео в трендах. Даже если мы правильно выбрали ключ шардирования, то один такой горячий ключ может превратить целый шарт в батлck или бутылочное горлышко. Горячий ключ - одна из причин перекоса распределения.

Так как данные у нас теперь живут на разных серверах, логичный вопрос: кто и как поймёт, куда же отправлять наш SQL-запрос? Есть три варианта: роутинг в приложении, прокси и координатор.

Роутинг в приложении подразумевает, что вся логика лежит в коде и приложение знает несколько ДСН. Это даёт полный контроль и позволяет проще отлаживать, дебажить наш код. Но кроссшардовые запросы и миграции придётся придумывать самостоятельно.

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

Если же мы хотим, чтобы наша прокси сама работала с шардированием, а приложение ничего об этом не знало, то нам необходим координатор. Координатор понимает внутреннюю логику, сам решает, какие шарды трогать, строит распределённый план, умеет работать с транзакциями. Из примеров это, например, цитус для постгрес или vitess поверх MySQL.

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

Теперь поговорим о ребалансировании. Это важно, потому что оно отвечает на вопрос, как вернуть здоровое распределение данных и нагрузки по кластерам, не роняя прот. Есть несколько причин, по которым может понадобиться ребалансирование. Неравномерный рост данных. Например, один шарт разросся до терабайта, остальные по 100 Гб. Горячие категории. Например, один шарт получает 80-90% RPS. Рост кластера. Добавили новые шарды. Их нужно заполнить частью существующих данных. Сжатие кластера. Убрали железо. Хотим сократить расходы. Смена ключа шардирования. резали по User ID, поняли, что правильное по seller ID или Tenant ID.

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

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

Также важно, как бы хорошо мы не порезали данные, нам всё равно придётся столкнуться с операциями, затрагивающие данные с нескольких шардов или даже со всех одновременно. Такие операции называются crossshar операциями. Вот некоторые из них cross, shart join. Например, на странице заказа нам требуются данные и продавца, и заказа, и товара. Cross shared aggregation, когда нам нужно посчитать оборот продавца за неделю, а его заказы уже нарезаны по user ID или по регионам. Cross Shar search, когда мы ищем товар по атрибутам, цвет, бренд, цена, размер, а они размазаны по шардированным индексам.

Перед следующей частью давай немного закрепим материал и разомнёмся. Представь, что мы с тобой наедине, you know, и у нас system design интервью, и я тебе задаю три следующих вопроса. Только не подглядывай в комменты, там уже, скорее всего, есть правильный ответ. Чем шаордирование отличается от партеционирования? Когда стоит шардировать, а когда лучше попробовать репликацию или кэширование? Как ты выбираешь ключ шардирования? Кстати, если с третьим вопросом у тебя возникли затруднения и ты не можешь дать однозначный ответ, заглядывай по ссылке в описании на бусте там в приватной части видео только для участников ОМ, как правильно выбирать ключ шардирования и как решать, какой ключ куда пойдёт.

Допустим, мы распределили данные по шардам, логику по сервисам, но бизнес-процессы у нас никуда не делись. Деньги должны списываться один раз, товары должны резервироваться, заказы создаваться. И всё это происходит через разные базы и сервисы. И вот главный вопрос: как нам выполнить многосервисную операцию так, чтобы сбой в её середине не привёл нас к неконсистентному состоянию системы? AST внутри одной базы решает эту проблему одной транзакцией. Но в распределённой системе у нас такого инструмента нету. У каждого сервиса своя разная база. Ещё сервис может ребутнуться или отвалиться. Так и сеть ещё добавляет новых проблем.

Дальше мы рассмотрим четыре варианта, как решить эту проблему. Только не смейтесь. Two PC, free PC, TCC. И вы угадали сага.

Начнём ступи. Это протокол распределённой транзакции между хранилищами. Есть координатор и несколько участников нот базы или шардов. На уровне протокола у нас две фазы. Координатор кластера говорит всем участникам: "Выполните свою часть". Вторая фаза фиксации. когда координатор получил ОК от всех участников. Если все готовы, то он рассылает commit prepared на каждую ноду. Если кто-то не смог подготовиться, то рассылает rollback prepared на каждую ноду. До тех пор, пока координатор не примет решение, каждая нода заморожена в состоянии подготовлена, но не закомичена.

УтуPC есть неприятные свойства. Во-первых, если координатор умер в неприятный момент, то часть транзакции зависает в состоянии prepaired. Во-вторых, prepair держит блокировки, пока координатор думает, а значит, растут очереди и хвосты задержек. В-третьих, хвост задержек тут определяется с самой медленной нодой и состоянием сети. В-четвёртых, при сетевых разрывах легко получить подготовлено в части участников, но не закомичено, и потом придётся разгребать это вручную. Поэтому между микросервисами PC практически не используется, а вот внутри одной базы или кластера PC применим.

Следующий протокол Free PC - это Free Comit Face. Это попытка сделать PC неблокирующим за счёт добавления дополнительной фазы прекомиit. Сначала координатор проверяет готовность участников, готов, не готов, потом рассылает им намерение комитить прикомит. И только потом следует финальный комит. Идея в том, что участник, доживший до прекомит и не получивший финальное решение, может принять самостоятельное решение ролбек или коit и не зависать бесконечно в состоянии prepared, как в случае с 2 PC. Но это всё держится на довольно нереалистичных допущениях и сети, что в ней маленькие прогнозируемые задержки, плюс дополнительно усложняет реализацию и всё равно плохо бьётся с реальным продом. Поэтому free PC также очень редко можно встретить в настоящих микросервисах. Ты можешь сказать: "Антон, ну какого хрена ты тогда это рассказываешь?" А потому что это те самые академические знания, которые у вас могут спросить на собеседовании, даже учитывая то, что в продакшене это не используют.

TCC try confirm cancel - это прикладной паттерн распределённые транзакции на уровне бизнес-логики. Реализуется контрактами между сервисами. Координатором выступает отдельный сервис, который вызывает участников три типа операций. зарезервировать ресурс, поставить на холд деньги, занять слот бронирования, пометить товар как зарезервированный. Confirm: зафиксировать операцию после успеха всех trй и CCEL снять резерв, если кто-то из участников не прошёл TR или произошёл сбой. Важно, каждый микросервис должен иметь отдельные endпоинты или методы, cancel confirm, хранить состояние резерва. ID-операции, объём, срок жизни и уметь корректно обработать повторные вызовы импотентность. В отличие от 2PC, здесь нет блокировок в СУБД. Подготовленное состояние живёт в бизнес-данных, а протокол не привязан к конкретной базе и может работать с разными стеками. TCC применяется там, где цена ошибки очень высока. Это финтех, бронирование, билинг, управление квотами. Минусы здесь рост сложности системы, больше состояний, больше координации, нужна очистка протухших резервов и строгая дисциплина по контрактам.

Более известной альтернативой TCC является сага. При ней мы не делаем резервы, чтобы потом их подтвердить, а просто по очереди выполняем шаги и в случае сбоя выполняем откат. Пример для оформления заказа. Биллинг списывает деньги. Inventory резервирует товар, orders создаёт заказ. Notifications отправляет письмо или push. Если на шаге два не удалось зарезервировать товар, вызываем компенсацию в билинге. Refund или снятие холд. Если на шаге три упало создание заказа, можно вернуть деньги либо, если домен операции не позволяют автоматический возврат, сделать напрямую компенсацию. Например, отправить задачу менеджеру, письмо в саппорт, сигнал в CRM. Дальше человечек разруливает ситуацию. Компенсация при этом не обязана быть симметричной обратной операцией. Главное, чтобы процесс приходил в согласованное состояние по бизнес-правила.

Что даёт нам сага? Нет глобальных блокировок и подготовленных состояний. Шаги отдельные можно компенсировать, мониторить или ретраить. удобно масштабировать процессы между разными сервисами и стеками. Цена здесь сложность самого процесса и продуманность сценариев компенсации. Также сага не подходит для областей, где важна строгая консистентность, например, при работе с деньгами.

Вообще в распределённых системы есть два кардинально различающихся класса решений в области согласованности. Тяжёлые протоколы транзакций TUPC и TCC, которые максимально приближают нас ктомарности между участниками ценой сложности и задержек. И процессные паттерны Sag Outbox Event Driven, которые упрощают жизнь сервисом, но делают расхождение между их состояниями нормой.

На уровне данных это выглядит так. Есть куски, где нам нужна более строгая консистентность. Это балансы, права доступа, критические инварианты. И это вид согласованности Strong consistency. И есть всё остальное, где мы готовы жить с тем, что разные сервисы некоторое время видят разное состояние данных. Это eventual consistency. Знать это нужно. Ну, во-первых, для собеседований, а во-вторых, чтобы понимать, как такие модели влияют на архитектуру системы и её поведение для пользователей.

На-на. Но я схожу с ума потихоньку уже от этих слов. У меня вытесняются детские воспоминания, имена родителей. Я помню только, [ __ ] консиistнcy, хуистенси, хвосты задержек и прочую залупу. Я больше так не могу.

Итак, eventual Consistency - это модель, которая допускает временную рассогласованность данных между репликами и сервисами, но при этом гарантирует, что в какой-то момент эти данные сойдутся. Используются в оценке товаров, комментах, каталогах, в общем, везде, где небольшие расхождения приемлемы, возможна компенсация и цена ошибки невысока. Ну, то есть ваша единица товару с ВБ может отобразиться не сразу, но в целом никто от этого особо сильно не пострадает. Что нам даёт эта модель? Ну, во-первых, высокую доступность. Не нужно блокировать всю систему из-за одной реплики. Более простое масштабирование. Можно шардировать и реплицировать без глобальных транзакций. Низкую стоимость записи, меньше блокировок, меньше координации. Также существуют локальные приёмы. которые помогают снять боль конкретного запроса или конкретного пользователя от потенциального расинхрона в этом подходе eventual consistency. Поэтому в большинстве случаев он неплох.

Теперь про Strong consistency. Это более жёсткая модель, которая нам пригодится, когда речь идёт о деньгах пользователя, например, или ограниченных ресурсах вроде гостиничных номеров. Потому что, согласись, будет очень неловко, если два пользователя одновременно увидят свободный номер. Первый забронит, у второго не отобразится, что он уже забронен, и в итоге потрачено. Все очень недовольны. Её также используют, когда работают с критичными правами доступа и операциями, компенсация которых невозможна. Суть её в том, что если запись успешно прошла, то любое последующее чтение должно видеть новое значение этого ключа, вне зависимости от узла, на который пришёл запрос. Также есть ещё более строгий её вариант линиаризуемость - это когда вдобавок сохраняется ещё и правильный порядок операций. Главный минус таких систем - стоимость, доступность и сложность масштабируемости. на практике почти никогда не делают всю систему жёсткой. Реализуют небольшой слой или сервис уровня strong, например, бронирование конкретного места на рейс, а вокруг сервисы с eventual consistency, например, предзаказ еды.

И напоследок возвращаемся. Мы с тобой в красной комнате. Если что, я Кристиан Грей, а ты Анаastши Стил. И вот вопросы, которые тебе могут задать на собеседовании. В чём разница между паттерном SG и two PC? Какую проблему решает Free PC? Какие группы решений проблемы согласованности существуют в принципе и в чём их особенности? Приведи по примеру системы, в котором ты бы использовал эти группы решений проблемы согласованности.

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