20 кирпичей system design

Senior-инженер Anurag Gharat выложил в X первую часть серии System Design Essentials, и за сутки она набрала больше 100 тысяч просмотров. Внутри 20 базовых концепций, из которых собирается почти любая архитектура: от клиент-серверной модели до вебхуков. Каждая сопровождается коротким объяснением, схемой и компромиссом, на который приходится идти. Пересказываю материал по-русски с небольшими пояснениями.
Клиент-сервер
Всё начинается с простой схемы: клиент отправляет запрос, сервер его обрабатывает и обращается к базе данных. Если клиент ходит в базу напрямую, архитектуру называют двухуровневой. Когда между ними появляется отдельный сервер приложений, получается трёхуровневая схема, на которой работает большинство современных сервисов. Кэширование, масштабирование и всё остальное строится поверх этой связки.
Вертикальное и горизонтальное масштабирование
Когда нагрузка растёт, систему можно усилить двумя способами. Вертикальный путь означает докупить памяти, процессоров и дисков в существующую машину. На старте это самый быстрый вариант, но у железа есть потолок, и рано или поздно вы в него упрётесь. Горизонтальный путь предполагает добавлять новые машины, которые делят работу между собой. Такой подход ещё называют fan-out.
Базы данных
Интернет-магазину нужно где-то хранить пользователей, товары и заказы, мессенджеру нужны группы и сообщения. Сервер сам по себе данные не держит, он только читает, создаёт, обновляет и удаляет их в базе. Эти четыре операции называют CRUD. Видов баз много: реляционные, нереляционные, графовые, объектно-ориентированные, иерархические и векторные.
ACID и BASE
Это два подхода к транзакциям. ACID расшифровывается как атомарность, согласованность, изолированность и долговечность. Атомарность удобно объяснять на переводе денег: если списание со счёта A прошло, а зачисление на счёт B упало, первое действие нужно откатить. Согласованность гарантирует, что база переходит из одного корректного состояния в другое. Изолированность защищает параллельные транзакции от взаимного влияния. Долговечность обещает, что подтверждённая запись переживёт падение сервера.
BASE (Basically Available, Soft State, Eventually Consistent) характерен для NoSQL. Система отвечает на чтение и запись всегда, состояние может меняться в фоне из-за синхронизации, а данные приходят к согласованности со временем.
CAP-теорема
В распределённой системе одновременно можно гарантировать только два свойства из трёх: согласованность (любое чтение видит последнюю запись), доступность (любой запрос получает ответ) и устойчивость к разделению сети (система работает, даже если узлы перестали видеть друг друга). Сетевые сбои рано или поздно случаются, и в этот момент приходится выбирать. CP-системы жертвуют доступностью ради корректности и готовы отклонить запрос. AP-системы продолжают отвечать и мирятся с временной рассинхронизацией. ACID тяготеет к CP, BASE к AP.
Реляционные базы
Данные лежат в таблицах со строгой схемой, таблицы связаны внешними ключами, например пользователь и его заказы. Это позволяет строить сложные запросы с JOIN и получать гарантии ACID, поэтому SQL выбирают банки и системы учёта заказов. Типичные представители: PostgreSQL, MySQL и Oracle.
Нереляционные базы
NoSQL хранят данные в документах, парах ключ-значение или широких колонках. Жёсткой схемы нет, у каждой записи может быть своя структура. Это удобно для быстро меняющихся данных и горизонтального масштабирования на много серверов. Большинство таких баз следуют модели BASE. Примеры: MongoDB, Redis и Cassandra.
Репликация
Базу копируют на несколько узлов и держат копии синхронизированными. Классическая схема называется primary-replica: основной узел принимает все записи, реплики обслуживают чтение. Большинство приложений читают гораздо чаще, чем пишут: товары смотрят тысячи людей, заказывают единицы. Поэтому чтение уносят на реплики и наращивают его пропускную способность, просто добавляя новые копии.
При синхронной репликации primary ждёт подтверждения от реплики и только потом отвечает клиенту. При асинхронной отвечает сразу и досылает запись на реплики в фоне.
Партиционирование
Большую базу режут на части. Вертикальное партиционирование делит таблицу по колонкам, горизонтальное по строкам. Маленькие куски проще обслуживать и масштабировать, поиск по ним быстрее, а данные, которые больше не нужны, удобно вынести в архив.
Шардинг
Это разновидность горизонтального партиционирования, при которой каждый шард живёт на своём сервере и хранит только часть данных. Куда попадёт запись, решает ключ шардирования: по диапазону, по хэшу или по географии. Шардинг выручает, когда одна база уже не вытягивает запись или объём. Репликация тут бессильна, потому что каждая реплика хранит полный набор данных. Цена вопроса в том, что запросы сразу по нескольким шардам усложняются, а межшардовые транзакции трудно сделать атомарными.
HTTP
Протокол, по которому общаются клиент и сервер. Он работает поверх TCP и не хранит состояние между запросами. Запрос состоит из метода, URL, заголовков и тела, ответ содержит код статуса, заголовки и тело. GET читает данные, POST создаёт, PUT заменяет, PATCH частично обновляет, DELETE удаляет. Коды, которые стоит держать в голове: 200, 201, 301, 304, 400, 401, 404, 500 и 503.
Монолит
Вся кодовая база в одном репозитории, одна сборка, один деплой. В традиционном монолите вместе живут интерфейс, бизнес-логика, слой доступа к данным и база, классический пример здесь Spring-приложение на Java. В модульном монолите единым остаётся только сервисный слой, а UI и база вынесены отдельно. Монолит легко поднять, протестировать и выкатить, он хорошо подходит небольшой команде с ограниченным скоупом. Проблемы начинаются с ростом: слои сцепляются, командам трудно работать независимо, стек сложно поменять.
Микросервисы
Приложение делят на небольшие независимые сервисы. Каждый отвечает за одну бизнес-функцию, имеет свой код, свой пайплайн и часто свою базу. В интернет-магазине сервис пользователей занимается авторизацией и профилями, сервис товаров каталогом и остатками, сервис заказов оформлением и отслеживанием, платёжный сервис транзакциями. Выигрыш в независимых релизах, масштабировании по частям, изоляции сбоев и свободе в выборе технологий. Расплачиваться приходится сложной коммуникацией между сервисами, распределёнными данными, операционной нагрузкой, трудным тестированием и дополнительными задержками.
API Gateway
Единая точка входа между клиентами и микросервисами. Шлюз проверяет авторизацию, маршрутизирует запросы, пишет логи, балансирует нагрузку, ограничивает частоту запросов, кэширует ответы и умеет переводить протоколы, например принимать HTTP и ходить к сервисам по gRPC. Популярные решения: Amazon API Gateway, NGINX и Kong.
REST
Набор правил поверх HTTP, который превращает API в предсказуемый контракт между клиентом и сервером. Автор выделяет шесть принципов: единообразный интерфейс, отсутствие состояния, разделение клиента и сервера, кэшируемость, многослойность и опциональную передачу исполняемого кода клиенту.
SOAP
Строгий протокол обмена структурированными сообщениями, который работает исключительно на XML. Каждый запрос и ответ заворачивается в конверт Envelope: в заголовке лежат метаданные, в теле данные и описание ошибок. По сравнению с REST он куда жёстче и многословнее.
GraphQL
Закрывает две боли REST: overfetching, когда приходит больше данных, чем нужно, и underfetching, когда для одного экрана приходится делать несколько запросов. У GraphQL один эндпоинт, POST /graphql, и клиент сам описывает, какие поля хочет получить. Страница профиля с тремя последними заказами в REST потребует двух вызовов, GET /users/12 и GET /users/12/orders?limit=3, плюс склейки на клиенте. В GraphQL хватит одного запроса, и ответ придёт ровно в той форме, в которой его запросили.
WebSocket
Постоянное двустороннее соединение. Оно начинается как обычный HTTP-запрос, сервер отвечает кодом 101, и дальше обе стороны свободно обмениваются сообщениями без новых рукопожатий. Так устроены чаты, онлайн-игры, биржевые и спортивные приложения, трекинг доставки еды и совместное редактирование документов.
gRPC
Фреймворк от Google, который позволяет вызвать метод на другом сервере так, будто это локальная функция. Внутри сервиса заказов можно написать ProductService.getProductDetails(order.productId), а под капотом уйдёт сетевой запрос на другую машину. Скорость gRPC объясняется двумя вещами: данные сериализуются в компактный бинарный формат Protobuf, а транспортом служит HTTP/2. Его выбирают для общения сервисов между собой, систем с жёсткими требованиями к задержкам и стриминга.
Polling
Представьте страницу оплаты, где пользователь сканирует QR-код телефоном, а экран должен сам обновиться после успешного платежа. Самый прямолинейный способ: каждую секунду спрашивать платёжный сервис, изменился ли статус. Это и есть polling. Он встречается в платёжных шлюзах, дашбордах и чатах, но тратит ресурсы впустую и может перегрузить сервер потоком одинаковых запросов.
Webhooks
Вебхук переворачивает направление общения. Клиент заранее оставляет серверу свой URL, и когда событие происходит, сервер сам присылает POST-запрос на этот адрес. REST, GraphQL и gRPC работают по модели pull, где клиент забирает данные сам. Вебхуки работают по модели push, и клиенту больше не нужно ничего спрашивать.
Что дальше
Во второй и третьей частях автор обещает разобрать событийную архитектуру, алгоритмы балансировки нагрузки и rate limiting, очереди задач и другие темы. Оригинал статьи со схемами к каждому блоку: System Design Essentials, часть 1.



